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()
|
browser()
|
||||||
}
|
}
|
||||||
linuxX64()
|
linuxX64()
|
||||||
linuxArm64()
|
// linuxArm64()
|
||||||
|
|
||||||
// macosX64()
|
// macosX64()
|
||||||
// macosArm64()
|
// macosArm64()
|
||||||
|
@ -2,12 +2,14 @@ package net.sergeych.crypto2
|
|||||||
|
|
||||||
import com.ionspin.kotlin.crypto.util.decodeFromUByteArray
|
import com.ionspin.kotlin.crypto.util.decodeFromUByteArray
|
||||||
import net.sergeych.bipack.BipackDecoder
|
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
|
* 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].
|
* 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
|
* 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
|
* 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.
|
* @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()
|
fun decryptString(cipherData: UByteArray): String = decrypt(cipherData).decodeFromUByteArray()
|
||||||
|
|
||||||
|
val decryptingTag: UByteArray
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
inline fun <reified T>DecryptingKey.decryptObject(cipherData: UByteArray): T =
|
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 com.ionspin.kotlin.crypto.util.encodeToUByteArray
|
||||||
import net.sergeych.bipack.BipackEncoder
|
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
|
* 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.
|
* It is not serializable by design.
|
||||||
* Custom implementations are, see [SymmetricKey] for example.
|
* Custom implementations are, see [SymmetricKey] for example.
|
||||||
*/
|
*/
|
||||||
interface EncryptingKey {
|
interface EncryptingKey : NonceBased {
|
||||||
/**
|
/**
|
||||||
* Authenticated encrypting with optional random fill to protect from message size analysis.
|
* Authenticated encrypting with optional random fill to protect from message size analysis.
|
||||||
* Note that [randomFill] if present should be positive.
|
* 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 =
|
fun encrypt(plainText: String,randomFill: IntRange? = null): UByteArray =
|
||||||
encrypt(plainText.encodeToUByteArray(),randomFill)
|
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 =
|
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.
|
* security level and allow using counters as nonce with no extra precautions.
|
||||||
*/
|
*/
|
||||||
@Serializable
|
@Serializable
|
||||||
class SessionKey internal constructor(
|
class SessionKey(
|
||||||
val sendingKey: EncryptingKey,
|
val sendingKey: EncryptingKey,
|
||||||
val receivingKey: DecryptingKey,
|
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.
|
* 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 =
|
fun clientSessionKey(serverPublicKey: PublicKey): SessionKey =
|
||||||
KeyExchange.clientSessionKeys(pair.publicKey, pair.secretKey, serverPublicKey.keyBytes)
|
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.
|
* Create an asymmetric [SessionKey] instance to work with [clientSessionKey] on the other side.
|
||||||
@ -74,7 +93,7 @@ class SafeKeyExchange {
|
|||||||
*/
|
*/
|
||||||
fun serverSessionKey(clientPublicKey: PublicKey): SessionKey =
|
fun serverSessionKey(clientPublicKey: PublicKey): SessionKey =
|
||||||
KeyExchange.serverSessionKeys(pair.publicKey, pair.secretKey, clientPublicKey.keyBytes)
|
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
|
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
|
* 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
|
* key, or return unchanged (same) object if it is already signed by this key; you
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
package net.sergeych.crypto2
|
package net.sergeych.crypto2
|
||||||
|
|
||||||
import com.ionspin.kotlin.crypto.secretbox.SecretBox
|
import com.ionspin.kotlin.crypto.secretbox.SecretBox
|
||||||
|
import com.ionspin.kotlin.crypto.secretbox.crypto_secretbox_NONCEBYTES
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
import net.sergeych.bintools.toDataSource
|
|
||||||
import net.sergeych.bipack.BipackDecoder
|
import net.sergeych.bipack.BipackDecoder
|
||||||
import net.sergeych.bipack.BipackEncoder
|
import net.sergeych.bipack.BipackEncoder
|
||||||
|
|
||||||
@ -19,8 +19,8 @@ import net.sergeych.bipack.BipackEncoder
|
|||||||
*/
|
*/
|
||||||
@Serializable
|
@Serializable
|
||||||
class SymmetricKey(
|
class SymmetricKey(
|
||||||
val keyBytes: UByteArray
|
val keyBytes: UByteArray,
|
||||||
): CipherKey {
|
) : CipherKey {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @suppress
|
* @suppress
|
||||||
@ -39,30 +39,29 @@ class SymmetricKey(
|
|||||||
@Serializable
|
@Serializable
|
||||||
data class WithFill(
|
data class WithFill(
|
||||||
val data: UByteArray,
|
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 {
|
val fill = randomFill?.let {
|
||||||
require(it.start >= 0)
|
require(it.start >= 0)
|
||||||
randomBytes(randomInt(it))
|
randomBytes(randomInt(it))
|
||||||
}
|
}
|
||||||
val filled = BipackEncoder.encode(WithFill(plainData, fill))
|
val filled = BipackEncoder.encode(WithFill(plainData, fill))
|
||||||
val nonce = randomSecretboxNonce()
|
return SecretBox.easy(filled.toUByteArray(), nonce, keyBytes)
|
||||||
val encrypted = SecretBox.easy(filled.toUByteArray(), nonce, keyBytes)
|
|
||||||
return BipackEncoder.encode(WithNonce(encrypted, nonce)).toUByteArray()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun decrypt(cipherData: UByteArray): UByteArray {
|
override val nonceBytesLength: Int = nonceByteLength
|
||||||
val wn: WithNonce = BipackDecoder.Companion.decode(cipherData.toDataSource())
|
|
||||||
try {
|
override val encryptingTag: UByteArray by lazy { blake2b2l(keyBytes) }
|
||||||
return BipackDecoder.Companion.decode<WithFill>(
|
override val decryptingTag: UByteArray get() = encryptingTag
|
||||||
SecretBox.openEasy(wn.cipherData, wn.nonce, keyBytes).toDataSource()
|
|
||||||
).data
|
override fun decryptWithNonce(cipherData: UByteArray, nonce: UByteArray): UByteArray =
|
||||||
}
|
protectDecryption {
|
||||||
catch(_: com.ionspin.kotlin.crypto.secretbox.SecretBoxCorruptedOrTamperedDataExceptionOrInvalidKey) {
|
BipackDecoder.decode<WithFill>(SecretBox.openEasy(cipherData, nonce, keyBytes).toByteArray())
|
||||||
throw DecryptionFailedException()
|
.data
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
@ -70,6 +69,8 @@ class SymmetricKey(
|
|||||||
* Create a secure random symmetric key.
|
* Create a secure random symmetric key.
|
||||||
*/
|
*/
|
||||||
fun random() = SymmetricKey(SecretBox.keygen())
|
fun random() = SymmetricKey(SecretBox.keygen())
|
||||||
|
|
||||||
|
val nonceByteLength = crypto_secretbox_NONCEBYTES
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
@ -2,11 +2,15 @@
|
|||||||
|
|
||||||
package net.sergeych.crypto2
|
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 com.ionspin.kotlin.crypto.util.LibsodiumRandom
|
||||||
|
import kotlinx.coroutines.CancellationException
|
||||||
import kotlinx.coroutines.channels.ReceiveChannel
|
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 {
|
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.limitMax(max: T) = if( this < max ) this else max
|
||||||
fun <T: Comparable<T>>T.limitMin(min: T) = if( this > min ) this else min
|
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
|
@Test
|
||||||
fun keyExchangeTest() = runTest {
|
fun keyExchangeTest() = runTest {
|
||||||
initCrypto()
|
initCrypto()
|
||||||
@ -105,6 +124,8 @@ class KeysTest {
|
|||||||
assertEquals(src, clientSessionKey.decryptString(serverSessionKey.encrypt(src)))
|
assertEquals(src, clientSessionKey.decryptString(serverSessionKey.encrypt(src)))
|
||||||
assertEquals(src, clientSessionKey.decryptString(serverSessionKey.encrypt(src)))
|
assertEquals(src, clientSessionKey.decryptString(serverSessionKey.encrypt(src)))
|
||||||
|
|
||||||
|
assertContentEquals(clientSessionKey.sessionTag, serverSessionKey.sessionTag)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
Loading…
x
Reference in New Issue
Block a user