added expernal nonce support,

fixed key-based crypt functions
added hashes support
This commit is contained in:
Sergey Chernov 2024-06-13 11:45:37 +07:00
parent a71a666568
commit 13c37b983c
10 changed files with 144 additions and 30 deletions

View File

@ -20,7 +20,7 @@ kotlin {
browser()
}
linuxX64()
linuxArm64()
// linuxArm64()
// macosX64()
// macosArm64()

View File

@ -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 <reified T>DecryptingKey.decryptObject(cipherData: UByteArray): T =

View File

@ -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 <reified T>EncryptingKey.encryptObject(value: T,randomFill: IntRange? = null): UByteArray =

View File

@ -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)

View File

@ -0,0 +1,7 @@
package net.sergeych.crypto2
interface NonceBased {
val nonceBytesLength: Int
}
fun NonceBased.randomNonce(): UByteArray = randomBytes(nonceBytesLength)

View File

@ -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) }
}

View File

@ -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

View File

@ -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<WithFill>(
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<WithFill>(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
}
}

View File

@ -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<UByte>): UInt {
@ -60,6 +64,25 @@ fun <T: Comparable<T>>T.limit(range: ClosedRange<T>) = when {
fun <T: Comparable<T>>T.limitMax(max: T) = if( this < max ) this else max
fun <T: Comparable<T>>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 <T> 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)
}
}
}
}

View File

@ -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<DecryptionFailedException> {
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