Skip to content

Add Digest.digestInto functionality #108

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Feb 4, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions library/digest/api/digest.api
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ public abstract class org/kotlincrypto/core/digest/Digest : java/security/Messag
public final fun digest ()[B
public final fun digest ([B)[B
public final fun digest ([BII)I
public final fun digestInto ([BI)I
protected fun digestIntoProtected ([BI[BI)V
public final fun digestLength ()I
protected abstract fun digestProtected ([BI)[B
protected final fun engineDigest ()[B
Expand Down
2 changes: 2 additions & 0 deletions library/digest/api/digest.klib.api
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ abstract class org.kotlincrypto.core.digest/Digest : org.kotlincrypto.core/Algor
final fun blockSize(): kotlin/Int // org.kotlincrypto.core.digest/Digest.blockSize|blockSize(){}[0]
final fun digest(): kotlin/ByteArray // org.kotlincrypto.core.digest/Digest.digest|digest(){}[0]
final fun digest(kotlin/ByteArray): kotlin/ByteArray // org.kotlincrypto.core.digest/Digest.digest|digest(kotlin.ByteArray){}[0]
final fun digestInto(kotlin/ByteArray, kotlin/Int): kotlin/Int // org.kotlincrypto.core.digest/Digest.digestInto|digestInto(kotlin.ByteArray;kotlin.Int){}[0]
final fun digestLength(): kotlin/Int // org.kotlincrypto.core.digest/Digest.digestLength|digestLength(){}[0]
final fun equals(kotlin/Any?): kotlin/Boolean // org.kotlincrypto.core.digest/Digest.equals|equals(kotlin.Any?){}[0]
final fun hashCode(): kotlin/Int // org.kotlincrypto.core.digest/Digest.hashCode|hashCode(){}[0]
Expand All @@ -25,6 +26,7 @@ abstract class org.kotlincrypto.core.digest/Digest : org.kotlincrypto.core/Algor
final fun update(kotlin/Byte) // org.kotlincrypto.core.digest/Digest.update|update(kotlin.Byte){}[0]
final fun update(kotlin/ByteArray) // org.kotlincrypto.core.digest/Digest.update|update(kotlin.ByteArray){}[0]
final fun update(kotlin/ByteArray, kotlin/Int, kotlin/Int) // org.kotlincrypto.core.digest/Digest.update|update(kotlin.ByteArray;kotlin.Int;kotlin.Int){}[0]
open fun digestIntoProtected(kotlin/ByteArray, kotlin/Int, kotlin/ByteArray, kotlin/Int) // org.kotlincrypto.core.digest/Digest.digestIntoProtected|digestIntoProtected(kotlin.ByteArray;kotlin.Int;kotlin.ByteArray;kotlin.Int){}[0]
open fun updateProtected(kotlin/Byte) // org.kotlincrypto.core.digest/Digest.updateProtected|updateProtected(kotlin.Byte){}[0]
open fun updateProtected(kotlin/ByteArray, kotlin/Int, kotlin/Int) // org.kotlincrypto.core.digest/Digest.updateProtected|updateProtected(kotlin.ByteArray;kotlin.Int;kotlin.Int){}[0]
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -101,12 +101,24 @@ public expect abstract class Digest: Algorithm, Copyable<Digest>, Resettable, Up
public fun digest(): ByteArray

/**
* Updates the instance with provided [input], then completes the computation,
* Updates the instance with provided [input] then completes the computation,
* performing final operations and returning the resultant array of bytes. The
* [Digest] is [reset] afterward.
* */
public fun digest(input: ByteArray): ByteArray

/**
* Completes the computation, performing final operations and placing the
* resultant bytes into the provided [dest] array starting at index [destOffset].
* The [Digest] is [reset] afterward.
*
* @return The number of bytes put into [dest] (i.e. the [digestLength])
* @throws [IndexOutOfBoundsException] if [destOffset] is inappropriate
* @throws [ShortBufferException] if [digestLength] number of bytes are unable
* to fit into [dest] for provided [destOffset]
* */
public fun digestInto(dest: ByteArray, destOffset: Int): Int

// See Resettable interface documentation
public final override fun reset()

Expand All @@ -121,14 +133,34 @@ public expect abstract class Digest: Algorithm, Copyable<Digest>, Resettable, Up
* Called to complete the computation, providing any input that may be buffered
* and awaiting processing.
*
* **NOTE:** The buffer from [bufPos] to the end will always be zeroized to clear
* **NOTE:** The buffer from [bufPos] to the end will always be zeroed out to clear
* any potentially stale input left over from a previous state.
*
* @param [buf] Unprocessed input
* @param [bufPos] The index at which the **next** input would be placed into [buf]
* */
protected abstract fun digestProtected(buf: ByteArray, bufPos: Int): ByteArray

/**
* Called to complete the computation, providing any input that may be buffered
* and awaiting processing.
*
* Implementations should override this addition to the API for performance reasons.
* If overridden, `super.digestIntoProtected` should **not** be called.
*
* **NOTE:** The buffer from [bufPos] to the end will always be zeroed out to clear
* any potentially stale input left over from a previous state.
*
* **NOTE:** The public [digestInto] function always checks [dest] for capacity of
* [digestLength], starting at [destOffset], before calling this function.
*
* @param [dest] The array to place resultant bytes
* @param [destOffset] The index to begin placing bytes into [dest]
* @param [buf] Unprocessed input
* @param [bufPos] The index at which the **next** input would be placed into [buf]
* */
protected open fun digestIntoProtected(dest: ByteArray, destOffset: Int, buf: ByteArray, bufPos: Int)

/**
* Optional override for implementations to intercept cleansed input before
* being processed by the [Digest] abstraction.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,11 @@

package org.kotlincrypto.core.digest.internal

import org.kotlincrypto.core.ShortBufferException
import org.kotlincrypto.core.digest.Digest
import kotlin.contracts.ExperimentalContracts
import kotlin.contracts.InvocationKind
import kotlin.contracts.contract
import kotlin.jvm.JvmInline

@JvmInline
Expand All @@ -39,12 +43,18 @@ internal inline fun Digest.initializeBuffer(

internal inline fun Buffer.copy(): Buffer = Buffer(value.copyOf())

@OptIn(ExperimentalContracts::class)
internal inline fun Buffer.commonUpdate(
input: Byte,
bufPosPlusPlus: Int,
bufPosSet: (zero: Int) -> Unit,
compressProtected: (ByteArray, Int) -> Unit,
) {
contract {
callsInPlace(bufPosSet, InvocationKind.AT_MOST_ONCE)
callsInPlace(compressProtected, InvocationKind.AT_MOST_ONCE)
}

val buf = value
buf[bufPosPlusPlus] = input

Expand All @@ -54,6 +64,7 @@ internal inline fun Buffer.commonUpdate(
bufPosSet(0)
}

@OptIn(ExperimentalContracts::class)
internal inline fun Buffer.commonUpdate(
input: ByteArray,
offset: Int,
Expand All @@ -62,6 +73,11 @@ internal inline fun Buffer.commonUpdate(
bufPosSet: (value: Int) -> Unit,
compressProtected: (ByteArray, Int) -> Unit,
) {
contract {
callsInPlace(bufPosSet, InvocationKind.EXACTLY_ONCE)
callsInPlace(compressProtected, InvocationKind.UNKNOWN)
}

val buf = value
val blockSize = buf.size
val limitInput = offset + len
Expand Down Expand Up @@ -105,6 +121,7 @@ internal inline fun Buffer.commonUpdate(
bufPosSet(posBuf)
}

@OptIn(ExperimentalContracts::class)
internal inline fun Buffer.commonDigest(
input: ByteArray,
updateProtected: (ByteArray, Int, Int) -> Unit,
Expand All @@ -113,27 +130,76 @@ internal inline fun Buffer.commonDigest(
resetProtected: () -> Unit,
bufPosSet: (zero: Int) -> Unit,
): ByteArray {
contract {
callsInPlace(updateProtected, InvocationKind.EXACTLY_ONCE)
callsInPlace(bufPosGet, InvocationKind.EXACTLY_ONCE)
callsInPlace(digestProtected, InvocationKind.EXACTLY_ONCE)
callsInPlace(resetProtected, InvocationKind.EXACTLY_ONCE)
callsInPlace(bufPosSet, InvocationKind.EXACTLY_ONCE)
}

updateProtected(input, 0, input.size)
return commonDigest(bufPosGet(), digestProtected, resetProtected, bufPosSet)
}

@OptIn(ExperimentalContracts::class)
internal inline fun Buffer.commonDigest(
bufPos: Int,
digestProtected: (buf: ByteArray, bufPos: Int) -> ByteArray,
resetProtected: () -> Unit,
bufPosSet: (zero: Int) -> Unit,
): ByteArray {
// Zeroize any stale input that may be left in the buffer
contract {
callsInPlace(digestProtected, InvocationKind.EXACTLY_ONCE)
callsInPlace(resetProtected, InvocationKind.EXACTLY_ONCE)
callsInPlace(bufPosSet, InvocationKind.EXACTLY_ONCE)
}

// Zero out any stale input that may be left in the buffer
value.fill(0, bufPos)
val digest = digestProtected(value, bufPos)
commonReset(resetProtected, bufPosSet)
return digest
}

@OptIn(ExperimentalContracts::class)
@Throws(ShortBufferException::class)
internal inline fun Buffer.commonDigestInto(
bufPos: Int,
dest: ByteArray,
destOffset: Int,
digestLength: Int,
digestIntoProtected: (dest: ByteArray, destOffset: Int, buf: ByteArray, bufPos: Int) -> Unit,
resetProtected: () -> Unit,
bufPosSet: (zero: Int) -> Unit,
): Int {
contract {
callsInPlace(digestIntoProtected, InvocationKind.AT_MOST_ONCE)
callsInPlace(resetProtected, InvocationKind.AT_MOST_ONCE)
callsInPlace(bufPosSet, InvocationKind.AT_MOST_ONCE)
}

dest.commonCheckArgs(destOffset, digestLength, onShortInput = {
ShortBufferException("Not enough room in dest for $digestLength bytes")
})

// Zero out any stale input that may be left in the buffer
value.fill(0, bufPos)
digestIntoProtected(dest, destOffset, value, bufPos)
commonReset(resetProtected, bufPosSet)
return digestLength
}

@OptIn(ExperimentalContracts::class)
internal inline fun Buffer.commonReset(
resetProtected: () -> Unit,
bufPosSet: (zero: Int) -> Unit,
) {
contract {
callsInPlace(resetProtected, InvocationKind.EXACTLY_ONCE)
callsInPlace(bufPosSet, InvocationKind.EXACTLY_ONCE)
}

value.fill(0)
bufPosSet(0)
resetProtected()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,15 +18,31 @@
package org.kotlincrypto.core.digest.internal

import org.kotlincrypto.core.digest.Digest
import kotlin.contracts.ExperimentalContracts
import kotlin.contracts.InvocationKind
import kotlin.contracts.contract

@Suppress("NOTHING_TO_INLINE")
internal inline fun Digest.commonToString(): String {
return "Digest[${algorithm()}]@${hashCode()}"
}

@Throws(Exception::class)
@Suppress("NOTHING_TO_INLINE")
@Throws(IllegalArgumentException::class, IndexOutOfBoundsException::class)
internal inline fun ByteArray.commonCheckArgs(offset: Int, len: Int) {
if (size - offset < len) throw IllegalArgumentException("Input too short")
if (offset < 0 || len < 0 || offset > size - len) throw IndexOutOfBoundsException()
@OptIn(ExperimentalContracts::class)
internal inline fun ByteArray.commonCheckArgs(
offset: Int,
len: Int,
onShortInput: () -> Exception = { IllegalArgumentException("Input too short") },
onOutOfBounds: (message: String) -> Exception = { IndexOutOfBoundsException(it) },
) {
contract {
callsInPlace(onShortInput, InvocationKind.AT_MOST_ONCE)
callsInPlace(onOutOfBounds, InvocationKind.AT_MOST_ONCE)
}

if (size - offset < len) throw onShortInput()
if (offset < 0) throw onOutOfBounds("offset[$offset] < 0")
if (len < 0) throw onOutOfBounds("len[$len] < 0")
if (offset > size - len) throw onOutOfBounds("offset[$offset] > size[$size] - len[$len]")
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ import kotlin.test.fail
* same for [Digest] as Java's MessageDigest would handle
* them.
* */
abstract class TestDigestException: Updatable {
abstract class AbstractTestUpdateExceptions: Updatable {

@Test
fun givenDigest_whenEmptyBytes_thenDoesNotThrow() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,11 @@
**/
package org.kotlincrypto.core.digest

import org.kotlincrypto.core.ShortBufferException
import kotlin.random.Random
import kotlin.test.*

class DigestUnitTest: TestDigestException() {
class DigestUnitTest: AbstractTestUpdateExceptions() {

private val digest = TestDigest()

Expand Down Expand Up @@ -117,7 +118,7 @@ class DigestUnitTest: TestDigestException() {
}

@Test
fun givenBuffer_whenDigestProtected_thenStaleInputIsZeroized() {
fun givenBuffer_whenDigestProtected_thenStaleInputIsZeroedOut() {
var bufCopy: ByteArray? = null
var bufCopyPos: Int = -1
val digest = TestDigest(digest = { buf, bufPos ->
Expand Down Expand Up @@ -148,4 +149,28 @@ class DigestUnitTest: TestDigestException() {
assertEquals(0, bufCopy!![i])
}
}

@Test
fun givenDigest_whenDigestInto_thenDefaultImplementationCopiesResultIntoDest() {
val expected = ByteArray(10) { 1 }
val digest = TestDigest(
digestLength = expected.size,
digest = { _, _ -> expected.copyOf() }
)
val actual = ByteArray(expected.size + 2) { 4 }
digest.digestInto(actual, 1)

assertEquals(4, actual[0])
assertEquals(4, actual[actual.size - 1])
for (i in expected.indices) {
assertEquals(expected[i], actual[i + 1])
}
}

@Test
fun givenDigest_whenDigestInto_thenThrowsExceptionsAsExpected() {
val dSize = digest.digestLength()
assertFailsWith<ShortBufferException> { digest.digestInto(ByteArray(dSize), 1) }
assertFailsWith<IndexOutOfBoundsException> { digest.digestInto(ByteArray(dSize), -1) }
}
}
Loading
Loading