forked from sergeych/crypto2
added expernal nonce support,
fixed key-based crypt functions added hashes support
This commit is contained in:
parent
a71a666568
commit
13c37b983c
@ -20,7 +20,7 @@ kotlin {
|
||||
browser()
|
||||
}
|
||||
linuxX64()
|
||||
linuxArm64()
|
||||
// linuxArm64()
|
||||
|
||||
// macosX64()
|
||||
// macosArm64()
|
||||
|
@ -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 =
|
||||
|
@ -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 =
|
||||
|
16
src/commonMain/kotlin/net/sergeych/crypto2/Hash.kt
Normal file
16
src/commonMain/kotlin/net/sergeych/crypto2/Hash.kt
Normal 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)
|
7
src/commonMain/kotlin/net/sergeych/crypto2/NonceBased.kt
Normal file
7
src/commonMain/kotlin/net/sergeych/crypto2/NonceBased.kt
Normal file
@ -0,0 +1,7 @@
|
||||
package net.sergeych.crypto2
|
||||
|
||||
interface NonceBased {
|
||||
val nonceBytesLength: Int
|
||||
}
|
||||
|
||||
fun NonceBased.randomNonce(): UByteArray = randomBytes(nonceBytesLength)
|
@ -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) }
|
||||
|
||||
|
||||
}
|
@ -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
|
||||
|
@ -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,30 +39,29 @@ 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
|
||||
}
|
||||
catch(_: com.ionspin.kotlin.crypto.secretbox.SecretBoxCorruptedOrTamperedDataExceptionOrInvalidKey) {
|
||||
throw DecryptionFailedException()
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
companion object {
|
||||
@ -70,6 +69,8 @@ class SymmetricKey(
|
||||
* Create a secure random symmetric key.
|
||||
*/
|
||||
fun random() = SymmetricKey(SecretBox.keygen())
|
||||
|
||||
val nonceByteLength = crypto_secretbox_NONCEBYTES
|
||||
}
|
||||
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
|
Loading…
x
Reference in New Issue
Block a user