Skip to content

Commit 8d285ce

Browse files
committed
Support incremental hashing via HashFunction
1 parent 2459eb1 commit 8d285ce

File tree

18 files changed

+567
-88
lines changed

18 files changed

+567
-88
lines changed

cryptography-core/api/cryptography-core.api

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -647,6 +647,23 @@ public final class dev/whyoleg/cryptography/algorithms/symmetric/SymmetricKeySiz
647647
public final fun getB256-5xWg6fk ()I
648648
}
649649

650+
public abstract interface class dev/whyoleg/cryptography/functions/HashFunction : dev/whyoleg/cryptography/functions/UpdateFunction {
651+
public fun hash ()Lkotlinx/io/bytestring/ByteString;
652+
public abstract fun hashIntoByteArray ([BI)I
653+
public static synthetic fun hashIntoByteArray$default (Ldev/whyoleg/cryptography/functions/HashFunction;[BIILjava/lang/Object;)I
654+
public abstract fun hashToByteArray ()[B
655+
public abstract fun reset ()V
656+
}
657+
658+
public abstract interface class dev/whyoleg/cryptography/functions/UpdateFunction : java/lang/AutoCloseable {
659+
public fun update (Lkotlinx/io/bytestring/ByteString;II)V
660+
public abstract fun update ([BII)V
661+
public static synthetic fun update$default (Ldev/whyoleg/cryptography/functions/UpdateFunction;Lkotlinx/io/bytestring/ByteString;IIILjava/lang/Object;)V
662+
public static synthetic fun update$default (Ldev/whyoleg/cryptography/functions/UpdateFunction;[BIIILjava/lang/Object;)V
663+
public fun updatingSink (Lkotlinx/io/RawSink;)Lkotlinx/io/RawSink;
664+
public fun updatingSource (Lkotlinx/io/RawSource;)Lkotlinx/io/RawSource;
665+
}
666+
650667
public abstract interface class dev/whyoleg/cryptography/materials/key/EncodableKey : dev/whyoleg/cryptography/materials/key/Key {
651668
public fun encodeTo (Ldev/whyoleg/cryptography/materials/key/KeyFormat;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
652669
public static synthetic fun encodeTo$suspendImpl (Ldev/whyoleg/cryptography/materials/key/EncodableKey;Ldev/whyoleg/cryptography/materials/key/KeyFormat;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
@@ -740,12 +757,16 @@ public abstract interface class dev/whyoleg/cryptography/operations/Encryptor {
740757
}
741758

742759
public abstract interface class dev/whyoleg/cryptography/operations/Hasher {
760+
public abstract fun createHashFunction ()Ldev/whyoleg/cryptography/functions/HashFunction;
761+
public fun hash (Lkotlinx/io/RawSource;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
743762
public fun hash (Lkotlinx/io/bytestring/ByteString;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
744763
public fun hash ([BLkotlin/coroutines/Continuation;)Ljava/lang/Object;
764+
public static synthetic fun hash$suspendImpl (Ldev/whyoleg/cryptography/operations/Hasher;Lkotlinx/io/RawSource;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
745765
public static synthetic fun hash$suspendImpl (Ldev/whyoleg/cryptography/operations/Hasher;Lkotlinx/io/bytestring/ByteString;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
746766
public static synthetic fun hash$suspendImpl (Ldev/whyoleg/cryptography/operations/Hasher;[BLkotlin/coroutines/Continuation;)Ljava/lang/Object;
767+
public fun hashBlocking (Lkotlinx/io/RawSource;)Lkotlinx/io/bytestring/ByteString;
747768
public fun hashBlocking (Lkotlinx/io/bytestring/ByteString;)Lkotlinx/io/bytestring/ByteString;
748-
public abstract fun hashBlocking ([B)[B
769+
public fun hashBlocking ([B)[B
749770
}
750771

751772
public abstract interface class dev/whyoleg/cryptography/operations/SecretDerivation {

cryptography-core/api/cryptography-core.klib.api

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -532,6 +532,20 @@ abstract interface dev.whyoleg.cryptography.algorithms/PBKDF2 : dev.whyoleg.cryp
532532
final object Companion : dev.whyoleg.cryptography/CryptographyAlgorithmId<dev.whyoleg.cryptography.algorithms/PBKDF2> // dev.whyoleg.cryptography.algorithms/PBKDF2.Companion|null[0]
533533
}
534534

535+
abstract interface dev.whyoleg.cryptography.functions/HashFunction : dev.whyoleg.cryptography.functions/UpdateFunction { // dev.whyoleg.cryptography.functions/HashFunction|null[0]
536+
abstract fun hashIntoByteArray(kotlin/ByteArray, kotlin/Int = ...): kotlin/Int // dev.whyoleg.cryptography.functions/HashFunction.hashIntoByteArray|hashIntoByteArray(kotlin.ByteArray;kotlin.Int){}[0]
537+
abstract fun hashToByteArray(): kotlin/ByteArray // dev.whyoleg.cryptography.functions/HashFunction.hashToByteArray|hashToByteArray(){}[0]
538+
abstract fun reset() // dev.whyoleg.cryptography.functions/HashFunction.reset|reset(){}[0]
539+
open fun hash(): kotlinx.io.bytestring/ByteString // dev.whyoleg.cryptography.functions/HashFunction.hash|hash(){}[0]
540+
}
541+
542+
abstract interface dev.whyoleg.cryptography.functions/UpdateFunction : kotlin/AutoCloseable { // dev.whyoleg.cryptography.functions/UpdateFunction|null[0]
543+
abstract fun update(kotlin/ByteArray, kotlin/Int = ..., kotlin/Int = ...) // dev.whyoleg.cryptography.functions/UpdateFunction.update|update(kotlin.ByteArray;kotlin.Int;kotlin.Int){}[0]
544+
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]
545+
open fun updatingSink(kotlinx.io/RawSink): kotlinx.io/RawSink // dev.whyoleg.cryptography.functions/UpdateFunction.updatingSink|updatingSink(kotlinx.io.RawSink){}[0]
546+
open fun updatingSource(kotlinx.io/RawSource): kotlinx.io/RawSource // dev.whyoleg.cryptography.functions/UpdateFunction.updatingSource|updatingSource(kotlinx.io.RawSource){}[0]
547+
}
548+
535549
abstract interface dev.whyoleg.cryptography.materials.key/Key // dev.whyoleg.cryptography.materials.key/Key|null[0]
536550

537551
abstract interface dev.whyoleg.cryptography.materials.key/KeyFormat { // dev.whyoleg.cryptography.materials.key/KeyFormat|null[0]
@@ -582,10 +596,13 @@ abstract interface dev.whyoleg.cryptography.operations/Encryptor { // dev.whyole
582596
}
583597

584598
abstract interface dev.whyoleg.cryptography.operations/Hasher { // dev.whyoleg.cryptography.operations/Hasher|null[0]
585-
abstract fun hashBlocking(kotlin/ByteArray): kotlin/ByteArray // dev.whyoleg.cryptography.operations/Hasher.hashBlocking|hashBlocking(kotlin.ByteArray){}[0]
599+
abstract fun createHashFunction(): dev.whyoleg.cryptography.functions/HashFunction // dev.whyoleg.cryptography.operations/Hasher.createHashFunction|createHashFunction(){}[0]
600+
open fun hashBlocking(kotlin/ByteArray): kotlin/ByteArray // dev.whyoleg.cryptography.operations/Hasher.hashBlocking|hashBlocking(kotlin.ByteArray){}[0]
586601
open fun hashBlocking(kotlinx.io.bytestring/ByteString): kotlinx.io.bytestring/ByteString // dev.whyoleg.cryptography.operations/Hasher.hashBlocking|hashBlocking(kotlinx.io.bytestring.ByteString){}[0]
602+
open fun hashBlocking(kotlinx.io/RawSource): kotlinx.io.bytestring/ByteString // dev.whyoleg.cryptography.operations/Hasher.hashBlocking|hashBlocking(kotlinx.io.RawSource){}[0]
587603
open suspend fun hash(kotlin/ByteArray): kotlin/ByteArray // dev.whyoleg.cryptography.operations/Hasher.hash|hash(kotlin.ByteArray){}[0]
588604
open suspend fun hash(kotlinx.io.bytestring/ByteString): kotlinx.io.bytestring/ByteString // dev.whyoleg.cryptography.operations/Hasher.hash|hash(kotlinx.io.bytestring.ByteString){}[0]
605+
open suspend fun hash(kotlinx.io/RawSource): kotlinx.io.bytestring/ByteString // dev.whyoleg.cryptography.operations/Hasher.hash|hash(kotlinx.io.RawSource){}[0]
589606
}
590607

591608
abstract interface dev.whyoleg.cryptography.operations/SecretDerivation { // dev.whyoleg.cryptography.operations/SecretDerivation|null[0]
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
/*
2+
* Copyright (c) 2024 Oleg Yukhnevich. Use of this source code is governed by the Apache 2.0 license.
3+
*/
4+
5+
package dev.whyoleg.cryptography.functions
6+
7+
import dev.whyoleg.cryptography.*
8+
import kotlinx.io.bytestring.*
9+
10+
public interface HashFunction : UpdateFunction {
11+
public fun hashIntoByteArray(destination: ByteArray, destinationOffset: Int = 0): Int
12+
public fun hashToByteArray(): ByteArray
13+
public fun hash(): ByteString = hashToByteArray().asByteString()
14+
public fun reset()
15+
}
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
/*
2+
* Copyright (c) 2024 Oleg Yukhnevich. Use of this source code is governed by the Apache 2.0 license.
3+
*/
4+
5+
package dev.whyoleg.cryptography.functions
6+
7+
import dev.whyoleg.cryptography.*
8+
import kotlinx.io.*
9+
import kotlinx.io.bytestring.*
10+
import kotlinx.io.unsafe.*
11+
12+
public interface UpdateFunction : AutoCloseable {
13+
public fun update(source: ByteArray, startIndex: Int = 0, endIndex: Int = source.size)
14+
public fun update(source: ByteString, startIndex: Int = 0, endIndex: Int = source.size) {
15+
update(source.asByteArray(), startIndex, endIndex)
16+
}
17+
18+
public fun updatingSource(source: RawSource): RawSource = UpdatingSource(this, source)
19+
public fun updatingSink(sink: RawSink): RawSink = UpdatingSink(this, sink)
20+
}
21+
22+
private class UpdatingSource(
23+
private val function: UpdateFunction,
24+
private val source: RawSource,
25+
) : RawSource {
26+
override fun readAtMostTo(sink: Buffer, byteCount: Long): Long {
27+
val result = source.readAtMostTo(sink, byteCount)
28+
if (result != -1L) {
29+
@OptIn(UnsafeIoApi::class)
30+
UnsafeBufferOperations.iterate(sink, sink.size - result) { context, head, _ ->
31+
var segment = head
32+
while (segment != null) {
33+
context.withData(segment, function::update)
34+
segment = context.next(segment)
35+
}
36+
}
37+
}
38+
return result
39+
}
40+
41+
override fun close(): Unit = source.close()
42+
}
43+
44+
private class UpdatingSink(
45+
private val function: UpdateFunction,
46+
private val sink: RawSink,
47+
) : RawSink {
48+
override fun write(source: Buffer, byteCount: Long) {
49+
source.require(byteCount)
50+
51+
@OptIn(UnsafeIoApi::class)
52+
UnsafeBufferOperations.iterate(source) { context, head ->
53+
var consumedCount = 0L
54+
var segment = head
55+
while (segment != null && consumedCount < byteCount) {
56+
context.withData(segment) { bytes, startIndex, endIndex ->
57+
val toUpdate = minOf(byteCount - consumedCount, (endIndex - startIndex).toLong()).toInt()
58+
function.update(bytes, startIndex, startIndex + toUpdate)
59+
consumedCount += toUpdate
60+
}
61+
segment = context.next(segment)
62+
}
63+
}
64+
65+
sink.write(source, byteCount)
66+
}
67+
68+
override fun flush(): Unit = sink.flush()
69+
override fun close(): Unit = sink.close()
70+
}

cryptography-core/src/commonMain/kotlin/operations/Hasher.kt

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,29 @@
55
package dev.whyoleg.cryptography.operations
66

77
import dev.whyoleg.cryptography.*
8+
import dev.whyoleg.cryptography.functions.*
9+
import kotlinx.io.*
810
import kotlinx.io.bytestring.*
911

1012
@SubclassOptInRequired(CryptographyProviderApi::class)
1113
public interface Hasher {
14+
public fun createHashFunction(): HashFunction
15+
1216
public suspend fun hash(data: ByteArray): ByteArray = hashBlocking(data)
13-
public fun hashBlocking(data: ByteArray): ByteArray
1417

1518
public suspend fun hash(data: ByteString): ByteString = hash(data.asByteArray()).asByteString()
19+
20+
public suspend fun hash(data: RawSource): ByteString = hashBlocking(data)
21+
22+
public fun hashBlocking(data: ByteArray): ByteArray = createHashFunction().use {
23+
it.update(data)
24+
it.hashToByteArray()
25+
}
26+
1627
public fun hashBlocking(data: ByteString): ByteString = hashBlocking(data.asByteArray()).asByteString()
28+
29+
public fun hashBlocking(data: RawSource): ByteString = createHashFunction().use {
30+
it.updatingSource(data).buffered().transferTo(discardingSink())
31+
it.hash()
32+
}
1733
}

cryptography-providers-tests-api/src/commonMain/kotlin/support.kt

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,13 @@ import dev.whyoleg.cryptography.serialization.asn1.modules.*
1212
import dev.whyoleg.cryptography.serialization.pem.*
1313
import kotlinx.io.bytestring.*
1414

15+
fun AlgorithmTestScope<*>.supportsFunctions() = supports {
16+
when {
17+
provider.isWebCrypto -> "Incremental functions"
18+
else -> null
19+
}
20+
}
21+
1522
fun AlgorithmTestScope<*>.supportsDigest(digest: CryptographyAlgorithmId<Digest>): Boolean = supports {
1623
val sha3Algorithms = setOf(SHA3_224, SHA3_256, SHA3_384, SHA3_512)
1724
when {

cryptography-providers-tests/src/commonMain/kotlin/default/DigestTest.kt

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ import dev.whyoleg.cryptography.*
88
import dev.whyoleg.cryptography.algorithms.*
99
import dev.whyoleg.cryptography.providers.tests.api.*
1010
import dev.whyoleg.cryptography.random.*
11+
import kotlinx.io.*
12+
import kotlinx.io.bytestring.*
1113
import kotlin.math.*
1214
import kotlin.test.*
1315

@@ -57,4 +59,80 @@ abstract class DigestTest(provider: CryptographyProvider) : ProviderTest(provide
5759

5860
@Test
5961
fun testSHA3_512() = test(SHA3_512, 64)
62+
63+
@Test
64+
fun testFunctionIndexes() = testAlgorithm(SHA256) {
65+
if (!supportsFunctions()) return@testAlgorithm
66+
67+
val hashFunction = algorithm.hasher().createHashFunction()
68+
val array = ByteArray(10)
69+
70+
assertFails { hashFunction.update(array, -1, 10) }
71+
assertFails { hashFunction.update(array, 0, -1) }
72+
assertFails { hashFunction.update(array, 20, 10) }
73+
assertFails { hashFunction.update(array, 0, 20) }
74+
75+
hashFunction.update(array)
76+
}
77+
78+
@Test
79+
fun testFunctionChunked() = testAlgorithm(SHA256) {
80+
if (!supportsFunctions()) return@testAlgorithm
81+
82+
val hasher = algorithm.hasher()
83+
val bytes = ByteString(CryptographyRandom.nextBytes(10000))
84+
85+
val digest = hasher.hash(bytes)
86+
hasher.createHashFunction().use { function ->
87+
repeat(10) {
88+
function.update(bytes, it * 1000, (it + 1) * 1000)
89+
}
90+
assertContentEquals(digest, function.hash())
91+
}
92+
}
93+
94+
@Test
95+
fun testFunctionReuse() = testAlgorithm(SHA256) {
96+
if (!supportsFunctions()) return@testAlgorithm
97+
98+
val hasher = algorithm.hasher()
99+
val bytes1 = ByteString(CryptographyRandom.nextBytes(10000))
100+
val bytes2 = ByteString(CryptographyRandom.nextBytes(10000))
101+
102+
val digest1 = hasher.hash(bytes1)
103+
val digest2 = hasher.hash(bytes2)
104+
hasher.createHashFunction().use { function ->
105+
function.update(bytes1)
106+
assertContentEquals(digest1, function.hash())
107+
108+
function.update(bytes2)
109+
assertContentEquals(digest2, function.hash())
110+
111+
// update and then discard
112+
function.update(bytes1)
113+
function.update(bytes1)
114+
function.reset()
115+
// update after reset
116+
function.update(bytes1)
117+
assertContentEquals(digest1, function.hash())
118+
}
119+
}
120+
121+
@Test
122+
fun testFunctionSource() = testAlgorithm(SHA256) {
123+
val hasher = algorithm.hasher()
124+
125+
val bytes = ByteString(CryptographyRandom.nextBytes(10000))
126+
val source = Buffer()
127+
source.write(bytes)
128+
val digest = hasher.hash(bytes)
129+
130+
assertContentEquals(digest, hasher.hash(source.copy()))
131+
132+
if (!supportsFunctions()) return@testAlgorithm
133+
hasher.createHashFunction().use { function ->
134+
assertContentEquals(bytes, function.updatingSource(source).buffered().readByteString())
135+
assertContentEquals(digest, function.hash())
136+
}
137+
}
60138
}

cryptography-providers/apple/src/commonMain/kotlin/algorithms/CCDigest.kt

Lines changed: 44 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -6,24 +6,58 @@ package dev.whyoleg.cryptography.providers.apple.algorithms
66

77
import dev.whyoleg.cryptography.*
88
import dev.whyoleg.cryptography.algorithms.*
9+
import dev.whyoleg.cryptography.functions.*
910
import dev.whyoleg.cryptography.operations.*
1011
import dev.whyoleg.cryptography.providers.apple.internal.*
1112
import kotlinx.cinterop.*
1213

13-
internal class CCDigest(
14-
private val hashAlgorithm: CCHashAlgorithm,
14+
internal class CCDigest<CTX : CPointed>(
15+
private val hashAlgorithm: CCHashAlgorithm<CTX>,
1516
override val id: CryptographyAlgorithmId<Digest>,
1617
) : Hasher, Digest {
1718
override fun hasher(): Hasher = this
19+
override fun createHashFunction(): HashFunction = CCHashFunction(
20+
algorithm = hashAlgorithm,
21+
context = Resource(hashAlgorithm.alloc(), nativeHeap::free)
22+
)
23+
}
24+
25+
private class CCHashFunction<CTX : CPointed>(
26+
private val algorithm: CCHashAlgorithm<CTX>,
27+
private val context: Resource<CPointer<CTX>>,
28+
) : HashFunction, SafeCloseable(SafeCloseAction(context, AutoCloseable::close)) {
29+
init {
30+
reset()
31+
}
32+
33+
override fun update(source: ByteArray, startIndex: Int, endIndex: Int) {
34+
checkBounds(source.size, startIndex, endIndex)
35+
36+
val context = context.access()
37+
source.usePinned {
38+
check(algorithm.ccUpdate(context, it.safeAddressOf(startIndex), (endIndex - startIndex).convert()) > 0)
39+
}
40+
}
1841

19-
@OptIn(ExperimentalUnsignedTypes::class)
20-
override fun hashBlocking(data: ByteArray): ByteArray {
21-
val output = ByteArray(hashAlgorithm.digestSize)
22-
hashAlgorithm.ccHash(
23-
data = data.fixEmpty().refTo(0),
24-
dataLength = data.size.convert(),
25-
digest = output.asUByteArray().refTo(0)
26-
)
42+
override fun hashIntoByteArray(destination: ByteArray, destinationOffset: Int): Int {
43+
checkBounds(destination.size, destinationOffset, destinationOffset + algorithm.digestSize)
44+
45+
val context = context.access()
46+
destination.usePinned {
47+
check(algorithm.ccFinal(context, it.safeAddressOf(destinationOffset).reinterpret()) > 0)
48+
}
49+
reset()
50+
return algorithm.digestSize
51+
}
52+
53+
override fun hashToByteArray(): ByteArray {
54+
val output = ByteArray(algorithm.digestSize)
55+
hashIntoByteArray(output)
2756
return output
2857
}
58+
59+
override fun reset() {
60+
val context = context.access()
61+
check(algorithm.ccInit(context) > 0)
62+
}
2963
}

0 commit comments

Comments
 (0)