From 13c37b983c11de62ae1b62e93731263cd17ecadf Mon Sep 17 00:00:00 2001 From: sergeych Date: Thu, 13 Jun 2024 11:45:37 +0700 Subject: [PATCH] added expernal nonce support, fixed key-based crypt functions added hashes support --- build.gradle.kts | 2 +- .../net/sergeych/crypto2/DecryptingKey.kt | 16 +++++++- .../net/sergeych/crypto2/EncryptingKey.kt | 15 +++++++- .../kotlin/net/sergeych/crypto2/Hash.kt | 16 ++++++++ .../kotlin/net/sergeych/crypto2/NonceBased.kt | 7 ++++ .../net/sergeych/crypto2/SafeKeyExchange.kt | 27 ++++++++++++-- .../kotlin/net/sergeych/crypto2/SealedBox.kt | 4 ++ .../net/sergeych/crypto2/SymmetricKey.kt | 37 ++++++++++--------- .../kotlin/net/sergeych/crypto2/tools.kt | 29 +++++++++++++-- src/commonTest/kotlin/KeysTest.kt | 21 +++++++++++ 10 files changed, 144 insertions(+), 30 deletions(-) create mode 100644 src/commonMain/kotlin/net/sergeych/crypto2/Hash.kt create mode 100644 src/commonMain/kotlin/net/sergeych/crypto2/NonceBased.kt diff --git a/build.gradle.kts b/build.gradle.kts index 269220e..b765995 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -20,7 +20,7 @@ kotlin { browser() } linuxX64() - linuxArm64() +// linuxArm64() // macosX64() // macosArm64() diff --git a/src/commonMain/kotlin/net/sergeych/crypto2/DecryptingKey.kt b/src/commonMain/kotlin/net/sergeych/crypto2/DecryptingKey.kt index 808caaa..19db003 100644 --- a/src/commonMain/kotlin/net/sergeych/crypto2/DecryptingKey.kt +++ b/src/commonMain/kotlin/net/sergeych/crypto2/DecryptingKey.kt @@ -2,12 +2,14 @@ package net.sergeych.crypto2 import com.ionspin.kotlin.crypto.util.decodeFromUByteArray import net.sergeych.bipack.BipackDecoder +import net.sergeych.bipack.decodeFromBipack +import net.sergeych.crypto2.SymmetricKey.WithNonce /** * Some key able to perform decrypting. It is not serializable by purpose, as not all such * keys are wise to transfer/save. Concrete implementations are, like [SymmetricKey]. */ -interface DecryptingKey { +interface DecryptingKey : NonceBased { /** * Authenticated decryption that checks the message is not tampered and therefor * the key is valid. It is not possible in general to distinguish whether the key is invalid @@ -15,9 +17,19 @@ interface DecryptingKey { * * @throws DecryptionFailedException if the key is not valid or [cipherData] tampered. */ - fun decrypt(cipherData: UByteArray): UByteArray + fun decrypt(cipherData: UByteArray): UByteArray = + protectDecryption { + val wn: WithNonce = cipherData.toByteArray().decodeFromBipack() + decryptWithNonce(wn.cipherData, wn.nonce) + } + + fun decryptWithNonce(cipherData: UByteArray, nonce: UByteArray): UByteArray + fun decryptString(cipherData: UByteArray): String = decrypt(cipherData).decodeFromUByteArray() + + val decryptingTag: UByteArray + } inline fun DecryptingKey.decryptObject(cipherData: UByteArray): T = diff --git a/src/commonMain/kotlin/net/sergeych/crypto2/EncryptingKey.kt b/src/commonMain/kotlin/net/sergeych/crypto2/EncryptingKey.kt index c61e90e..6b8a4fd 100644 --- a/src/commonMain/kotlin/net/sergeych/crypto2/EncryptingKey.kt +++ b/src/commonMain/kotlin/net/sergeych/crypto2/EncryptingKey.kt @@ -2,6 +2,7 @@ package net.sergeych.crypto2 import com.ionspin.kotlin.crypto.util.encodeToUByteArray import net.sergeych.bipack.BipackEncoder +import net.sergeych.crypto2.SymmetricKey.WithNonce /** * Some key able to encrypt data with optional random fill that conceals message size @@ -10,15 +11,25 @@ import net.sergeych.bipack.BipackEncoder * It is not serializable by design. * Custom implementations are, see [SymmetricKey] for example. */ -interface EncryptingKey { +interface EncryptingKey : NonceBased { /** * Authenticated encrypting with optional random fill to protect from message size analysis. * Note that [randomFill] if present should be positive. */ - fun encrypt(plainData: UByteArray,randomFill: IntRange?=null): UByteArray + fun encrypt(plainData: UByteArray,randomFill: IntRange?=null): UByteArray { + val nonce = randomNonce() + return BipackEncoder.encode(WithNonce( + encryptWithNonce(plainData,nonce,randomFill), + nonce) + ).toUByteArray() + } fun encrypt(plainText: String,randomFill: IntRange? = null): UByteArray = encrypt(plainText.encodeToUByteArray(),randomFill) + + val encryptingTag: UByteArray + + fun encryptWithNonce(plainData: UByteArray, nonce: UByteArray, randomFill: IntRange? = null): UByteArray } inline fun EncryptingKey.encryptObject(value: T,randomFill: IntRange? = null): UByteArray = diff --git a/src/commonMain/kotlin/net/sergeych/crypto2/Hash.kt b/src/commonMain/kotlin/net/sergeych/crypto2/Hash.kt new file mode 100644 index 0000000..63c8cfe --- /dev/null +++ b/src/commonMain/kotlin/net/sergeych/crypto2/Hash.kt @@ -0,0 +1,16 @@ +package net.sergeych.crypto2 + +import com.ionspin.kotlin.crypto.generichash.GenericHash +import org.komputing.khash.keccak.Keccak +import org.komputing.khash.keccak.KeccakParameter + +@Suppress("unused") +enum class Hash(val perform: (UByteArray)->UByteArray) { + Blake2b({ GenericHash.genericHash(it) }), + Blake2b2l({ blake2b2l(it) }), + Sha3_384({ Keccak.digest(it.toByteArray(), KeccakParameter.SHA3_384).toUByteArray()}), + Sha3_256({ Keccak.digest(it.toByteArray(), KeccakParameter.SHA3_256).toUByteArray()}), +} + +fun blake2b(src: UByteArray): UByteArray = Hash.Blake2b.perform(src) +fun blake2b2l(src: UByteArray): UByteArray = blake2b(blake2b(src) + src) diff --git a/src/commonMain/kotlin/net/sergeych/crypto2/NonceBased.kt b/src/commonMain/kotlin/net/sergeych/crypto2/NonceBased.kt new file mode 100644 index 0000000..4804d4d --- /dev/null +++ b/src/commonMain/kotlin/net/sergeych/crypto2/NonceBased.kt @@ -0,0 +1,7 @@ +package net.sergeych.crypto2 + +interface NonceBased { + val nonceBytesLength: Int +} + +fun NonceBased.randomNonce(): UByteArray = randomBytes(nonceBytesLength) diff --git a/src/commonMain/kotlin/net/sergeych/crypto2/SafeKeyExchange.kt b/src/commonMain/kotlin/net/sergeych/crypto2/SafeKeyExchange.kt index 913d37f..84cc297 100644 --- a/src/commonMain/kotlin/net/sergeych/crypto2/SafeKeyExchange.kt +++ b/src/commonMain/kotlin/net/sergeych/crypto2/SafeKeyExchange.kt @@ -38,10 +38,29 @@ class SafeKeyExchange { * security level and allow using counters as nonce with no extra precautions. */ @Serializable - class SessionKey internal constructor( + class SessionKey( val sendingKey: EncryptingKey, val receivingKey: DecryptingKey, - ): CipherKey, EncryptingKey by sendingKey, DecryptingKey by receivingKey + val isClient: Boolean, + ) : CipherKey, EncryptingKey by sendingKey, DecryptingKey by receivingKey { + + override val nonceBytesLength: Int = sendingKey.nonceBytesLength + + /** + * The unique per-session confidential tag, same on both sides. + * It can't be derived from public keys alone. + * It is often as a base for authentication tokens and nonce generation as + * is known immediately at session start and does not need additional data exchange. + */ + @Suppress("unused") + val sessionTag: UByteArray by lazy { + if (!isClient) + blake2b(decryptingTag + encryptingTag) + else + blake2b(encryptingTag + decryptingTag) + } + + } /** * The public key; it should be transmitted to the other party, this is serializable. @@ -66,7 +85,7 @@ class SafeKeyExchange { */ fun clientSessionKey(serverPublicKey: PublicKey): SessionKey = KeyExchange.clientSessionKeys(pair.publicKey, pair.secretKey, serverPublicKey.keyBytes) - .let { SessionKey(SymmetricKey(it.sendKey), SymmetricKey(it.receiveKey))} + .let { SessionKey(SymmetricKey(it.sendKey), SymmetricKey(it.receiveKey), isClient = true) } /** * Create an asymmetric [SessionKey] instance to work with [clientSessionKey] on the other side. @@ -74,7 +93,7 @@ class SafeKeyExchange { */ fun serverSessionKey(clientPublicKey: PublicKey): SessionKey = KeyExchange.serverSessionKeys(pair.publicKey, pair.secretKey, clientPublicKey.keyBytes) - .let { SessionKey(SymmetricKey(it.sendKey), SymmetricKey(it.receiveKey))} + .let { SessionKey(SymmetricKey(it.sendKey), SymmetricKey(it.receiveKey), isClient = false) } } \ No newline at end of file diff --git a/src/commonMain/kotlin/net/sergeych/crypto2/SealedBox.kt b/src/commonMain/kotlin/net/sergeych/crypto2/SealedBox.kt index 92bac31..1c32dc0 100644 --- a/src/commonMain/kotlin/net/sergeych/crypto2/SealedBox.kt +++ b/src/commonMain/kotlin/net/sergeych/crypto2/SealedBox.kt @@ -29,6 +29,10 @@ class SealedBox( private val checkOnInit: Boolean = true ) { + @Suppress("unused") + constructor(message: UByteArray, vararg keys: SigningKey.Secret) : + this(message, keys.map { it.seal(message) } ) + /** * If this instance is not signed by a given key, return new instance signed also by this * key, or return unchanged (same) object if it is already signed by this key; you diff --git a/src/commonMain/kotlin/net/sergeych/crypto2/SymmetricKey.kt b/src/commonMain/kotlin/net/sergeych/crypto2/SymmetricKey.kt index 6b0f5d0..f5a8f30 100644 --- a/src/commonMain/kotlin/net/sergeych/crypto2/SymmetricKey.kt +++ b/src/commonMain/kotlin/net/sergeych/crypto2/SymmetricKey.kt @@ -1,8 +1,8 @@ package net.sergeych.crypto2 import com.ionspin.kotlin.crypto.secretbox.SecretBox +import com.ionspin.kotlin.crypto.secretbox.crypto_secretbox_NONCEBYTES import kotlinx.serialization.Serializable -import net.sergeych.bintools.toDataSource import net.sergeych.bipack.BipackDecoder import net.sergeych.bipack.BipackEncoder @@ -19,8 +19,8 @@ import net.sergeych.bipack.BipackEncoder */ @Serializable class SymmetricKey( - val keyBytes: UByteArray -): CipherKey { + val keyBytes: UByteArray, +) : CipherKey { /** * @suppress @@ -39,37 +39,38 @@ class SymmetricKey( @Serializable data class WithFill( val data: UByteArray, - val safetyFill: UByteArray? = null + val safetyFill: UByteArray? = null, ) - override fun encrypt(plainData: UByteArray,randomFill: IntRange?): UByteArray { + override fun encryptWithNonce(plainData: UByteArray, nonce: UByteArray, randomFill: IntRange?): UByteArray { + require(nonce.size == nonceByteLength) + val fill = randomFill?.let { require(it.start >= 0) randomBytes(randomInt(it)) } val filled = BipackEncoder.encode(WithFill(plainData, fill)) - val nonce = randomSecretboxNonce() - val encrypted = SecretBox.easy(filled.toUByteArray(), nonce, keyBytes) - return BipackEncoder.encode(WithNonce(encrypted, nonce)).toUByteArray() + return SecretBox.easy(filled.toUByteArray(), nonce, keyBytes) } - override fun decrypt(cipherData: UByteArray): UByteArray { - val wn: WithNonce = BipackDecoder.Companion.decode(cipherData.toDataSource()) - try { - return BipackDecoder.Companion.decode( - SecretBox.openEasy(wn.cipherData, wn.nonce, keyBytes).toDataSource() - ).data + override val nonceBytesLength: Int = nonceByteLength + + override val encryptingTag: UByteArray by lazy { blake2b2l(keyBytes) } + override val decryptingTag: UByteArray get() = encryptingTag + + override fun decryptWithNonce(cipherData: UByteArray, nonce: UByteArray): UByteArray = + protectDecryption { + BipackDecoder.decode(SecretBox.openEasy(cipherData, nonce, keyBytes).toByteArray()) + .data } - catch(_: com.ionspin.kotlin.crypto.secretbox.SecretBoxCorruptedOrTamperedDataExceptionOrInvalidKey) { - throw DecryptionFailedException() - } - } companion object { /** * Create a secure random symmetric key. */ fun random() = SymmetricKey(SecretBox.keygen()) + + val nonceByteLength = crypto_secretbox_NONCEBYTES } } \ No newline at end of file diff --git a/src/commonMain/kotlin/net/sergeych/crypto2/tools.kt b/src/commonMain/kotlin/net/sergeych/crypto2/tools.kt index b86a752..1d6176d 100644 --- a/src/commonMain/kotlin/net/sergeych/crypto2/tools.kt +++ b/src/commonMain/kotlin/net/sergeych/crypto2/tools.kt @@ -2,11 +2,15 @@ package net.sergeych.crypto2 -import com.ionspin.kotlin.crypto.secretbox.crypto_secretbox_NONCEBYTES +import com.ionspin.kotlin.crypto.secretbox.SecretBoxCorruptedOrTamperedDataExceptionOrInvalidKey import com.ionspin.kotlin.crypto.util.LibsodiumRandom +import kotlinx.coroutines.CancellationException import kotlinx.coroutines.channels.ReceiveChannel +import net.sergeych.bintools.DataSource -class DecryptionFailedException : RuntimeException("can't encrypt: wrong key or tampered message") +class DecryptionFailedException(text: String="can't decrypt: wrong key or tampered message", + cause: Throwable?=null) : RuntimeException(text, cause) { +} suspend fun readVarUnsigned(input: ReceiveChannel): UInt { @@ -60,6 +64,25 @@ fun >T.limit(range: ClosedRange) = when { fun >T.limitMax(max: T) = if( this < max ) this else max fun >T.limitMin(min: T) = if( this > min ) this else min -fun randomSecretboxNonce(): UByteArray = randomBytes(crypto_secretbox_NONCEBYTES) +/** + * Properly catch various exceptions, and rethrow them as [DecryptionFailedException], but rethrow + * [CancellationException] and [DecryptionFailedException] if thrown. + */ +fun protectDecryption(f: () -> T): T { + return try { + f() + } catch (x: Exception) { + when (x) { + is SecretBoxCorruptedOrTamperedDataExceptionOrInvalidKey, + is DataSource.EndOfData, + -> throw DecryptionFailedException(cause = x) + is CancellationException, is DecryptionFailedException -> throw x + else -> { + println("unexpected exception while decrypting:\n${x.stackTraceToString()}") + throw DecryptionFailedException(cause = x) + } + } + } +} diff --git a/src/commonTest/kotlin/KeysTest.kt b/src/commonTest/kotlin/KeysTest.kt index acaf34e..786aa2c 100644 --- a/src/commonTest/kotlin/KeysTest.kt +++ b/src/commonTest/kotlin/KeysTest.kt @@ -85,6 +85,25 @@ class KeysTest { } } + @Test + fun symmetricKeyTest() = runTest { + initCrypto() + val k1 = SymmetricKey.random() + val src = "Buena Vista".encodeToUByteArray() + val nonce = k1.randomNonce() + + assertContentEquals(src, k1.decryptWithNonce(k1.encryptWithNonce(src, nonce), nonce)) + assertThrows { + val n2 = nonce.copyOf() + n2[4] = n2[4].inv() + k1.decryptWithNonce(k1.encryptWithNonce(src, nonce), n2) + } + + assertContentEquals(src, k1.decrypt(k1.encrypt(src))) + assertContentEquals(src, k1.decrypt(k1.encrypt(src, 0..117))) + assertContentEquals(src, k1.decrypt(k1.encrypt(src, 7..117))) + } + @Test fun keyExchangeTest() = runTest { initCrypto() @@ -105,6 +124,8 @@ class KeysTest { assertEquals(src, clientSessionKey.decryptString(serverSessionKey.encrypt(src))) assertEquals(src, clientSessionKey.decryptString(serverSessionKey.encrypt(src))) + assertContentEquals(clientSessionKey.sessionTag, serverSessionKey.sessionTag) + } @Test