diff --git a/build.gradle.kts b/build.gradle.kts index 5086bc6..3071a5a 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -20,7 +20,7 @@ plugins { } group = "net.sergeych" -version = "0.9.0" +version = "0.9.1-SNAPSHOT" repositories { mavenCentral() diff --git a/src/commonMain/kotlin/net/sergeych/crypto2/SymmetricKey.kt b/src/commonMain/kotlin/net/sergeych/crypto2/SymmetricKey.kt index 7c689de..6805b4b 100644 --- a/src/commonMain/kotlin/net/sergeych/crypto2/SymmetricKey.kt +++ b/src/commonMain/kotlin/net/sergeych/crypto2/SymmetricKey.kt @@ -46,13 +46,54 @@ class SymmetricKey( data class WithNonce( val cipherData: UByteArray, val nonce: UByteArray, - ) + ) { + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other == null || this::class != other::class) return false + + other as WithNonce + + if (!cipherData.contentEquals(other.cipherData)) return false + if (!nonce.contentEquals(other.nonce)) return false + + return true + } + + override fun hashCode(): Int { + var result = cipherData.contentHashCode() + result = 31 * result + nonce.contentHashCode() + return result + } + } override fun encryptWithNonce(plainData: UByteArray, nonce: UByteArray, randomFill: IntRange?): UByteArray { require(nonce.size == nonceLength) return SecretBox.easy(WithFill.encode(plainData, randomFill), nonce, keyBytes) } + /** + * Compact authenticated encryption with a caller-provided nonce. + * + * This method is meant for storage formats where the nonce is derived or stored elsewhere + * and every byte of encoded output matters, for example short encrypted file names. + * The output is the raw SecretBox ciphertext only: [plainData] size plus 16 authentication + * bytes. The nonce is not stored in the output and must be reproduced exactly for + * [decryptCompactWithNonce]. + * + * Unlike [encryptWithNonce], this method does not wrap [plainData] with [WithFill]. For + * short payloads this usually saves only 1 byte over [encryptWithNonce], because both + * methods already require the caller to manage the nonce separately. Use it only when this + * small size gain is important or when the surrounding format explicitly requires raw + * SecretBox output. + * + * Never reuse the same nonce with the same key for different plaintext. Reusing a + * `(key, nonce)` pair with SecretBox breaks the security of the stream cipher. + */ + fun encryptCompactWithNonce(plainData: UByteArray, nonce: UByteArray): UByteArray { + require(nonce.size == nonceLength) + return SecretBox.easy(plainData, nonce, keyBytes) + } + override val nonceBytesLength: Int = nonceLength override val id by lazy { @@ -64,6 +105,22 @@ class SymmetricKey( WithFill.decode(SecretBox.openEasy(cipherData, nonce, keyBytes)) } + /** + * Compact authenticated decryption with a caller-provided nonce. + * + * Decrypts data produced by [encryptCompactWithNonce]. The nonce is not read from + * [cipherData] and must be the exact same nonce that was used for encryption. Use this + * method only for compact formats that intentionally omit both the nonce and the [WithFill] + * wrapper. + * + * @throws DecryptionFailedException if the key, nonce, or cipher data is invalid. + */ + fun decryptCompactWithNonce(cipherData: UByteArray, nonce: UByteArray): UByteArray = + protectDecryption { + require(nonce.size == nonceLength) + SecretBox.openEasy(cipherData, nonce, keyBytes) + } + override fun equals(other: Any?): Boolean { if (this === other) return true if (other !is SymmetricKey) return false @@ -85,4 +142,4 @@ class SymmetricKey( val keyLength = crypto_secretbox_KEYBYTES } -} \ No newline at end of file +} diff --git a/src/commonTest/kotlin/KeysTest.kt b/src/commonTest/kotlin/KeysTest.kt index 6c43217..be8f7e2 100644 --- a/src/commonTest/kotlin/KeysTest.kt +++ b/src/commonTest/kotlin/KeysTest.kt @@ -11,7 +11,6 @@ import com.ionspin.kotlin.crypto.util.decodeFromUByteArray import com.ionspin.kotlin.crypto.util.encodeToUByteArray import kotlinx.coroutines.test.runTest -import kotlinx.serialization.encodeToString import kotlinx.serialization.json.Json import net.sergeych.bipack.BipackDecoder import net.sergeych.bipack.BipackEncoder @@ -118,6 +117,44 @@ class KeysTest { assertContentEquals(src, k1.decrypt(k1.encrypt(src, 7..117))) } + @Test + fun symmetricKeyCompactTest() = runTest { + initCrypto() + val key = SymmetricKey.new() + val otherKey = SymmetricKey.new() + val src = "readme.md".encodeToUByteArray() + val nonce = key.randomNonce() + val cipher = key.encryptCompactWithNonce(src, nonce) + + assertEquals(src.size + 16, cipher.size) + assertTrue(cipher.size < key.encryptWithNonce(src, nonce).size) + assertTrue(cipher.size < key.encrypt(src).size) + assertContentEquals(src, key.decryptCompactWithNonce(cipher, nonce)) + + assertThrows { + otherKey.decryptCompactWithNonce(cipher, nonce) + } + + assertThrows { + val n2 = nonce.copyOf() + n2[4] = n2[4].inv() + key.decryptCompactWithNonce(cipher, n2) + } + + assertThrows { + val c2 = cipher.copyOf() + c2[3] = c2[3].inv() + key.decryptCompactWithNonce(c2, nonce) + } + + assertThrows { + key.encryptCompactWithNonce(src, nonce.dropLast(1).toUByteArray()) + } + assertThrows { + key.decryptCompactWithNonce(cipher, nonce.dropLast(1).toUByteArray()) + } + } + @Test fun keyExchangeTest() = runTest { initCrypto()