diff --git a/src/commonMain/kotlin/net/sergeych/crypto2/BinaryId.kt b/src/commonMain/kotlin/net/sergeych/crypto2/BinaryId.kt index 6bfbca1..3522776 100644 --- a/src/commonMain/kotlin/net/sergeych/crypto2/BinaryId.kt +++ b/src/commonMain/kotlin/net/sergeych/crypto2/BinaryId.kt @@ -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()) diff --git a/src/commonMain/kotlin/net/sergeych/crypto2/KeyId.kt b/src/commonMain/kotlin/net/sergeych/crypto2/KeyId.kt index 8f0af25..24ae071 100644 --- a/src/commonMain/kotlin/net/sergeych/crypto2/KeyId.kt +++ b/src/commonMain/kotlin/net/sergeych/crypto2/KeyId.kt @@ -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. diff --git a/src/commonMain/kotlin/net/sergeych/crypto2/PublicKey.kt b/src/commonMain/kotlin/net/sergeych/crypto2/PublicKey.kt index 6f51241..d040741 100644 --- a/src/commonMain/kotlin/net/sergeych/crypto2/PublicKey.kt +++ b/src/commonMain/kotlin/net/sergeych/crypto2/PublicKey.kt @@ -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() }.getOrNull() + ?: kotlin.runCatching { data.decodeFromBipack() as PublicKey } + .getOrElse { throw IllegalArgumentException("can't parse verifying key") } + } + } + } + } + } } \ No newline at end of file diff --git a/src/commonMain/kotlin/net/sergeych/crypto2/UniversalKey.kt b/src/commonMain/kotlin/net/sergeych/crypto2/UniversalKey.kt index b22780b..dbf72cf 100644 --- a/src/commonMain/kotlin/net/sergeych/crypto2/UniversalKey.kt +++ b/src/commonMain/kotlin/net/sergeych/crypto2/UniversalKey.kt @@ -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() + } + } + } + } } +inline fun T.asString() = + BipackEncoder.encode(this).encodeToBase64Compact() + + open class IllegalSignatureException(text: String = "signed data is tampered or signature is corrupted") : IllegalStateException(text) diff --git a/src/commonMain/kotlin/net/sergeych/crypto2/VerifyingPublicKey.kt b/src/commonMain/kotlin/net/sergeych/crypto2/VerifyingPublicKey.kt index 82d3d4f..c278105 100644 --- a/src/commonMain/kotlin/net/sergeych/crypto2/VerifyingPublicKey.kt +++ b/src/commonMain/kotlin/net/sergeych/crypto2/VerifyingPublicKey.kt @@ -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() }.getOrNull() + ?: kotlin.runCatching { data.decodeFromBipack() as VerifyingPublicKey } + .getOrElse { throw IllegalArgumentException("can't parse verifying key") } + } + } + } + } + } } \ No newline at end of file diff --git a/src/commonMain/kotlin/net/sergeych/crypto2/kdf.kt b/src/commonMain/kotlin/net/sergeych/crypto2/kdf.kt index 1e05e23..18e1a72 100644 --- a/src/commonMain/kotlin/net/sergeych/crypto2/kdf.kt +++ b/src/commonMain/kotlin/net/sergeych/crypto2/kdf.kt @@ -50,6 +50,11 @@ sealed class KDF { @Suppress("unused") fun deriveMultiple(password: String, count: Int): List = 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() } /** diff --git a/src/commonTest/kotlin/KeysTest.kt b/src/commonTest/kotlin/KeysTest.kt index 9ec6030..d4ec5ec 100644 --- a/src/commonTest/kotlin/KeysTest.kt +++ b/src/commonTest/kotlin/KeysTest.kt @@ -416,4 +416,45 @@ class KeysTest { assertEquals(x, BipackDecoder.decode(y)) assertContentEquals(x.keyBytes, BipackDecoder.decode(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) + } }