redesigned signed box now Sealed box, refactored seals to incorporate expiration, introduced symmetric keys support
This commit is contained in:
parent
b11ea4d35a
commit
aad44c5af5
@ -16,6 +16,9 @@ repositories {
|
|||||||
|
|
||||||
kotlin {
|
kotlin {
|
||||||
jvm()
|
jvm()
|
||||||
|
js {
|
||||||
|
browser()
|
||||||
|
}
|
||||||
linuxX64()
|
linuxX64()
|
||||||
linuxArm64()
|
linuxArm64()
|
||||||
|
|
||||||
@ -25,7 +28,8 @@ kotlin {
|
|||||||
// iosArm64()
|
// iosArm64()
|
||||||
// iosSimulatorArm64()
|
// iosSimulatorArm64()
|
||||||
mingwX64()
|
mingwX64()
|
||||||
// wasmJs() no libsodimu bindings yet (strangely)
|
// @OptIn(ExperimentalWasmDsl::class)
|
||||||
|
// wasmJs() //no libsodium bindings yet (strangely)
|
||||||
// val ktor_version = "2.3.6"
|
// val ktor_version = "2.3.6"
|
||||||
|
|
||||||
sourceSets {
|
sourceSets {
|
||||||
@ -41,9 +45,8 @@ kotlin {
|
|||||||
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.8.1")
|
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.8.1")
|
||||||
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.7.0")
|
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.7.0")
|
||||||
|
|
||||||
implementation("com.ionspin.kotlin:multiplatform-crypto-libsodium-bindings:0.9.0")
|
implementation("com.ionspin.kotlin:multiplatform-crypto-libsodium-bindings:0.9.2")
|
||||||
api("com.ionspin.kotlin:bignum:0.3.9")
|
api("com.ionspin.kotlin:bignum:0.3.9")
|
||||||
|
|
||||||
api("net.sergeych:mp_bintools:0.1.5-SNAPSHOT")
|
api("net.sergeych:mp_bintools:0.1.5-SNAPSHOT")
|
||||||
api("net.sergeych:mp_stools:1.4.1")
|
api("net.sergeych:mp_stools:1.4.1")
|
||||||
}
|
}
|
||||||
|
24
src/commonMain/kotlin/net/sergeych/crypto2/DecryptingKey.kt
Normal file
24
src/commonMain/kotlin/net/sergeych/crypto2/DecryptingKey.kt
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
package net.sergeych.crypto2
|
||||||
|
|
||||||
|
import com.ionspin.kotlin.crypto.util.decodeFromUByteArray
|
||||||
|
import net.sergeych.bipack.BipackDecoder
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 {
|
||||||
|
/**
|
||||||
|
* 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
|
||||||
|
* or the encrypted data is tampered so the only one exception is used.
|
||||||
|
*
|
||||||
|
* @throws DecryptionFailedException if the key is not valid or [cipherData] tampered.
|
||||||
|
*/
|
||||||
|
fun decrypt(cipherData: UByteArray): UByteArray
|
||||||
|
|
||||||
|
fun decryptString(cipherData: UByteArray): String = decrypt(cipherData).decodeFromUByteArray()
|
||||||
|
}
|
||||||
|
|
||||||
|
inline fun <reified T>DecryptingKey.decryptObject(cipherData: UByteArray): T =
|
||||||
|
BipackDecoder.decode<T>(decrypt(cipherData).toByteArray())
|
25
src/commonMain/kotlin/net/sergeych/crypto2/EncryptingKey.kt
Normal file
25
src/commonMain/kotlin/net/sergeych/crypto2/EncryptingKey.kt
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
package net.sergeych.crypto2
|
||||||
|
|
||||||
|
import com.ionspin.kotlin.crypto.util.encodeToUByteArray
|
||||||
|
import net.sergeych.bipack.BipackEncoder
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Some key able to encrypt data with optional random fill that conceals message size
|
||||||
|
* when needed.
|
||||||
|
*
|
||||||
|
* It is not serializable by design.
|
||||||
|
* Custom implementations are, see [SymmetricKey] for example.
|
||||||
|
*/
|
||||||
|
interface EncryptingKey {
|
||||||
|
/**
|
||||||
|
* 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(plainText: String,randomFill: IntRange? = null): UByteArray =
|
||||||
|
encrypt(plainText.encodeToUByteArray(),randomFill)
|
||||||
|
}
|
||||||
|
|
||||||
|
inline fun <reified T>EncryptingKey.encryptObject(value: T,randomFill: IntRange? = null): UByteArray =
|
||||||
|
encrypt(BipackEncoder.encode(value).toUByteArray(),randomFill)
|
@ -3,20 +3,44 @@ package net.sergeych.crypto2
|
|||||||
import kotlinx.datetime.Instant
|
import kotlinx.datetime.Instant
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
import net.sergeych.bipack.BipackEncoder
|
import net.sergeych.bipack.BipackEncoder
|
||||||
|
import net.sergeych.bipack.decodeFromBipack
|
||||||
|
import net.sergeych.crypto2.Seal.Companion.create
|
||||||
import net.sergeych.utools.now
|
import net.sergeych.utools.now
|
||||||
|
import kotlin.random.Random
|
||||||
|
import kotlin.random.nextUBytes
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extended public-key signature.
|
||||||
|
*
|
||||||
|
* See [Seal.create] for details and usage.
|
||||||
|
*
|
||||||
|
* This constructor normally should not be used directly, please use [Seal.create] instead
|
||||||
|
*
|
||||||
|
* @param publicKey the public key that could be used to verify the message. It could safely be published.
|
||||||
|
* @param signature the signature generated by [create]. Do not generate it yourself.
|
||||||
|
* @param nonce if [create] was called with `isDeterministic = true` there will be a random nonce.
|
||||||
|
* that makes it impossible to judge whether known seals are related to the same unknown message.
|
||||||
|
* @param createdAt creation time as specified when creating.
|
||||||
|
* @param expiresAt if set by creator, determines the time instant from when the seal is invalid.
|
||||||
|
*/
|
||||||
@Serializable
|
@Serializable
|
||||||
class Seal(
|
class Seal(
|
||||||
val publicKey: SigningKey.Public,
|
val publicKey: SigningKey.Public,
|
||||||
val signature: UByteArray,
|
val signature: UByteArray,
|
||||||
|
val nonce: UByteArray?,
|
||||||
val createdAt: Instant,
|
val createdAt: Instant,
|
||||||
val expiresAt: Instant? = null,
|
val expiresAt: Instant?,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @suppress
|
||||||
|
* This is the structure that is actually signed/verified with a key.
|
||||||
|
*/
|
||||||
@Suppress("unused")
|
@Suppress("unused")
|
||||||
@Serializable
|
@Serializable
|
||||||
class SealedData(
|
class SealedData(
|
||||||
val message: UByteArray,
|
val message: UByteArray,
|
||||||
|
val nonce: UByteArray?,
|
||||||
val createdAt: Instant?,
|
val createdAt: Instant?,
|
||||||
val validUntil: Instant?,
|
val validUntil: Instant?,
|
||||||
)
|
)
|
||||||
@ -35,10 +59,13 @@ class Seal(
|
|||||||
* Check that message is correct for this seal and throws exception if it is not.
|
* Check that message is correct for this seal and throws exception if it is not.
|
||||||
* Note that tampering [createdAt] and [expiresAt] invalidate the seal too.
|
* Note that tampering [createdAt] and [expiresAt] invalidate the seal too.
|
||||||
*
|
*
|
||||||
|
* It checks first the signature. _If it is ok, it also checks_ [expiresAt].
|
||||||
|
*
|
||||||
* See [check] and [isValid] for non-throwing checks.
|
* See [check] and [isValid] for non-throwing checks.
|
||||||
*
|
*
|
||||||
* @throws ExpiredSignatureException
|
* @throws IllegalSignatureException if the signature is not valid for this [message]
|
||||||
* @throws IllegalSignatureException
|
* @throws ExpiredSignatureException if the signature is valid, but [expiresAt] is not null and the seal
|
||||||
|
* is expired.
|
||||||
*/
|
*/
|
||||||
fun verify(message: UByteArray) {
|
fun verify(message: UByteArray) {
|
||||||
val n = now()
|
val n = now()
|
||||||
@ -46,19 +73,80 @@ class Seal(
|
|||||||
expiresAt?.let {
|
expiresAt?.let {
|
||||||
if (n >= it) throw ExpiredSignatureException("signature expired at $it")
|
if (n >= it) throw ExpiredSignatureException("signature expired at $it")
|
||||||
}
|
}
|
||||||
val data = BipackEncoder.encode(SealedData(message, createdAt, expiresAt))
|
val data = BipackEncoder.encode(SealedData(message, nonce, createdAt, expiresAt))
|
||||||
if (!publicKey.verify(signature, data.toUByteArray()))
|
if (!publicKey.verify(signature, data.toUByteArray()))
|
||||||
throw IllegalSignatureException()
|
throw IllegalSignatureException()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check that the seal is not expired.
|
||||||
|
* __It is important__ that you can't determine whether the seal
|
||||||
|
* is genuine and [expiresAt] not tampered if you do not have a message. If you do, be sure to call
|
||||||
|
* [verify] or check [isValid] before calling this method.
|
||||||
|
*/
|
||||||
|
fun isExpired(): Boolean = expiresAt?.let { it > now() } != false
|
||||||
|
|
||||||
|
/**
|
||||||
|
* In the rare cases you want to use it alone, the packed binary representation, use [unpack] to restore.
|
||||||
|
* Most often, all you need is to put your Seal inside some kotlinx-serializable class.
|
||||||
|
*/
|
||||||
|
val packed: UByteArray by lazy { BipackEncoder.encode(this).toUByteArray() }
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
operator fun invoke(
|
/**
|
||||||
key: SigningKey.Secret, message: UByteArray,
|
* Seal [message] with a [key].
|
||||||
|
*
|
||||||
|
* Seals are kotlinx-serializable and can be used
|
||||||
|
* to check the authenticity of the arbitrary [message] using a public key, [SigningKey.Public]
|
||||||
|
* instance, using public-key signing algorithms.
|
||||||
|
*
|
||||||
|
* Unlike a regular binary signature, Seal contains the signer's [publicKey], and also
|
||||||
|
* [createdAt] and [expiresAt] fields which are also signed and are guaranteed to be non-tampered
|
||||||
|
* if the [isValid] returns true (or [verify] does not throw). See [isExpired].
|
||||||
|
*
|
||||||
|
* It is important to understand that the seal itself could not be checked _having no message
|
||||||
|
* it was created for_.
|
||||||
|
* This is made intentionally: if you have no message, what means you are not the intended recipient, you can't
|
||||||
|
* analyze the seal.
|
||||||
|
*
|
||||||
|
* To check that the message is genuine using a seal use:
|
||||||
|
*
|
||||||
|
* - [verify] which throws [IllegalSignatureException] or [ExpiredSignatureException] if it is not
|
||||||
|
* - [check] that returns success or the exception without throwing it
|
||||||
|
* - [isValid] that returns true or false, so you can't judge what was the reason (invalid signature or
|
||||||
|
* expiration, do not check for expiration _after_ false is returned as you can't trust
|
||||||
|
* into your seal yet).
|
||||||
|
*
|
||||||
|
* When you need to have a message and one or more seals all together, use [SealedBox].
|
||||||
|
*
|
||||||
|
* Please note for in the very rare key you want to trust the Seal having no message you need to create
|
||||||
|
* another Seal to seal the seal. It sounds crazy as it is, and you should avoid such designs.
|
||||||
|
*
|
||||||
|
*
|
||||||
|
* @param key secret key to sign with
|
||||||
|
* @param message message to seal
|
||||||
|
* @param createdAt seal creation time, usually current time
|
||||||
|
* @param expiresAt optional seal expiration time
|
||||||
|
* @param nonDeterministic if true, it is not possible to check whether two seals correspond to the same
|
||||||
|
* unknown message (if the message is known, it is trivial by verifying the seal). It is a
|
||||||
|
* rare case so default os false.
|
||||||
|
*/
|
||||||
|
fun create(
|
||||||
|
key: SigningKey.Secret,
|
||||||
|
message: UByteArray,
|
||||||
createdAt: Instant = now(),
|
createdAt: Instant = now(),
|
||||||
expiresAt: Instant? = null,
|
expiresAt: Instant? = null,
|
||||||
|
nonDeterministic: Boolean = false
|
||||||
): Seal {
|
): Seal {
|
||||||
val data = BipackEncoder.encode(SealedData(message, createdAt, expiresAt)).toUByteArray()
|
val nonce = if( nonDeterministic ) Random.nextUBytes(32) else null
|
||||||
return Seal(key.publicKey, key.sign(data), createdAt, expiresAt)
|
val data = BipackEncoder.encode(SealedData(message, nonce, createdAt, expiresAt)).toUByteArray()
|
||||||
}
|
return Seal(key.publicKey, key.sign(data), nonce, createdAt, expiresAt)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Int the rare case you need a packed seal alone, unpack it. Normally just add seal to some [Serializable]
|
||||||
|
* class, it is serializable.
|
||||||
|
*/
|
||||||
|
fun unpack(packed: UByteArray): Seal = packed.toByteArray().decodeFromBipack()
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -1,17 +1,19 @@
|
|||||||
package net.sergeych.crypto2
|
package net.sergeych.crypto2
|
||||||
|
|
||||||
|
import kotlinx.datetime.Instant
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
import kotlinx.serialization.Transient
|
import kotlinx.serialization.Transient
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Multi-signed data box. Use [SignedBox.invoke] to easily create
|
* Multi-signed data box. Do not use the constructori directly, use [SealedBox.create]
|
||||||
* instances and [SignedBox.plus] to add more signatures (signing keys), and
|
* instead to create the box and [SealedBox.plus] or [addSeal] to add more signatures
|
||||||
* [SignedBox.contains] to check for a specific key signature presence.
|
* with different signing keys.
|
||||||
|
* [SealedBox.contains] checks for a specific key signature presence.
|
||||||
*
|
*
|
||||||
* Signatures, [Seal], incorporate creation time and optional expiration which are
|
* Signatures, [Seal], incorporate creation time and optional expiration which are
|
||||||
* also signed and checked upon deserialization.
|
* also signed and checked upon deserialization.
|
||||||
*
|
*
|
||||||
* It is serializable and checks integrity on deserialization. If any of seals does not
|
* It is serializable and checks integrity __on deserialization__k. If any of seals does not
|
||||||
* match the signed [message], it throws [IllegalSignatureException] _on deserialization_.
|
* match the signed [message], it throws [IllegalSignatureException] _on deserialization_.
|
||||||
* E.g., if you have it deserialized, it is ok, check it contains all needed keys among
|
* E.g., if you have it deserialized, it is ok, check it contains all needed keys among
|
||||||
* signers.
|
* signers.
|
||||||
@ -20,7 +22,7 @@ import kotlinx.serialization.Transient
|
|||||||
* know what you are doing as it may be dangerous.Use one of the above to create or change it.
|
* know what you are doing as it may be dangerous.Use one of the above to create or change it.
|
||||||
*/
|
*/
|
||||||
@Serializable
|
@Serializable
|
||||||
class SignedBox(
|
class SealedBox(
|
||||||
val message: UByteArray,
|
val message: UByteArray,
|
||||||
private val seals: List<Seal>,
|
private val seals: List<Seal>,
|
||||||
@Transient
|
@Transient
|
||||||
@ -32,9 +34,18 @@ class SignedBox(
|
|||||||
* 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
|
||||||
* _can't assume it always returns a copied object!_
|
* _can't assume it always returns a copied object!_
|
||||||
*/
|
*/
|
||||||
operator fun plus(key: SigningKey.Secret): SignedBox =
|
operator fun plus(key: SigningKey.Secret): SealedBox =
|
||||||
if (key.publicKey in this) this
|
if (key.publicKey in this) this
|
||||||
else SignedBox(message, seals + key.seal(message),false)
|
else SealedBox(message, seals + key.seal(message),false)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add expiring seal, otherwise use [plus]. Overrides exising seal for [key]
|
||||||
|
* if present:
|
||||||
|
*/
|
||||||
|
fun addSeal(key: SigningKey.Secret,expresAt: Instant): SealedBox {
|
||||||
|
val filtered = seals.filter { it.publicKey != key.publicKey }
|
||||||
|
return SealedBox(message, filtered + key.seal(message, expresAt), false)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check that it is signed with a specified key.
|
* Check that it is signed with a specified key.
|
||||||
@ -55,15 +66,15 @@ class SignedBox(
|
|||||||
/**
|
/**
|
||||||
* Create a new instance with a specific data sealed by one or more
|
* Create a new instance with a specific data sealed by one or more
|
||||||
* keys. At least one key is required to disallow providing not-signed
|
* keys. At least one key is required to disallow providing not-signed
|
||||||
* instances, e.g. [SignedBox] is guaranteed to be properly sealed when
|
* instances, e.g. [SealedBox] is guaranteed to be properly sealed when
|
||||||
* successfully instantiated.
|
* successfully instantiated.
|
||||||
*
|
*
|
||||||
* @param data a message to sign
|
* @param data a message to sign
|
||||||
* @param keys a list of keys to sign with, should be at least one key.
|
* @param keys a list of keys to sign with, should be at least one key.
|
||||||
* @throws IllegalArgumentException if keys are not specified.
|
* @throws IllegalArgumentException if keys are not specified.
|
||||||
*/
|
*/
|
||||||
operator fun invoke(data: UByteArray, vararg keys: SigningKey.Secret): SignedBox {
|
fun create(data: UByteArray, vararg keys: SigningKey.Secret): SealedBox {
|
||||||
return SignedBox(data, keys.map { it.seal(data) }, false)
|
return SealedBox(data, keys.map { it.seal(data) }, false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -62,8 +62,8 @@ sealed class SigningKey {
|
|||||||
|
|
||||||
fun sign(message: UByteArray): UByteArray = Signature.detached(message, packed)
|
fun sign(message: UByteArray): UByteArray = Signature.detached(message, packed)
|
||||||
|
|
||||||
fun seal(message: UByteArray, validUntil: Instant? = null): Seal =
|
fun seal(message: UByteArray, expiresAt: Instant? = null): Seal =
|
||||||
Seal(this, message, now(), validUntil)
|
Seal.create(this, message, now(), expiresAt)
|
||||||
|
|
||||||
override fun toString(): String = "Sct:${super.toString()}"
|
override fun toString(): String = "Sct:${super.toString()}"
|
||||||
|
|
||||||
|
79
src/commonMain/kotlin/net/sergeych/crypto2/SymmetricKey.kt
Normal file
79
src/commonMain/kotlin/net/sergeych/crypto2/SymmetricKey.kt
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
package net.sergeych.crypto2
|
||||||
|
|
||||||
|
import com.ionspin.kotlin.crypto.secretbox.SecretBox
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
import net.sergeych.bintools.toDataSource
|
||||||
|
import net.sergeych.bipack.BipackDecoder
|
||||||
|
import net.sergeych.bipack.BipackEncoder
|
||||||
|
import net.sergeych.crypto2.SymmetricKey.Companion.random
|
||||||
|
import kotlin.random.Random
|
||||||
|
import kotlin.random.nextInt
|
||||||
|
import kotlin.random.nextUBytes
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Symmetric key implements authenticated encrypting with random nonce and optional fill.
|
||||||
|
* Random fill is normally used when cryptanalysis of the message size is a threat.
|
||||||
|
*
|
||||||
|
* Do not call this constructor directly, use [random] or deserialize it.
|
||||||
|
*
|
||||||
|
* __Algorithms:__
|
||||||
|
*
|
||||||
|
* - Encryption: XSalsa20 stream cipher.
|
||||||
|
* - Authentication: Poly1305 MAC
|
||||||
|
*/
|
||||||
|
@Serializable
|
||||||
|
class SymmetricKey(
|
||||||
|
val keyBytes: UByteArray
|
||||||
|
): EncryptingKey, DecryptingKey {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @suppress
|
||||||
|
* nonce + ciphered data serialization aid
|
||||||
|
*/
|
||||||
|
@Serializable
|
||||||
|
data class WithNonce(
|
||||||
|
val cipherData: UByteArray,
|
||||||
|
val nonce: UByteArray,
|
||||||
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @suppress
|
||||||
|
* add some random data to the end of the plain message
|
||||||
|
*/
|
||||||
|
@Serializable
|
||||||
|
data class WithFill(
|
||||||
|
val data: UByteArray,
|
||||||
|
val safetyFill: UByteArray? = null
|
||||||
|
)
|
||||||
|
|
||||||
|
override fun encrypt(plainData: UByteArray,randomFill: IntRange?): UByteArray {
|
||||||
|
val fill = randomFill?.let {
|
||||||
|
require(it.start >= 0)
|
||||||
|
Random.nextUBytes(Random.nextInt(it))
|
||||||
|
}
|
||||||
|
val filled = BipackEncoder.encode(WithFill(plainData, fill))
|
||||||
|
val nonce = randomNonce()
|
||||||
|
val encrypted = SecretBox.easy(filled.toUByteArray(), nonce, keyBytes)
|
||||||
|
return BipackEncoder.encode(WithNonce(encrypted, nonce)).toUByteArray()
|
||||||
|
}
|
||||||
|
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
/**
|
||||||
|
* Create a secure random symmetric key.
|
||||||
|
*/
|
||||||
|
fun random() = SymmetricKey(SecretBox.keygen())
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -2,30 +2,12 @@
|
|||||||
|
|
||||||
package net.sergeych.crypto2
|
package net.sergeych.crypto2
|
||||||
|
|
||||||
import com.ionspin.kotlin.crypto.secretbox.SecretBox
|
|
||||||
import com.ionspin.kotlin.crypto.secretbox.crypto_secretbox_NONCEBYTES
|
import com.ionspin.kotlin.crypto.secretbox.crypto_secretbox_NONCEBYTES
|
||||||
import com.ionspin.kotlin.crypto.util.LibsodiumRandom
|
import com.ionspin.kotlin.crypto.util.LibsodiumRandom
|
||||||
import kotlinx.coroutines.channels.ReceiveChannel
|
import kotlinx.coroutines.channels.ReceiveChannel
|
||||||
import kotlinx.serialization.Serializable
|
|
||||||
import net.sergeych.bintools.toDataSource
|
|
||||||
import net.sergeych.bipack.BipackDecoder
|
|
||||||
import net.sergeych.bipack.BipackEncoder
|
|
||||||
|
|
||||||
class DecryptionFailedException : RuntimeException("can't encrypt: wrong key or tampered message")
|
class DecryptionFailedException : RuntimeException("can't encrypt: wrong key or tampered message")
|
||||||
|
|
||||||
@Serializable
|
|
||||||
data class WithNonce(
|
|
||||||
val cipherData: UByteArray,
|
|
||||||
val nonce: UByteArray,
|
|
||||||
)
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
data class WithFill(
|
|
||||||
val data: UByteArray,
|
|
||||||
val safetyFill: UByteArray? = null
|
|
||||||
) {
|
|
||||||
constructor(data: UByteArray, fillSize: Int) : this(data, randomBytes(fillSize))
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun readVarUnsigned(input: ReceiveChannel<UByte>): UInt {
|
suspend fun readVarUnsigned(input: ReceiveChannel<UByte>): UInt {
|
||||||
var result = 0u
|
var result = 0u
|
||||||
@ -73,39 +55,4 @@ fun <T: Comparable<T>>T.limitMin(min: T) = if( this > min ) this else min
|
|||||||
|
|
||||||
fun randomNonce(): UByteArray = randomBytes(crypto_secretbox_NONCEBYTES)
|
fun randomNonce(): UByteArray = randomBytes(crypto_secretbox_NONCEBYTES)
|
||||||
|
|
||||||
/**
|
|
||||||
* Secret-key encrypt with authentication.
|
|
||||||
* Generates random nonce and add some random fill to protect
|
|
||||||
* against some analysis attacks. Nonce is included in the result. To be
|
|
||||||
* used with [decrypt].
|
|
||||||
* @param secretKey a _secret_ key, see [SecretBox.keygen()] or like.
|
|
||||||
* @param plain data to encrypt
|
|
||||||
* @param fillSize number of random fill data to add. Use random value or default.
|
|
||||||
*/
|
|
||||||
fun encrypt(
|
|
||||||
secretKey: UByteArray,
|
|
||||||
plain: UByteArray,
|
|
||||||
fillSize: Int = randomUInt((plain.size * 3 / 10).limitMin(3)).toInt()
|
|
||||||
): UByteArray {
|
|
||||||
val filled = BipackEncoder.encode(WithFill(plain, fillSize))
|
|
||||||
val nonce = randomNonce()
|
|
||||||
val encrypted = SecretBox.easy(filled.toUByteArray(), nonce, secretKey)
|
|
||||||
return BipackEncoder.encode(WithNonce(encrypted, nonce)).toUByteArray()
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Decrypt a secret-key-based message, normally encrypted with [encrypt].
|
|
||||||
* @throws DecryptionFailedException if the key is wrong or a message is tampered with (MAC
|
|
||||||
* check failed).
|
|
||||||
*/
|
|
||||||
fun decrypt(secretKey: UByteArray, cipher: UByteArray): UByteArray {
|
|
||||||
val wn: WithNonce = BipackDecoder.decode(cipher.toDataSource())
|
|
||||||
try {
|
|
||||||
return BipackDecoder.decode<WithFill>(
|
|
||||||
SecretBox.openEasy(wn.cipherData, wn.nonce, secretKey).toDataSource()
|
|
||||||
).data
|
|
||||||
}
|
|
||||||
catch(_: com.ionspin.kotlin.crypto.secretbox.SecretBoxCorruptedOrTamperedDataExceptionOrInvalidKey) {
|
|
||||||
throw DecryptionFailedException()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
@ -1,15 +1,15 @@
|
|||||||
import com.ionspin.kotlin.crypto.secretbox.SecretBox
|
|
||||||
import com.ionspin.kotlin.crypto.util.decodeFromUByteArray
|
import com.ionspin.kotlin.crypto.util.decodeFromUByteArray
|
||||||
import com.ionspin.kotlin.crypto.util.encodeToUByteArray
|
import com.ionspin.kotlin.crypto.util.encodeToUByteArray
|
||||||
import kotlinx.coroutines.test.runTest
|
import kotlinx.coroutines.test.runTest
|
||||||
import net.sergeych.crypto2.*
|
import net.sergeych.crypto2.*
|
||||||
|
import net.sergeych.utools.now
|
||||||
import net.sergeych.utools.pack
|
import net.sergeych.utools.pack
|
||||||
import net.sergeych.utools.unpack
|
import net.sergeych.utools.unpack
|
||||||
import kotlin.test.*
|
import kotlin.test.*
|
||||||
|
|
||||||
class KeysTest {
|
class KeysTest {
|
||||||
@Test
|
@Test
|
||||||
fun testCreationAndMap() = runTest {
|
fun testSigningCreationAndMap() = runTest {
|
||||||
initCrypto()
|
initCrypto()
|
||||||
val (stk,pbk) = SigningKey.pair()
|
val (stk,pbk) = SigningKey.pair()
|
||||||
|
|
||||||
@ -30,28 +30,56 @@ class KeysTest {
|
|||||||
val p2 = SigningKey.pair()
|
val p2 = SigningKey.pair()
|
||||||
val p3 = SigningKey.pair()
|
val p3 = SigningKey.pair()
|
||||||
|
|
||||||
val ms = SignedBox(data, s1) + p2.secretKey
|
val ms = SealedBox.create(data, s1) + p2.secretKey
|
||||||
|
|
||||||
// non tampered:
|
// non tampered:
|
||||||
val ms1 = unpack<SignedBox>(pack(ms))
|
val ms1 = unpack<SealedBox>(pack(ms))
|
||||||
assertContentEquals(data, ms1.message)
|
assertContentEquals(data, ms1.message)
|
||||||
assertTrue(pbk in ms1)
|
assertTrue(pbk in ms1)
|
||||||
assertTrue(p2.publicKey in ms1)
|
assertTrue(p2.publicKey in ms1)
|
||||||
assertTrue(p3.publicKey !in ms1)
|
assertTrue(p3.publicKey !in ms1)
|
||||||
|
|
||||||
assertThrows<IllegalSignatureException> {
|
assertThrows<IllegalSignatureException> {
|
||||||
unpack<SignedBox>(pack(ms).also { it[3] = 1u })
|
unpack<SealedBox>(pack(ms).also { it[3] = 1u })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testNonDeterministicSeals() = runTest {
|
||||||
|
initCrypto()
|
||||||
|
val data = "Welcome to the Miami, bitch!".encodeToUByteArray()
|
||||||
|
val (sk,_) = SigningKey.pair()
|
||||||
|
val t = now()
|
||||||
|
val s1 = Seal.create(sk, data, createdAt = t)
|
||||||
|
val s2 = Seal.create(sk, data, createdAt = t)
|
||||||
|
val s2bad = Seal.create(sk, data + "!".encodeToUByteArray())
|
||||||
|
val s3 = Seal.create(sk, data, createdAt = t, nonDeterministic = true)
|
||||||
|
val s4 = Seal.create(sk, data, createdAt = t, nonDeterministic = true)
|
||||||
|
|
||||||
|
for( seal in listOf(s1,s2,s3,s4)) {
|
||||||
|
assertTrue { seal.isValid(data) }
|
||||||
|
assertTrue { Seal.unpack(seal.packed).isValid(data) }
|
||||||
|
}
|
||||||
|
assertFalse { s2bad.isValid(data)}
|
||||||
|
assertContentEquals(s1.packed, s2.packed)
|
||||||
|
assertFalse { s1.packed contentEquals s3.packed }
|
||||||
|
assertFalse { s4.packed contentEquals s3.packed }
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun secretEncryptTest() = runTest {
|
fun secretEncryptTest() = runTest {
|
||||||
initCrypto()
|
initCrypto()
|
||||||
val key = SecretBox.keygen()
|
val key = SymmetricKey.random()
|
||||||
val key1 = SecretBox.keygen()
|
val key1 = SymmetricKey.random()
|
||||||
assertEquals("hello", decrypt(key, encrypt(key, "hello".encodeToUByteArray())).decodeFromUByteArray())
|
assertEquals("hello", key.decrypt(key.encrypt("hello".encodeToUByteArray())).decodeFromUByteArray())
|
||||||
|
assertEquals("hello", key.decryptString(key.encrypt("hello")))
|
||||||
|
assertEquals("hello", key.decryptObject(key.encryptObject("hello")))
|
||||||
|
assertEquals("hello", key.decrypt(key.encrypt("hello".encodeToUByteArray(), 18..334)).decodeFromUByteArray())
|
||||||
|
assertEquals("hello", key.decryptString(key.encrypt("hello", 18..334)))
|
||||||
|
assertEquals("hello", key.decryptObject(key.encryptObject("hello", 18..334)))
|
||||||
assertThrows<DecryptionFailedException> {
|
assertThrows<DecryptionFailedException> {
|
||||||
decrypt(key, encrypt(key1, "hello".encodeToUByteArray())).decodeFromUByteArray()
|
key.decrypt(key1.encrypt("hello".encodeToUByteArray())).decodeFromUByteArray()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user