add general support from encoding to string and restoring from string various keys formats. + docs.

This commit is contained in:
Sergey Chernov 2024-11-27 18:11:45 +07:00
parent e8fa634640
commit cef7e4abed
7 changed files with 165 additions and 4 deletions

View File

@ -48,7 +48,7 @@ open class BinaryId protected constructor (
/**
* Bad format (crc does not match)
*/
class InvalidException(text: String) : IllegalArgumentException(text)
class InvalidException(text: String,reason: Throwable?=null) : IllegalArgumentException(text,reason)
/**
* Attempt to compare binary ids with different magic. In this case only [equals]
@ -136,8 +136,9 @@ open class BinaryId protected constructor (
* Restore a string representation of existing BinaryId.
*/
@Suppress("unused")
fun restoreFromString(str: String): BinaryId =
fun restoreFromString(str: String): BinaryId = kotlin.runCatching {
BinaryId(str.decodeBase64Url().toUByteArray())
}.getOrElse { throw InvalidException("can't parse binary id: $str", it) }
fun createFromBytes(magic: Int, bytes: ByteArray): BinaryId = createFromUBytes(magic, bytes.toUByteArray())

View File

@ -18,6 +18,9 @@ import kotlinx.serialization.Serializable
*
* See [PBKD.Params.deriveKey] for deriving keys from id.
*
* See [id], and [BinaryId] class for more. Note that for [PublicKey] and [VerifyingPublicKey] [BinaryId.asPublicKey]
* and [BinaryId.asVerifyingKey] restore actual keys, providing [BinaryId.magic] has proper value, see [KeysmagicNumber]]
*
* @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.

View File

@ -3,6 +3,9 @@ package net.sergeych.crypto2
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import kotlinx.serialization.Transient
import net.sergeych.bipack.decodeFromBipack
import net.sergeych.crypto2.VerifyingPublicKey.Companion.toString
import net.sergeych.mp_tools.decodeBase64Url
/**
* The public for public-key encryption. It encrypts messages that can only be decrypted with corresponding
@ -71,4 +74,40 @@ class PublicKey(override val keyBytes: UByteArray) : UniversalKey(), EncryptingK
Asymmetric.createMessage(senderKey, this, WithFill.encode(plainData, randomFill))
override val id by lazy { KeyId(magic, keyBytes, null, true) }
companion object {
/**
* Parse any known public key text representation, including what [toString] return (for public keys it is
* possible)
* @throws IllegalArgumentException the public key isn't recognized
*/
fun parse(text: String): PublicKey {
val s = text.trim()
fun parseId(t: String): PublicKey{
val id = BinaryId.restoreFromString(t)
if( id.magic != KeysmagicNumber.defaultAssymmetric.ordinal)
throw IllegalArgumentException("invalid magick ${id.magic} for PublicKey")
return id.asPublicKey
}
// 🗝sig#I1po9Y2I7p2aOxeh4nFyGPm3e0YunBEu1Mo-PmIqP84Evg
return when {
s.startsWith("\uD83D\uDDDDpub#") -> parseId(s.drop(6))
s.startsWith("pub#") -> parseId(s.drop(4))
s.startsWith("#") -> parseId(s.drop(1))
else -> {
// consider it is serialized key in base64 format
val data = s.decodeBase64Url().asUByteArray()
if (data.size == 32)
PublicKey(data)
else {
runCatching { data.decodeFromBipack<PublicKey>() }.getOrNull()
?: kotlin.runCatching { data.decodeFromBipack<UniversalKey>() as PublicKey }
.getOrElse { throw IllegalArgumentException("can't parse verifying key") }
}
}
}
}
}
}

View File

