diff --git a/library/mac/api/mac.api b/library/mac/api/mac.api index 791da70..3b363e9 100644 --- a/library/mac/api/mac.api +++ b/library/mac/api/mac.api @@ -12,8 +12,10 @@ public abstract class org/kotlincrypto/core/mac/Mac : javax/crypto/Mac, org/kotl } protected abstract class org/kotlincrypto/core/mac/Mac$Engine : javax/crypto/MacSpi, java/lang/Cloneable, org/kotlincrypto/core/Copyable, org/kotlincrypto/core/Resettable, org/kotlincrypto/core/Updatable { + public final field resetOnDoFinal Z protected fun (Lorg/kotlincrypto/core/mac/Mac$Engine;)V public fun ([B)V + public fun ([BZ)V public final fun clone ()Ljava/lang/Object; public abstract fun doFinal ()[B public fun doFinalInto ([BI)V diff --git a/library/mac/api/mac.klib.api b/library/mac/api/mac.klib.api index ca9ae69..c269e22 100644 --- a/library/mac/api/mac.klib.api +++ b/library/mac/api/mac.klib.api @@ -27,8 +27,12 @@ abstract class org.kotlincrypto.core.mac/Mac : org.kotlincrypto.core/Algorithm, abstract class Engine : org.kotlincrypto.core/Copyable, org.kotlincrypto.core/Resettable, org.kotlincrypto.core/Updatable { // org.kotlincrypto.core.mac/Mac.Engine|null[0] constructor (kotlin/ByteArray) // org.kotlincrypto.core.mac/Mac.Engine.|(kotlin.ByteArray){}[0] + constructor (kotlin/ByteArray, kotlin/Boolean) // org.kotlincrypto.core.mac/Mac.Engine.|(kotlin.ByteArray;kotlin.Boolean){}[0] constructor (org.kotlincrypto.core.mac/Mac.Engine) // org.kotlincrypto.core.mac/Mac.Engine.|(org.kotlincrypto.core.mac.Mac.Engine){}[0] + final val resetOnDoFinal // org.kotlincrypto.core.mac/Mac.Engine.resetOnDoFinal|(){}[0] + final fun (): kotlin/Boolean // org.kotlincrypto.core.mac/Mac.Engine.resetOnDoFinal.|(){}[0] + abstract fun doFinal(): kotlin/ByteArray // org.kotlincrypto.core.mac/Mac.Engine.doFinal|doFinal(){}[0] abstract fun macLength(): kotlin/Int // org.kotlincrypto.core.mac/Mac.Engine.macLength|macLength(){}[0] abstract fun reset(kotlin/ByteArray) // org.kotlincrypto.core.mac/Mac.Engine.reset|reset(kotlin.ByteArray){}[0] diff --git a/library/mac/src/commonMain/kotlin/org/kotlincrypto/core/mac/Mac.kt b/library/mac/src/commonMain/kotlin/org/kotlincrypto/core/mac/Mac.kt index a12b771..019103c 100644 --- a/library/mac/src/commonMain/kotlin/org/kotlincrypto/core/mac/Mac.kt +++ b/library/mac/src/commonMain/kotlin/org/kotlincrypto/core/mac/Mac.kt @@ -18,6 +18,7 @@ package org.kotlincrypto.core.mac import org.kotlincrypto.core.* +import kotlin.jvm.JvmField /** * Core abstraction for Message Authentication Code implementations. @@ -141,13 +142,40 @@ public expect abstract class Mac: Algorithm, Copyable, Resettable, Updatabl protected abstract class Engine: Copyable, Resettable, Updatable { /** - * Initializes a new [Engine] with the provided [key]. + * Most [Mac.Engine] are backed by a `Digest`, whereby calling [reset] after + * [doFinal] will cause a double reset (because `Digest.digest` does this inherently). + * By setting this value to `false`, [Engine.reset] will **not** be called whenever + * [doFinal] gets invoked. * - * @throws [IllegalArgumentException] if [key] is empty. + * **NOTE:** Implementations taking ownership of the automatic reset functionality + * by setting this to `false` must ensure that whatever re-initialization steps were + * taken in their [Engine.reset] function body are executed before their [doFinal] + * and [doFinalInto] implementations return. + * */ + @JvmField + public val resetOnDoFinal: Boolean + + /** + * Initializes a new [Engine] with the provided [key] with the default [resetOnDoFinal] + * value of `true` (i.e. [Engine.reset] will be called automatically after [Engine.doFinal] + * or [Engine.doFinalInto] have been invoked). + * + * @param [key] The key that this [Engine] instance will use to apply its function to + * @throws [IllegalArgumentException] if [key] is empty * */ @Throws(IllegalArgumentException::class) public constructor(key: ByteArray) + /** + * Initializes a new [Engine] with the provided [key] and [resetOnDoFinal] configuration. + * + * @param [key] the key that this [Engine] instance will use to apply its function to + * @param [resetOnDoFinal] See [Engine.resetOnDoFinal] documentation + * @throws [IllegalArgumentException] if [key] is empty + * */ + @Throws(IllegalArgumentException::class) + public constructor(key: ByteArray, resetOnDoFinal: Boolean) + /** * Creates a new [Engine] from [other], copying its state. * */ diff --git a/library/mac/src/commonMain/kotlin/org/kotlincrypto/core/mac/internal/-CommonPlatform.kt b/library/mac/src/commonMain/kotlin/org/kotlincrypto/core/mac/internal/-CommonPlatform.kt index 47e97ba..4aaa0f7 100644 --- a/library/mac/src/commonMain/kotlin/org/kotlincrypto/core/mac/internal/-CommonPlatform.kt +++ b/library/mac/src/commonMain/kotlin/org/kotlincrypto/core/mac/internal/-CommonPlatform.kt @@ -77,6 +77,7 @@ internal inline fun Mac.commonClearKey(engineReset: (ByteArray) -> Unit) { internal inline fun Mac.commonDoFinalInto( dest: ByteArray, destOffset: Int, + engineResetOnDoFinal: Boolean, engineDoFinalInto: (dest: ByteArray, destOffset: Int) -> Unit, engineReset: () -> Unit, ): Int { @@ -90,6 +91,6 @@ internal inline fun Mac.commonDoFinalInto( ShortBufferException("Not enough room in dest for $len bytes") }) engineDoFinalInto(dest, destOffset) - engineReset() + if (engineResetOnDoFinal) engineReset() return len } diff --git a/library/mac/src/commonTest/kotlin/org/kotlincrypto/core/mac/MacUnitTest.kt b/library/mac/src/commonTest/kotlin/org/kotlincrypto/core/mac/MacUnitTest.kt index 9cdb26b..0f2dce9 100644 --- a/library/mac/src/commonTest/kotlin/org/kotlincrypto/core/mac/MacUnitTest.kt +++ b/library/mac/src/commonTest/kotlin/org/kotlincrypto/core/mac/MacUnitTest.kt @@ -91,6 +91,43 @@ class MacUnitTest { assertEquals(3, resetCount) } + @Test + fun givenMacEngine_whenResetOnDoFinalFalse_thenEngineResetIsNOTCalled() { + var resetCount = 0 + var doFinalCount = 0 + val finalExpected = ByteArray(15) { (it + 25).toByte() } + + val mac = TestMac( + ByteArray(5), + algorithm = "test resetOnDoFinal false", + macLen = finalExpected.size, + resetOnDoFinal = false, + reset = { resetCount++ }, + doFinal = { doFinalCount++; finalExpected }, + ) + + mac.reset() + assertEquals(1, resetCount) + + // doFinal + mac.doFinal() + assertEquals(1, doFinalCount) + assertEquals(1, resetCount) + + // update & doFinal + mac.doFinal(ByteArray(25)) + assertEquals(2, doFinalCount) + assertEquals(1, resetCount) + + // doFinalInto + mac.doFinalInto(ByteArray(finalExpected.size), 0) + assertEquals(3, doFinalCount) + assertEquals(1, resetCount) + + mac.reset() + assertEquals(2, resetCount) + } + @Test fun givenMac_whenClearKey_thenSingle0ByteKeyPassedToEngine() { var zeroKey: ByteArray? = null diff --git a/library/mac/src/commonTest/kotlin/org/kotlincrypto/core/mac/TestMac.kt b/library/mac/src/commonTest/kotlin/org/kotlincrypto/core/mac/TestMac.kt index 82b71f2..30477ce 100644 --- a/library/mac/src/commonTest/kotlin/org/kotlincrypto/core/mac/TestMac.kt +++ b/library/mac/src/commonTest/kotlin/org/kotlincrypto/core/mac/TestMac.kt @@ -21,10 +21,11 @@ class TestMac : Mac { key: ByteArray, algorithm: String, macLen: Int = 0, + resetOnDoFinal: Boolean = true, reset: () -> Unit = {}, rekey: (new: ByteArray) -> Unit = {}, doFinal: () -> ByteArray = { ByteArray(macLen) }, - ): super(algorithm, TestEngine(key, reset, rekey, doFinal, macLen)) + ): super(algorithm, TestEngine(key, reset, rekey, doFinal, macLen, resetOnDoFinal)) private constructor(algorithm: String, engine: TestEngine): super(algorithm, engine) private constructor(other: TestMac): super(other) @@ -44,7 +45,8 @@ class TestMac : Mac { rekey: (new: ByteArray) -> Unit, doFinal: () -> ByteArray, macLen: Int, - ): super(key) { + resetOnDoFinal: Boolean, + ): super(key, resetOnDoFinal) { this.reset = reset this.rekey = rekey this.doFinal = doFinal diff --git a/library/mac/src/jvmMain/kotlin/org/kotlincrypto/core/mac/Mac.kt b/library/mac/src/jvmMain/kotlin/org/kotlincrypto/core/mac/Mac.kt index 0c34a21..ec24be6 100644 --- a/library/mac/src/jvmMain/kotlin/org/kotlincrypto/core/mac/Mac.kt +++ b/library/mac/src/jvmMain/kotlin/org/kotlincrypto/core/mac/Mac.kt @@ -116,6 +116,7 @@ public actual abstract class Mac: javax.crypto.Mac, Algorithm, Copyable, Re public actual fun doFinalInto(dest: ByteArray, destOffset: Int): Int = commonDoFinalInto( dest = dest, destOffset = destOffset, + engineResetOnDoFinal = engine.resetOnDoFinal, engineDoFinalInto = engine::doFinalInto, engineReset = engine::reset, ) @@ -150,19 +151,49 @@ public actual abstract class Mac: javax.crypto.Mac, Algorithm, Copyable, Re protected actual abstract class Engine: MacSpi, Cloneable, Copyable, Resettable, Updatable { /** - * Initializes a new [Engine] with the provided [key]. + * Most [Mac.Engine] are backed by a `Digest`, whereby calling [reset] after + * [doFinal] will cause a double reset (because `Digest.digest` does this inherently). + * By setting this value to `false`, [Engine.reset] will **not** be called whenever + * [doFinal] gets invoked. * - * @throws [IllegalArgumentException] if [key] is empty. + * **NOTE:** Implementations taking ownership of the automatic reset functionality + * by setting this to `false` must ensure that whatever re-initialization steps were + * taken in their [Engine.reset] function body are executed before their [doFinal] + * and [doFinalInto] implementations return. + * */ + @JvmField + public actual val resetOnDoFinal: Boolean + + /** + * Initializes a new [Engine] with the provided [key] with the default [resetOnDoFinal] + * value of `true` (i.e. [Engine.reset] will be called automatically after [Engine.doFinal] + * or [Engine.doFinalInto] have been invoked). + * + * @param [key] The key that this [Engine] instance will use to apply its function to + * @throws [IllegalArgumentException] if [key] is empty + * */ + @Throws(IllegalArgumentException::class) + public actual constructor(key: ByteArray): this(key, resetOnDoFinal = true) + + /** + * Initializes a new [Engine] with the provided [key] and [resetOnDoFinal] configuration. + * + * @param [key] the key that this [Engine] instance will use to apply its function to + * @param [resetOnDoFinal] See [Engine.resetOnDoFinal] documentation + * @throws [IllegalArgumentException] if [key] is empty * */ @Throws(IllegalArgumentException::class) - public actual constructor(key: ByteArray) { + public actual constructor(key: ByteArray, resetOnDoFinal: Boolean) { require(key.isNotEmpty()) { "key cannot be empty" } + this.resetOnDoFinal = resetOnDoFinal } /** * Creates a new [Engine] from [other], copying its state. * */ - protected actual constructor(other: Engine) + protected actual constructor(other: Engine) { + this.resetOnDoFinal = other.resetOnDoFinal + } /** * The number of bytes the implementation returns when [doFinal] is called. @@ -212,6 +243,10 @@ public actual abstract class Mac: javax.crypto.Mac, Algorithm, Copyable, Re @Throws(IllegalArgumentException::class) public actual abstract fun reset(newKey: ByteArray) + // Gets set in engineDoFinal if resetOnDoFinal is set to false. Subsequent + // engineReset call will then change it back to false and return early. + private var consumeNextEngineReset = false + // MacSpi /** @suppress */ @Deprecated("Do not use. Will be marked as ERROR in a later release") @@ -230,7 +265,13 @@ public actual abstract class Mac: javax.crypto.Mac, Algorithm, Copyable, Re } /** @suppress */ @Deprecated("Do not use. Will be marked as ERROR in a later release") - protected final override fun engineReset() { reset() } + protected final override fun engineReset() { + if (consumeNextEngineReset) { + consumeNextEngineReset = false + return + } + reset() + } /** @suppress */ @Deprecated("Do not use. Will be marked as ERROR in a later release") protected final override fun engineGetMacLength(): Int = macLength() @@ -257,11 +298,14 @@ public actual abstract class Mac: javax.crypto.Mac, Algorithm, Copyable, Re /** @suppress */ @Deprecated("Do not use. Will be marked as ERROR in a later release") protected final override fun engineDoFinal(): ByteArray { + if (!resetOnDoFinal) consumeNextEngineReset = true + val b = doFinal() // Android API 23 and below javax.crypto.Mac does not call engineReset() + @Suppress("DEPRECATION") @OptIn(InternalKotlinCryptoApi::class) - KC_ANDROID_SDK_INT?.let { if (it <= 23) reset() } + if ((KC_ANDROID_SDK_INT ?: 24) < 24) engineReset() return b } diff --git a/library/mac/src/nonJvmMain/kotlin/org/kotlincrypto/core/mac/Mac.kt b/library/mac/src/nonJvmMain/kotlin/org/kotlincrypto/core/mac/Mac.kt index c267501..8765aa7 100644 --- a/library/mac/src/nonJvmMain/kotlin/org/kotlincrypto/core/mac/Mac.kt +++ b/library/mac/src/nonJvmMain/kotlin/org/kotlincrypto/core/mac/Mac.kt @@ -19,6 +19,7 @@ package org.kotlincrypto.core.mac import org.kotlincrypto.core.* import org.kotlincrypto.core.mac.internal.* +import kotlin.jvm.JvmField /** * Core abstraction for Message Authentication Code implementations. @@ -109,7 +110,7 @@ public actual abstract class Mac: Algorithm, Copyable, Resettable, Updatabl * */ public actual fun doFinal(): ByteArray { val final = engine.doFinal() - engine.reset() + if (engine.resetOnDoFinal) engine.reset() return final } @@ -136,6 +137,7 @@ public actual abstract class Mac: Algorithm, Copyable, Resettable, Updatabl public actual fun doFinalInto(dest: ByteArray, destOffset: Int): Int = commonDoFinalInto( dest = dest, destOffset = destOffset, + engineResetOnDoFinal = engine.resetOnDoFinal, engineDoFinalInto = engine::doFinalInto, engineReset = engine::reset ) @@ -172,19 +174,49 @@ public actual abstract class Mac: Algorithm, Copyable, Resettable, Updatabl protected actual abstract class Engine: Copyable, Resettable, Updatable { /** - * Initializes a new [Engine] with the provided [key]. + * Most [Mac.Engine] are backed by a `Digest`, whereby calling [reset] after + * [doFinal] will cause a double reset (because `Digest.digest` does this inherently). + * By setting this value to `false`, [Engine.reset] will **not** be called whenever + * [doFinal] gets invoked. * - * @throws [IllegalArgumentException] if [key] is empty. + * **NOTE:** Implementations taking ownership of the automatic reset functionality + * by setting this to `false` must ensure that whatever re-initialization steps were + * taken in their [Engine.reset] function body are executed before their [doFinal] + * and [doFinalInto] implementations return. + * */ + @JvmField + public actual val resetOnDoFinal: Boolean + + /** + * Initializes a new [Engine] with the provided [key] with the default [resetOnDoFinal] + * value of `true` (i.e. [Engine.reset] will be called automatically after [Engine.doFinal] + * or [Engine.doFinalInto] have been invoked). + * + * @param [key] The key that this [Engine] instance will use to apply its function to + * @throws [IllegalArgumentException] if [key] is empty + * */ + @Throws(IllegalArgumentException::class) + public actual constructor(key: ByteArray): this(key, resetOnDoFinal = true) + + /** + * Initializes a new [Engine] with the provided [key] and [resetOnDoFinal] configuration. + * + * @param [key] the key that this [Engine] instance will use to apply its function to + * @param [resetOnDoFinal] See [Engine.resetOnDoFinal] documentation + * @throws [IllegalArgumentException] if [key] is empty * */ @Throws(IllegalArgumentException::class) - public actual constructor(key: ByteArray) { + public actual constructor(key: ByteArray, resetOnDoFinal: Boolean) { require(key.isNotEmpty()) { "key cannot be empty" } + this.resetOnDoFinal = resetOnDoFinal } /** * Creates a new [Engine] from [other], copying its state. * */ - protected actual constructor(other: Engine) + protected actual constructor(other: Engine) { + this.resetOnDoFinal = other.resetOnDoFinal + } /** * The number of bytes the implementation returns when [doFinal] is called.