PBKD with tests. Some improvements in keys listing

This commit is contained in:
Sergey Chernov 2024-06-25 09:48:50 +07:00
parent 46a8b5bc06
commit 9efa763b45
10 changed files with 94 additions and 51 deletions

View File

@ -125,7 +125,9 @@ object Asymmetric {
@SerialName("encp") @SerialName("encp")
class PublicKey(override val keyBytes: UByteArray) : UniversalKey(), EncryptingKey { class PublicKey(override val keyBytes: UByteArray) : UniversalKey(), EncryptingKey {
override val magick: KeysMagickNumber = KeysMagickNumber.defaultAssymmetric override val magic: KeysmagicNumber = KeysmagicNumber.defaultAssymmetric
@Transient
override val label: String = "pub"
/** /**
* Create an anonymous message that could be decrypted only with the [SecretKey] that corresponds this. * Create an anonymous message that could be decrypted only with the [SecretKey] that corresponds this.
@ -193,6 +195,8 @@ object Asymmetric {
val _cachedPublicKey: PublicKey? = null, val _cachedPublicKey: PublicKey? = null,
) : DecryptingKey, UniversalKey() { ) : DecryptingKey, UniversalKey() {
@Transient
override val label: String = "sec"
/** /**
* Decrypt with authentication checks the message which must have [Message.senderPublicKey] set. * Decrypt with authentication checks the message which must have [Message.senderPublicKey] set.
* Use [decryptWithSenderKey] otherwise. Note that the authenticated encryption is always use, even if * Use [decryptWithSenderKey] otherwise. Note that the authenticated encryption is always use, even if
@ -238,7 +242,7 @@ object Asymmetric {
return message.decrypt(this) return message.decrypt(this)
} }
override val magick: KeysMagickNumber = KeysMagickNumber.defaultAssymmetric override val magic: KeysmagicNumber = KeysmagicNumber.defaultAssymmetric
override val id: KeyId by lazy { publicKey.id } override val id: KeyId by lazy { publicKey.id }
override val nonceBytesLength: Int override val nonceBytesLength: Int

View File

@ -17,7 +17,7 @@ class BinaryId private constructor (
class IncomparableException(text: String) : IllegalArgumentException(text) class IncomparableException(text: String) : IllegalArgumentException(text)
@Transient @Transient
val magick: Int = run { val magic: Int = run {
if (id.size < 4) throw InvalidException("BinaryId is too short") if (id.size < 4) throw InvalidException("BinaryId is too short")
val crc = id.last() val crc = id.last()
val rest = id.dropLast(1).toUByteArray() val rest = id.dropLast(1).toUByteArray()
@ -32,7 +32,7 @@ class BinaryId private constructor (
override fun toString(): String = id.encodeToBase64Url() override fun toString(): String = id.encodeToBase64Url()
override fun compareTo(other: BinaryId): Int { override fun compareTo(other: BinaryId): Int {
if (other.magick != magick) throw IncomparableException("Mask mismatch (my=$magick their=${other.magick})") if (other.magic != magic) throw IncomparableException("Mask mismatch (my=$magic their=${other.magic})")
val id1 = other.id val id1 = other.id
if (id1.size != id.size) throw IncomparableException("different sizes") if (id1.size != id.size) throw IncomparableException("different sizes")
for ((a, b) in innerData.zip(other.innerData)) { for ((a, b) in innerData.zip(other.innerData)) {
@ -66,11 +66,11 @@ class BinaryId private constructor (
BinaryId(str.decodeBase64Url().toUByteArray()) BinaryId(str.decodeBase64Url().toUByteArray())
fun createFromBytes(magick: Int, bytes: ByteArray): BinaryId = createFromUBytes(magick, bytes.toUByteArray()) fun createFromBytes(magic: Int, bytes: ByteArray): BinaryId = createFromUBytes(magic, bytes.toUByteArray())
fun createFromUBytes(magick: Int, bytes: UByteArray): BinaryId { fun createFromUBytes(magic: Int, bytes: UByteArray): BinaryId {
val crc = CRC8() val crc = CRC8()
val mn = magick.toUByte() val mn = magic.toUByte()
crc.update(bytes) crc.update(bytes)
crc.update(mn) crc.update(mn)
return BinaryId(UByteArray(bytes.size + 2).also { return BinaryId(UByteArray(bytes.size + 2).also {
@ -82,13 +82,13 @@ class BinaryId private constructor (
} }
@Suppress("unused") @Suppress("unused")
fun createRandom(magickNumber: Int, size: Int=16) = fun createRandom(magicNumber: Int, size: Int=16) =
createFromBytes(magickNumber, Random.Default.nextBytes(size-2)) createFromBytes(magicNumber, Random.Default.nextBytes(size-2))
/** /**
* Encode a string as UTF and create a binaryId from its bytes and provided magick. * Encode a string as UTF and create a binaryId from its bytes and provided magic.
*/ */
fun createFromString(magickNumber: Int, text: String): BinaryId = fun createFromString(magicNumber: Int, text: String): BinaryId =
createFromUBytes(magickNumber, text.encodeToUByteArray()) createFromUBytes(magicNumber, text.encodeToUByteArray())
} }
} }

