diff --git a/.gitignore b/.gitignore index 5256e90..903b262 100644 --- a/.gitignore +++ b/.gitignore @@ -41,4 +41,5 @@ out/ # Other .kotlin .idea +.gigaide /kotlin-js-store/yarn.lock diff --git a/build.gradle.kts b/build.gradle.kts index b9c8057..04c1269 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -8,7 +8,7 @@ plugins { } group = "net.sergeych" -version = "0.5.9-SNAPSHOT" +version = "0.6.1-SNAPSHOT" repositories { mavenCentral() diff --git a/src/commonMain/kotlin/net/sergeych/crypto2/Container.kt b/src/commonMain/kotlin/net/sergeych/crypto2/Container.kt index 42f4fa2..0a3e59f 100644 --- a/src/commonMain/kotlin/net/sergeych/crypto2/Container.kt +++ b/src/commonMain/kotlin/net/sergeych/crypto2/Container.kt @@ -20,6 +20,8 @@ import net.sergeych.crypto2.Container.Companion.createWith * - [addRecipients] and various [plus] operators to add recipients * - [updateData] to change decrypted content for the same recipient keys * + * Note that container _is serialized encrypted_. + * * Some rules: * * When adding public key recipient, it is faster to use your known [SecretKey], but you @@ -126,8 +128,9 @@ sealed class Container { abstract fun updateData(newPlainData: UByteArray, randomFill: IntRange? = null): Container /** - * Binary encoded version. It is desirable to include [Container] as an object, though, - * especially when using custom serialization (Json, Boss, etc), it is serializable. + * Binary encoded _encrypted_ version. It is desirable to include [Container] as an object, though, + * especially when using custom serialization (Json, Boss, etc.), it is serializable. Note that + * serialized data is always encrypted. * Still, if you need it in binary form, this is a shortcut. You can use [decode] or call * [BipackDecoder.decode] to deserialize the binary form. */ @@ -474,6 +477,12 @@ sealed class Container { inline fun decrypt(cipherData: UByteArray, vararg keys: DecryptingKey): T? = decryptAsUBytes(cipherData, *keys)?.let { BipackDecoder.decode(it.asByteArray()) } + inline fun decrypt(cipherData: UByteArray, ring: UniversalRing): T? = + decode(cipherData) + .decryptWith(ring)?.let { + BipackDecoder.decode(it.asByteArray()) + } + fun decryptAsUBytes(cipherData: UByteArray, vararg keys: DecryptingKey): UByteArray? = decode(cipherData).decryptWith(*keys) diff --git a/src/commonMain/kotlin/net/sergeych/crypto2/MagickNumbers.kt b/src/commonMain/kotlin/net/sergeych/crypto2/MagickNumbers.kt index c317bbd..67388e5 100644 --- a/src/commonMain/kotlin/net/sergeych/crypto2/MagickNumbers.kt +++ b/src/commonMain/kotlin/net/sergeych/crypto2/MagickNumbers.kt @@ -6,6 +6,11 @@ enum class KeysmagicNumber(val label: String) { defaultSymmetric( "sym"), defaultSession( "ssn"), defaultVerifying( "ver"), + + defaultSigningSecret( "sig"), + + defaultUniversalPublic( "pub+"), + defaultUniversalPrivate( "prv+"), ; } \ No newline at end of file diff --git a/src/commonMain/kotlin/net/sergeych/crypto2/SigningSecretKey.kt b/src/commonMain/kotlin/net/sergeych/crypto2/SigningSecretKey.kt index d3f6df3..f2a341e 100644 --- a/src/commonMain/kotlin/net/sergeych/crypto2/SigningSecretKey.kt +++ b/src/commonMain/kotlin/net/sergeych/crypto2/SigningSecretKey.kt @@ -23,6 +23,8 @@ class SigningSecretKey( VerifyingPublicKey(Signature.ed25519SkToPk(keyBytes)).also { cachedPublicKey = it } } + override val magic: KeysmagicNumber = KeysmagicNumber.defaultSigningSecret + override fun sign(message: UByteArray): UByteArray = Signature.detached(message, keyBytes) override fun seal(message: UByteArray, expiresAt: Instant?): Seal = diff --git a/src/commonMain/kotlin/net/sergeych/crypto2/UniversalKey.kt b/src/commonMain/kotlin/net/sergeych/crypto2/UniversalKey.kt index b819475..b22780b 100644 --- a/src/commonMain/kotlin/net/sergeych/crypto2/UniversalKey.kt +++ b/src/commonMain/kotlin/net/sergeych/crypto2/UniversalKey.kt @@ -8,7 +8,6 @@ sealed class UniversalKey: KeyInstance { abstract val keyBytes: UByteArray - @Transient open val magic: KeysmagicNumber = KeysmagicNumber.Unknown diff --git a/src/commonMain/kotlin/net/sergeych/crypto2/UniversalPrivateKey.kt b/src/commonMain/kotlin/net/sergeych/crypto2/UniversalPrivateKey.kt new file mode 100644 index 0000000..f8e9a66 --- /dev/null +++ b/src/commonMain/kotlin/net/sergeych/crypto2/UniversalPrivateKey.kt @@ -0,0 +1,47 @@ +package net.sergeych.crypto2 + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable +import kotlinx.serialization.Transient + +/** + * Combination of private/secret keys suitable for both decryption and signing. + * + * It contains two cryptographically independent keys to raise security to a maximum. + * Any converted keys poses a threat while technically possible so we avoid it. + */ +@Serializable +@SerialName("uprv") +class UniversalPrivateKey( + val signingKey: SigningSecretKey, + val decryptingKey: SecretKey +) : UniversalKey(), DecryptingKey by decryptingKey, SigningKey by signingKey { + + override val keyBytes by lazy { signingKey.keyBytes + decryptingKey.keyBytes } + + @Transient + override val magic = KeysmagicNumber.defaultUniversalPrivate + + /** + * Important! Private key combines signing and decrypting keys, but uses + * it of the decrypting one to be used in keyring. + */ + @Transient + override val id: KeyId = decryptingKey.id + + /** + * Corresponding public key able to verify amd encrypt data created by this + * private key. + */ + val publicKey by lazy { + UniversalPublicKey(signingKey.verifyingKey, decryptingKey.publicKey) + } + + companion object { + /** + * Generate 2 new random keys (4 key pairs under the hood) to securely signd and + * decrypt data. + */ + fun new() = UniversalPrivateKey(SigningSecretKey.new(), SecretKey.new()) + } +} \ No newline at end of file diff --git a/src/commonMain/kotlin/net/sergeych/crypto2/UniversalPublicKey.kt b/src/commonMain/kotlin/net/sergeych/crypto2/UniversalPublicKey.kt new file mode 100644 index 0000000..2119356 --- /dev/null +++ b/src/commonMain/kotlin/net/sergeych/crypto2/UniversalPublicKey.kt @@ -0,0 +1,34 @@ +package net.sergeych.crypto2 + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable +import kotlinx.serialization.Transient + +/** + * Combination of public keys suitable for both encryption and verification. A counterpart + * of the [UniversalPrivateKey], available also as [UniversalPrivateKey.publicKey]. + * + * When using [UniversalRing] and [Container], data encrypted with instances og this class + * can be decrypted with rings containing the corresponding [UniversalPrivateKey]. + */ +@Serializable +@SerialName("upub") +class UniversalPublicKey( + val verifyingKey: VerifyingPublicKey, + val encryptingKey: PublicKey +): UniversalKey(), VerifyingKey by verifyingKey, EncryptingKey by encryptingKey{ + + override val keyBytes by lazy { verifyingKey.keyBytes + encryptingKey.keyBytes } + + @Transient + override val magic = KeysmagicNumber.defaultUniversalPublic + + /** + * Important! Private key combines signing and decrypting keys, but uses + * it of the decrypting one to be used in keyring. + */ + @Transient + override val id: KeyId = encryptingKey.id + + +} \ No newline at end of file diff --git a/src/commonTest/kotlin/KeysTest.kt b/src/commonTest/kotlin/KeysTest.kt index bb93edf..109f1c1 100644 --- a/src/commonTest/kotlin/KeysTest.kt +++ b/src/commonTest/kotlin/KeysTest.kt @@ -360,4 +360,34 @@ class KeysTest { assertTrue { mk.check(k4.verifyingKey) } assertTrue { mk.check(k5.verifyingKey) } } + + @Test + fun testCombinedKeys() = runTest { + initCrypto() + val k1 = UniversalPrivateKey.new() + val k2 = UniversalPrivateKey.new() + val k3: UniversalPrivateKey = unpack(pack(k1)) + assertEquals(k1, k3) + assertEquals(k1.publicKey, k3.publicKey) + assertEquals(k1.signingKey, k3.signingKey) + assertEquals(k1.verifyingKey, k3.verifyingKey) + + val k4: UniversalPublicKey = unpack(pack(k1.publicKey)) + assertEquals(k1.publicKey, k4) + assertEquals(k1.publicKey.encryptingKey, k4.encryptingKey) + assertEquals(k1.publicKey.verifyingKey, k4.verifyingKey) + + val data = + """We hold these truths to be self-evident, that all men are created equal, + |that they are endowed by their Creator with certain unalienable Rights, + |that among these are Life, Liberty and the pursuit of Happiness.""" + .trimMargin() + + val kr1 = UniversalRing.from(k1) + val kr2: UniversalRing = UniversalRing.from(k2) + + val bytes = Container.encrypt(data, k2.publicKey) + assertNull(Container.decrypt(bytes, kr1)) + assertEquals(data, Container.decrypt(bytes, kr2)) + } }