@ -2,16 +2,28 @@ package net.sergeych.crypto2
import kotlinx.serialization.Serializable
import kotlinx.serialization.Transient
import net.sergeych.bipack.BipackEncoder
import net.sergeych.bipack.decodeFromBipack
import net.sergeych.mp_tools.decodeBase64Compact
import net.sergeych.mp_tools.encodeToBase64Compact
@Serializable
sealed class UniversalKey: KeyInstance {
sealed class UniversalKey : KeyInstance {
abstract val keyBytes: UByteArray
@Transient
open val magic: KeysmagicNumber = KeysmagicNumber.Unknown
/**
* Key ID positively identify key from the point of view of _decrypting or verifying_. So matching [VerifyingKey]
* and [SigningKey] will have the same id, same as matching [PublicKey] and [SecretKey].
*
* KeyId is based on [BinaryId] which includes checksum (crc8) and magick number for additional security,
* see [KeysmagicNumber].
*
* Also "public" keys can be restored from id using [BinaryId.asPublicKey] and [BinaryId.asVerifyingKey].
*/
override val id by lazy { KeyId(magic, keyBytes, null) }
// Important: id can be overridden, so we use it, not magic:
@ -33,12 +45,34 @@ sealed class UniversalKey: KeyInstance {
companion object {
fun newSecretKey() = SecretKey.new()
fun newSigningKey() = SigningSecretKey.new()
@Suppress("unused")
fun newSymmetricKey() = SymmetricKey.new()
/**
* Parse all known string representations of the universal key
* @throws IllegalArgumentException if it can't parse any key.
*/
fun parseString(text: String): UniversalKey {
val s = text.trim()
return when {
s.startsWith("\uD83D\uDDDDpub#") || s.startsWith("pub#") ->
PublicKey.parse(s)
s.startsWith("\uD83D\uDDDDver#") || s.startsWith("ver#") ->
VerifyingPublicKey.parse(s)
else -> {
s.decodeBase64Compact().decodeFromBipack<UniversalKey>()
}
}
}
}
}
inline fun <reified T : UniversalKey> T.asString() =
BipackEncoder.encode<T>(this).encodeToBase64Compact()
open class IllegalSignatureException(text: String = "signed data is tampered or signature is corrupted") :
IllegalStateException(text)

View File

@ -5,6 +5,8 @@ import com.ionspin.kotlin.crypto.signature.Signature
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import kotlinx.serialization.Transient
import net.sergeych.bipack.decodeFromBipack
import net.sergeych.mp_tools.decodeBase64Url
/**
* Public key to verify signatures only
@ -53,5 +55,41 @@ class VerifyingPublicKey(override val keyBytes: UByteArray) : UniversalKey(), Ve
*/
infix fun and(other: Multikey) = Multikey(this) and other
companion object {
/**
* Parse any known public key text representation, including what [toString] return (for public keys it is
* possible)
* @throws IllegalArgumentException the public key isn't recognized, in particular [BinaryId.InvalidException]
* if the text is corrupt
*/
fun parse(text: String): VerifyingPublicKey {
val s = text.trim()
fun parseId(t: String): VerifyingPublicKey {
// assume it is an id:
val id = BinaryId.restoreFromString(t)
return if (id.magic == KeysmagicNumber.defaultVerifying.ordinal)
id.asVerifyingKey as VerifyingPublicKey
else throw IllegalArgumentException("Invalid magick: ${id.magic} when parsing[$t]")
}
// 🗝sig#I1po9Y2I7p2aOxeh4nFyGPm3e0YunBEu1Mo-PmIqP84Evg
return when {
s.startsWith("\uD83D\uDDDDver#") -> parseId(s.drop(6))
s.startsWith("ver#") -> parseId(s.drop(4))
s.startsWith("#") -> parseId(s.drop(1))
else -> {
// consider it is serialized key in base64 format
val data = s.decodeBase64Url().asUByteArray()
if (data.size == 32)
VerifyingPublicKey(data)
else {
runCatching { data.decodeFromBipack<VerifyingPublicKey>() }.getOrNull()
?: kotlin.runCatching { data.decodeFromBipack<UniversalKey>() as VerifyingPublicKey }
.getOrElse { throw IllegalArgumentException("can't parse verifying key") }
}
}
}
}
}
}

View File

@ -50,6 +50,11 @@ sealed class KDF {
@Suppress("unused")
fun deriveMultiple(password: String, count: Int): List<SymmetricKey> =
kdfForSize(count).deriveMultipleKeys(password, count)
/**
* Derive single key from password, same as [deriveMultiple] with count=1.
*/
fun derive(password: String): SymmetricKey = deriveMultiple(password, 1).first()
}
/**

View File

@ -416,4 +416,45 @@ class KeysTest {
assertEquals(x, BipackDecoder.decode<SecretKey>(y))
assertContentEquals(x.keyBytes, BipackDecoder.decode<SecretKey>(y).keyBytes)
}
@Test
fun testStringRepresentationAndParse() = runTest {
initCrypto()
val k1 = SigningSecretKey.new()
val k2 = k1.verifyingKey
val k3 = SecretKey.new()
val k4 = k3.publicKey
val k5 = UniversalPrivateKey.new()
val k6 = k5.publicKey
assertEquals(32, k2.keyBytes.size)
assertContentEquals(k2.keyBytes, k2.id.id.body)
val k7 = SymmetricKey.new()
val k8 = KDF.Complexity.Interactive.derive("super")
fun testToString(k: UniversalKey) {
val s = k.toString()
val kx = UniversalKey.parseString(s)
assertEquals(kx::class, k::class)
assertContentEquals(k.keyBytes, kx.keyBytes)
assertEquals(k.id, kx.id)
assertEquals(k, kx)
}
fun testAsString(k: UniversalKey) {
val s = k.asString()
val kx = UniversalKey.parseString(s)
assertEquals(kx::class, k::class)
assertContentEquals(k.keyBytes, kx.keyBytes)
assertEquals(k.id, kx.id)
assertEquals(k, kx)
}
testToString(k2)
testToString(k4)
for( i in listOf(k1, k2, k3, k4, k5, k6, k7, k8)) testAsString(i)
}
}