View File

@ -5,7 +5,7 @@ import kotlinx.serialization.Serializable
/** /**
* Key Identity. Can be used to find matching keys for decryption or verifying, etc. The identity * Key Identity. Can be used to find matching keys for decryption or verifying, etc. The identity
* may contain [KDF] parameters if the corresponding key could be derived from a password. * may contain [KDF] parameters if the corresponding key could be derived from a password.
* Note that [kdf] part is not respected in [equals]. * Note that [kdp] part is not respected in [equals].
* *
* Important. `KeyId` of matching keys are the same, so you can use it to identify * Important. `KeyId` of matching keys are the same, so you can use it to identify
* and find matching keys in the [UniversalRing], etc. For example: * and find matching keys in the [UniversalRing], etc. For example:
@ -16,16 +16,23 @@ import kotlinx.serialization.Serializable
* - [SigningSecretKey] and corresponding [VerifyingKey] have the same `KeyId`. Use it to pick a proper key for * - [SigningSecretKey] and corresponding [VerifyingKey] have the same `KeyId`. Use it to pick a proper key for
* signing from a ring with [UniversalRing.findKey] * signing from a ring with [UniversalRing.findKey]
* *
* See [PBKD.Params.deriveKey] for deriving keys from id.
*
* @param id actual id used in equality test amd hash code generation. `Id` of the matching keys is the same.
* @param kdp optional key derivation parameters. Does not affect equality. Allow deriving the key from proper
* password, see above.
*/ */
@Serializable @Serializable
data class KeyId(val id: BinaryId, val kdf: PBKD.Params?=null ) { data class KeyId(val id: BinaryId, val kdp: PBKD.Params?=null ) {
/** /**
* Binary array representation of the [id], not including the [kdf]. Used in [SafeKeyExchange] * Binary array representation of the [id], not including the [kdp]. Used in [SafeKeyExchange]
* and other key exchanges to generate session tokens, etc. * and other key exchanges to generate session tokens, etc.
*/ */
val binaryTag: UByteArray by lazy { id.id } val binaryTag: UByteArray by lazy { id.id }
val keymagic: KeysmagicNumber by lazy { KeysmagicNumber.entries[id.magic]}
override fun equals(other: Any?): Boolean { override fun equals(other: Any?): Boolean {
if (this === other) return true if (this === other) return true
if (other !is KeyId) return false if (other !is KeyId) return false
@ -38,6 +45,6 @@ data class KeyId(val id: BinaryId, val kdf: PBKD.Params?=null ) {
override fun toString() = id.toString() override fun toString() = id.toString()
constructor(magickNumber: KeysMagickNumber, keyData: UByteArray, kdp: PBKD.Params?=null) constructor(magicNumber: KeysmagicNumber, keyData: UByteArray, kdp: PBKD.Params?=null)
: this(BinaryId.createFromUBytes(magickNumber.number, blake2b3l(keyData)), kdp) : this(BinaryId.createFromUBytes(magicNumber.ordinal, blake2b3l(keyData)), kdp)
} }

View File

@ -1,18 +1,11 @@
package net.sergeych.crypto2 package net.sergeych.crypto2
enum class KeysMagickNumber(val number: Int) { enum class KeysmagicNumber(val label: String) {
defaultAssymmetric(0), Unknown( "???"),
defaultSymmetric(1), defaultAssymmetric( "asm"),
defaultSession(2), defaultSymmetric( "sym"),
defaultSigning(3), defaultSession( "ssn"),
defaultVerifying(4), defaultVerifying( "ver"),
; ;
companion object {
val forNumber = entries.map { it.number to it }.toMap()
@Suppress("unused")
fun findFor(binaryId: BinaryId): KeysMagickNumber? = forNumber[binaryId.magick]
}
} }

View File

@ -47,8 +47,8 @@ object PBKD {
fun deriveKey(password: String, keyId: KeyId? = null): SymmetricKey { fun deriveKey(password: String, keyId: KeyId? = null): SymmetricKey {
// check key id sanity // check key id sanity
keyId?.let { keyId?.let {
if (it.id.magick != KeysMagickNumber.defaultSymmetric.number) { if (it.id.magic != KeysmagicNumber.defaultSymmetric.ordinal) {
throw NotSupportedException("deriving key of type ${it.id.magick} is not implemented") throw NotSupportedException("deriving key of type ${it.id.magic} is not implemented")
} }
} }
// this will not be ok when we support other key types: // this will not be ok when we support other key types:
@ -56,11 +56,7 @@ object PBKD {
throw NotSupportedException("can't create key from $length byte(s), required ${SymmetricKey.keyLength}") throw NotSupportedException("can't create key from $length byte(s), required ${SymmetricKey.keyLength}")
// derive // derive
val key = SymmetricKey( val key = SymmetricKey(deriveUBytes(password),this)
deriveUBytes(password)
.sliceArray(startOffset..<startOffset + SymmetricKey.keyLength),
this
)
// check if we can // check if we can
if (keyId != null && keyId != key.id) if (keyId != null && keyId != key.id)
@ -121,6 +117,15 @@ object PBKD {
entry.data!! entry.data!!
} }
} }
/**
* Generated cache bytes are cached and present in memory probably longer than needed.
* After this call, the password derivation cache is clear and the new deriving will
* be performed.
*/
fun clearCache() {
op.invoke { cache.clear() }
}
} }

View File

@ -22,8 +22,6 @@ class SigningPublicKey(override val keyBytes: UByteArray) : UniversalKey(), Veri
false false
} }
override fun toString(): String = "Pub:${super.toString()}"
@Transient @Transient
override val magick: KeysMagickNumber = KeysMagickNumber.defaultVerifying override val magic: KeysmagicNumber = KeysmagicNumber.defaultVerifying
} }

