Skip to content

Commit e14c9db

Browse files
authored
Add Digest.digestInto functionality (#108)
1 parent ec5226b commit e14c9db

File tree

11 files changed

+342
-20
lines changed

11 files changed

+342
-20
lines changed

library/digest/api/digest.api

+2
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ public abstract class org/kotlincrypto/core/digest/Digest : java/security/Messag
88
public final fun digest ()[B
99
public final fun digest ([B)[B
1010
public final fun digest ([BII)I
11+
public final fun digestInto ([BI)I
12+
protected fun digestIntoProtected ([BI[BI)V
1113
public final fun digestLength ()I
1214
protected abstract fun digestProtected ([BI)[B
1315
protected final fun engineDigest ()[B

library/digest/api/digest.klib.api

+2
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ abstract class org.kotlincrypto.core.digest/Digest : org.kotlincrypto.core/Algor
1717
final fun blockSize(): kotlin/Int // org.kotlincrypto.core.digest/Digest.blockSize|blockSize(){}[0]
1818
final fun digest(): kotlin/ByteArray // org.kotlincrypto.core.digest/Digest.digest|digest(){}[0]
1919
final fun digest(kotlin/ByteArray): kotlin/ByteArray // org.kotlincrypto.core.digest/Digest.digest|digest(kotlin.ByteArray){}[0]
20+
final fun digestInto(kotlin/ByteArray, kotlin/Int): kotlin/Int // org.kotlincrypto.core.digest/Digest.digestInto|digestInto(kotlin.ByteArray;kotlin.Int){}[0]
2021
final fun digestLength(): kotlin/Int // org.kotlincrypto.core.digest/Digest.digestLength|digestLength(){}[0]
2122
final fun equals(kotlin/Any?): kotlin/Boolean // org.kotlincrypto.core.digest/Digest.equals|equals(kotlin.Any?){}[0]
2223
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
2526
final fun update(kotlin/Byte) // org.kotlincrypto.core.digest/Digest.update|update(kotlin.Byte){}[0]
2627
final fun update(kotlin/ByteArray) // org.kotlincrypto.core.digest/Digest.update|update(kotlin.ByteArray){}[0]
2728
final fun update(kotlin/ByteArray, kotlin/Int, kotlin/Int) // org.kotlincrypto.core.digest/Digest.update|update(kotlin.ByteArray;kotlin.Int;kotlin.Int){}[0]
29+
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]
2830
open fun updateProtected(kotlin/Byte) // org.kotlincrypto.core.digest/Digest.updateProtected|updateProtected(kotlin.Byte){}[0]
2931
open fun updateProtected(kotlin/ByteArray, kotlin/Int, kotlin/Int) // org.kotlincrypto.core.digest/Digest.updateProtected|updateProtected(kotlin.ByteArray;kotlin.Int;kotlin.Int){}[0]
3032
}

library/digest/src/commonMain/kotlin/org/kotlincrypto/core/digest/Digest.kt

+34-2
Original file line numberDiff line numberDiff line change
@@ -101,12 +101,24 @@ public expect abstract class Digest: Algorithm, Copyable<Digest>, Resettable, Up
101101
public fun digest(): ByteArray
102102

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

110+
/**
111+
* Completes the computation, performing final operations and placing the
112+
* resultant bytes into the provided [dest] array starting at index [destOffset].
113+
* The [Digest] is [reset] afterward.
114+
*
115+
* @return The number of bytes put into [dest] (i.e. the [digestLength])
116+
* @throws [IndexOutOfBoundsException] if [destOffset] is inappropriate
117+
* @throws [ShortBufferException] if [digestLength] number of bytes are unable
118+
* to fit into [dest] for provided [destOffset]
119+
* */
120+
public fun digestInto(dest: ByteArray, destOffset: Int): Int
121+
110122
// See Resettable interface documentation
111123
public final override fun reset()
112124

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

144+
/**
145+
* Called to complete the computation, providing any input that may be buffered
146+
* and awaiting processing.
147+
*
148+
* Implementations should override this addition to the API for performance reasons.
149+
* If overridden, `super.digestIntoProtected` should **not** be called.
150+
*
151+
* **NOTE:** The buffer from [bufPos] to the end will always be zeroed out to clear
152+
* any potentially stale input left over from a previous state.
153+
*
154+
* **NOTE:** The public [digestInto] function always checks [dest] for capacity of
155+
* [digestLength], starting at [destOffset], before calling this function.
156+
*
157+
* @param [dest] The array to place resultant bytes
158+
* @param [destOffset] The index to begin placing bytes into [dest]
159+
* @param [buf] Unprocessed input
160+
* @param [bufPos] The index at which the **next** input would be placed into [buf]
161+
* */
162+
protected open fun digestIntoProtected(dest: ByteArray, destOffset: Int, buf: ByteArray, bufPos: Int)
163+
132164
/**
133165
* Optional override for implementations to intercept cleansed input before
134166
* being processed by the [Digest] abstraction.

library/digest/src/commonMain/kotlin/org/kotlincrypto/core/digest/internal/-Buffer.kt

+67-1
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,11 @@
1717

1818
package org.kotlincrypto.core.digest.internal
1919

20+
import org.kotlincrypto.core.ShortBufferException
2021
import org.kotlincrypto.core.digest.Digest
22+
import kotlin.contracts.ExperimentalContracts
23+
import kotlin.contracts.InvocationKind
24+
import kotlin.contracts.contract
2125
import kotlin.jvm.JvmInline
2226

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

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

46+
@OptIn(ExperimentalContracts::class)
4247
internal inline fun Buffer.commonUpdate(
4348
input: Byte,
4449
bufPosPlusPlus: Int,
4550
bufPosSet: (zero: Int) -> Unit,
4651
compressProtected: (ByteArray, Int) -> Unit,
4752
) {
53+
contract {
54+
callsInPlace(bufPosSet, InvocationKind.AT_MOST_ONCE)
55+
callsInPlace(compressProtected, InvocationKind.AT_MOST_ONCE)
56+
}
57+
4858
val buf = value
4959
buf[bufPosPlusPlus] = input
5060

@@ -54,6 +64,7 @@ internal inline fun Buffer.commonUpdate(
5464
bufPosSet(0)
5565
}
5666

67+
@OptIn(ExperimentalContracts::class)
5768
internal inline fun Buffer.commonUpdate(
5869
input: ByteArray,
5970
offset: Int,
@@ -62,6 +73,11 @@ internal inline fun Buffer.commonUpdate(
6273
bufPosSet: (value: Int) -> Unit,
6374
compressProtected: (ByteArray, Int) -> Unit,
6475
) {
76+
contract {
77+
callsInPlace(bufPosSet, InvocationKind.EXACTLY_ONCE)
78+
callsInPlace(compressProtected, InvocationKind.UNKNOWN)
79+
}
80+
6581
val buf = value
6682
val blockSize = buf.size
6783
val limitInput = offset + len
@@ -105,6 +121,7 @@ internal inline fun Buffer.commonUpdate(
105121
bufPosSet(posBuf)
106122
}
107123

124+
@OptIn(ExperimentalContracts::class)
108125
internal inline fun Buffer.commonDigest(
109126
input: ByteArray,
110127
updateProtected: (ByteArray, Int, Int) -> Unit,
@@ -113,27 +130,76 @@ internal inline fun Buffer.commonDigest(
113130
resetProtected: () -> Unit,
114131
bufPosSet: (zero: Int) -> Unit,
115132
): ByteArray {
133+
contract {
134+
callsInPlace(updateProtected, InvocationKind.EXACTLY_ONCE)
135+
callsInPlace(bufPosGet, InvocationKind.EXACTLY_ONCE)
136+
callsInPlace(digestProtected, InvocationKind.EXACTLY_ONCE)
137+
callsInPlace(resetProtected, InvocationKind.EXACTLY_ONCE)
138+
callsInPlace(bufPosSet, InvocationKind.EXACTLY_ONCE)
139+
}
140+
116141
updateProtected(input, 0, input.size)
117142
return commonDigest(bufPosGet(), digestProtected, resetProtected, bufPosSet)
118143
}
119144

145+
@OptIn(ExperimentalContracts::class)
120146
internal inline fun Buffer.commonDigest(
121147
bufPos: Int,
122148
digestProtected: (buf: ByteArray, bufPos: Int) -> ByteArray,
123149
resetProtected: () -> Unit,
124150
bufPosSet: (zero: Int) -> Unit,
125151
): ByteArray {
126-
// Zeroize any stale input that may be left in the buffer
152+
contract {
153+
callsInPlace(digestProtected, InvocationKind.EXACTLY_ONCE)
154+
callsInPlace(resetProtected, InvocationKind.EXACTLY_ONCE)
155+
callsInPlace(bufPosSet, InvocationKind.EXACTLY_ONCE)
156+
}
157+
158+
// Zero out any stale input that may be left in the buffer
127159
value.fill(0, bufPos)
128160
val digest = digestProtected(value, bufPos)
129161
commonReset(resetProtected, bufPosSet)
130162
return digest
131163
}
132164

165+
@OptIn(ExperimentalContracts::class)
166+
@Throws(ShortBufferException::class)
167+
internal inline fun Buffer.commonDigestInto(
168+
bufPos: Int,
169+
dest: ByteArray,
170+
destOffset: Int,
171+
digestLength: Int,
172+
digestIntoProtected: (dest: ByteArray, destOffset: Int, buf: ByteArray, bufPos: Int) -> Unit,
173+
resetProtected: () -> Unit,
174+
bufPosSet: (zero: Int) -> Unit,
175+
): Int {
176+
contract {
177+
callsInPlace(digestIntoProtected, InvocationKind.AT_MOST_ONCE)
178+
callsInPlace(resetProtected, InvocationKind.AT_MOST_ONCE)
179+
callsInPlace(bufPosSet, InvocationKind.AT_MOST_ONCE)
180+
}
181+
182+
dest.commonCheckArgs(destOffset, digestLength, onShortInput = {
183+
ShortBufferException("Not enough room in dest for $digestLength bytes")
184+
})
185+
186+
// Zero out any stale input that may be left in the buffer
187+
value.fill(0, bufPos)
188+
digestIntoProtected(dest, destOffset, value, bufPos)
189+
commonReset(resetProtected, bufPosSet)
190+
return digestLength
191+
}
192+
193+
@OptIn(ExperimentalContracts::class)
133194
internal inline fun Buffer.commonReset(
134195
resetProtected: () -> Unit,
135196
bufPosSet: (zero: Int) -> Unit,
136197
) {
198+
contract {
199+
callsInPlace(resetProtected, InvocationKind.EXACTLY_ONCE)
200+
callsInPlace(bufPosSet, InvocationKind.EXACTLY_ONCE)
201+
}
202+
137203
value.fill(0)
138204
bufPosSet(0)
139205
resetProtected()

library/digest/src/commonMain/kotlin/org/kotlincrypto/core/digest/internal/-CommonPlatform.kt

+20-4
Original file line numberDiff line numberDiff line change
@@ -18,15 +18,31 @@
1818
package org.kotlincrypto.core.digest.internal
1919

2020
import org.kotlincrypto.core.digest.Digest
21+
import kotlin.contracts.ExperimentalContracts
22+
import kotlin.contracts.InvocationKind
23+
import kotlin.contracts.contract
2124

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

30+
@Throws(Exception::class)
2731
@Suppress("NOTHING_TO_INLINE")
28-
@Throws(IllegalArgumentException::class, IndexOutOfBoundsException::class)
29-
internal inline fun ByteArray.commonCheckArgs(offset: Int, len: Int) {
30-
if (size - offset < len) throw IllegalArgumentException("Input too short")
31-
if (offset < 0 || len < 0 || offset > size - len) throw IndexOutOfBoundsException()
32+
@OptIn(ExperimentalContracts::class)
33+
internal inline fun ByteArray.commonCheckArgs(
34+
offset: Int,
35+
len: Int,
36+
onShortInput: () -> Exception = { IllegalArgumentException("Input too short") },
37+
onOutOfBounds: (message: String) -> Exception = { IndexOutOfBoundsException(it) },
38+
) {
39+
contract {
40+
callsInPlace(onShortInput, InvocationKind.AT_MOST_ONCE)
41+
callsInPlace(onOutOfBounds, InvocationKind.AT_MOST_ONCE)
42+
}
43+
44+
if (size - offset < len) throw onShortInput()
45+
if (offset < 0) throw onOutOfBounds("offset[$offset] < 0")
46+
if (len < 0) throw onOutOfBounds("len[$len] < 0")
47+
if (offset > size - len) throw onOutOfBounds("offset[$offset] > size[$size] - len[$len]")
3248
}

library/digest/src/commonTest/kotlin/org/kotlincrypto/core/digest/TestDigestException.kt renamed to library/digest/src/commonTest/kotlin/org/kotlincrypto/core/digest/AbstractTestUpdateExceptions.kt

+1-1
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ import kotlin.test.fail
2424
* same for [Digest] as Java's MessageDigest would handle
2525
* them.
2626
* */
27-
abstract class TestDigestException: Updatable {
27+
abstract class AbstractTestUpdateExceptions: Updatable {
2828

2929
@Test
3030
fun givenDigest_whenEmptyBytes_thenDoesNotThrow() {

library/digest/src/commonTest/kotlin/org/kotlincrypto/core/digest/DigestUnitTest.kt

+27-2
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,11 @@
1515
**/
1616
package org.kotlincrypto.core.digest
1717

18+
import org.kotlincrypto.core.ShortBufferException
1819
import kotlin.random.Random
1920
import kotlin.test.*
2021

21-
class DigestUnitTest: TestDigestException() {
22+
class DigestUnitTest: AbstractTestUpdateExceptions() {
2223

2324
private val digest = TestDigest()
2425

@@ -117,7 +118,7 @@ class DigestUnitTest: TestDigestException() {
117118
}
118119

119120
@Test
120-
fun givenBuffer_whenDigestProtected_thenStaleInputIsZeroized() {
121+
fun givenBuffer_whenDigestProtected_thenStaleInputIsZeroedOut() {
121122
var bufCopy: ByteArray? = null
122123
var bufCopyPos: Int = -1
123124
val digest = TestDigest(digest = { buf, bufPos ->
@@ -148,4 +149,28 @@ class DigestUnitTest: TestDigestException() {
148149
assertEquals(0, bufCopy!![i])
149150
}
150151
}
152+
153+
@Test
154+
fun givenDigest_whenDigestInto_thenDefaultImplementationCopiesResultIntoDest() {
155+
val expected = ByteArray(10) { 1 }
156+
val digest = TestDigest(
157+
digestLength = expected.size,
158+
digest = { _, _ -> expected.copyOf() }
159+
)
160+
val actual = ByteArray(expected.size + 2) { 4 }
161+
digest.digestInto(actual, 1)
162+
163+
assertEquals(4, actual[0])
164+
assertEquals(4, actual[actual.size - 1])
165+
for (i in expected.indices) {
166+
assertEquals(expected[i], actual[i + 1])
167+
}
168+
}
169+
170+
@Test
171+
fun givenDigest_whenDigestInto_thenThrowsExceptionsAsExpected() {
172+
val dSize = digest.digestLength()
173+
assertFailsWith<ShortBufferException> { digest.digestInto(ByteArray(dSize), 1) }
174+
assertFailsWith<IndexOutOfBoundsException> { digest.digestInto(ByteArray(dSize), -1) }
175+
}
151176
}

0 commit comments

Comments
 (0)