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