Asymmetric.Public now can be converted to Universal and stored into a ring

This commit is contained in:
Sergey Chernov 2024-06-23 09:55:31 +07:00
parent bb383b5457
commit 435604379e
12 changed files with 149 additions and 53 deletions

View File

@ -109,7 +109,7 @@ object Asymmetric {
private fun randomNonce(): UByteArray = randomUBytes(crypto_box_NONCEBYTES)
fun new(): SecretKey = generateKeys().secretKey
fun newSecretKey(): SecretKey = generateKeys().secretKey
@Suppress("unused")
val nonceBytesLength = crypto_box_NONCEBYTES
@ -121,7 +121,7 @@ object Asymmetric {
* Anonymous encryption is very slow in comparison.
*/
@Serializable
class PublicKey(override val keyBytes: UByteArray) : BinaryKeyBase() {
class PublicKey(override val keyBytes: UByteArray) : BinaryKeyBase(), KeyInstance {
override val magick: KeysMagickNumber = KeysMagickNumber.defaultAssymmetric
@ -137,7 +137,7 @@ object Asymmetric {
* proves that the message was not altered after creation.
*/
fun encryptAnonymousMessage(plainData: UByteArray, randomFill: IntRange? = null): Message =
encryptMessage(plainData,randomFill=randomFill)
encryptMessage(plainData, randomFill = randomFill)
/**
* Anonymous encryption, see [encryptAnonymousMessage], to binary data. Sender could not be identified.
@ -158,7 +158,7 @@ object Asymmetric {
fun encryptMessage(
plainData: UByteArray,
nonce: UByteArray = randomNonce(),
senderKey: SecretKey = new(),
senderKey: SecretKey = newSecretKey(),
randomFill: IntRange? = null,
) = createMessage(senderKey, this, WithFill.encode(plainData, randomFill), nonce)
@ -167,11 +167,14 @@ object Asymmetric {
* [SecretKey] corresponding to this one, will be able to decrypt the message and be sure that [senderKey]
* was the author and the message was not altered.
*/
fun encryptMessage(plainData: UByteArray,
senderKey: SecretKey,
randomFill: IntRange? = null): Message =
fun encryptMessage(
plainData: UByteArray,
senderKey: SecretKey,
randomFill: IntRange? = null,
): Message =
createMessage(senderKey, this, WithFill.encode(plainData, randomFill))
fun toUniversalKey(): UniversalKey.Public = UniversalKey.Public(this)
}
/**
@ -209,8 +212,11 @@ object Asymmetric {
* The corresponding public key
*/
val publicKey: PublicKey by lazy {
PublicKey(ScalarMultiplication.scalarMultiplicationBase(keyBytes))
.also { cachedPublicKey = it }
if (cachedPublicKey != null)
cachedPublicKey!!
else
PublicKey(ScalarMultiplication.scalarMultiplicationBase(keyBytes))
.also { cachedPublicKey = it }
}
/**
@ -240,4 +246,4 @@ object Asymmetric {
* Shortcut type: a pair of sender secret key and recipient private key could be used so
* simplify such interfaces
*/
typealias AsymmetricEncryptionPair = Pair<SecretKey?,PublicKey>
typealias AsymmetricEncryptionPair = Pair<SecretKey?, PublicKey>

View File

@ -1,23 +1,7 @@
package net.sergeych.crypto2
import kotlinx.datetime.Instant
import kotlinx.serialization.Serializable
interface VerifyingKey {
val id: KeyId
/**
* Verify the signature and return true if it is correct.
*/
fun verify(signature: UByteArray, message: UByteArray): Boolean
}
interface SigningKey {
val verifyingKey: SigningPublicKey
fun sign(message: UByteArray): UByteArray
fun seal(message: UByteArray, expiresAt: Instant? = null): Seal
}
@Serializable
abstract class BinaryKeyBase() {

View File

@ -235,7 +235,7 @@ sealed class Container {
recipient.id,
recipient.encryptMessage(
encodeMainKey,
senderKey = sender ?: Asymmetric.new(),
senderKey = sender ?: Asymmetric.newSecretKey(),
).encoded
)
}
@ -391,7 +391,7 @@ sealed class Container {
Single(
pk.id, pk.encryptMessage(
plainData,
senderKey = sk ?: Asymmetric.new(),
senderKey = sk ?: Asymmetric.newSecretKey(),
randomFill = fillRange
).encoded,
plainData,

View File

@ -9,7 +9,7 @@ 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 : NonceBased {
interface DecryptingKey : NonceBased, KeyInstance {
/**
* 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
@ -28,8 +28,6 @@ interface DecryptingKey : NonceBased {
fun decryptString(cipherData: UByteArray): String = decrypt(cipherData).decodeFromUByteArray()
val id: KeyId
}
inline fun <reified T>DecryptingKey.decryptObject(cipherData: UByteArray): T =

View File

@ -11,7 +11,7 @@ import net.sergeych.crypto2.SymmetricKey.WithNonce
* It is not serializable by design.
* Custom implementations are, see [SymmetricKey] for example.
*/
interface EncryptingKey : NonceBased {
interface EncryptingKey : NonceBased, KeyInstance {
/**
* Authenticated encrypting with optional random fill to protect from message size analysis.
* Note that [randomFill] if present should be positive.
@ -27,8 +27,6 @@ interface EncryptingKey : NonceBased {
fun encrypt(plainText: String,randomFill: IntRange? = null): UByteArray =
encrypt(plainText.encodeToUByteArray(),randomFill)
val id: KeyId
fun encryptWithNonce(plainData: UByteArray, nonce: UByteArray, randomFill: IntRange? = null): UByteArray
}

View File

@ -0,0 +1,14 @@
package net.sergeych.crypto2
interface KeyInstance {
val id: KeyId
}
/**
* Create a new instance of the corresponding key.
*/
@Suppress("unused")
fun KeyInstance.toUniversalKey(): UniversalKey {
if (this is UniversalKey) return this
return UniversalKey.from(this)
}

View File

@ -0,0 +1,9 @@
package net.sergeych.crypto2
import kotlinx.datetime.Instant
interface SigningKey: KeyInstance {
val verifyingKey: SigningPublicKey
fun sign(message: UByteArray): UByteArray
fun seal(message: UByteArray, expiresAt: Instant? = null): Seal
}

View File

@ -4,8 +4,18 @@ import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import kotlinx.serialization.Transient
/**
* Serializable implementation of the _any key_ conception. Allows serializing collections
* of different keys with arbitrary types, such as [UniversalRing].
*
* To create an `UniversalKey` instance use [UniversalKey.from] or [KeyInstance]
*/
@Serializable
sealed class UniversalKey {
/**
* Propagate real ley instance. Base class itself can't be a key instance, but
* has the same id. This allows requiring key instances in [UniversalRing].
*/
abstract val id: KeyId
@Serializable
@ -84,17 +94,18 @@ sealed class UniversalKey {
companion object {
fun from(key: DecryptingKey): UniversalKey =
fun from(key: KeyInstance): UniversalKey =
when (key) {
is UniversalKey -> key
is Asymmetric.SecretKey -> Secret(key)
is Asymmetric.PublicKey -> Public(key)
is SymmetricKey -> Symmetric(key)
is SafeKeyExchange.SessionKey -> Session(key)
else -> throw UnsupportedOperationException("can't create universal key from ${key::class.simpleName}")
}
fun newSecretKey(): Secret =
Secret(Asymmetric.new())
Secret(Asymmetric.newSecretKey())
fun newSigningKey(): Signing =
Signing(SigningSecretKey.new())
@Suppress("unused")

View File

@ -2,6 +2,17 @@ package net.sergeych.crypto2
import kotlinx.serialization.Serializable
/**
* Keyring capable of holding [UniversalKey] instances (serializable keys of different types)
* associated with a set of string tags. Rings are used to decrypt [Container], to store
* and find keys, etc.
*
* It is _immutable_ so it is safe to share id with no precautions on possible RC. Every
* function that modifies the ring returns a _new instance_ of it.
*
* __important__ serialized keyring is not protected and can disclose keys. Encrypt it after
* serializing before sending over the network or store on media.
*/
@Serializable
class UniversalRing(
val keyWithTags: Map<UniversalKey, Set<String>>,
@ -11,19 +22,44 @@ class UniversalRing(
constructor(vararg keyTags: Pair<UniversalKey, String>)
: this(keyTags.associate { it.first to setOf(it.second) })
val decryptingKeys: Set<DecryptingKey> by lazy { keys<DecryptingKey>() }
/**
* Only decrypting keys. This is a shortcut for [keysOfType]:
* `keysOfType<DecryptingKey>()`.
*/
val decryptingKeys: Set<DecryptingKey> by lazy { keysOfType<DecryptingKey>() }
/**
* Select keys of the specified type
* Select all keys of the specified type. To select all keys, use [allKeys].
*/
inline fun <reified T> keys(): Set<T> =
inline fun <reified T: KeyInstance> keysOfType(): Set<T> =
allKeys.mapNotNull { it as? T }.toSet()
val allKeys: Set<UniversalKey> by lazy { keyWithTags.keys }
inline fun <reified T> findKey(id: KeyId): UniversalKey? =
allKeys.find { it is T && it.id == id }
/**
* Find a key of the specified type that matches the id. __Important__ it is not possible to
* require [UniversalKey] as [T], it is not a [KeyInstance], while its descendants are, [UniversalKey.Secret], etc.
* You can freely use [UniversalKey] subtypes or general key interfaces, e.g.
* [EncryptingKey], [DecryptingKey], [SigningKey] and [VerifyingKey].
*
* Please avoid selecting parameters that make possible to pick more than one key, it will cause an exception.
*
* @return found key by id of the specified type or null
* @throws IllegalArgumentException if there is more than one keys matching the criteria
*/
inline fun <reified T: KeyInstance> findKey(id: KeyId): T? {
val kk = allKeys.filter { it is T && it.id == id }
if( kk.size > 1 )
throw IllegalArgumentException(
"ambiguous type selector ${T::class::simpleName}: ${kk.size} instances found, at most 1 allowed"
)
return kk.firstOrNull() as T?
}
/**
* Get all keys for the specified id (normally it could be 0, 1 or 2). See [KeyId] about
* matching id keys.
*/
fun keysById(id: KeyId): List<UniversalKey> = allKeys.filter { it.id == id }
/**
@ -40,8 +76,12 @@ class UniversalRing(
*/
inline fun <reified T> keyByTag(tag: String) = keysByTags(tag).first { it is T }
/**
* Get keys of the specified type having any of the specified tags associated.
*/
@Suppress("unused")
inline fun <reified T> keyByAnyTag(vararg tags: String) = keysByTags(*tags).first { it is T }
inline fun <reified T> keysByAnyTag(vararg tags: String): Sequence<UniversalKey> =
keysByTags(*tags).filter { it is T }
/**
* Get all keys with a given id. Note that _matching keys_ have the same id, see [KeyId] for more.
@ -72,7 +112,7 @@ class UniversalRing(
* both rings.
*/
operator fun plus(other: UniversalRing): UniversalRing {
var result = keyWithTags.toMutableMap()
val result = keyWithTags.toMutableMap()
for (e in other.keyWithTags.entries) {
result[e.key]?.let {
result[e.key] = it + e.value
@ -158,6 +198,7 @@ class UniversalRing(
/**
* Create string "report" of the ring contents. Note it has no trailing `\n`
*/
@Suppress("unused")
fun ls(): String {
val result = mutableListOf<String>()
for( e in keyWithTags.entries) {
@ -182,8 +223,19 @@ class UniversalRing(
return keyRings.reduce { l, r -> l + r }
}
fun from(vararg keys: DecryptingKey): UniversalRing =
/**
* Convert any keys to [UniversalKey] and form a ring with it.
* @throws UnsupportedOperationException if [UniversalKey] can't build an instance of the specified class.
*/
fun from(vararg keys: KeyInstance): UniversalRing =
UniversalRing(keys.associate { UniversalKey.from(it) to setOf() } )
/**
* Convert any keys to [UniversalKey] and form a ring with it.
* @throws UnsupportedOperationException if [UniversalKey] can't build an instance of the specified class.
*/
fun from(vararg keyTags: Pair<KeyInstance,String>): UniversalRing =
UniversalRing(keyTags.associate { UniversalKey.from(it.first) to setOf(it.second) } )
}
}

View File

@ -0,0 +1,8 @@
package net.sergeych.crypto2
interface VerifyingKey: KeyInstance {
/**
* Verify the signature and return true if it is correct.
*/
fun verify(signature: UByteArray, message: UByteArray): Boolean
}

View File

@ -195,9 +195,9 @@ class KeysTest {
assertEquals(usy2, usy1)
assertFalse { usy1 == usy3 }
val sk1 = Asymmetric.new()
val sk1 = Asymmetric.newSecretKey()
val sk2 = Asymmetric.SecretKey(sk1.keyBytes)
val sk3 = Asymmetric.new()
val sk3 = Asymmetric.newSecretKey()
assertEquals(sk1, sk2)
assertEquals(sk2, sk1)

View File

@ -16,7 +16,7 @@ class RingTest {
val y2 = SymmetricKey("1234567890Hello,dolly.here-we-go".encodeToUByteArray())
assertEquals(y1, y2)
val e1 = Asymmetric.new()
val e1 = Asymmetric.newSecretKey()
val e2: Asymmetric.SecretKey = BipackDecoder.decode(BipackEncoder.encode(e1))
assertEquals(e1, e2)
@ -26,8 +26,8 @@ class RingTest {
assertEquals(k1, k11)
val k2 = UniversalKey.from(Asymmetric.new())
val k3 = UniversalKey.from(Asymmetric.new())
val k2 = UniversalKey.from(Asymmetric.newSecretKey())
val k3 = UniversalKey.from(Asymmetric.newSecretKey())
//
val r = UniversalRing(k1, k2)
// val r = UniversalRing(k1)
@ -35,8 +35,8 @@ class RingTest {
assertTrue(k1 in r)
assertFalse { k3 in r }
println(Asymmetric.new().keyBytes.size)
println(BipackEncoder.encode(Asymmetric.new()).size)
println(Asymmetric.newSecretKey().keyBytes.size)
println(BipackEncoder.encode(Asymmetric.newSecretKey()).size)
val encoded = BipackEncoder.encode(r)
println(encoded.toDump())
println(encoded.size)
@ -56,12 +56,28 @@ class RingTest {
assertEquals(r, r2)
}
@Test
fun testAsymmetricPublic() = runTest {
initCrypto()
val sk = Asymmetric.newSecretKey()
assertEquals(sk.id, sk.publicKey.id)
val r = UniversalRing.from(sk.publicKey to "foo")
println(sk.publicKey.id)
println(sk.id)
println(r.findKey<EncryptingKey>(sk.id))
println(BipackEncoder.encode(r).toDump())
val r1 = deepCopy(r)
println(r1.findKey<EncryptingKey>(sk.id))
println(sk)
assertTrue { sk.publicKey.toUniversalKey() == r1.findKey<EncryptingKey>(sk.id) }
}
@Test
fun testTags() = runTest {
initCrypto()
val k1 = UniversalKey.from(SymmetricKey("1234567890Hello,dolly.here-we-go".encodeToUByteArray()))
val k2 = UniversalKey.from(Asymmetric.new())
val k2 = UniversalKey.from(Asymmetric.newSecretKey())
val r1 = UniversalRing(k1, k2)
var r2 = UniversalRing(deepCopy(k1), deepCopy(k2))