add general support from encoding to string and restoring from string various keys formats. + docs.
This commit is contained in:
parent
e8fa634640
commit
cef7e4abed
@ -48,7 +48,7 @@ open class BinaryId protected constructor (
|
|||||||
/**
|
/**
|
||||||
* Bad format (crc does not match)
|
* 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]
|
* 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.
|
* Restore a string representation of existing BinaryId.
|
||||||
*/
|
*/
|
||||||
@Suppress("unused")
|
@Suppress("unused")
|
||||||
fun restoreFromString(str: String): BinaryId =
|
fun restoreFromString(str: String): BinaryId = kotlin.runCatching {
|
||||||
BinaryId(str.decodeBase64Url().toUByteArray())
|
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())
|
fun createFromBytes(magic: Int, bytes: ByteArray): BinaryId = createFromUBytes(magic, bytes.toUByteArray())
|
||||||
|
@ -18,6 +18,9 @@ import kotlinx.serialization.Serializable
|
|||||||
*
|
*
|
||||||
* See [PBKD.Params.deriveKey] for deriving keys from id.
|
* 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 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
|
* @param kdp optional key derivation parameters. Does not affect equality. Allow deriving the key from proper
|
||||||
* password, see above.
|
* password, see above.
|
||||||
|
@ -3,6 +3,9 @@ package net.sergeych.crypto2
|
|||||||
import kotlinx.serialization.SerialName
|
import kotlinx.serialization.SerialName
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
import kotlinx.serialization.Transient
|
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
|
* 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))
|
Asymmetric.createMessage(senderKey, this, WithFill.encode(plainData, randomFill))
|
||||||
|
|
||||||
override val id by lazy { KeyId(magic, keyBytes, null, true) }
|
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") }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
@ -2,16 +2,28 @@ package net.sergeych.crypto2
|
|||||||
|
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
import kotlinx.serialization.Transient
|
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
|
@Serializable
|
||||||
sealed class UniversalKey: KeyInstance {
|
sealed class UniversalKey : KeyInstance {
|
||||||
|
|
||||||
abstract val keyBytes: UByteArray
|
abstract val keyBytes: UByteArray
|
||||||
|
|
||||||
@Transient
|
@Transient
|
||||||
open val magic: KeysmagicNumber = KeysmagicNumber.Unknown
|
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) }
|
override val id by lazy { KeyId(magic, keyBytes, null) }
|
||||||
|
|
||||||
// Important: id can be overridden, so we use it, not magic:
|
// Important: id can be overridden, so we use it, not magic:
|
||||||
@ -33,12 +45,34 @@ sealed class UniversalKey: KeyInstance {
|
|||||||
companion object {
|
companion object {
|
||||||
fun newSecretKey() = SecretKey.new()
|
fun newSecretKey() = SecretKey.new()
|
||||||
fun newSigningKey() = SigningSecretKey.new()
|
fun newSigningKey() = SigningSecretKey.new()
|
||||||
|
|
||||||
@Suppress("unused")
|
@Suppress("unused")
|
||||||
fun newSymmetricKey() = SymmetricKey.new()
|
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") :
|
open class IllegalSignatureException(text: String = "signed data is tampered or signature is corrupted") :
|
||||||
IllegalStateException(text)
|
IllegalStateException(text)
|
||||||
|
|
||||||
|
@ -5,6 +5,8 @@ import com.ionspin.kotlin.crypto.signature.Signature
|
|||||||
import kotlinx.serialization.SerialName
|
import kotlinx.serialization.SerialName
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
import kotlinx.serialization.Transient
|
import kotlinx.serialization.Transient
|
||||||
|
import net.sergeych.bipack.decodeFromBipack
|
||||||
|
import net.sergeych.mp_tools.decodeBase64Url
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Public key to verify signatures only
|
* 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
|
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") }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
@ -50,6 +50,11 @@ sealed class KDF {
|
|||||||
@Suppress("unused")
|
@Suppress("unused")
|
||||||
fun deriveMultiple(password: String, count: Int): List<SymmetricKey> =
|
fun deriveMultiple(password: String, count: Int): List<SymmetricKey> =
|
||||||
kdfForSize(count).deriveMultipleKeys(password, count)
|
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()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -416,4 +416,45 @@ class KeysTest {
|
|||||||
assertEquals(x, BipackDecoder.decode<SecretKey>(y))
|
assertEquals(x, BipackDecoder.decode<SecretKey>(y))
|
||||||
assertContentEquals(x.keyBytes, BipackDecoder.decode<SecretKey>(y).keyBytes)
|
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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user