View File

@ -28,12 +28,10 @@ class SigningSecretKey(
override fun seal(message: UByteArray, expiresAt: Instant?): Seal = override fun seal(message: UByteArray, expiresAt: Instant?): Seal =
Seal.create(this, message, now(), expiresAt) Seal.create(this, message, now(), expiresAt)
override fun toString(): String = "Sct:${super.toString()}"
override val id: KeyId = verifyingKey.id override val id: KeyId = verifyingKey.id
@Transient override val label: String
override val magick = KeysMagickNumber.defaultSigning get() = "sig"
companion object { companion object {

View File

@ -23,10 +23,10 @@ import kotlinx.serialization.Transient
class SymmetricKey( class SymmetricKey(
override val keyBytes: UByteArray, override val keyBytes: UByteArray,
@Transient @Transient
val pbkdfParams: PBKD.Params?=null private val pbkdfParams: PBKD.Params?=null
) : EncryptingKey, DecryptingKey, UniversalKey() { ) : EncryptingKey, DecryptingKey, UniversalKey() {
override val magick: KeysMagickNumber = KeysMagickNumber.defaultSymmetric override val magic: KeysmagicNumber = KeysmagicNumber.defaultSymmetric
/** /**
* @suppress * @suppress
@ -46,7 +46,7 @@ class SymmetricKey(
override val nonceBytesLength: Int = nonceLength override val nonceBytesLength: Int = nonceLength
override val id by lazy { override val id by lazy {
KeyId(KeysMagickNumber.defaultSymmetric,blake2b3l(keyBytes), pbkdfParams) KeyId(KeysmagicNumber.defaultSymmetric,blake2b3l(keyBytes), pbkdfParams)
} }
override fun decryptWithNonce(cipherData: UByteArray, nonce: UByteArray): UByteArray = override fun decryptWithNonce(cipherData: UByteArray, nonce: UByteArray): UByteArray =

View File

@ -1,14 +1,22 @@
package net.sergeych.crypto2 package net.sergeych.crypto2
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import kotlinx.serialization.Transient
@Serializable @Serializable
sealed class UniversalKey: KeyInstance { sealed class UniversalKey: KeyInstance {
abstract val keyBytes: UByteArray abstract val keyBytes: UByteArray
abstract val magick: KeysMagickNumber
override val id by lazy { KeyId(magick, keyBytes) }
@Transient
open val magic: KeysmagicNumber = KeysmagicNumber.Unknown
override val id by lazy { KeyId(magic, keyBytes) }
// Important: id can be overridden, so we use it, not magic:
open val label by lazy { id.keymagic.label }
override fun equals(other: Any?): Boolean { override fun equals(other: Any?): Boolean {
return other is UniversalKey && other.keyBytes contentEquals keyBytes return other is UniversalKey && other.keyBytes contentEquals keyBytes
@ -18,7 +26,10 @@ sealed class UniversalKey: KeyInstance {
return keyBytes.contentHashCode() return keyBytes.contentHashCode()
} }
override fun toString(): String = keyBytes.encodeToBase64Url() override fun toString(): String {
val pwdSign = if (id.kdp != null) '*' else '#'
return "🗝${label}$pwdSign$id"
}
companion object { companion object {
fun newSecretKey() = Asymmetric.newSecretKey() fun newSecretKey() = Asymmetric.newSecretKey()

View File

@ -1,9 +1,36 @@
import kotlinx.coroutines.test.runTest
import net.sergeych.crypto2.PBKD
import net.sergeych.crypto2.initCrypto
import kotlin.test.Test import kotlin.test.Test
import kotlin.test.assertEquals
class PBKDTest { class PBKDTest {
@Test @Test
fun testDerive() { fun testDerive() = runTest {
initCrypto()
val (k1, k2, k3) = PBKD.deriveMultipleKeys("foobar", 3)
PBKD.clearCache()
println(k1)
println(k2)
println(k3)
// println("------------------- Secret ket encryption!")
// val s = UniversalKey.newSecretKey()
// println(s)
// println(s.publicKey)
// println("------------------- public key signing")
// val i = UniversalKey.newSigningKey()
// println(i)
// println(i.verifyingKey)
// println("------------------- symmetric")
// println(UniversalKey.newSymmetricKey())
for( k in listOf(k1,k2,k3)) {
val i = k.id
val kx = i.kdp!!.deriveKey("foobar")
assertEquals(k, kx)
assertEquals(i, kx.id)
assertEquals(i.kdp, kx.id.kdp)
} }
} }
}