From cd55acd14446aa90233e86574783556513f22d7c Mon Sep 17 00:00:00 2001 From: Oleg Yukhnevich Date: Sun, 8 Sep 2024 10:26:10 +0300 Subject: [PATCH] Support incremental hashing via HashFunction --- cryptography-core/api/cryptography-core.api | 36 ++++++-- .../api/cryptography-core.klib.api | 24 +++++- .../kotlin/functions/HashFunction.kt | 13 +++ .../kotlin/functions/UpdateFunction.kt | 70 +++++++++++++++ .../commonMain/kotlin/operations/Hasher.kt | 32 ++++++- .../src/commonMain/kotlin/support.kt | 7 ++ .../commonMain/kotlin/default/DigestTest.kt | 86 +++++++++++++++++++ .../commonMain/kotlin/algorithms/CCDigest.kt | 60 ++++++++++--- .../kotlin/algorithms/CCHashAlgorithm.kt | 86 +++++++++---------- .../src/commonMain/kotlin/internal/clozy.kt | 46 ++++++++++ .../src/commonMain/kotlin/internal/utils.kt | 18 ++++ .../jvmMain/kotlin/algorithms/JdkDigest.kt | 33 ++++++- .../jdk/src/jvmMain/kotlin/pooling.kt | 32 +++++-- .../kotlin/algorithms/Openssl3Digest.kt | 70 +++++++++++---- .../src/commonMain/kotlin/internal/bytes.kt | 18 +++- .../src/commonMain/kotlin/internal/clozy.kt | 46 ++++++++++ .../kotlin/algorithms/WebCryptoDigest.kt | 19 +++- 17 files changed, 593 insertions(+), 103 deletions(-) create mode 100644 cryptography-core/src/commonMain/kotlin/functions/HashFunction.kt create mode 100644 cryptography-core/src/commonMain/kotlin/functions/UpdateFunction.kt create mode 100644 cryptography-providers/apple/src/commonMain/kotlin/internal/clozy.kt create mode 100644 cryptography-providers/openssl3/api/src/commonMain/kotlin/internal/clozy.kt diff --git a/cryptography-core/api/cryptography-core.api b/cryptography-core/api/cryptography-core.api index 5b9e6cdf..0f2de5b7 100644 --- a/cryptography-core/api/cryptography-core.api +++ b/cryptography-core/api/cryptography-core.api @@ -618,6 +618,22 @@ public final class dev/whyoleg/cryptography/algorithms/symmetric/SymmetricKeySiz public final fun getB256-5xWg6fk ()I } +public abstract interface class dev/whyoleg/cryptography/functions/HashFunction : dev/whyoleg/cryptography/functions/UpdateFunction { + public abstract fun hash ()Lkotlinx/io/bytestring/ByteString; + public abstract fun hashInto ([BI)I + public static synthetic fun hashInto$default (Ldev/whyoleg/cryptography/functions/HashFunction;[BIILjava/lang/Object;)I + public abstract fun reset ()V +} + +public abstract interface class dev/whyoleg/cryptography/functions/UpdateFunction : java/lang/AutoCloseable { + public fun update (Lkotlinx/io/bytestring/ByteString;II)V + public abstract fun update ([BII)V + public static synthetic fun update$default (Ldev/whyoleg/cryptography/functions/UpdateFunction;Lkotlinx/io/bytestring/ByteString;IIILjava/lang/Object;)V + public static synthetic fun update$default (Ldev/whyoleg/cryptography/functions/UpdateFunction;[BIIILjava/lang/Object;)V + public fun updatingSink (Lkotlinx/io/RawSink;)Lkotlinx/io/RawSink; + public fun updatingSource (Lkotlinx/io/RawSource;)Lkotlinx/io/RawSource; +} + public abstract interface class dev/whyoleg/cryptography/materials/key/EncodableKey : dev/whyoleg/cryptography/materials/key/Key { public fun encodeTo (Ldev/whyoleg/cryptography/materials/key/KeyFormat;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public static synthetic fun encodeTo$suspendImpl (Ldev/whyoleg/cryptography/materials/key/EncodableKey;Ldev/whyoleg/cryptography/materials/key/KeyFormat;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; @@ -711,12 +727,20 @@ public abstract interface class dev/whyoleg/cryptography/operations/Encryptor { } public abstract interface class dev/whyoleg/cryptography/operations/Hasher { - public fun hash (Lkotlinx/io/bytestring/ByteString;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; - public fun hash ([BLkotlin/coroutines/Continuation;)Ljava/lang/Object; - public static synthetic fun hash$suspendImpl (Ldev/whyoleg/cryptography/operations/Hasher;Lkotlinx/io/bytestring/ByteString;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; - public static synthetic fun hash$suspendImpl (Ldev/whyoleg/cryptography/operations/Hasher;[BLkotlin/coroutines/Continuation;)Ljava/lang/Object; - public fun hashBlocking (Lkotlinx/io/bytestring/ByteString;)Lkotlinx/io/bytestring/ByteString; - public abstract fun hashBlocking ([B)[B + public abstract fun createHashFunction ()Ldev/whyoleg/cryptography/functions/HashFunction; + public fun hash (Lkotlinx/io/RawSource;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public fun hash (Lkotlinx/io/bytestring/ByteString;IILkotlin/coroutines/Continuation;)Ljava/lang/Object; + public fun hash ([BIILkotlin/coroutines/Continuation;)Ljava/lang/Object; + public static synthetic fun hash$default (Ldev/whyoleg/cryptography/operations/Hasher;Lkotlinx/io/bytestring/ByteString;IILkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object; + public static synthetic fun hash$default (Ldev/whyoleg/cryptography/operations/Hasher;[BIILkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object; + public static synthetic fun hash$suspendImpl (Ldev/whyoleg/cryptography/operations/Hasher;Lkotlinx/io/RawSource;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public static synthetic fun hash$suspendImpl (Ldev/whyoleg/cryptography/operations/Hasher;Lkotlinx/io/bytestring/ByteString;IILkotlin/coroutines/Continuation;)Ljava/lang/Object; + public static synthetic fun hash$suspendImpl (Ldev/whyoleg/cryptography/operations/Hasher;[BIILkotlin/coroutines/Continuation;)Ljava/lang/Object; + public fun hashBlocking (Lkotlinx/io/RawSource;)Lkotlinx/io/bytestring/ByteString; + public fun hashBlocking (Lkotlinx/io/bytestring/ByteString;II)Lkotlinx/io/bytestring/ByteString; + public fun hashBlocking ([BII)[B + public static synthetic fun hashBlocking$default (Ldev/whyoleg/cryptography/operations/Hasher;Lkotlinx/io/bytestring/ByteString;IIILjava/lang/Object;)Lkotlinx/io/bytestring/ByteString; + public static synthetic fun hashBlocking$default (Ldev/whyoleg/cryptography/operations/Hasher;[BIIILjava/lang/Object;)[B } public abstract interface class dev/whyoleg/cryptography/operations/SecretDerivation { diff --git a/cryptography-core/api/cryptography-core.klib.api b/cryptography-core/api/cryptography-core.klib.api index 857f6dfe..2019544f 100644 --- a/cryptography-core/api/cryptography-core.klib.api +++ b/cryptography-core/api/cryptography-core.klib.api @@ -514,6 +514,19 @@ abstract interface dev.whyoleg.cryptography.algorithms/PBKDF2 : dev.whyoleg.cryp final object Companion : dev.whyoleg.cryptography/CryptographyAlgorithmId // dev.whyoleg.cryptography.algorithms/PBKDF2.Companion|null[0] } +abstract interface dev.whyoleg.cryptography.functions/HashFunction : dev.whyoleg.cryptography.functions/UpdateFunction { // dev.whyoleg.cryptography.functions/HashFunction|null[0] + abstract fun hash(): kotlinx.io.bytestring/ByteString // dev.whyoleg.cryptography.functions/HashFunction.hash|hash(){}[0] + abstract fun hashInto(kotlin/ByteArray, kotlin/Int = ...): kotlin/Int // dev.whyoleg.cryptography.functions/HashFunction.hashInto|hashInto(kotlin.ByteArray;kotlin.Int){}[0] + abstract fun reset() // dev.whyoleg.cryptography.functions/HashFunction.reset|reset(){}[0] +} + +abstract interface dev.whyoleg.cryptography.functions/UpdateFunction : kotlin/AutoCloseable { // dev.whyoleg.cryptography.functions/UpdateFunction|null[0] + abstract fun update(kotlin/ByteArray, kotlin/Int = ..., kotlin/Int = ...) // dev.whyoleg.cryptography.functions/UpdateFunction.update|update(kotlin.ByteArray;kotlin.Int;kotlin.Int){}[0] + open fun update(kotlinx.io.bytestring/ByteString, kotlin/Int = ..., kotlin/Int = ...) // dev.whyoleg.cryptography.functions/UpdateFunction.update|update(kotlinx.io.bytestring.ByteString;kotlin.Int;kotlin.Int){}[0] + open fun updatingSink(kotlinx.io/RawSink): kotlinx.io/RawSink // dev.whyoleg.cryptography.functions/UpdateFunction.updatingSink|updatingSink(kotlinx.io.RawSink){}[0] + open fun updatingSource(kotlinx.io/RawSource): kotlinx.io/RawSource // dev.whyoleg.cryptography.functions/UpdateFunction.updatingSource|updatingSource(kotlinx.io.RawSource){}[0] +} + abstract interface dev.whyoleg.cryptography.materials.key/Key // dev.whyoleg.cryptography.materials.key/Key|null[0] abstract interface dev.whyoleg.cryptography.materials.key/KeyFormat { // dev.whyoleg.cryptography.materials.key/KeyFormat|null[0] @@ -564,10 +577,13 @@ abstract interface dev.whyoleg.cryptography.operations/Encryptor { // dev.whyole } abstract interface dev.whyoleg.cryptography.operations/Hasher { // dev.whyoleg.cryptography.operations/Hasher|null[0] - abstract fun hashBlocking(kotlin/ByteArray): kotlin/ByteArray // dev.whyoleg.cryptography.operations/Hasher.hashBlocking|hashBlocking(kotlin.ByteArray){}[0] - open fun hashBlocking(kotlinx.io.bytestring/ByteString): kotlinx.io.bytestring/ByteString // dev.whyoleg.cryptography.operations/Hasher.hashBlocking|hashBlocking(kotlinx.io.bytestring.ByteString){}[0] - open suspend fun hash(kotlin/ByteArray): kotlin/ByteArray // dev.whyoleg.cryptography.operations/Hasher.hash|hash(kotlin.ByteArray){}[0] - open suspend fun hash(kotlinx.io.bytestring/ByteString): kotlinx.io.bytestring/ByteString // dev.whyoleg.cryptography.operations/Hasher.hash|hash(kotlinx.io.bytestring.ByteString){}[0] + abstract fun createHashFunction(): dev.whyoleg.cryptography.functions/HashFunction // dev.whyoleg.cryptography.operations/Hasher.createHashFunction|createHashFunction(){}[0] + open fun hashBlocking(kotlin/ByteArray, kotlin/Int = ..., kotlin/Int = ...): kotlin/ByteArray // dev.whyoleg.cryptography.operations/Hasher.hashBlocking|hashBlocking(kotlin.ByteArray;kotlin.Int;kotlin.Int){}[0] + open fun hashBlocking(kotlinx.io.bytestring/ByteString, kotlin/Int = ..., kotlin/Int = ...): kotlinx.io.bytestring/ByteString // dev.whyoleg.cryptography.operations/Hasher.hashBlocking|hashBlocking(kotlinx.io.bytestring.ByteString;kotlin.Int;kotlin.Int){}[0] + open fun hashBlocking(kotlinx.io/RawSource): kotlinx.io.bytestring/ByteString // dev.whyoleg.cryptography.operations/Hasher.hashBlocking|hashBlocking(kotlinx.io.RawSource){}[0] + open suspend fun hash(kotlin/ByteArray, kotlin/Int = ..., kotlin/Int = ...): kotlin/ByteArray // dev.whyoleg.cryptography.operations/Hasher.hash|hash(kotlin.ByteArray;kotlin.Int;kotlin.Int){}[0] + open suspend fun hash(kotlinx.io.bytestring/ByteString, kotlin/Int = ..., kotlin/Int = ...): kotlinx.io.bytestring/ByteString // dev.whyoleg.cryptography.operations/Hasher.hash|hash(kotlinx.io.bytestring.ByteString;kotlin.Int;kotlin.Int){}[0] + open suspend fun hash(kotlinx.io/RawSource): kotlinx.io.bytestring/ByteString // dev.whyoleg.cryptography.operations/Hasher.hash|hash(kotlinx.io.RawSource){}[0] } abstract interface dev.whyoleg.cryptography.operations/SecretDerivation { // dev.whyoleg.cryptography.operations/SecretDerivation|null[0] diff --git a/cryptography-core/src/commonMain/kotlin/functions/HashFunction.kt b/cryptography-core/src/commonMain/kotlin/functions/HashFunction.kt new file mode 100644 index 00000000..280c6ea9 --- /dev/null +++ b/cryptography-core/src/commonMain/kotlin/functions/HashFunction.kt @@ -0,0 +1,13 @@ +/* + * Copyright (c) 2024 Oleg Yukhnevich. Use of this source code is governed by the Apache 2.0 license. + */ + +package dev.whyoleg.cryptography.functions + +import kotlinx.io.bytestring.* + +public interface HashFunction : UpdateFunction { + public fun hash(): ByteString + public fun hashInto(destination: ByteArray, destinationOffset: Int = 0): Int + public fun reset() +} diff --git a/cryptography-core/src/commonMain/kotlin/functions/UpdateFunction.kt b/cryptography-core/src/commonMain/kotlin/functions/UpdateFunction.kt new file mode 100644 index 00000000..0d3799db --- /dev/null +++ b/cryptography-core/src/commonMain/kotlin/functions/UpdateFunction.kt @@ -0,0 +1,70 @@ +/* + * Copyright (c) 2024 Oleg Yukhnevich. Use of this source code is governed by the Apache 2.0 license. + */ + +package dev.whyoleg.cryptography.functions + +import dev.whyoleg.cryptography.* +import kotlinx.io.* +import kotlinx.io.bytestring.* +import kotlinx.io.unsafe.* + +public interface UpdateFunction : AutoCloseable { + public fun update(source: ByteArray, startIndex: Int = 0, endIndex: Int = source.size) + public fun update(source: ByteString, startIndex: Int = 0, endIndex: Int = source.size) { + update(source.asByteArray(), startIndex, endIndex) + } + + public fun updatingSource(source: RawSource): RawSource = UpdatingSource(this, source) + public fun updatingSink(sink: RawSink): RawSink = UpdatingSink(this, sink) +} + +private class UpdatingSource( + private val function: UpdateFunction, + private val source: RawSource, +) : RawSource { + override fun readAtMostTo(sink: Buffer, byteCount: Long): Long { + val result = source.readAtMostTo(sink, byteCount) + if (result != -1L) { + @OptIn(UnsafeIoApi::class) + UnsafeBufferOperations.iterate(sink, sink.size - result) { context, head, _ -> + var segment = head + while (segment != null) { + context.withData(segment, function::update) + segment = context.next(segment) + } + } + } + return result + } + + override fun close(): Unit = source.close() +} + +private class UpdatingSink( + private val function: UpdateFunction, + private val sink: RawSink, +) : RawSink { + override fun write(source: Buffer, byteCount: Long) { + source.require(byteCount) + + @OptIn(UnsafeIoApi::class) + UnsafeBufferOperations.iterate(source) { context, head -> + var consumedCount = 0L + var segment = head + while (segment != null && consumedCount < byteCount) { + context.withData(segment) { bytes, startIndex, endIndex -> + val toUpdate = minOf(byteCount - consumedCount, (endIndex - startIndex).toLong()).toInt() + function.update(bytes, startIndex, startIndex + toUpdate) + consumedCount += toUpdate + } + segment = context.next(segment) + } + } + + sink.write(source, byteCount) + } + + override fun flush(): Unit = sink.flush() + override fun close(): Unit = sink.close() +} diff --git a/cryptography-core/src/commonMain/kotlin/operations/Hasher.kt b/cryptography-core/src/commonMain/kotlin/operations/Hasher.kt index 95fcfacf..69ebcba5 100644 --- a/cryptography-core/src/commonMain/kotlin/operations/Hasher.kt +++ b/cryptography-core/src/commonMain/kotlin/operations/Hasher.kt @@ -5,13 +5,37 @@ package dev.whyoleg.cryptography.operations import dev.whyoleg.cryptography.* +import dev.whyoleg.cryptography.functions.* +import kotlinx.io.* import kotlinx.io.bytestring.* @SubclassOptInRequired(CryptographyProviderApi::class) public interface Hasher { - public suspend fun hash(data: ByteArray): ByteArray = hashBlocking(data) - public fun hashBlocking(data: ByteArray): ByteArray + public fun createHashFunction(): HashFunction - public suspend fun hash(data: ByteString): ByteString = hash(data.asByteArray()).asByteString() - public fun hashBlocking(data: ByteString): ByteString = hashBlocking(data.asByteArray()).asByteString() + public suspend fun hash(data: ByteArray, startIndex: Int = 0, endIndex: Int = data.size): ByteArray { + return hashBlocking(data, startIndex, endIndex) + } + + public suspend fun hash(data: ByteString, startIndex: Int = 0, endIndex: Int = data.size): ByteString { + return hash(data.asByteArray(), startIndex, endIndex).asByteString() + } + + public suspend fun hash(data: RawSource): ByteString { + return hashBlocking(data) + } + + public fun hashBlocking(data: ByteArray, startIndex: Int = 0, endIndex: Int = data.size): ByteArray = createHashFunction().use { + it.update(data, startIndex, endIndex) + it.hash().asByteArray() + } + + public fun hashBlocking(data: ByteString, startIndex: Int = 0, endIndex: Int = data.size): ByteString { + return hashBlocking(data.asByteArray(), startIndex, endIndex).asByteString() + } + + public fun hashBlocking(data: RawSource): ByteString = createHashFunction().use { + it.updatingSource(data).buffered().transferTo(discardingSink()) + it.hash() + } } diff --git a/cryptography-providers-tests-api/src/commonMain/kotlin/support.kt b/cryptography-providers-tests-api/src/commonMain/kotlin/support.kt index 47c7bf04..8aee96c6 100644 --- a/cryptography-providers-tests-api/src/commonMain/kotlin/support.kt +++ b/cryptography-providers-tests-api/src/commonMain/kotlin/support.kt @@ -12,6 +12,13 @@ import dev.whyoleg.cryptography.serialization.asn1.modules.* import dev.whyoleg.cryptography.serialization.pem.* import kotlinx.io.bytestring.* +fun AlgorithmTestScope<*>.supportsFunctions() = supports { + when { + provider.isWebCrypto -> "Incremental functions" + else -> null + } +} + fun AlgorithmTestScope<*>.supportsDigest(digest: CryptographyAlgorithmId): Boolean = supports { val sha3Algorithms = setOf(SHA3_224, SHA3_256, SHA3_384, SHA3_512) when { diff --git a/cryptography-providers-tests/src/commonMain/kotlin/default/DigestTest.kt b/cryptography-providers-tests/src/commonMain/kotlin/default/DigestTest.kt index 58d8d386..14c3d7dd 100644 --- a/cryptography-providers-tests/src/commonMain/kotlin/default/DigestTest.kt +++ b/cryptography-providers-tests/src/commonMain/kotlin/default/DigestTest.kt @@ -8,6 +8,8 @@ import dev.whyoleg.cryptography.* import dev.whyoleg.cryptography.algorithms.* import dev.whyoleg.cryptography.providers.tests.api.* import dev.whyoleg.cryptography.random.* +import kotlinx.io.* +import kotlinx.io.bytestring.* import kotlin.math.* import kotlin.test.* @@ -57,4 +59,88 @@ abstract class DigestTest(provider: CryptographyProvider) : ProviderTest(provide @Test fun testSHA3_512() = test(SHA3_512, 64) + + @Test + fun testIndexes() = testAlgorithm(SHA256) { + val hasher = algorithm.hasher() + + val array = ByteArray(10) + + assertFails { hasher.hash(array, -1, 10) } + assertFails { hasher.hash(array, 0, -1) } + assertFails { hasher.hash(array, 20, 10) } + assertFails { hasher.hash(array, 0, 20) } + + if (!supportsFunctions()) return@testAlgorithm + + val hashFunction = algorithm.hasher().createHashFunction() + + assertFails { hashFunction.update(array, -1, 10) } + assertFails { hashFunction.update(array, 0, -1) } + assertFails { hashFunction.update(array, 20, 10) } + assertFails { hashFunction.update(array, 0, 20) } + + hashFunction.update(array) + } + + @Test + fun testFunctionChunked() = testAlgorithm(SHA256) { + if (!supportsFunctions()) return@testAlgorithm + + val hasher = algorithm.hasher() + val bytes = ByteString(CryptographyRandom.nextBytes(10000)) + + val digest = hasher.hash(bytes) + hasher.createHashFunction().use { function -> + repeat(10) { + function.update(bytes, it * 1000, (it + 1) * 1000) + } + assertContentEquals(digest, function.hash()) + } + } + + @Test + fun testFunctionReuse() = testAlgorithm(SHA256) { + if (!supportsFunctions()) return@testAlgorithm + + val hasher = algorithm.hasher() + val bytes1 = ByteString(CryptographyRandom.nextBytes(10000)) + val bytes2 = ByteString(CryptographyRandom.nextBytes(10000)) + + val digest1 = hasher.hash(bytes1) + val digest2 = hasher.hash(bytes2) + hasher.createHashFunction().use { function -> + function.update(bytes1) + assertContentEquals(digest1, function.hash()) + + function.update(bytes2) + assertContentEquals(digest2, function.hash()) + + // update and then discard + function.update(bytes1) + function.update(bytes1) + function.reset() + // update after reset + function.update(bytes1) + assertContentEquals(digest1, function.hash()) + } + } + + @Test + fun testFunctionSource() = testAlgorithm(SHA256) { + val hasher = algorithm.hasher() + + val bytes = ByteString(CryptographyRandom.nextBytes(10000)) + val source = Buffer() + source.write(bytes) + val digest = hasher.hash(bytes) + + assertContentEquals(digest, hasher.hash(source.copy())) + + if (!supportsFunctions()) return@testAlgorithm + hasher.createHashFunction().use { function -> + assertContentEquals(bytes, function.updatingSource(source).buffered().readByteString()) + assertContentEquals(digest, function.hash()) + } + } } diff --git a/cryptography-providers/apple/src/commonMain/kotlin/algorithms/CCDigest.kt b/cryptography-providers/apple/src/commonMain/kotlin/algorithms/CCDigest.kt index c1e812e6..1e755db0 100644 --- a/cryptography-providers/apple/src/commonMain/kotlin/algorithms/CCDigest.kt +++ b/cryptography-providers/apple/src/commonMain/kotlin/algorithms/CCDigest.kt @@ -6,24 +6,62 @@ package dev.whyoleg.cryptography.providers.apple.algorithms import dev.whyoleg.cryptography.* import dev.whyoleg.cryptography.algorithms.* +import dev.whyoleg.cryptography.functions.* import dev.whyoleg.cryptography.operations.* import dev.whyoleg.cryptography.providers.apple.internal.* import kotlinx.cinterop.* +import kotlinx.io.bytestring.* +import kotlinx.io.bytestring.unsafe.* -internal class CCDigest( - private val hashAlgorithm: CCHashAlgorithm, +internal class CCDigest( + private val hashAlgorithm: CCHashAlgorithm, override val id: CryptographyAlgorithmId, ) : Hasher, Digest { override fun hasher(): Hasher = this + override fun createHashFunction(): HashFunction = CCHashFunction( + algorithm = hashAlgorithm, + context = Resource(hashAlgorithm.alloc(), nativeHeap::free) + ) +} + +private class CCHashFunction( + private val algorithm: CCHashAlgorithm, + private val context: Resource>, +) : HashFunction, SafeCloseable(SafeCloseAction(context, AutoCloseable::close)) { + init { + reset() + } + + override fun update(source: ByteArray, startIndex: Int, endIndex: Int) { + checkBounds(source.size, startIndex, endIndex) + + val context = context.access() + source.usePinned { + check(algorithm.ccUpdate(context, it.safeAddressOf(startIndex), (endIndex - startIndex).convert()) > 0) + } + } + + override fun hashInto(destination: ByteArray, destinationOffset: Int): Int { + checkBounds(destination.size, destinationOffset, destinationOffset + algorithm.digestSize) + + val context = context.access() + destination.usePinned { + check(algorithm.ccFinal(context, it.safeAddressOf(destinationOffset).reinterpret()) > 0) + } + reset() + return algorithm.digestSize + } + + @OptIn(UnsafeByteStringApi::class) + override fun hash(): ByteString { + val output = ByteArray(algorithm.digestSize) + hashInto(output) + return UnsafeByteStringOperations.wrapUnsafe(output) + } - @OptIn(ExperimentalUnsignedTypes::class) - override fun hashBlocking(data: ByteArray): ByteArray { - val output = ByteArray(hashAlgorithm.digestSize) - hashAlgorithm.ccHash( - data = data.fixEmpty().refTo(0), - dataLength = data.size.convert(), - digest = output.asUByteArray().refTo(0) - ) - return output + // TODO: recheck how it should work + override fun reset() { + val context = context.access() + check(algorithm.ccInit(context) > 0) } } diff --git a/cryptography-providers/apple/src/commonMain/kotlin/algorithms/CCHashAlgorithm.kt b/cryptography-providers/apple/src/commonMain/kotlin/algorithms/CCHashAlgorithm.kt index 9d16b0a6..ace087a3 100644 --- a/cryptography-providers/apple/src/commonMain/kotlin/algorithms/CCHashAlgorithm.kt +++ b/cryptography-providers/apple/src/commonMain/kotlin/algorithms/CCHashAlgorithm.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2023 Oleg Yukhnevich. Use of this source code is governed by the Apache 2.0 license. + * Copyright (c) 2023-2024 Oleg Yukhnevich. Use of this source code is governed by the Apache 2.0 license. */ package dev.whyoleg.cryptography.providers.apple.algorithms @@ -7,65 +7,65 @@ package dev.whyoleg.cryptography.providers.apple.algorithms import kotlinx.cinterop.* import platform.CoreCrypto.* -internal sealed class CCHashAlgorithm { +internal abstract class CCHashAlgorithm { abstract val digestSize: Int - abstract fun ccHash( - data: CValuesRef, - dataLength: CC_LONG, - digest: CValuesRef, - ): CPointer? - object SHA1 : CCHashAlgorithm() { + abstract fun alloc(): CPointer + abstract fun ccInit(context: CPointer): Int + abstract fun ccFinal(context: CPointer, digest: CValuesRef): Int + abstract fun ccUpdate(context: CPointer, data: CValuesRef, dataLength: CC_LONG): Int + + object SHA1 : CCHashAlgorithm() { override val digestSize: Int get() = CC_SHA1_DIGEST_LENGTH - override fun ccHash( - data: CValuesRef, - dataLength: CC_LONG, - digest: CValuesRef, - ): CPointer? = CC_SHA1(data, dataLength, digest) + override fun alloc(): CPointer = nativeHeap.alloc().ptr + override fun ccInit(context: CPointer): Int = CC_SHA1_Init(context) + override fun ccFinal(context: CPointer, digest: CValuesRef): Int = CC_SHA1_Final(digest, context) + override fun ccUpdate(context: CPointer, data: CValuesRef, dataLength: CC_LONG): Int = + CC_SHA1_Update(context, data, dataLength) } - object SHA224 : CCHashAlgorithm() { + object SHA224 : CCHashAlgorithm() { override val digestSize: Int get() = CC_SHA224_DIGEST_LENGTH - override fun ccHash( - data: CValuesRef, - dataLength: CC_LONG, - digest: CValuesRef, - ): CPointer? = CC_SHA224(data, dataLength, digest) + override fun alloc(): CPointer = nativeHeap.alloc().ptr + override fun ccInit(context: CPointer): Int = CC_SHA224_Init(context) + override fun ccFinal(context: CPointer, digest: CValuesRef): Int = CC_SHA224_Final(digest, context) + override fun ccUpdate(context: CPointer, data: CValuesRef, dataLength: CC_LONG): Int = + CC_SHA224_Update(context, data, dataLength) } - object SHA256 : CCHashAlgorithm() { + object SHA256 : CCHashAlgorithm() { override val digestSize: Int get() = CC_SHA256_DIGEST_LENGTH - override fun ccHash( - data: CValuesRef, - dataLength: CC_LONG, - digest: CValuesRef, - ): CPointer? = CC_SHA256(data, dataLength, digest) + override fun alloc(): CPointer = nativeHeap.alloc().ptr + override fun ccInit(context: CPointer): Int = CC_SHA256_Init(context) + override fun ccFinal(context: CPointer, digest: CValuesRef): Int = CC_SHA256_Final(digest, context) + override fun ccUpdate(context: CPointer, data: CValuesRef, dataLength: CC_LONG): Int = + CC_SHA256_Update(context, data, dataLength) } - object SHA384 : CCHashAlgorithm() { + object SHA384 : CCHashAlgorithm() { override val digestSize: Int get() = CC_SHA384_DIGEST_LENGTH - override fun ccHash( - data: CValuesRef, - dataLength: CC_LONG, - digest: CValuesRef, - ): CPointer? = CC_SHA384(data, dataLength, digest) + override fun alloc(): CPointer = nativeHeap.alloc().ptr + override fun ccInit(context: CPointer): Int = CC_SHA384_Init(context) + override fun ccFinal(context: CPointer, digest: CValuesRef): Int = CC_SHA384_Final(digest, context) + override fun ccUpdate(context: CPointer, data: CValuesRef, dataLength: CC_LONG): Int = + CC_SHA384_Update(context, data, dataLength) } - object SHA512 : CCHashAlgorithm() { + object SHA512 : CCHashAlgorithm() { override val digestSize: Int get() = CC_SHA512_DIGEST_LENGTH - override fun ccHash( - data: CValuesRef, - dataLength: CC_LONG, - digest: CValuesRef, - ): CPointer? = CC_SHA512(data, dataLength, digest) + override fun alloc(): CPointer = nativeHeap.alloc().ptr + override fun ccInit(context: CPointer): Int = CC_SHA512_Init(context) + override fun ccFinal(context: CPointer, digest: CValuesRef): Int = CC_SHA512_Final(digest, context) + override fun ccUpdate(context: CPointer, data: CValuesRef, dataLength: CC_LONG): Int = + CC_SHA512_Update(context, data, dataLength) } - object MD5 : CCHashAlgorithm() { + object MD5 : CCHashAlgorithm() { override val digestSize: Int get() = CC_MD5_DIGEST_LENGTH - override fun ccHash( - data: CValuesRef, - dataLength: CC_LONG, - digest: CValuesRef, - ): CPointer? = CC_MD5(data, dataLength, digest) + override fun alloc(): CPointer = nativeHeap.alloc().ptr + override fun ccInit(context: CPointer): Int = CC_MD5_Init(context) + override fun ccFinal(context: CPointer, digest: CValuesRef): Int = CC_MD5_Final(digest, context) + override fun ccUpdate(context: CPointer, data: CValuesRef, dataLength: CC_LONG): Int = + CC_MD5_Update(context, data, dataLength) } } diff --git a/cryptography-providers/apple/src/commonMain/kotlin/internal/clozy.kt b/cryptography-providers/apple/src/commonMain/kotlin/internal/clozy.kt new file mode 100644 index 00000000..c87e9576 --- /dev/null +++ b/cryptography-providers/apple/src/commonMain/kotlin/internal/clozy.kt @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2024 Oleg Yukhnevich. Use of this source code is governed by the Apache 2.0 license. + */ + +package dev.whyoleg.cryptography.providers.apple.internal + +import kotlin.concurrent.* +import kotlin.experimental.* +import kotlin.native.ref.* + +// TODO: clozy use-cases + +@OptIn(ExperimentalNativeApi::class) +internal abstract class SafeCloseable(closeAction: SafeCloseAction) : AutoCloseable { + private val handler = CloseHandler(closeAction) + private val cleaner = createCleaner(handler, CloseHandler::onClose) + override fun close(): Unit = handler.onClose() +} + +internal interface SafeCloseAction { + fun onClose() +} + +internal inline fun SafeCloseAction(resource: T, crossinline closeAction: (T) -> Unit): SafeCloseAction = + object : SafeCloseAction { + override fun onClose(): Unit = closeAction(resource) + } + +private class CloseHandler(private val closeAction: SafeCloseAction) { + private val executed = AtomicInt(0) + fun onClose() { + if (executed.compareAndSet(0, 1)) closeAction.onClose() + } +} + +internal class Resource( + private var value: T?, + private val recycle: (T) -> Unit, +) : AutoCloseable { + fun access(): T = checkNotNull(value) { "Already closed" } + + override fun close() { + recycle(value ?: return) + value = null + } +} diff --git a/cryptography-providers/apple/src/commonMain/kotlin/internal/utils.kt b/cryptography-providers/apple/src/commonMain/kotlin/internal/utils.kt index bc7fd2ed..285bd362 100644 --- a/cryptography-providers/apple/src/commonMain/kotlin/internal/utils.kt +++ b/cryptography-providers/apple/src/commonMain/kotlin/internal/utils.kt @@ -13,6 +13,13 @@ private val EmptyByteArray = ByteArray(0) private val almostEmptyArray = ByteArray(1) +private val almostEmptyArrayPinned = ByteArray(1).pin() + +internal fun Pinned.safeAddressOf(index: Int): CPointer { + if (index == get().size) return almostEmptyArrayPinned.addressOf(0) + return addressOf(index) +} + //this hack will be dropped with introducing of new IO or functions APIs internal fun ByteArray.fixEmpty(): ByteArray = if (isNotEmpty()) this else almostEmptyArray @@ -78,3 +85,14 @@ internal fun ByteArray.useNSData(block: (NSData) -> R): R { ) } } + +internal fun checkBounds(size: Int, startIndex: Int, endIndex: Int) { + if (startIndex < 0 || endIndex > size) { + throw IndexOutOfBoundsException( + "startIndex ($startIndex) and endIndex ($endIndex) are not within the range [0..size($size))" + ) + } + if (startIndex > endIndex) { + throw IllegalArgumentException("startIndex ($startIndex) > endIndex ($endIndex)") + } +} diff --git a/cryptography-providers/jdk/src/jvmMain/kotlin/algorithms/JdkDigest.kt b/cryptography-providers/jdk/src/jvmMain/kotlin/algorithms/JdkDigest.kt index dc538845..fc3374fd 100644 --- a/cryptography-providers/jdk/src/jvmMain/kotlin/algorithms/JdkDigest.kt +++ b/cryptography-providers/jdk/src/jvmMain/kotlin/algorithms/JdkDigest.kt @@ -6,20 +6,47 @@ package dev.whyoleg.cryptography.providers.jdk.algorithms import dev.whyoleg.cryptography.* import dev.whyoleg.cryptography.algorithms.* +import dev.whyoleg.cryptography.functions.* import dev.whyoleg.cryptography.operations.* import dev.whyoleg.cryptography.providers.jdk.* +import kotlinx.io.bytestring.* +import kotlinx.io.bytestring.unsafe.* internal class JdkDigest( state: JdkCryptographyState, algorithm: String, override val id: CryptographyAlgorithmId, ) : Hasher, Digest { + private val messageDigest = state.messageDigest(algorithm) override fun hasher(): Hasher = this + override fun createHashFunction(): HashFunction = JdkHashFunction(messageDigest.borrowResource()) +} - private val messageDigest = state.messageDigest(algorithm) +private class JdkHashFunction(private val messageDigest: Pooled.Resource) : HashFunction { + override fun update(source: ByteArray, startIndex: Int, endIndex: Int) { + val messageDigest = messageDigest.access() + messageDigest.update(source, startIndex, endIndex - startIndex) + } + + override fun hashInto(destination: ByteArray, destinationOffset: Int): Int { + val messageDigest = messageDigest.access() + val result = messageDigest.digest(destination, destinationOffset, messageDigest.digestLength) + return result + } + + @OptIn(UnsafeByteStringApi::class) + override fun hash(): ByteString { + val messageDigest = messageDigest.access() + val result = UnsafeByteStringOperations.wrapUnsafe(messageDigest.digest()) + return result + } - override fun hashBlocking(data: ByteArray): ByteArray = messageDigest.use { messageDigest -> + override fun reset() { + val messageDigest = messageDigest.access() messageDigest.reset() - messageDigest.digest(data) + } + + override fun close() { + messageDigest.close() } } diff --git a/cryptography-providers/jdk/src/jvmMain/kotlin/pooling.kt b/cryptography-providers/jdk/src/jvmMain/kotlin/pooling.kt index e98111d3..4d28021b 100644 --- a/cryptography-providers/jdk/src/jvmMain/kotlin/pooling.kt +++ b/cryptography-providers/jdk/src/jvmMain/kotlin/pooling.kt @@ -1,27 +1,28 @@ /* - * Copyright (c) 2023 Oleg Yukhnevich. Use of this source code is governed by the Apache 2.0 license. + * Copyright (c) 2023-2024 Oleg Yukhnevich. Use of this source code is governed by the Apache 2.0 license. */ package dev.whyoleg.cryptography.providers.jdk private val maxPooled = Runtime.getRuntime().availableProcessors() + 2 +// TODO: clozy use-case internal sealed class Pooled(protected val instantiate: () -> T) { class Empty(instantiate: () -> T) : Pooled(instantiate) { - override fun get(): T = instantiate() - override fun put(value: T) {} + override fun borrow(): T = instantiate() + override fun recycle(value: T) {} } class Cached(instantiate: () -> T) : Pooled(instantiate) { private val pooled = ArrayDeque() - override fun get(): T { + override fun borrow(): T { return synchronized(this) { pooled.removeLastOrNull() } ?: instantiate() } - override fun put(value: T) { + override fun recycle(value: T) { synchronized(this) { if (pooled.size < maxPooled) { pooled.addLast(value) @@ -30,15 +31,28 @@ internal sealed class Pooled(protected val instantiate: () -> T) { } } - protected abstract fun get(): T - protected abstract fun put(value: T) + protected abstract fun borrow(): T + protected abstract fun recycle(value: T) + + fun borrowResource(): Resource = Resource(this) inline fun use(block: (T) -> R): R { - val instance = get() + val instance = borrow() try { return block(instance) } finally { - put(instance) + recycle(instance) + } + } + + class Resource(private val pooled: Pooled) : AutoCloseable { + private var _value: T? = pooled.borrow() + + fun access(): T = checkNotNull(_value) { "Already closed" } + + override fun close() { + pooled.recycle(_value ?: return) + _value = null } } } diff --git a/cryptography-providers/openssl3/api/src/commonMain/kotlin/algorithms/Openssl3Digest.kt b/cryptography-providers/openssl3/api/src/commonMain/kotlin/algorithms/Openssl3Digest.kt index afeeb7a5..1ceeb576 100644 --- a/cryptography-providers/openssl3/api/src/commonMain/kotlin/algorithms/Openssl3Digest.kt +++ b/cryptography-providers/openssl3/api/src/commonMain/kotlin/algorithms/Openssl3Digest.kt @@ -6,37 +6,69 @@ package dev.whyoleg.cryptography.providers.openssl3.algorithms import dev.whyoleg.cryptography.* import dev.whyoleg.cryptography.algorithms.* +import dev.whyoleg.cryptography.functions.* import dev.whyoleg.cryptography.operations.* import dev.whyoleg.cryptography.providers.openssl3.internal.* import dev.whyoleg.cryptography.providers.openssl3.internal.cinterop.* import kotlinx.cinterop.* -import kotlin.experimental.* -import kotlin.native.ref.* +import kotlinx.io.bytestring.* +import kotlinx.io.bytestring.unsafe.* internal class Openssl3Digest( - algorithm: String, + private val md: CPointer, override val id: CryptographyAlgorithmId, -) : Hasher, Digest { +) : Hasher, Digest, SafeCloseable(SafeCloseAction(md, ::EVP_MD_free)) { + private val digestSize get() = EVP_MD_get_size(md) + + constructor( + algorithm: String, + id: CryptographyAlgorithmId, + ) : this(checkError(EVP_MD_fetch(null, algorithm, null)), id) + override fun hasher(): Hasher = this + override fun createHashFunction(): HashFunction { + return Openssl3HashFunction(Resource(checkError(EVP_MD_CTX_new()), ::EVP_MD_CTX_free)) + } + + // inner class to have a reference to class and so `md` cleaner - so that `md` can be freed at the right time + private inner class Openssl3HashFunction( + private val context: Resource>, + ) : HashFunction, SafeCloseable(SafeCloseAction(context, AutoCloseable::close)) { + init { + // TODO: recheck + reset() + } - private val md = EVP_MD_fetch(null, algorithm, null) + @OptIn(UnsafeNumber::class) + override fun update(source: ByteArray, startIndex: Int, endIndex: Int) { + checkBounds(source.size, startIndex, endIndex) - @OptIn(ExperimentalNativeApi::class) - private val cleaner = createCleaner(md, ::EVP_MD_free) + val context = context.access() + source.usePinned { + checkError(EVP_DigestUpdate(context, it.safeAddressOf(startIndex), (endIndex - startIndex).convert())) + } + } + + override fun hashInto(destination: ByteArray, destinationOffset: Int): Int { + checkBounds(destination.size, destinationOffset, destinationOffset + digestSize) - private val digestSize = EVP_MD_get_size(md) + val context = context.access() + destination.usePinned { + checkError(EVP_DigestFinal(context, it.safeAddressOf(destinationOffset).reinterpret(), null)) + } + reset() + return digestSize // TODO + } + + @OptIn(UnsafeByteStringApi::class) + override fun hash(): ByteString { + val output = ByteArray(digestSize) + hashInto(output) + return UnsafeByteStringOperations.wrapUnsafe(output) + } - override fun hashBlocking(data: ByteArray): ByteArray { - val context = checkError(EVP_MD_CTX_new()) - try { - val digest = ByteArray(digestSize) - checkError(EVP_DigestInit(context, md)) - @OptIn(UnsafeNumber::class) - checkError(EVP_DigestUpdate(context, data.safeRefTo(0), data.size.convert())) - checkError(EVP_DigestFinal(context, digest.refToU(0), null)) - return digest - } finally { - EVP_MD_CTX_free(context) + override fun reset() { + checkError(EVP_DigestInit(context.access(), md)) } } } diff --git a/cryptography-providers/openssl3/api/src/commonMain/kotlin/internal/bytes.kt b/cryptography-providers/openssl3/api/src/commonMain/kotlin/internal/bytes.kt index 105af2f5..f1af42d4 100644 --- a/cryptography-providers/openssl3/api/src/commonMain/kotlin/internal/bytes.kt +++ b/cryptography-providers/openssl3/api/src/commonMain/kotlin/internal/bytes.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2023 Oleg Yukhnevich. Use of this source code is governed by the Apache 2.0 license. + * Copyright (c) 2023-2024 Oleg Yukhnevich. Use of this source code is governed by the Apache 2.0 license. */ package dev.whyoleg.cryptography.providers.openssl3.internal @@ -8,6 +8,11 @@ import kotlinx.cinterop.* private val almostEmptyArray = ByteArray(1).pin() +internal fun Pinned.safeAddressOf(index: Int): CPointer { + if (index == get().size) return almostEmptyArray.addressOf(0) + return addressOf(index) +} + //this hack should be dropped (or not?) with introducing of new IO or functions APIs internal fun ByteArray.safeRefTo(index: Int): CValuesRef { if (index == size) return almostEmptyArray.addressOf(0) @@ -25,3 +30,14 @@ internal fun ByteArray.ensureSizeExactly(expectedSize: Int): ByteArray = when (s expectedSize -> this else -> copyOf(expectedSize) } + +internal fun checkBounds(size: Int, startIndex: Int, endIndex: Int) { + if (startIndex < 0 || endIndex > size) { + throw IndexOutOfBoundsException( + "startIndex ($startIndex) and endIndex ($endIndex) are not within the range [0..size($size))" + ) + } + if (startIndex > endIndex) { + throw IllegalArgumentException("startIndex ($startIndex) > endIndex ($endIndex)") + } +} diff --git a/cryptography-providers/openssl3/api/src/commonMain/kotlin/internal/clozy.kt b/cryptography-providers/openssl3/api/src/commonMain/kotlin/internal/clozy.kt new file mode 100644 index 00000000..dc191ded --- /dev/null +++ b/cryptography-providers/openssl3/api/src/commonMain/kotlin/internal/clozy.kt @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2024 Oleg Yukhnevich. Use of this source code is governed by the Apache 2.0 license. + */ + +package dev.whyoleg.cryptography.providers.openssl3.internal + +import kotlin.concurrent.* +import kotlin.experimental.* +import kotlin.native.ref.* + +// TODO: clozy use-cases + +@OptIn(ExperimentalNativeApi::class) +internal abstract class SafeCloseable(closeAction: SafeCloseAction) : AutoCloseable { + private val handler = CloseHandler(closeAction) + private val cleaner = createCleaner(handler, CloseHandler::onClose) + override fun close(): Unit = handler.onClose() +} + +internal interface SafeCloseAction { + fun onClose() +} + +internal inline fun SafeCloseAction(resource: T, crossinline closeAction: (T) -> Unit): SafeCloseAction = + object : SafeCloseAction { + override fun onClose(): Unit = closeAction(resource) + } + +private class CloseHandler(private val closeAction: SafeCloseAction) { + private val executed = AtomicInt(0) + fun onClose() { + if (executed.compareAndSet(0, 1)) closeAction.onClose() + } +} + +internal class Resource( + private var value: T?, + private val recycle: (T) -> Unit, +) : AutoCloseable { + fun access(): T = checkNotNull(value) { "Already closed" } + + override fun close() { + recycle(value ?: return) + value = null + } +} diff --git a/cryptography-providers/webcrypto/src/commonMain/kotlin/algorithms/WebCryptoDigest.kt b/cryptography-providers/webcrypto/src/commonMain/kotlin/algorithms/WebCryptoDigest.kt index 37340619..51744264 100644 --- a/cryptography-providers/webcrypto/src/commonMain/kotlin/algorithms/WebCryptoDigest.kt +++ b/cryptography-providers/webcrypto/src/commonMain/kotlin/algorithms/WebCryptoDigest.kt @@ -6,8 +6,12 @@ package dev.whyoleg.cryptography.providers.webcrypto.algorithms import dev.whyoleg.cryptography.* import dev.whyoleg.cryptography.algorithms.* +import dev.whyoleg.cryptography.functions.* import dev.whyoleg.cryptography.operations.* import dev.whyoleg.cryptography.providers.webcrypto.internal.* +import kotlinx.io.* +import kotlinx.io.bytestring.* +import kotlinx.io.bytestring.unsafe.* internal class WebCryptoDigest private constructor( private val algorithm: String, @@ -22,9 +26,18 @@ internal class WebCryptoDigest private constructor( override fun hasher(): Hasher = this - override suspend fun hash(data: ByteArray): ByteArray { - return WebCrypto.digest(algorithm, data) + override suspend fun hash(data: ByteArray, startIndex: Int, endIndex: Int): ByteArray { + return WebCrypto.digest(algorithm, data.copyOfRange(startIndex, endIndex)) } - override fun hashBlocking(data: ByteArray): ByteArray = nonBlocking() + @OptIn(UnsafeByteStringApi::class) + override suspend fun hash(data: RawSource): ByteString { + return UnsafeByteStringOperations.wrapUnsafe( + WebCrypto.digest(algorithm, data.buffered().readByteArray()) + ) + } + + override fun createHashFunction(): HashFunction = nonBlocking() + override fun hashBlocking(data: ByteArray, startIndex: Int, endIndex: Int): ByteArray = nonBlocking() + override fun hashBlocking(data: RawSource): ByteString = nonBlocking() }