Skip to content

Add ability to inhibit Mac.Engine.reset calls whenever doFinal is invoked #111

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/mac/api/mac.api
Original file line number Diff line number Diff line change
Expand Up @@ -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 <init> (Lorg/kotlincrypto/core/mac/Mac$Engine;)V
public fun <init> ([B)V
public fun <init> ([BZ)V
public final fun clone ()Ljava/lang/Object;
public abstract fun doFinal ()[B
public fun doFinalInto ([BI)V
Expand Down
4 changes: 4 additions & 0 deletions library/mac/api/mac.klib.api
Original file line number Diff line number Diff line change
Expand Up @@ -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.mac/Mac.Engine>, org.kotlincrypto.core/Resettable, org.kotlincrypto.core/Updatable { // org.kotlincrypto.core.mac/Mac.Engine|null[0]
constructor <init>(kotlin/ByteArray) // org.kotlincrypto.core.mac/Mac.Engine.<init>|<init>(kotlin.ByteArray){}[0]
constructor <init>(kotlin/ByteArray, kotlin/Boolean) // org.kotlincrypto.core.mac/Mac.Engine.<init>|<init>(kotlin.ByteArray;kotlin.Boolean){}[0]
constructor <init>(org.kotlincrypto.core.mac/Mac.Engine) // org.kotlincrypto.core.mac/Mac.Engine.<init>|<init>(org.kotlincrypto.core.mac.Mac.Engine){}[0]

final val resetOnDoFinal // org.kotlincrypto.core.mac/Mac.Engine.resetOnDoFinal|<get-resetOnDoFinal>(){}[0]
final fun <get-resetOnDoFinal>(): kotlin/Boolean // org.kotlincrypto.core.mac/Mac.Engine.resetOnDoFinal.<get-resetOnDoFinal>|<get-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]
Expand Down
32 changes: 30 additions & 2 deletions library/mac/src/commonMain/kotlin/org/kotlincrypto/core/mac/Mac.kt
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
package org.kotlincrypto.core.mac

import org.kotlincrypto.core.*
import kotlin.jvm.JvmField

/**
* Core abstraction for Message Authentication Code implementations.
Expand Down Expand Up @@ -141,13 +142,40 @@ public expect abstract class Mac: Algorithm, Copyable<Mac>, Resettable, Updatabl
protected abstract class Engine: Copyable<Engine>, 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.
* */
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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
Expand Down
56 changes: 50 additions & 6 deletions library/mac/src/jvmMain/kotlin/org/kotlincrypto/core/mac/Mac.kt
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,7 @@ public actual abstract class Mac: javax.crypto.Mac, Algorithm, Copyable<Mac>, Re
public actual fun doFinalInto(dest: ByteArray, destOffset: Int): Int = commonDoFinalInto(
dest = dest,
destOffset = destOffset,
engineResetOnDoFinal = engine.resetOnDoFinal,
engineDoFinalInto = engine::doFinalInto,
engineReset = engine::reset,
)
Expand Down Expand Up @@ -150,19 +151,49 @@ public actual abstract class Mac: javax.crypto.Mac, Algorithm, Copyable<Mac>, Re
protected actual abstract class Engine: MacSpi, Cloneable, Copyable<Engine>, 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.
Expand Down Expand Up @@ -212,6 +243,10 @@ public actual abstract class Mac: javax.crypto.Mac, Algorithm, Copyable<Mac>, 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")
Expand All @@ -230,7 +265,13 @@ public actual abstract class Mac: javax.crypto.Mac, Algorithm, Copyable<Mac>, 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()
Expand All @@ -257,11 +298,14 @@ public actual abstract class Mac: javax.crypto.Mac, Algorithm, Copyable<Mac>, 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
}
Expand Down
42 changes: 37 additions & 5 deletions library/mac/src/nonJvmMain/kotlin/org/kotlincrypto/core/mac/Mac.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -109,7 +110,7 @@ public actual abstract class Mac: Algorithm, Copyable<Mac>, Resettable, Updatabl
* */
public actual fun doFinal(): ByteArray {
val final = engine.doFinal()
engine.reset()
if (engine.resetOnDoFinal) engine.reset()
return final
}

Expand All @@ -136,6 +137,7 @@ public actual abstract class Mac: Algorithm, Copyable<Mac>, 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
)
Expand Down Expand Up @@ -172,19 +174,49 @@ public actual abstract class Mac: Algorithm, Copyable<Mac>, Resettable, Updatabl
protected actual abstract class Engine: Copyable<Engine>, 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.
Expand Down
Loading