Skip to content

Commit

Permalink
Support incremental hashing via HashFunction
Browse files Browse the repository at this point in the history
  • Loading branch information
whyoleg committed Sep 9, 2024
1 parent 297f9dc commit c2684c6
Show file tree
Hide file tree
Showing 18 changed files with 577 additions and 89 deletions.
24 changes: 23 additions & 1 deletion cryptography-core/api/cryptography-core.api
Original file line number Diff line number Diff line change
Expand Up @@ -618,6 +618,24 @@ 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 getDigestSize ()I
public fun hash ()Lkotlinx/io/bytestring/ByteString;
public abstract fun hashIntoByteArray ([BI)I
public static synthetic fun hashIntoByteArray$default (Ldev/whyoleg/cryptography/functions/HashFunction;[BIILjava/lang/Object;)I
public fun hashToByteArray ()[B
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;
Expand Down Expand Up @@ -711,12 +729,16 @@ public abstract interface class dev/whyoleg/cryptography/operations/Encryptor {
}

public abstract interface class dev/whyoleg/cryptography/operations/Hasher {
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;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/RawSource;Lkotlin/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/RawSource;)Lkotlinx/io/bytestring/ByteString;
public fun hashBlocking (Lkotlinx/io/bytestring/ByteString;)Lkotlinx/io/bytestring/ByteString;
public abstract fun hashBlocking ([B)[B
public fun hashBlocking ([B)[B
}

public abstract interface class dev/whyoleg/cryptography/operations/SecretDerivation {
Expand Down
22 changes: 21 additions & 1 deletion cryptography-core/api/cryptography-core.klib.api
Original file line number Diff line number Diff line change
Expand Up @@ -516,6 +516,23 @@ abstract interface dev.whyoleg.cryptography.algorithms/PBKDF2 : dev.whyoleg.cryp
final object Companion : dev.whyoleg.cryptography/CryptographyAlgorithmId<dev.whyoleg.cryptography.algorithms/PBKDF2> // 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 val digestSize // dev.whyoleg.cryptography.functions/HashFunction.digestSize|{}digestSize[0]
abstract fun <get-digestSize>(): kotlin/Int // dev.whyoleg.cryptography.functions/HashFunction.digestSize.<get-digestSize>|<get-digestSize>(){}[0]

abstract fun hashIntoByteArray(kotlin/ByteArray, kotlin/Int = ...): kotlin/Int // dev.whyoleg.cryptography.functions/HashFunction.hashIntoByteArray|hashIntoByteArray(kotlin.ByteArray;kotlin.Int){}[0]
abstract fun reset() // dev.whyoleg.cryptography.functions/HashFunction.reset|reset(){}[0]
open fun hash(): kotlinx.io.bytestring/ByteString // dev.whyoleg.cryptography.functions/HashFunction.hash|hash(){}[0]
open fun hashToByteArray(): kotlin/ByteArray // dev.whyoleg.cryptography.functions/HashFunction.hashToByteArray|hashToByteArray(){}[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]
Expand Down Expand Up @@ -566,10 +583,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]
abstract fun createHashFunction(): dev.whyoleg.cryptography.functions/HashFunction // dev.whyoleg.cryptography.operations/Hasher.createHashFunction|createHashFunction(){}[0]
open 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 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/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]
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]
Expand Down
23 changes: 23 additions & 0 deletions cryptography-core/src/commonMain/kotlin/functions/HashFunction.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
/*
* 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.bytestring.*

public interface HashFunction : UpdateFunction {
public val digestSize: Int

public fun hashIntoByteArray(destination: ByteArray, destinationOffset: Int = 0): Int
public fun hashToByteArray(): ByteArray {
val output = ByteArray(digestSize)
hashIntoByteArray(output)
return output
}

public fun hash(): ByteString = hashToByteArray().asByteString()

public fun reset()
}
Original file line number Diff line number Diff line change
@@ -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()
}
18 changes: 17 additions & 1 deletion cryptography-core/src/commonMain/kotlin/operations/Hasher.kt
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,29 @@
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 fun createHashFunction(): HashFunction

public suspend fun hash(data: ByteArray): ByteArray = hashBlocking(data)
public fun hashBlocking(data: ByteArray): ByteArray

public suspend fun hash(data: ByteString): ByteString = hash(data.asByteArray()).asByteString()

public suspend fun hash(data: RawSource): ByteString = hashBlocking(data)

public fun hashBlocking(data: ByteArray): ByteArray = createHashFunction().use {
it.update(data)
it.hashToByteArray()
}

public fun hashBlocking(data: ByteString): ByteString = hashBlocking(data.asByteArray()).asByteString()

public fun hashBlocking(data: RawSource): ByteString = createHashFunction().use {
it.updatingSource(data).buffered().transferTo(discardingSink())
it.hash()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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<Digest>): Boolean = supports {
val sha3Algorithms = setOf(SHA3_224, SHA3_256, SHA3_384, SHA3_512)
when {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.*

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

@Test
fun testSHA3_512() = test(SHA3_512, 64)

@Test
fun testFunctionIndexes() = testAlgorithm(SHA256) {
if (!supportsFunctions()) return@testAlgorithm

val hashFunction = algorithm.hasher().createHashFunction()
val array = ByteArray(10)

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())
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,24 +6,55 @@ 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.*

internal class CCDigest(
private val hashAlgorithm: CCHashAlgorithm,
internal class CCDigest<CTX : CPointed>(
private val hashAlgorithm: CCHashAlgorithm<CTX>,
override val id: CryptographyAlgorithmId<Digest>,
) : Hasher, Digest {
override fun hasher(): Hasher = this
override fun createHashFunction(): HashFunction = CCHashFunction(
algorithm = hashAlgorithm,
context = Resource(hashAlgorithm.alloc(), nativeHeap::free)
)
}

private class CCHashFunction<CTX : CPointed>(
private val algorithm: CCHashAlgorithm<CTX>,
private val context: Resource<CPointer<CTX>>,
) : HashFunction, SafeCloseable(SafeCloseAction(context, AutoCloseable::close)) {
init {
reset()
}

override val digestSize: Int
get() = algorithm.digestSize

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 hashIntoByteArray(destination: ByteArray, destinationOffset: Int): Int {
checkBounds(destination.size, destinationOffset, destinationOffset + digestSize)

val context = context.access()
destination.usePinned {
check(algorithm.ccFinal(context, it.safeAddressOf(destinationOffset).reinterpret()) > 0)
}
reset()
return digestSize
}

@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
override fun reset() {
val context = context.access()
check(algorithm.ccInit(context) > 0)
}
}
Loading

0 comments on commit c2684c6

Please sign in to comment.