From 888c7038c0dce3f4dcf918644c10874bc8026d53 Mon Sep 17 00:00:00 2001 From: Matthew Nelson Date: Tue, 4 Feb 2025 07:57:30 -0500 Subject: [PATCH] Add Digest.digestInto functionality --- library/digest/api/digest.api | 2 + library/digest/api/digest.klib.api | 2 + .../org/kotlincrypto/core/digest/Digest.kt | 36 ++++++++- .../core/digest/internal/-Buffer.kt | 68 +++++++++++++++- .../core/digest/internal/-CommonPlatform.kt | 24 +++++- ...ion.kt => AbstractTestUpdateExceptions.kt} | 2 +- .../core/digest/DigestUnitTest.kt | 29 ++++++- .../org/kotlincrypto/core/digest/Digest.kt | 78 +++++++++++++++++-- ...nUnitTest.kt => JvmDigestExceptionTest.kt} | 6 +- .../core/digest/JvmDigestUnitTest.kt | 66 ++++++++++++++++ .../org/kotlincrypto/core/digest/Digest.kt | 49 +++++++++++- 11 files changed, 342 insertions(+), 20 deletions(-) rename library/digest/src/commonTest/kotlin/org/kotlincrypto/core/digest/{TestDigestException.kt => AbstractTestUpdateExceptions.kt} (96%) rename library/digest/src/jvmTest/kotlin/org/kotlincrypto/core/digest/{DigestExceptionUnitTest.kt => JvmDigestExceptionTest.kt} (84%) diff --git a/library/digest/api/digest.api b/library/digest/api/digest.api index cf36ca3..1349e2a 100644 --- a/library/digest/api/digest.api +++ b/library/digest/api/digest.api @@ -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 diff --git a/library/digest/api/digest.klib.api b/library/digest/api/digest.klib.api index 9934453..95224da 100644 --- a/library/digest/api/digest.klib.api +++ b/library/digest/api/digest.klib.api @@ -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] @@ -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] } diff --git a/library/digest/src/commonMain/kotlin/org/kotlincrypto/core/digest/Digest.kt b/library/digest/src/commonMain/kotlin/org/kotlincrypto/core/digest/Digest.kt index a77d17a..6efe2bf 100644 --- a/library/digest/src/commonMain/kotlin/org/kotlincrypto/core/digest/Digest.kt +++ b/library/digest/src/commonMain/kotlin/org/kotlincrypto/core/digest/Digest.kt @@ -101,12 +101,24 @@ public expect abstract class Digest: Algorithm, Copyable, 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() @@ -121,7 +133,7 @@ public expect abstract class Digest: Algorithm, Copyable, 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 @@ -129,6 +141,26 @@ public expect abstract class Digest: Algorithm, Copyable, Resettable, Up * */ 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. diff --git a/library/digest/src/commonMain/kotlin/org/kotlincrypto/core/digest/internal/-Buffer.kt b/library/digest/src/commonMain/kotlin/org/kotlincrypto/core/digest/internal/-Buffer.kt index 7537155..2b14821 100644 --- a/library/digest/src/commonMain/kotlin/org/kotlincrypto/core/digest/internal/-Buffer.kt +++ b/library/digest/src/commonMain/kotlin/org/kotlincrypto/core/digest/internal/-Buffer.kt @@ -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 @@ -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 @@ -54,6 +64,7 @@ internal inline fun Buffer.commonUpdate( bufPosSet(0) } +@OptIn(ExperimentalContracts::class) internal inline fun Buffer.commonUpdate( input: ByteArray, offset: Int, @@ -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 @@ -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, @@ -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() diff --git a/library/digest/src/commonMain/kotlin/org/kotlincrypto/core/digest/internal/-CommonPlatform.kt b/library/digest/src/commonMain/kotlin/org/kotlincrypto/core/digest/internal/-CommonPlatform.kt index 1aea6db..904dbed 100644 --- a/library/digest/src/commonMain/kotlin/org/kotlincrypto/core/digest/internal/-CommonPlatform.kt +++ b/library/digest/src/commonMain/kotlin/org/kotlincrypto/core/digest/internal/-CommonPlatform.kt @@ -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]") } diff --git a/library/digest/src/commonTest/kotlin/org/kotlincrypto/core/digest/TestDigestException.kt b/library/digest/src/commonTest/kotlin/org/kotlincrypto/core/digest/AbstractTestUpdateExceptions.kt similarity index 96% rename from library/digest/src/commonTest/kotlin/org/kotlincrypto/core/digest/TestDigestException.kt rename to library/digest/src/commonTest/kotlin/org/kotlincrypto/core/digest/AbstractTestUpdateExceptions.kt index 519f791..e5f0564 100644 --- a/library/digest/src/commonTest/kotlin/org/kotlincrypto/core/digest/TestDigestException.kt +++ b/library/digest/src/commonTest/kotlin/org/kotlincrypto/core/digest/AbstractTestUpdateExceptions.kt @@ -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() { diff --git a/library/digest/src/commonTest/kotlin/org/kotlincrypto/core/digest/DigestUnitTest.kt b/library/digest/src/commonTest/kotlin/org/kotlincrypto/core/digest/DigestUnitTest.kt index 3f5d2ae..6f0bd8e 100644 --- a/library/digest/src/commonTest/kotlin/org/kotlincrypto/core/digest/DigestUnitTest.kt +++ b/library/digest/src/commonTest/kotlin/org/kotlincrypto/core/digest/DigestUnitTest.kt @@ -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() @@ -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 -> @@ -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 { digest.digestInto(ByteArray(dSize), 1) } + assertFailsWith { digest.digestInto(ByteArray(dSize), -1) } + } } diff --git a/library/digest/src/jvmMain/kotlin/org/kotlincrypto/core/digest/Digest.kt b/library/digest/src/jvmMain/kotlin/org/kotlincrypto/core/digest/Digest.kt index 4171c62..73a57dc 100644 --- a/library/digest/src/jvmMain/kotlin/org/kotlincrypto/core/digest/Digest.kt +++ b/library/digest/src/jvmMain/kotlin/org/kotlincrypto/core/digest/Digest.kt @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. **/ -@file:Suppress("EXPECT_ACTUAL_CLASSIFIERS_ARE_IN_BETA_WARNING") +@file:Suppress("EXPECT_ACTUAL_CLASSIFIERS_ARE_IN_BETA_WARNING", "DeprecatedCallableAddReplaceWith") package org.kotlincrypto.core.digest @@ -132,7 +132,7 @@ public actual abstract class Digest: MessageDigest, Algorithm, Cloneable, Copyab ) /** - * 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. * */ @@ -145,6 +145,26 @@ public actual abstract class Digest: MessageDigest, Algorithm, Cloneable, Copyab bufPosSet = { bufPos = it }, ) + /** + * 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 actual fun digestInto(dest: ByteArray, destOffset: Int): Int = buf.commonDigestInto( + bufPos = bufPos, + dest = dest, + destOffset = destOffset, + digestLength = digestLength, + digestIntoProtected = ::digestIntoProtected, + resetProtected = ::resetProtected, + bufPosSet = { bufPos = it }, + ) + // See Resettable interface documentation public actual final override fun reset() { buf.commonReset( @@ -164,7 +184,7 @@ public actual abstract class Digest: MessageDigest, Algorithm, Cloneable, Copyab * 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 @@ -172,6 +192,31 @@ public actual abstract class Digest: MessageDigest, Algorithm, Cloneable, Copyab * */ protected actual 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 actual open fun digestIntoProtected(dest: ByteArray, destOffset: Int, buf: ByteArray, bufPos: Int) { + // Default implementation. Extenders of Digest should override. + val result = digestProtected(buf, bufPos) + result.copyInto(dest, destOffset) + result.fill(0) + } + /** * Optional override for implementations to intercept cleansed input before * being processed by the [Digest] abstraction. @@ -205,29 +250,48 @@ public actual abstract class Digest: MessageDigest, Algorithm, Cloneable, Copyab // MessageDigest /** @suppress */ - @Throws(IllegalArgumentException::class, DigestException::class) - public final override fun digest(buf: ByteArray, offset: Int, len: Int): Int = super.digest(buf, offset, len) + @Throws(DigestException::class) + @Deprecated("Use digestInto", ReplaceWith("digestInto(buf, offset)")) + public final override fun digest(buf: ByteArray?, offset: Int, len: Int): Int { + requireNotNull(buf) { "buf cannot be null" } + buf.commonCheckArgs(offset, len, onOutOfBounds = { reason -> DigestException(reason) }) + @Suppress("DEPRECATION") + return engineDigest(buf, offset, len) + } /** @suppress */ public final override fun clone(): Any = copy() // MessageDigestSpi /** @suppress */ + @Deprecated("Do not use. Will be marked as ERROR in a later release") protected final override fun engineGetDigestLength(): Int = digestLength /** @suppress */ + @Deprecated("Do not use. Will be marked as ERROR in a later release") protected final override fun engineUpdate(p0: Byte) { updateProtected(p0) } /** @suppress */ + @Deprecated("Do not use. Will be marked as ERROR in a later release") protected final override fun engineUpdate(input: ByteBuffer) { super.engineUpdate(input) } /** @suppress */ - @Throws(IllegalArgumentException::class, IndexOutOfBoundsException::class) + @Deprecated("Do not use. Will be marked as ERROR in a later release") protected final override fun engineUpdate(p0: ByteArray, p1: Int, p2: Int) { update(p0, p1, p2) } /** @suppress */ + @Deprecated("Do not use. Will be marked as ERROR in a later release") protected final override fun engineDigest(): ByteArray = digest() /** @suppress */ @Throws(DigestException::class) + @Deprecated("Do not use. Will be marked as ERROR in a later release") protected final override fun engineDigest(buf: ByteArray, offset: Int, len: Int): Int { - return super.engineDigest(buf, offset, len) + if (len < digestLength) { + throw DigestException("partial digests not returned. len[$len] < digestLength[$digestLength]") + } + if (buf.size - offset < digestLength) { + throw DigestException("insufficient space in the output buffer to store the digest.") + } + + return digestInto(buf, offset) } /** @suppress */ + @Deprecated("Do not use. Will be marked as ERROR in a later release") protected final override fun engineReset() { reset() } /** @suppress */ diff --git a/library/digest/src/jvmTest/kotlin/org/kotlincrypto/core/digest/DigestExceptionUnitTest.kt b/library/digest/src/jvmTest/kotlin/org/kotlincrypto/core/digest/JvmDigestExceptionTest.kt similarity index 84% rename from library/digest/src/jvmTest/kotlin/org/kotlincrypto/core/digest/DigestExceptionUnitTest.kt rename to library/digest/src/jvmTest/kotlin/org/kotlincrypto/core/digest/JvmDigestExceptionTest.kt index 3ed3885..0353943 100644 --- a/library/digest/src/jvmTest/kotlin/org/kotlincrypto/core/digest/DigestExceptionUnitTest.kt +++ b/library/digest/src/jvmTest/kotlin/org/kotlincrypto/core/digest/JvmDigestExceptionTest.kt @@ -17,7 +17,11 @@ package org.kotlincrypto.core.digest import java.security.MessageDigest -class DigestExceptionUnitTest: TestDigestException() { +/** + * Verifies that [Digest] abstraction produces the same exceptions that + * the Jvm's [MessageDigest] does. + * */ +class JvmDigestExceptionTest: AbstractTestUpdateExceptions() { private val digest = MessageDigest.getInstance("MD5") diff --git a/library/digest/src/jvmTest/kotlin/org/kotlincrypto/core/digest/JvmDigestUnitTest.kt b/library/digest/src/jvmTest/kotlin/org/kotlincrypto/core/digest/JvmDigestUnitTest.kt index a7d8657..144b652 100644 --- a/library/digest/src/jvmTest/kotlin/org/kotlincrypto/core/digest/JvmDigestUnitTest.kt +++ b/library/digest/src/jvmTest/kotlin/org/kotlincrypto/core/digest/JvmDigestUnitTest.kt @@ -16,14 +16,18 @@ package org.kotlincrypto.core.digest import java.lang.AssertionError +import java.security.DigestException import java.security.MessageDigest import kotlin.random.Random import kotlin.test.Test import kotlin.test.assertContentEquals +import kotlin.test.assertEquals +import kotlin.test.assertFailsWith /** * Compares [Digest] functionality to [MessageDigest] * */ +@Suppress("DEPRECATION") class JvmDigestUnitTest { class MessageDigestWrap: Digest { @@ -93,4 +97,66 @@ class JvmDigestUnitTest { assertContentEquals(jvm.digest(), wrap.digest()) } + + @Test + fun givenWrappedMessageDigest_whenDigestToBuf_thenWorksAsExpected() { + wrap.update(bytes, 10, 100) + jvm.update(bytes, 10, 100) + val expected = wrap.digest() + wrap.update(bytes, 10, 100) + + assertFailsWith { wrap.digest(ByteArray(2), 0, wrap.digestLength()) } + assertFailsWith { jvm.digest(ByteArray(2), 0, wrap.digestLength()) } + + assertFailsWith { wrap.digest(ByteArray(wrap.digestLength()), 1, wrap.digestLength()) } + assertFailsWith { jvm.digest(ByteArray(wrap.digestLength()), 1, wrap.digestLength()) } + + assertFailsWith { wrap.digest(ByteArray(wrap.digestLength()), -1, wrap.digestLength()) } + assertFailsWith { jvm.digest(ByteArray(wrap.digestLength()), -1, wrap.digestLength()) } + + assertFailsWith { wrap.digest(ByteArray(wrap.digestLength()), 0, 0) } + assertFailsWith { jvm.digest(ByteArray(wrap.digestLength()), 0, 0) } + + assertFailsWith { wrap.digest(ByteArray(wrap.digestLength()), wrap.digestLength() + 1, wrap.digestLength()) } + assertFailsWith { jvm.digest(ByteArray(wrap.digestLength()), wrap.digestLength() + 1, wrap.digestLength()) } + + assertFailsWith { wrap.digest(null, 0, wrap.digestLength()) } + assertFailsWith { jvm.digest(null, 0, wrap.digestLength()) } + + assertContentEquals(expected, wrap.digest()) + assertContentEquals(expected, jvm.digest()) + + wrap.update(bytes, 10, 100) + jvm.update(bytes, 10, 100) + + assertFailsWith { wrap.digest(ByteArray(wrap.digestLength()), 0, wrap.digestLength() - 1) } + assertFailsWith { jvm.digest(ByteArray(wrap.digestLength()), 0, wrap.digestLength() - 1) } + + assertContentEquals(expected, wrap.digest()) + assertContentEquals(expected, jvm.digest()) + + wrap.update(bytes, 10, 100) + jvm.update(bytes, 10, 100) + + val resultWrap = ByteArray(wrap.digestLength() + 4) + val resultJvm = resultWrap.copyOf() + + // Expressing longer length than what digest outputs should be ignored + assertEquals(wrap.digestLength(), wrap.digest(resultWrap, 2, resultWrap.size - 2)) + assertEquals(wrap.digestLength(), jvm.digest(resultJvm, 2, resultJvm.size - 2)) + + assertContentEquals(resultWrap, resultJvm) + for (i in expected.indices) { + assertEquals(expected[i], resultWrap[i + 2]) + assertEquals(expected[i], resultJvm[i + 2]) + } + for (i in 0 until 2) { + assertEquals(0, resultWrap[i]) + assertEquals(0, resultJvm[i]) + } + for (i in (resultWrap.size - 2) until resultWrap.size) { + assertEquals(0, resultWrap[i]) + assertEquals(0, resultJvm[i]) + } + } } diff --git a/library/digest/src/nonJvmMain/kotlin/org/kotlincrypto/core/digest/Digest.kt b/library/digest/src/nonJvmMain/kotlin/org/kotlincrypto/core/digest/Digest.kt index 3633aae..02fa7f6 100644 --- a/library/digest/src/nonJvmMain/kotlin/org/kotlincrypto/core/digest/Digest.kt +++ b/library/digest/src/nonJvmMain/kotlin/org/kotlincrypto/core/digest/Digest.kt @@ -129,7 +129,7 @@ public actual abstract class Digest: Algorithm, Copyable, Resettable, Up ) /** - * 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. * */ @@ -142,6 +142,26 @@ public actual abstract class Digest: Algorithm, Copyable, Resettable, Up bufPosSet = { bufPos = it }, ) + /** + * 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 actual fun digestInto(dest: ByteArray, destOffset: Int): Int = buf.commonDigestInto( + bufPos = bufPos, + dest = dest, + destOffset = destOffset, + digestLength = digestLength, + digestIntoProtected = ::digestIntoProtected, + resetProtected = ::resetProtected, + bufPosSet = { bufPos = it }, + ) + // See Resettable interface documentation public actual final override fun reset() { buf.commonReset( @@ -161,7 +181,7 @@ public actual abstract class Digest: Algorithm, Copyable, 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 @@ -169,6 +189,31 @@ public actual abstract class Digest: Algorithm, Copyable, Resettable, Up * */ protected actual 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 actual open fun digestIntoProtected(dest: ByteArray, destOffset: Int, buf: ByteArray, bufPos: Int) { + // Default implementation. Extenders of Digest should override. + val result = digestProtected(buf, bufPos) + result.copyInto(dest, destOffset) + result.fill(0) + } + /** * Optional override for implementations to intercept cleansed input before * being processed by the [Digest] abstraction.