diff --git a/build.gradle.kts b/build.gradle.kts index bd056f3..f506a39 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -23,7 +23,7 @@ kotlin { // linuxArm64() // macosX64() -// macosArm64() +// macosArm64() ?: break // iosX64() // iosArm64() // iosSimulatorArm64() diff --git a/src/commonMain/kotlin/net/sergeych/crypto2/AsymmetricKey.kt b/src/commonMain/kotlin/net/sergeych/crypto2/AsymmetricKey.kt index d445c1e..4ff6758 100644 --- a/src/commonMain/kotlin/net/sergeych/crypto2/AsymmetricKey.kt +++ b/src/commonMain/kotlin/net/sergeych/crypto2/AsymmetricKey.kt @@ -105,7 +105,7 @@ object Asymmetric { return KeyPair(SecretKey(p.secretKey, pk), pk) } - private fun randomNonce(): UByteArray = randomBytes(crypto_box_NONCEBYTES) + private fun randomNonce(): UByteArray = randomUBytes(crypto_box_NONCEBYTES) /** * The public key used as the recipient for [Message] (see [SecretKey.encrypt], etc.). It also diff --git a/src/commonMain/kotlin/net/sergeych/crypto2/NonceBased.kt b/src/commonMain/kotlin/net/sergeych/crypto2/NonceBased.kt index 4804d4d..a41baf7 100644 --- a/src/commonMain/kotlin/net/sergeych/crypto2/NonceBased.kt +++ b/src/commonMain/kotlin/net/sergeych/crypto2/NonceBased.kt @@ -4,4 +4,4 @@ interface NonceBased { val nonceBytesLength: Int } -fun NonceBased.randomNonce(): UByteArray = randomBytes(nonceBytesLength) +fun NonceBased.randomNonce(): UByteArray = randomUBytes(nonceBytesLength) diff --git a/src/commonMain/kotlin/net/sergeych/crypto2/NumericNonce.kt b/src/commonMain/kotlin/net/sergeych/crypto2/NumericNonce.kt new file mode 100644 index 0000000..caf45c6 --- /dev/null +++ b/src/commonMain/kotlin/net/sergeych/crypto2/NumericNonce.kt @@ -0,0 +1,60 @@ +package net.sergeych.crypto2 + +/** + * Numeric nonce is most often used to compress nonce data up to several bytes long counter, + * or even recalculate counter on both sides synchronously. This class combines (XORs) a number + * with a byte-array nonce specified on creation. This "extends" number to the desired size. + */ +class NumericNonce(val base: UByteArray) { + + /** + * Combines [base] by XORing its byte representation with a [base] + * @throws NonceOutOfBoundsException if the argument is too big to be kept in the nonce. For example, + * nonce of 4 bytes [base] can't be combined with a number > 2^32, etc. + */ + fun withULong(numericNonce: ULong): UByteArray { + val result = base.copyOf() + var x = numericNonce + var i = 0 + while (x > 0u) { + result[i] = result[i] xor (x and 0xFFu).toUByte() + x = x shr 8 + if( i++ >= base.size) + throw NonceOutOfBoundsException("this nonce has only ${base.size} bytes length and can hold nonce $numericNonce") + } + return result + } + + /** + * Combines [base] by XORing its byte representation with a [base], see [withULong] + */ + @Suppress("unused") + fun withLong(intNonce: Long): UByteArray { + require( intNonce >= 0 ) + return withULong(intNonce.toULong()) + } + + /** + * Combines [base] by XORing its byte representation with a [base], see [withULong] + */ + fun withInt(intNonce: Int): UByteArray { + require( intNonce >= 0 ) + return withULong(intNonce.toULong()) + } + + + /** + * Combines [base] by XORing its byte representation with a [base], see [withULong] + */ + @Suppress("unused") + fun withUInt(intNonce: UInt): UByteArray { + return withULong(intNonce.toULong()) + } + + companion object { + /** + * Create a random numeric nonce with a specified size. + */ + fun random(size: Int) = NumericNonce(randomUBytes(size)) + } +} diff --git a/src/commonMain/kotlin/net/sergeych/crypto2/Seal.kt b/src/commonMain/kotlin/net/sergeych/crypto2/Seal.kt index 296e3c7..33a04d6 100644 --- a/src/commonMain/kotlin/net/sergeych/crypto2/Seal.kt +++ b/src/commonMain/kotlin/net/sergeych/crypto2/Seal.kt @@ -136,7 +136,7 @@ class Seal( expiresAt: Instant? = null, nonDeterministic: Boolean = false ): Seal { - val nonce = if( nonDeterministic ) randomBytes(32) else null + val nonce = if( nonDeterministic ) randomUBytes(32) else null val data = BipackEncoder.encode(SealedData(message, nonce, createdAt, expiresAt)).toUByteArray() return Seal(key.publicKey, key.sign(data), nonce, createdAt, expiresAt) } diff --git a/src/commonMain/kotlin/net/sergeych/crypto2/SymmetricKey.kt b/src/commonMain/kotlin/net/sergeych/crypto2/SymmetricKey.kt index f5a8f30..906144a 100644 --- a/src/commonMain/kotlin/net/sergeych/crypto2/SymmetricKey.kt +++ b/src/commonMain/kotlin/net/sergeych/crypto2/SymmetricKey.kt @@ -47,7 +47,7 @@ class SymmetricKey( val fill = randomFill?.let { require(it.start >= 0) - randomBytes(randomInt(it)) + randomUBytes(randomInt(it)) } val filled = BipackEncoder.encode(WithFill(plainData, fill)) return SecretBox.easy(filled.toUByteArray(), nonce, keyBytes) diff --git a/src/commonMain/kotlin/net/sergeych/crypto2/exceptions.kt b/src/commonMain/kotlin/net/sergeych/crypto2/exceptions.kt new file mode 100644 index 0000000..fdec44a --- /dev/null +++ b/src/commonMain/kotlin/net/sergeych/crypto2/exceptions.kt @@ -0,0 +1,11 @@ +package net.sergeych.crypto2 + +/** + * Any exception initiated by crypto2 library has this base class + */ +open class Crypto2Exception(text: String,cause: Throwable?=null): RuntimeException(text, cause) + +class DecryptionFailedException(text: String="can't decrypt: wrong key or tampered message", + cause: Throwable?=null): Crypto2Exception(text,cause) + +class NonceOutOfBoundsException(text: String): Crypto2Exception(text) \ 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 1d6176d..280a91f 100644 --- a/src/commonMain/kotlin/net/sergeych/crypto2/tools.kt +++ b/src/commonMain/kotlin/net/sergeych/crypto2/tools.kt @@ -8,10 +8,6 @@ import kotlinx.coroutines.CancellationException import kotlinx.coroutines.channels.ReceiveChannel import net.sergeych.bintools.DataSource -class DecryptionFailedException(text: String="can't decrypt: wrong key or tampered message", - cause: Throwable?=null) : RuntimeException(text, cause) { -} - suspend fun readVarUnsigned(input: ReceiveChannel): UInt { var result = 0u @@ -38,9 +34,9 @@ fun encodeVarUnsigned(value: UInt): UByteArray { } -fun randomBytes(n: Int): UByteArray = if (n > 0) LibsodiumRandom.buf(n) else ubyteArrayOf() +fun randomUBytes(n: Int): UByteArray = if (n > 0) LibsodiumRandom.buf(n) else ubyteArrayOf() -fun randomBytes(n: UInt): UByteArray = if (n > 0u) LibsodiumRandom.buf(n.toInt()) else ubyteArrayOf() +fun randomUBytes(n: UInt): UByteArray = if (n > 0u) LibsodiumRandom.buf(n.toInt()) else ubyteArrayOf() /** * Uniform random in `0 ..< max` range diff --git a/src/commonTest/kotlin/ToolsTest.kt b/src/commonTest/kotlin/ToolsTest.kt index ae19541..4319a5f 100644 --- a/src/commonTest/kotlin/ToolsTest.kt +++ b/src/commonTest/kotlin/ToolsTest.kt @@ -1,11 +1,6 @@ import kotlinx.coroutines.test.runTest -import net.sergeych.crypto2.createContrail -import net.sergeych.crypto2.initCrypto -import net.sergeych.crypto2.isValidContrail -import kotlin.test.Test -import kotlin.test.assertEquals -import kotlin.test.assertFalse -import kotlin.test.assertTrue +import net.sergeych.crypto2.* +import kotlin.test.* class ToolsTest { @Test @@ -17,4 +12,18 @@ class ToolsTest { c[2] = 11u assertFalse { isValidContrail(c) } } + + @Test + fun numericNonceTest() = runTest{ + initCrypto() + val nn = NumericNonce.random(32) + val counter = 1031 + val x1 = nn.withInt(0) + val x2 = nn.withInt(counter) + println(x1.toDump()) + println(x2.toDump()) + val t = (x1[0] xor x2[0]).toInt() + ((x1[1] xor x2[1]).toInt() shl 8) + assertEquals(counter, t) + assertContentEquals(x1.drop(2), x2.drop(2)) + } } \ No newline at end of file