diff --git a/.github/workflows/shared-core-tests.yml b/.github/workflows/shared-core-tests.yml index 63b9509076..d1fbb7eb79 100644 --- a/.github/workflows/shared-core-tests.yml +++ b/.github/workflows/shared-core-tests.yml @@ -72,6 +72,7 @@ jobs: :libs:codes:kikcode:iosSimulatorArm64Test \ :libs:currency-math:discrete-curve:iosSimulatorArm64Test \ :libs:encryption:base58:iosSimulatorArm64Test \ + :libs:encryption:chat-cipher:iosSimulatorArm64Test \ :libs:encryption:ed25519:iosSimulatorArm64Test \ :libs:encryption:hmac:iosSimulatorArm64Test \ :libs:encryption:mnemonic:iosSimulatorArm64Test \ diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 77cff614f0..299b147351 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -271,6 +271,7 @@ mixpanel = { module = "com.mixpanel.android:mixpanel-android", version.ref = "mi # Crypto sodium-bindings = { module = "com.ionspin.kotlin:multiplatform-crypto-libsodium-bindings-android", version.ref = "sodium-bindings" } +sodium-bindings-kmp = { module = "com.ionspin.kotlin:multiplatform-crypto-libsodium-bindings", version.ref = "sodium-bindings" } ionspin-bignum = { module = "com.ionspin.kotlin:bignum", version.ref = "bignum" } eddsa = { module = "net.i2p.crypto:eddsa", version = "0.3.0" } kotlincrypto-hash-sha2 = { module = "org.kotlincrypto.hash:sha2", version.ref = "kotlincrypto-hash" } diff --git a/kmp/shared-core/build.gradle.kts b/kmp/shared-core/build.gradle.kts index cb9fb0a06f..5b274a4017 100644 --- a/kmp/shared-core/build.gradle.kts +++ b/kmp/shared-core/build.gradle.kts @@ -43,6 +43,7 @@ kotlin { export(project(":libs:encryption:sha512")) export(project(":libs:encryption:hmac")) export(project(":libs:encryption:ed25519")) + export(project(":libs:encryption:chat-cipher")) export(project(":libs:encryption:mnemonic")) export(project(":libs:currency-math:discrete-curve")) export(project(":libs:reporting")) @@ -59,6 +60,7 @@ kotlin { api(project(":libs:encryption:sha512")) api(project(":libs:encryption:hmac")) api(project(":libs:encryption:ed25519")) + api(project(":libs:encryption:chat-cipher")) api(project(":libs:encryption:mnemonic")) api(project(":libs:currency-math:discrete-curve")) api(project(":libs:reporting")) diff --git a/libs/encryption/chat-cipher/.gitignore b/libs/encryption/chat-cipher/.gitignore new file mode 100644 index 0000000000..796b96d1c4 --- /dev/null +++ b/libs/encryption/chat-cipher/.gitignore @@ -0,0 +1 @@ +/build diff --git a/libs/encryption/chat-cipher/build.gradle.kts b/libs/encryption/chat-cipher/build.gradle.kts new file mode 100644 index 0000000000..3491f546a8 --- /dev/null +++ b/libs/encryption/chat-cipher/build.gradle.kts @@ -0,0 +1,62 @@ +import org.jetbrains.kotlin.gradle.plugin.KotlinSourceSetTree + +plugins { + kotlin("multiplatform") + id("com.android.kotlin.multiplatform.library") + alias(libs.plugins.flipcash.kmp.test.fixtures) +} + +// Compiles `src/commonTest/resources` (chat_cipher.json, canonical copy in the orchestrator's +// `test-vectors/`) into a generated `TestFixtures.kt` on `commonTest`, readable from every target. +testFixtures { + packageName = "com.getcode.chatcipher" +} + +kotlin { + android { + namespace = "com.getcode.encryption.chatcipher" + compileSdk { + version = release(libs.versions.android.compileSdk.get().toInt()) { + minorApiLevel = libs.versions.android.compileSdkMinor.get().toInt() + } + } + minSdk = 29 + // No host test: libsodium's Android binding loads its .so from the APK and the Ed25519 + // actual is JNI, so neither loads on a JVM host. The device test runs commonTest instead, + // and the Apple targets run it natively. + withDeviceTestBuilder { + sourceSetTreeName = KotlinSourceSetTree.test.name + }.configure { + instrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + } + } + + iosArm64() + iosSimulatorArm64() + iosX64() + macosArm64() + macosX64() + + sourceSets { + commonMain { + dependencies { + // KeyPair is part of this module's API. + api(project(":libs:encryption:ed25519")) + implementation(project(":libs:encryption:hmac")) + implementation(libs.sodium.bindings.kmp) + } + } + commonTest { + dependencies { + implementation(kotlin("test")) + implementation(libs.kotlinx.serialization.json) + } + } + getByName("androidDeviceTest") { + dependencies { + implementation(libs.androidx.junit) + implementation(libs.androidx.test.runner) + } + } + } +} diff --git a/libs/encryption/chat-cipher/src/commonMain/kotlin/com/getcode/chatcipher/ChatCipher.kt b/libs/encryption/chat-cipher/src/commonMain/kotlin/com/getcode/chatcipher/ChatCipher.kt new file mode 100644 index 0000000000..42863888b8 --- /dev/null +++ b/libs/encryption/chat-cipher/src/commonMain/kotlin/com/getcode/chatcipher/ChatCipher.kt @@ -0,0 +1,75 @@ +package com.getcode.chatcipher + +import com.getcode.ed25519kmp.KeyPair + +/** A message or blob could not be encrypted or decrypted. Callers render it as unsupported. */ +class ChatCipherException(message: String, cause: Throwable? = null) : Exception(message, cause) + +/** A message ciphertext (with its 16-byte tag) and its 24-byte nonce, as carried by `EncryptedContent`. */ +class EncryptedPayload(val nonce: ByteArray, val ciphertext: ByteArray) + +/** + * Scheme `X25519_XCHACHA20POLY1305` from `messaging.v1.EncryptedContent` in flipcash2-protobuf-api. + * Shared by both apps; `test-vectors/chat_cipher.json` in the orchestrator repo pins it. + * [DefaultChatCipher] is the implementation; the interface is the seam for fakes in either app. + * + * All keys are raw bytes: Ed25519 public keys are 32 bytes, the chat key is 32 bytes. `chatId` and + * `blobId` are the raw `value` bytes of `ChatId` and `BlobId`. The chat key depends only on the two + * members and the chat, so callers derive it once per chat and cache it. + */ +interface ChatCipher { + /** + * The chat key for [ownKeyPair] and [peerPublicKey] (an Ed25519 public key) in [chatId]. + * Both members derive the same key. + * + * @throws ChatCipherException if the own key pair is malformed, the peer key is not a valid point, is low-order, or the shared secret is all zeros. + */ + @Throws(ChatCipherException::class) + fun chatKey(ownKeyPair: KeyPair, peerPublicKey: ByteArray, chatId: ByteArray): ByteArray + + /** Encrypts serialized `Content` bytes from [senderPk] to [recipientPk] under a fresh random nonce. */ + @Throws(ChatCipherException::class) + fun encrypt( + content: ByteArray, + chatKey: ByteArray, + senderPk: ByteArray, + recipientPk: ByteArray, + chatId: ByteArray, + ): EncryptedPayload + + /** + * Decrypts [payload] to the serialized `Content` bytes. + * + * @throws ChatCipherException if authentication fails, including a swapped [senderPk]/[recipientPk]. + */ + @Throws(ChatCipherException::class) + fun decrypt( + payload: EncryptedPayload, + chatKey: ByteArray, + senderPk: ByteArray, + recipientPk: ByteArray, + chatId: ByteArray, + ): ByteArray + + /** Returns `nonce || ciphertext || tag`, the bytes to upload. [senderPk] is the uploader. */ + @Throws(ChatCipherException::class) + fun encryptBlob( + image: ByteArray, + chatKey: ByteArray, + senderPk: ByteArray, + recipientPk: ByteArray, + chatId: ByteArray, + blobId: ByteArray, + ): ByteArray + + /** @throws ChatCipherException if [blob] is too short or fails authentication. */ + @Throws(ChatCipherException::class) + fun decryptBlob( + blob: ByteArray, + chatKey: ByteArray, + senderPk: ByteArray, + recipientPk: ByteArray, + chatId: ByteArray, + blobId: ByteArray, + ): ByteArray +} diff --git a/libs/encryption/chat-cipher/src/commonMain/kotlin/com/getcode/chatcipher/ChatEncryptionPolicy.kt b/libs/encryption/chat-cipher/src/commonMain/kotlin/com/getcode/chatcipher/ChatEncryptionPolicy.kt new file mode 100644 index 0000000000..7e5663ca14 --- /dev/null +++ b/libs/encryption/chat-cipher/src/commonMain/kotlin/com/getcode/chatcipher/ChatEncryptionPolicy.kt @@ -0,0 +1,28 @@ +package com.getcode.chatcipher + +/** Which chats send `EncryptedContent`. A product rule, separate from the [ChatCipher] scheme. */ +object ChatEncryptionPolicy { + private val FLIPCASH_USER_ID_BYTES = "70c4a3df54af439a88fea7de606d04cb".chunked(2) + .map { it.toInt(16).toByte() } + .toByteArray() + + /** + * The raw 16-byte user id of the @flipcash account (UUID `70c4a3df-54af-439a-88fe-a7de606d04cb`). + * DMs with it are not end-to-end encrypted for now: the backend sends its onboarding messages in + * plaintext. Returns a copy. + */ + val FLIPCASH_USER_ID: ByteArray get() = FLIPCASH_USER_ID_BYTES.copyOf() + + /** + * Whether a message to [peerUserId] should be sent as `EncryptedContent`: the chat is a DM + * (`CONTACT_DM` or `TIP_DM`), the chat's `use_e2ee` flag is set, and the peer is not @flipcash. + * + * [useE2ee] is a transitional server flag. After launch the rule drops it and becomes "a DM with + * anyone but @flipcash"; this is the only place that reads it. + * + * [peerUserId] is the raw `UserId.value`, a 16-byte UUID. An id of any other length is treated + * as not @flipcash rather than rejected. + */ + fun shouldEncrypt(isDirectMessage: Boolean, useE2ee: Boolean, peerUserId: ByteArray): Boolean = + isDirectMessage && useE2ee && !peerUserId.contentEquals(FLIPCASH_USER_ID_BYTES) +} diff --git a/libs/encryption/chat-cipher/src/commonMain/kotlin/com/getcode/chatcipher/DefaultChatCipher.kt b/libs/encryption/chat-cipher/src/commonMain/kotlin/com/getcode/chatcipher/DefaultChatCipher.kt new file mode 100644 index 0000000000..ce376a0c31 --- /dev/null +++ b/libs/encryption/chat-cipher/src/commonMain/kotlin/com/getcode/chatcipher/DefaultChatCipher.kt @@ -0,0 +1,190 @@ +@file:OptIn(ExperimentalUnsignedTypes::class) + +package com.getcode.chatcipher + +import com.getcode.crypt.Hmac +import com.getcode.ed25519kmp.KeyPair +import com.ionspin.kotlin.crypto.LibsodiumInitializer +import com.ionspin.kotlin.crypto.aead.AuthenticatedEncryptionWithAssociatedData +import com.ionspin.kotlin.crypto.scalarmult.ScalarMultiplication +import com.ionspin.kotlin.crypto.signature.Signature +import com.ionspin.kotlin.crypto.util.LibsodiumRandom + +/** + * [ChatCipher] for scheme `X25519_XCHACHA20POLY1305`, on libsodium. Stateless; libsodium is + * initialized on first use if the app has not done it already. + */ +object DefaultChatCipher : ChatCipher { + private const val KEY_SIZE = 32 + private const val NONCE_SIZE = 24 + private const val TAG_SIZE = 16 + private val LABEL = "flipcash-dm-e2ee-v1".encodeToByteArray() + private val BLOB_LABEL = "flipcash-dm-e2ee-blob-v1".encodeToByteArray() + + override fun chatKey(ownKeyPair: KeyPair, peerPublicKey: ByteArray, chatId: ByteArray): ByteArray { + if (ownKeyPair.publicKey.size != KEY_SIZE || ownKeyPair.privateKey.size != 64) { + throw ChatCipherException("own key pair must be a 32-byte public and 64-byte private key") + } + val peerX = peerToX25519(peerPublicKey) + val ss = sharedSecret(ownToX25519(ownKeyPair), peerX) + val a = ownKeyPair.publicKey + val b = peerPublicKey + val salt = if (compareBytes(a, b) <= 0) a + b else b + a + return hkdfSha256(ikm = ss, salt = salt, info = LABEL + chatId, length = KEY_SIZE) + } + + override fun encrypt( + content: ByteArray, + chatKey: ByteArray, + senderPk: ByteArray, + recipientPk: ByteArray, + chatId: ByteArray, + ): EncryptedPayload = encrypt(content, chatKey, senderPk, recipientPk, chatId, randomNonce()) + + internal fun encrypt( + content: ByteArray, + chatKey: ByteArray, + senderPk: ByteArray, + recipientPk: ByteArray, + chatId: ByteArray, + nonce: ByteArray, + ): EncryptedPayload { + val aad = LABEL + chatId + senderPk + recipientPk + return EncryptedPayload(nonce, seal(content, aad, nonce, chatKey)) + } + + override fun decrypt( + payload: EncryptedPayload, + chatKey: ByteArray, + senderPk: ByteArray, + recipientPk: ByteArray, + chatId: ByteArray, + ): ByteArray = open(payload.ciphertext, LABEL + chatId + senderPk + recipientPk, payload.nonce, chatKey) + + override fun encryptBlob( + image: ByteArray, + chatKey: ByteArray, + senderPk: ByteArray, + recipientPk: ByteArray, + chatId: ByteArray, + blobId: ByteArray, + ): ByteArray = encryptBlob(image, chatKey, senderPk, recipientPk, chatId, blobId, randomNonce()) + + internal fun encryptBlob( + image: ByteArray, + chatKey: ByteArray, + senderPk: ByteArray, + recipientPk: ByteArray, + chatId: ByteArray, + blobId: ByteArray, + nonce: ByteArray, + ): ByteArray { + val aad = BLOB_LABEL + chatId + senderPk + recipientPk + blobId + return nonce + seal(image, aad, nonce, chatKey) + } + + override fun decryptBlob( + blob: ByteArray, + chatKey: ByteArray, + senderPk: ByteArray, + recipientPk: ByteArray, + chatId: ByteArray, + blobId: ByteArray, + ): ByteArray { + if (blob.size < NONCE_SIZE + TAG_SIZE) throw ChatCipherException("blob shorter than nonce and tag") + val aad = BLOB_LABEL + chatId + senderPk + recipientPk + blobId + return open(blob.copyOfRange(NONCE_SIZE, blob.size), aad, blob.copyOfRange(0, NONCE_SIZE), chatKey) + } + + // -- steps ------------------------------------------------------------------------------- + + /** `crypto_sign_ed25519_sk_to_curve25519`: clamp(SHA-512(seed)[0..32]). The orlp private key already is that expansion. */ + internal fun ownToX25519(own: KeyPair): ByteArray { + val x = own.privateKey.copyOfRange(0, KEY_SIZE) + x[0] = (x[0].toInt() and 248).toByte() + x[31] = ((x[31].toInt() and 127) or 64).toByte() + return x + } + + /** `crypto_sign_ed25519_pk_to_curve25519`; libsodium rejects invalid, low-order and non-prime-order points. */ + internal fun peerToX25519(edPublicKey: ByteArray): ByteArray { + if (edPublicKey.size != KEY_SIZE) throw ChatCipherException("public key must be 32 bytes") + ensureSodium() + return try { + Signature.ed25519PkToCurve25519(edPublicKey.toUByteArray()).toByteArray() + } catch (e: Throwable) { + throw ChatCipherException("peer public key rejected", e) + } + } + + /** `X25519(priv, pub)`, aborting on an all-zero result. */ + internal fun sharedSecret(xPrivate: ByteArray, xPublic: ByteArray): ByteArray { + ensureSodium() + val ss = try { + ScalarMultiplication.scalarMultiplication(xPrivate.toUByteArray(), xPublic.toUByteArray()).toByteArray() + } catch (e: Throwable) { + throw ChatCipherException("X25519 rejected the peer key", e) + } + if (ss.all { it.toInt() == 0 }) throw ChatCipherException("all-zero shared secret") + return ss + } + + /** HKDF-SHA256 (RFC 5869) on the shared HMAC module. */ + internal fun hkdfSha256(ikm: ByteArray, salt: ByteArray, info: ByteArray, length: Int): ByteArray { + val prk = Hmac.hmac("HmacSHA256", salt, ikm) + var t = ByteArray(0) + var out = ByteArray(0) + var counter = 1 + while (out.size < length) { + t = Hmac.hmac("HmacSHA256", prk, t + info + byteArrayOf(counter.toByte())) + out += t + counter++ + } + return out.copyOfRange(0, length) + } + + private fun seal(plain: ByteArray, aad: ByteArray, nonce: ByteArray, key: ByteArray): ByteArray { + checkKeyAndNonce(key, nonce) + ensureSodium() + return AuthenticatedEncryptionWithAssociatedData.xChaCha20Poly1305IetfEncrypt( + plain.toUByteArray(), aad.toUByteArray(), nonce.toUByteArray(), key.toUByteArray(), + ).toByteArray() + } + + private fun open(cipher: ByteArray, aad: ByteArray, nonce: ByteArray, key: ByteArray): ByteArray { + checkKeyAndNonce(key, nonce) + if (cipher.size < TAG_SIZE) throw ChatCipherException("ciphertext shorter than tag") + ensureSodium() + return try { + AuthenticatedEncryptionWithAssociatedData.xChaCha20Poly1305IetfDecrypt( + cipher.toUByteArray(), aad.toUByteArray(), nonce.toUByteArray(), key.toUByteArray(), + ).toByteArray() + } catch (e: Throwable) { + throw ChatCipherException("authentication failed", e) + } + } + + private fun checkKeyAndNonce(key: ByteArray, nonce: ByteArray) { + if (key.size != KEY_SIZE) throw ChatCipherException("chat key must be 32 bytes") + if (nonce.size != NONCE_SIZE) throw ChatCipherException("nonce must be 24 bytes") + } + + private fun randomNonce(): ByteArray { + ensureSodium() + return LibsodiumRandom.buf(NONCE_SIZE).toByteArray() + } + + /** Bytewise unsigned comparison, as the spec orders the two public keys. */ + private fun compareBytes(a: ByteArray, b: ByteArray): Int { + for (i in 0 until minOf(a.size, b.size)) { + val d = (a[i].toInt() and 0xFF) - (b[i].toInt() and 0xFF) + if (d != 0) return d + } + return a.size - b.size + } + + private fun ensureSodium() { + // Synchronous on JVM/Android/Native; the app also initializes it at startup. + if (!LibsodiumInitializer.isInitialized()) LibsodiumInitializer.initializeWithCallback { } + } +} diff --git a/libs/encryption/chat-cipher/src/commonTest/kotlin/com/getcode/chatcipher/ChatCipherVectorTest.kt b/libs/encryption/chat-cipher/src/commonTest/kotlin/com/getcode/chatcipher/ChatCipherVectorTest.kt new file mode 100644 index 0000000000..6a177801af --- /dev/null +++ b/libs/encryption/chat-cipher/src/commonTest/kotlin/com/getcode/chatcipher/ChatCipherVectorTest.kt @@ -0,0 +1,179 @@ +package com.getcode.chatcipher + +import com.getcode.ed25519kmp.Ed25519Kmp +import com.getcode.ed25519kmp.KeyPair +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.boolean +import kotlinx.serialization.json.jsonArray +import kotlinx.serialization.json.jsonObject +import kotlinx.serialization.json.jsonPrimitive +import kotlin.test.Test +import kotlin.test.assertContentEquals +import kotlin.test.assertEquals +import kotlin.test.assertFailsWith +import kotlin.test.assertTrue + +/** GATE: reproduces every vector in `chat_cipher.json` (canonical: test-vectors/chat_cipher.json). */ +class ChatCipherVectorTest { + private val root = Json.parseToJsonElement(readTestResource("chat_cipher.json")).jsonObject + private fun list(key: String) = root[key]!!.jsonArray.map { it.jsonObject } + private fun rejects(key: String) = root["rejects"]!!.jsonObject[key]!!.jsonArray.map { it.jsonObject } + + @Test + fun keyConversion() { + val vectors = list("keyConversion") + assertTrue(vectors.isNotEmpty()) + for (v in vectors) { + val pair = Ed25519Kmp.createKeyPair(v.hex("edSeed")) + assertContentEquals(v.hex("edPublicKey"), pair.publicKey, v.name()) + assertContentEquals(v.hex("x25519PrivateKey"), DefaultChatCipher.ownToX25519(pair), v.name()) + assertContentEquals(v.hex("x25519PublicKey"), DefaultChatCipher.peerToX25519(pair.publicKey), v.name()) + } + } + + @Test + fun chatKey_isTheSameFromBothMembers() { + val vectors = list("chatKey") + assertTrue(vectors.isNotEmpty()) + for (v in vectors) { + val a = pair(v["memberA"]!!.jsonObject) + val b = pair(v["memberB"]!!.jsonObject) + val chatId = v.hex("chatId") + assertContentEquals(v.hex("chatKey"), DefaultChatCipher.chatKey(a, b.publicKey, chatId), "${v.name()} A->B") + assertContentEquals(v.hex("chatKey"), DefaultChatCipher.chatKey(b, a.publicKey, chatId), "${v.name()} B->A") + val ss = DefaultChatCipher.sharedSecret(DefaultChatCipher.ownToX25519(a), DefaultChatCipher.peerToX25519(b.publicKey)) + assertContentEquals(v.hex("sharedSecret"), ss, v.name()) + } + } + + @Test + fun messages_encryptAndDecrypt() { + val vectors = list("messages") + assertTrue(vectors.size >= 7) + for (v in vectors) { + val key = v.hex("chatKey") + val payload = DefaultChatCipher.encrypt( + v.hex("content"), key, v.hex("senderPublicKey"), v.hex("recipientPublicKey"), + v.hex("chatId"), v.hex("nonce"), + ) + assertContentEquals(v.hex("ciphertext"), payload.ciphertext, v.name()) + val plain = DefaultChatCipher.decrypt( + v.payload(), key, v.hex("senderPublicKey"), v.hex("recipientPublicKey"), v.hex("chatId"), + ) + assertContentEquals(v.hex("content"), plain, v.name()) + } + } + + @Test + fun blobs_encryptAndDecrypt() { + val vectors = list("blobs") + assertTrue(vectors.isNotEmpty()) + for (v in vectors) { + val key = v.hex("chatKey") + val blob = DefaultChatCipher.encryptBlob( + v.hex("image"), key, v.hex("senderPublicKey"), v.hex("recipientPublicKey"), + v.hex("chatId"), v.hex("blobId"), v.hex("nonce"), + ) + assertContentEquals(v.hex("blob"), blob, v.name()) + val plain = DefaultChatCipher.decryptBlob( + v.hex("blob"), key, v.hex("senderPublicKey"), v.hex("recipientPublicKey"), + v.hex("chatId"), v.hex("blobId"), + ) + assertContentEquals(v.hex("image"), plain, v.name()) + } + } + + @Test + fun randomNonces_roundTripAndDiffer() { + val v = list("messages").first() + val args = arrayOf(v.hex("chatKey"), v.hex("senderPublicKey"), v.hex("recipientPublicKey"), v.hex("chatId")) + val a = DefaultChatCipher.encrypt(v.hex("content"), args[0], args[1], args[2], args[3]) + val b = DefaultChatCipher.encrypt(v.hex("content"), args[0], args[1], args[2], args[3]) + assertTrue(!a.nonce.contentEquals(b.nonce)) + assertContentEquals(v.hex("content"), DefaultChatCipher.decrypt(a, args[0], args[1], args[2], args[3])) + } + + @Test + fun rejects_invalidPeerKeys() { + val vectors = rejects("invalidEdPublicKeys") + assertTrue(vectors.size >= 4) + for (v in vectors) { + val own = Ed25519Kmp.createKeyPair(v.hex("ownSeed")) + assertFailsWith(v.name()) { DefaultChatCipher.chatKey(own, v.hex("edPublicKey"), v.hex("chatId")) } + } + } + + @Test + fun rejects_zeroSharedSecret() { + for (v in rejects("zeroSharedSecret")) { + assertFailsWith(v.name()) { + DefaultChatCipher.sharedSecret(v.hex("x25519PrivateKey"), v.hex("peerX25519PublicKey")) + } + } + } + + @Test + fun rejects_messages() { + for (key in listOf("decrypt", "decryptBlobAsMessage")) { + val vectors = rejects(key) + assertTrue(vectors.isNotEmpty()) + for (v in vectors) { + assertFailsWith("$key/${v.name()}") { + DefaultChatCipher.decrypt( + v.payload(), v.hex("chatKey"), v.hex("senderPublicKey"), v.hex("recipientPublicKey"), + v.hex("chatId"), + ) + } + } + } + } + + @Test + fun rejects_blobs() { + val vectors = rejects("decryptBlob") + assertTrue(vectors.isNotEmpty()) + for (v in vectors) { + assertFailsWith(v.name()) { + DefaultChatCipher.decryptBlob( + v.hex("blob"), v.hex("chatKey"), v.hex("senderPublicKey"), v.hex("recipientPublicKey"), + v.hex("chatId"), v.hex("blobId"), + ) + } + } + } + + @Test + fun policy_shouldEncrypt() { + val policy = root["policy"]!!.jsonObject + assertContentEquals(policy.hex("flipcashUserId"), ChatEncryptionPolicy.FLIPCASH_USER_ID) + val vectors = policy["cases"]!!.jsonArray.map { it.jsonObject } + assertTrue(vectors.size >= 4) + for (v in vectors) { + val actual = ChatEncryptionPolicy.shouldEncrypt( + isDirectMessage = v.bool("isDirectMessage"), + useE2ee = v.bool("useE2ee"), + peerUserId = v.hex("peerUserId"), + ) + assertEquals(v.bool("shouldEncrypt"), actual, v.name()) + } + } + + @Test + fun flipcashUserId_isACopy() { + ChatEncryptionPolicy.FLIPCASH_USER_ID[0] = 0 + assertTrue(!ChatEncryptionPolicy.shouldEncrypt(true, true, root["policy"]!!.jsonObject.hex("flipcashUserId"))) + } + + private fun pair(o: JsonObject) = Ed25519Kmp.createKeyPair(o.hex("edSeed")).also { + assertContentEquals(o.hex("edPublicKey"), it.publicKey) + } + + private fun JsonObject.payload() = EncryptedPayload(hex("nonce"), hex("ciphertext")) + private fun JsonObject.bool(key: String) = this[key]!!.jsonPrimitive.boolean + private fun JsonObject.name() = this["name"]!!.jsonPrimitive.content + private fun JsonObject.hex(key: String): ByteArray { + val s = this[key]!!.jsonPrimitive.content + return ByteArray(s.length / 2) { i -> ((s[i * 2].digitToInt(16) shl 4) or s[i * 2 + 1].digitToInt(16)).toByte() } + } +} diff --git a/libs/encryption/chat-cipher/src/commonTest/resources/chat_cipher.json b/libs/encryption/chat-cipher/src/commonTest/resources/chat_cipher.json new file mode 100644 index 0000000000..ca8cd038cb --- /dev/null +++ b/libs/encryption/chat-cipher/src/commonTest/resources/chat_cipher.json @@ -0,0 +1,438 @@ +{ + "algorithm": "flipcash-dm-e2ee-v1 / X25519_XCHACHA20POLY1305", + "spec": "EncryptedContent in flipcash2-protobuf-api proto/messaging/v1/model.proto. aad = 'flipcash-dm-e2ee-v1' || chatId || senderPk || recipientPk; blob aad uses 'flipcash-dm-e2ee-blob-v1' and appends blobId. All fields are lowercase hex unless noted.", + "note": "Cross-platform parity gate for ChatCipher (:libs:encryption:chat-cipher). `content`/`plaintext` are serialized protobuf Content bytes; the crypto never parses them. Nonces are fixed here; production nonces are random.", + "keyConversion": [ + { + "name": "alice", + "edSeed": "0102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f20", + "edPublicKey": "79b5562e8fe654f94078b112e8a98ba7901f853ae695bed7e0e3910bad049664", + "x25519PrivateKey": "70788f1a0cea001a2631dae5d05dbd062008d5b30f50b9e29beb2a7822289044", + "x25519PublicKey": "4a3807d064d077181cc070989e76891d20dca5559548dc2c77c1a50273882b38" + }, + { + "name": "bob", + "edSeed": "65666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f8081828384", + "edPublicKey": "da29e95b02e00ffa15645775fb1d2ba222a1943395eea06b94e2c057b7be69d0", + "x25519PrivateKey": "d8dbfc120cc56970a8c144b559dcb86677e786abb1b278003762986791f7c06a", + "x25519PublicKey": "ad6c082b1b7d59403617c495d135151af3dd8936fc6c3e07de914b55c8b64f5d" + }, + { + "name": "carol", + "edSeed": "c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0", + "edPublicKey": "5b9437adeaffbe8f41b13d96ed49d2f51cd6c266cd8ecc284b0552ec4912b8dd", + "x25519PrivateKey": "f8b9cc73f9fa1817e212ff1bffe6b504f193a265243f910fe61cf2eed9380067", + "x25519PublicKey": "8c66226c3938112135bf2c7b02b3ed3fafd31ab7fc774ee6448aa9a2ff71e923" + }, + { + "name": "rfc8032-test1", + "edSeed": "9d61b19deffd5a60ba844af492ec2cc44449c5697b326919703bac031cae7f60", + "edPublicKey": "d75a980182b10ab7d54bfed3c964073a0ee172f3daa62325af021a68f707511a", + "x25519PrivateKey": "307c83864f2833cb427a2ef1c00a013cfdff2768d980c0a3a520f006904de94f", + "x25519PublicKey": "d85e07ec22b0ad881537c2f44d662d1a143cf830c57aca4305d85c7a90f6b62e" + } + ], + "chatKey": [ + { + "name": "alice-bob", + "chatId": "97dfb79f2c2c44a9bd581b3b4fd30e7aee470d3b53062288886a976dd8ade15e", + "memberA": { + "edSeed": "0102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f20", + "edPublicKey": "79b5562e8fe654f94078b112e8a98ba7901f853ae695bed7e0e3910bad049664" + }, + "memberB": { + "edSeed": "65666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f8081828384", + "edPublicKey": "da29e95b02e00ffa15645775fb1d2ba222a1943395eea06b94e2c057b7be69d0" + }, + "sharedSecret": "f4220c7019c0ae5bb8011a5058e871f1166b777d2903d5c3a4793d9a9b084940", + "chatKey": "6c2005541fb82abe2f639ae77e3b146e7f0dd97a9b7d3b2dccef000c69e9df22", + "note": "identical from either member's side" + }, + { + "name": "alice-bob-other-chat", + "chatId": "9027a70126863980b94e9b31ecc25b7c2a48723af73662f6c297939dd2ed6e0e", + "memberA": { + "edSeed": "0102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f20", + "edPublicKey": "79b5562e8fe654f94078b112e8a98ba7901f853ae695bed7e0e3910bad049664" + }, + "memberB": { + "edSeed": "65666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f8081828384", + "edPublicKey": "da29e95b02e00ffa15645775fb1d2ba222a1943395eea06b94e2c057b7be69d0" + }, + "sharedSecret": "f4220c7019c0ae5bb8011a5058e871f1166b777d2903d5c3a4793d9a9b084940", + "chatKey": "0c5e7a136858058a4aeee4252bbc906226b8308ea3eed2c67b58a2b2015e7920", + "note": "identical from either member's side" + }, + { + "name": "alice-carol", + "chatId": "97dfb79f2c2c44a9bd581b3b4fd30e7aee470d3b53062288886a976dd8ade15e", + "memberA": { + "edSeed": "0102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f20", + "edPublicKey": "79b5562e8fe654f94078b112e8a98ba7901f853ae695bed7e0e3910bad049664" + }, + "memberB": { + "edSeed": "c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0", + "edPublicKey": "5b9437adeaffbe8f41b13d96ed49d2f51cd6c266cd8ecc284b0552ec4912b8dd" + }, + "sharedSecret": "4659cd4eb4e457bef2ca2b6d3a14ab447f21b427c9403824634cf81d0bac893f", + "chatKey": "aee337958634ffa8401fe02ef1d3f2880f0886838a757daffecfdee10e10e19d", + "note": "identical from either member's side" + } + ], + "messages": [ + { + "name": "text-ascii/alice-to-bob", + "chatId": "97dfb79f2c2c44a9bd581b3b4fd30e7aee470d3b53062288886a976dd8ade15e", + "chatKey": "6c2005541fb82abe2f639ae77e3b146e7f0dd97a9b7d3b2dccef000c69e9df22", + "senderPublicKey": "79b5562e8fe654f94078b112e8a98ba7901f853ae695bed7e0e3910bad049664", + "recipientPublicKey": "da29e95b02e00ffa15645775fb1d2ba222a1943395eea06b94e2c057b7be69d0", + "content": "0a070a0568656c6c6f", + "nonce": "000102030405060708090a0b0c0d0e0f1011121314151617", + "aad": "666c6970636173682d646d2d653265652d763197dfb79f2c2c44a9bd581b3b4fd30e7aee470d3b53062288886a976dd8ade15e79b5562e8fe654f94078b112e8a98ba7901f853ae695bed7e0e3910bad049664da29e95b02e00ffa15645775fb1d2ba222a1943395eea06b94e2c057b7be69d0", + "ciphertext": "b81415e496a7c65aca660ab329de4a5aa66a90579b9fabf5ae" + }, + { + "name": "text-ascii/bob-to-alice", + "chatId": "97dfb79f2c2c44a9bd581b3b4fd30e7aee470d3b53062288886a976dd8ade15e", + "chatKey": "6c2005541fb82abe2f639ae77e3b146e7f0dd97a9b7d3b2dccef000c69e9df22", + "senderPublicKey": "da29e95b02e00ffa15645775fb1d2ba222a1943395eea06b94e2c057b7be69d0", + "recipientPublicKey": "79b5562e8fe654f94078b112e8a98ba7901f853ae695bed7e0e3910bad049664", + "content": "0a070a0568656c6c6f", + "nonce": "bcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3", + "aad": "666c6970636173682d646d2d653265652d763197dfb79f2c2c44a9bd581b3b4fd30e7aee470d3b53062288886a976dd8ade15eda29e95b02e00ffa15645775fb1d2ba222a1943395eea06b94e2c057b7be69d079b5562e8fe654f94078b112e8a98ba7901f853ae695bed7e0e3910bad049664", + "ciphertext": "365edbb5cd2a9e6101f85bfd36f95e28c08b5ad035b9170694" + }, + { + "name": "text-unicode/alice-to-bob", + "chatId": "97dfb79f2c2c44a9bd581b3b4fd30e7aee470d3b53062288886a976dd8ade15e", + "chatKey": "6c2005541fb82abe2f639ae77e3b146e7f0dd97a9b7d3b2dccef000c69e9df22", + "senderPublicKey": "79b5562e8fe654f94078b112e8a98ba7901f853ae695bed7e0e3910bad049664", + "recipientPublicKey": "da29e95b02e00ffa15645775fb1d2ba222a1943395eea06b94e2c057b7be69d0", + "content": "0a140a1268c3a96c6c6f20f09f988020e4b8ade69687", + "nonce": "0708090a0b0c0d0e0f101112131415161718191a1b1c1d1e", + "aad": "666c6970636173682d646d2d653265652d763197dfb79f2c2c44a9bd581b3b4fd30e7aee470d3b53062288886a976dd8ade15e79b5562e8fe654f94078b112e8a98ba7901f853ae695bed7e0e3910bad049664da29e95b02e00ffa15645775fb1d2ba222a1943395eea06b94e2c057b7be69d0", + "ciphertext": "b97444a6449240b4f35a2465fddcedfa4a1338a422d03fb1b9428b3d9549c31901bc7cecf23c" + }, + { + "name": "text-long/alice-to-bob", + "chatId": "97dfb79f2c2c44a9bd581b3b4fd30e7aee470d3b53062288886a976dd8ade15e", + "chatKey": "6c2005541fb82abe2f639ae77e3b146e7f0dd97a9b7d3b2dccef000c69e9df22", + "senderPublicKey": "79b5562e8fe654f94078b112e8a98ba7901f853ae695bed7e0e3910bad049664", + "recipientPublicKey": "da29e95b02e00ffa15645775fb1d2ba222a1943395eea06b94e2c057b7be69d0", + "content": "0a80040afd036c6f6e67206c6f6e67206c6f6e67206c6f6e67206c6f6e67206c6f6e67206c6f6e67206c6f6e67206c6f6e67206c6f6e67206c6f6e67206c6f6e67206c6f6e67206c6f6e67206c6f6e67206c6f6e67206c6f6e67206c6f6e67206c6f6e67206c6f6e67206c6f6e67206c6f6e67206c6f6e67206c6f6e67206c6f6e67206c6f6e67206c6f6e67206c6f6e67206c6f6e67206c6f6e67206c6f6e67206c6f6e67206c6f6e67206c6f6e67206c6f6e67206c6f6e67206c6f6e67206c6f6e67206c6f6e67206c6f6e67206c6f6e67206c6f6e67206c6f6e67206c6f6e67206c6f6e67206c6f6e67206c6f6e67206c6f6e67206c6f6e67206c6f6e67206c6f6e67206c6f6e67206c6f6e67206c6f6e67206c6f6e67206c6f6e67206c6f6e67206c6f6e67206c6f6e67206c6f6e67206c6f6e67206c6f6e67206c6f6e67206c6f6e67206c6f6e67206c6f6e67206c6f6e67206c6f6e67206c6f6e67206c6f6e67206c6f6e67206c6f6e67206c6f6e67206c6f6e67206c6f6e67206c6f6e67206c6f6e67206c6f6e67206c6f6e67206c6f6e67206c6f6e67206c6f6e67206c6f6e67206c6f6e67206c6f6e67206c6f6e67206c6f6e67206c6f6e67206c6f6e67206c6f6e67206c6f6e67206c6f6e67206c6f6e67206c6f6e67206c6f6e67206c6f6e67206c6f6e67206c6f6e67206c6f6e67206c6f6e6720c3a9e4b8adf09f9880", + "nonce": "0e0f101112131415161718191a1b1c1d1e1f202122232425", + "aad": "666c6970636173682d646d2d653265652d763197dfb79f2c2c44a9bd581b3b4fd30e7aee470d3b53062288886a976dd8ade15e79b5562e8fe654f94078b112e8a98ba7901f853ae695bed7e0e3910bad049664da29e95b02e00ffa15645775fb1d2ba222a1943395eea06b94e2c057b7be69d0", + "ciphertext": "35d7df9422937a5b06c4e23ce805fc57851bf5ea0de216b34224bef129947079def6f6eab35d288e7543c58cab98637f89d444eeda2e6dd1e6d4d7c43d1e302a22ab2f81e302d154d4a7d5052484633ac267eefbecae6137015e5a5d474ce38a95dc20d68114bf484dc1668175498a6c63f725af40fa88393c817fb2656f97f7996842a2ae2638463984bd9b5766c2053bb283dc8a866715ae3636998da5c2b2be656b6b07e1378de6b707ad8153344987991492f922a1c70b93b0fe40e30183efa9d21d8b1748fef91b40dbe282e6bd2f0faa335d54bd06e36d1a97b0e071564909beb2630e0b5096236bca8ba7bbe34b772433054321350f2f38c7705285f301d29ab4808dbadfb561400fe82b852ca69bc72be984eb94c23d0c8a1e445af163a7717f166a600ab42d86572849dd389edcf762d9128b29ec6e80b801f7159e398e1baf03e1e00121d5c44fbbd15a9c7fe62106ac856a0d4d68ac943f904b97466d60cf6567e2cc1a5a78c28a155b001b8f80db861d8c4d25274038de09f76f9e002115675d27c868ca4b9e7efeb1ccc698655b63d183a3eef1776be306c6581300cb1121d63ad4166aabbbdd9b6084e5a67dcbabb23399077344e72c28be745f61d9ed2c35ca7be68bb7f05c83f4138cb10bd01476e8fc2055cdd835d4284ffda8b18c0012dd99ac5bb20080764812888901131e6cf775a6ba04be296a42041fdf0449add1b513fc73972bc41edbfdf98553" + }, + { + "name": "reply-to-text/alice-to-bob", + "chatId": "97dfb79f2c2c44a9bd581b3b4fd30e7aee470d3b53062288886a976dd8ade15e", + "chatKey": "6c2005541fb82abe2f639ae77e3b146e7f0dd97a9b7d3b2dccef000c69e9df22", + "senderPublicKey": "79b5562e8fe654f94078b112e8a98ba7901f853ae695bed7e0e3910bad049664", + "recipientPublicKey": "da29e95b02e00ffa15645775fb1d2ba222a1943395eea06b94e2c057b7be69d0", + "content": "1a120a02082a120c0a0a0a087265706c79696e67", + "nonce": "15161718191a1b1c1d1e1f202122232425262728292a2b2c", + "aad": "666c6970636173682d646d2d653265652d763197dfb79f2c2c44a9bd581b3b4fd30e7aee470d3b53062288886a976dd8ade15e79b5562e8fe654f94078b112e8a98ba7901f853ae695bed7e0e3910bad049664da29e95b02e00ffa15645775fb1d2ba222a1943395eea06b94e2c057b7be69d0", + "ciphertext": "a754be8c2f7973dd8c039f2391e047a0843f82983a803c95fc471a7f66b79ed6b9609efd" + }, + { + "name": "reply-to-media/alice-to-bob", + "chatId": "97dfb79f2c2c44a9bd581b3b4fd30e7aee470d3b53062288886a976dd8ade15e", + "chatKey": "6c2005541fb82abe2f639ae77e3b146e7f0dd97a9b7d3b2dccef000c69e9df22", + "senderPublicKey": "79b5562e8fe654f94078b112e8a98ba7901f853ae695bed7e0e3910bad049664", + "recipientPublicKey": "da29e95b02e00ffa15645775fb1d2ba222a1943395eea06b94e2c057b7be69d0", + "content": "1a630a020807125d225b0a510a4f0a4d080112120a10a0a1a2a3a4a5a6a7a8a9aaabacadaeaf1a350a0a696d6167652f6a706567108010222408800510e0031a1c4c454856366e574232796b3870796f306164522a2e376b434d646e6a12060a046c6f6f6b", + "nonce": "1c1d1e1f202122232425262728292a2b2c2d2e2f30313233", + "aad": "666c6970636173682d646d2d653265652d763197dfb79f2c2c44a9bd581b3b4fd30e7aee470d3b53062288886a976dd8ade15e79b5562e8fe654f94078b112e8a98ba7901f853ae695bed7e0e3910bad049664da29e95b02e00ffa15645775fb1d2ba222a1943395eea06b94e2c057b7be69d0", + "ciphertext": "ac0c7f4412c9460f2edc49af54845acf0c7974d0a0675531809acbbfa3d69efedf9f910031d5283b1dd887eea937d8247af80af83724ea2ded914f706924a6f230d3d3b50fa638c1467afefc82d6a6dba291670c1c1a100c206429d1868f688a889b18dfed6fec14c46fb7b2c9ca7fec7d791a034d" + }, + { + "name": "media-with-caption/alice-to-bob", + "chatId": "97dfb79f2c2c44a9bd581b3b4fd30e7aee470d3b53062288886a976dd8ade15e", + "chatKey": "6c2005541fb82abe2f639ae77e3b146e7f0dd97a9b7d3b2dccef000c69e9df22", + "senderPublicKey": "79b5562e8fe654f94078b112e8a98ba7901f853ae695bed7e0e3910bad049664", + "recipientPublicKey": "da29e95b02e00ffa15645775fb1d2ba222a1943395eea06b94e2c057b7be69d0", + "content": "22610a520a500a4e080112120a10a0a1a2a3a4a5a6a7a8a9aaabacadaeaf1a360a0a696d6167652f6a70656710c0c40722240880081080061a1c4c454856366e574232796b3870796f306164522a2e376b434d646e6a120b0a09612063617074696f6e", + "nonce": "232425262728292a2b2c2d2e2f303132333435363738393a", + "aad": "666c6970636173682d646d2d653265652d763197dfb79f2c2c44a9bd581b3b4fd30e7aee470d3b53062288886a976dd8ade15e79b5562e8fe654f94078b112e8a98ba7901f853ae695bed7e0e3910bad049664da29e95b02e00ffa15645775fb1d2ba222a1943395eea06b94e2c057b7be69d0", + "ciphertext": "4ab537c7488ad8ba2920b6cbe24ad7b11b54e97c3b555131c6de47a68652e047f756bd5ea0ba10628a7c59a2f1caf39bfa804a3815b4cf572b8b1fdaedefc5a854bd9bfd0df5cf6543a9b049111e6a1847068ab769f6219f2e9ed30630786b304f861e0e6c53fb6b8b4b8bed6217421eaae0b3" + }, + { + "name": "media-no-caption/alice-to-bob", + "chatId": "97dfb79f2c2c44a9bd581b3b4fd30e7aee470d3b53062288886a976dd8ade15e", + "chatKey": "6c2005541fb82abe2f639ae77e3b146e7f0dd97a9b7d3b2dccef000c69e9df22", + "senderPublicKey": "79b5562e8fe654f94078b112e8a98ba7901f853ae695bed7e0e3910bad049664", + "recipientPublicKey": "da29e95b02e00ffa15645775fb1d2ba222a1943395eea06b94e2c057b7be69d0", + "content": "22310a2f0a2d0a2b080112120a10a0a1a2a3a4a5a6a7a8a9aaabacadaeaf1a130a09696d6167652f706e671063220408011001", + "nonce": "2a2b2c2d2e2f303132333435363738393a3b3c3d3e3f4041", + "aad": "666c6970636173682d646d2d653265652d763197dfb79f2c2c44a9bd581b3b4fd30e7aee470d3b53062288886a976dd8ade15e79b5562e8fe654f94078b112e8a98ba7901f853ae695bed7e0e3910bad049664da29e95b02e00ffa15645775fb1d2ba222a1943395eea06b94e2c057b7be69d0", + "ciphertext": "98d4a798de8f047f8323779d12824dfe9623a312fc76be8d53cd97926734949ec939e84197942e91ff71363d40d208338e3eda5f3dca803dba7e2883541e8ae5a5e978" + } + ], + "blobs": [ + { + "name": "empty", + "chatId": "97dfb79f2c2c44a9bd581b3b4fd30e7aee470d3b53062288886a976dd8ade15e", + "chatKey": "6c2005541fb82abe2f639ae77e3b146e7f0dd97a9b7d3b2dccef000c69e9df22", + "senderPublicKey": "79b5562e8fe654f94078b112e8a98ba7901f853ae695bed7e0e3910bad049664", + "recipientPublicKey": "da29e95b02e00ffa15645775fb1d2ba222a1943395eea06b94e2c057b7be69d0", + "blobId": "a0a1a2a3a4a5a6a7a8a9aaabacadaeaf", + "image": "", + "nonce": "78797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f", + "aad": "666c6970636173682d646d2d653265652d626c6f622d763197dfb79f2c2c44a9bd581b3b4fd30e7aee470d3b53062288886a976dd8ade15e79b5562e8fe654f94078b112e8a98ba7901f853ae695bed7e0e3910bad049664da29e95b02e00ffa15645775fb1d2ba222a1943395eea06b94e2c057b7be69d0a0a1a2a3a4a5a6a7a8a9aaabacadaeaf", + "blob": "78797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f49f238db29bd32172ba692003faac4c9" + }, + { + "name": "small", + "chatId": "97dfb79f2c2c44a9bd581b3b4fd30e7aee470d3b53062288886a976dd8ade15e", + "chatKey": "6c2005541fb82abe2f639ae77e3b146e7f0dd97a9b7d3b2dccef000c69e9df22", + "senderPublicKey": "79b5562e8fe654f94078b112e8a98ba7901f853ae695bed7e0e3910bad049664", + "recipientPublicKey": "da29e95b02e00ffa15645775fb1d2ba222a1943395eea06b94e2c057b7be69d0", + "blobId": "a0a1a2a3a4a5a6a7a8a9aaabacadaeaf", + "image": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7", + "nonce": "7f808182838485868788898a8b8c8d8e8f90919293949596", + "aad": "666c6970636173682d646d2d653265652d626c6f622d763197dfb79f2c2c44a9bd581b3b4fd30e7aee470d3b53062288886a976dd8ade15e79b5562e8fe654f94078b112e8a98ba7901f853ae695bed7e0e3910bad049664da29e95b02e00ffa15645775fb1d2ba222a1943395eea06b94e2c057b7be69d0a0a1a2a3a4a5a6a7a8a9aaabacadaeaf", + "blob": "7f808182838485868788898a8b8c8d8e8f90919293949596860fbbbcc67390506bbf4ab45371f88340950f328b6579bacd333b2b2ebf1c38b09f22e213f4402f93812474672094caef33bb48a8f981789ebf22756d518253ac7cf4f44b165add56ab28206c0fda7636fb0f98f1a66172130b7a057f4ccda7f4e202151b5430779a1c79a6a954e59be0b8691d9b20b576f5df8257e6214bf6d433de712b10ddda50c09f059c7bda06ddbf04fef33fc23260e67ea05ee8dd4fd69c78c6277616ed895bec587fec5816d93e4da28648533347fd0c30b755dd5a5f703996b2f2fd4da6378889123223b06976fd823dd9513c" + }, + { + "name": "4k", + "chatId": "97dfb79f2c2c44a9bd581b3b4fd30e7aee470d3b53062288886a976dd8ade15e", + "chatKey": "6c2005541fb82abe2f639ae77e3b146e7f0dd97a9b7d3b2dccef000c69e9df22", + "senderPublicKey": "79b5562e8fe654f94078b112e8a98ba7901f853ae695bed7e0e3910bad049664", + "recipientPublicKey": "da29e95b02e00ffa15645775fb1d2ba222a1943395eea06b94e2c057b7be69d0", + "blobId": "a0a1a2a3a4a5a6a7a8a9aaabacadaeaf", + "image": "001f3e5d7c9bbad9f81736557493b2d1f00f2e4d6c8baac9e80726456483a2c1e0ff1e3d5c7b9ab9d8f71635547392b1d0ef0e2d4c6b8aa9c8e70625446382a1c0dffe1d3c5b7a99b8d7f61534537291b0cfee0d2c4b6a89a8c7e60524436281a0bfdefd1c3b5a7998b7d6f51433527190afceed0c2b4a6988a7c6e504234261809fbeddfc1b3a597897b6d5f4133251708faecdec0b2a496887a6c5e4032241607f9ebddcfb1a39587796b5d4f31231506f8eadcceb0a29486786a5c4e30221405f7e9dbcdbfa1938577695b4d3f211304f6e8daccbea0928476685a4c3e201203f5e7d9cbbdaf91837567594b3d2f1102f4e6d8cabcae90827466584a3c2e1001f3e5d7c9bbad9f81736557493b2d1f00f2e4d6c8baac9e80726456483a2c1e0ff1e3d5c7b9ab9d8f71635547392b1d0ef0e2d4c6b8aa9c8e70625446382a1c0dffe1d3c5b7a99b8d7f61534537291b0cfee0d2c4b6a89a8c7e60524436281a0bfdefd1c3b5a7998b7d6f51433527190afceed0c2b4a6988a7c6e504234261809fbeddfc1b3a597897b6d5f4133251708faecdec0b2a496887a6c5e4032241607f9ebddcfb1a39587796b5d4f31231506f8eadcceb0a29486786a5c4e30221405f7e9dbcdbfa1938577695b4d3f211304f6e8daccbea0928476685a4c3e201203f5e7d9cbbdaf91837567594b3d2f1102f4e6d8cabcae90827466584a3c2e1001f3e5d7c9bbad9f81736557493b2d1f00f2e4d6c8baac9e80726456483a2c1e0ff1e3d5c7b9ab9d8f71635547392b1d0ef0e2d4c6b8aa9c8e70625446382a1c0dffe1d3c5b7a99b8d7f61534537291b0cfee0d2c4b6a89a8c7e60524436281a0bfdefd1c3b5a7998b7d6f51433527190afceed0c2b4a6988a7c6e504234261809fbeddfc1b3a597897b6d5f4133251708faecdec0b2a496887a6c5e4032241607f9ebddcfb1a39587796b5d4f31231506f8eadcceb0a29486786a5c4e30221405f7e9dbcdbfa1938577695b4d3f211304f6e8daccbea0928476685a4c3e201203f5e7d9cbbdaf91837567594b3d2f1102f4e6d8cabcae90827466584a3c2e1001f3e5d7c9bbad9f81736557493b2d1f00f2e4d6c8baac9e80726456483a2c1e0ff1e3d5c7b9ab9d8f71635547392b1d0ef0e2d4c6b8aa9c8e70625446382a1c0dffe1d3c5b7a99b8d7f61534537291b0cfee0d2c4b6a89a8c7e60524436281a0bfdefd1c3b5a7998b7d6f51433527190afceed0c2b4a6988a7c6e504234261809fbeddfc1b3a597897b6d5f4133251708faecdec0b2a496887a6c5e4032241607f9ebddcfb1a39587796b5d4f31231506f8eadcceb0a29486786a5c4e30221405f7e9dbcdbfa1938577695b4d3f211304f6e8daccbea0928476685a4c3e201203f5e7d9cbbdaf91837567594b3d2f1102f4e6d8cabcae90827466584a3c2e1001f3e5d7c9bbad9f81736557493b2d1f00f2e4d6c8baac9e80726456483a2c1e0ff1e3d5c7b9ab9d8f71635547392b1d0ef0e2d4c6b8aa9c8e70625446382a1c0dffe1d3c5b7a99b8d7f61534537291b0cfee0d2c4b6a89a8c7e60524436281a0bfdefd1c3b5a7998b7d6f51433527190afceed0c2b4a6988a7c6e504234261809fbeddfc1b3a597897b6d5f4133251708faecdec0b2a496887a6c5e4032241607f9ebddcfb1a39587796b5d4f31231506f8eadcceb0a29486786a5c4e30221405f7e9dbcdbfa1938577695b4d3f211304f6e8daccbea0928476685a4c3e201203f5e7d9cbbdaf91837567594b3d2f1102f4e6d8cabcae90827466584a3c2e1001f3e5d7c9bbad9f81736557493b2d1f00f2e4d6c8baac9e80726456483a2c1e0ff1e3d5c7b9ab9d8f71635547392b1d0ef0e2d4c6b8aa9c8e70625446382a1c0dffe1d3c5b7a99b8d7f61534537291b0cfee0d2c4b6a89a8c7e60524436281a0bfdefd1c3b5a7998b7d6f51433527190afceed0c2b4a6988a7c6e504234261809fbeddfc1b3a597897b6d5f4133251708faecdec0b2a496887a6c5e4032241607f9ebddcfb1a39587796b5d4f31231506f8eadcceb0a29486786a5c4e30221405f7e9dbcdbfa1938577695b4d3f211304f6e8daccbea0928476685a4c3e201203f5e7d9cbbdaf91837567594b3d2f1102f4e6d8cabcae90827466584a3c2e1001f3e5d7c9bbad9f81736557493b2d1f00f2e4d6c8baac9e80726456483a2c1e0ff1e3d5c7b9ab9d8f71635547392b1d0ef0e2d4c6b8aa9c8e70625446382a1c0dffe1d3c5b7a99b8d7f61534537291b0cfee0d2c4b6a89a8c7e60524436281a0bfdefd1c3b5a7998b7d6f51433527190afceed0c2b4a6988a7c6e504234261809fbeddfc1b3a597897b6d5f4133251708faecdec0b2a496887a6c5e4032241607f9ebddcfb1a39587796b5d4f31231506f8eadcceb0a29486786a5c4e30221405f7e9dbcdbfa1938577695b4d3f211304f6e8daccbea0928476685a4c3e201203f5e7d9cbbdaf91837567594b3d2f1102f4e6d8cabcae90827466584a3c2e1001f3e5d7c9bbad9f81736557493b2d1f00f2e4d6c8baac9e80726456483a2c1e0ff1e3d5c7b9ab9d8f71635547392b1d0ef0e2d4c6b8aa9c8e70625446382a1c0dffe1d3c5b7a99b8d7f61534537291b0cfee0d2c4b6a89a8c7e60524436281a0bfdefd1c3b5a7998b7d6f51433527190afceed0c2b4a6988a7c6e504234261809fbeddfc1b3a597897b6d5f4133251708faecdec0b2a496887a6c5e4032241607f9ebddcfb1a39587796b5d4f31231506f8eadcceb0a29486786a5c4e30221405f7e9dbcdbfa1938577695b4d3f211304f6e8daccbea0928476685a4c3e201203f5e7d9cbbdaf91837567594b3d2f1102f4e6d8cabcae90827466584a3c2e1001f3e5d7c9bbad9f81736557493b2d1f00f2e4d6c8baac9e80726456483a2c1e0ff1e3d5c7b9ab9d8f71635547392b1d0ef0e2d4c6b8aa9c8e70625446382a1c0dffe1d3c5b7a99b8d7f61534537291b0cfee0d2c4b6a89a8c7e60524436281a0bfdefd1c3b5a7998b7d6f51433527190afceed0c2b4a6988a7c6e504234261809fbeddfc1b3a597897b6d5f4133251708faecdec0b2a496887a6c5e4032241607f9ebddcfb1a39587796b5d4f31231506f8eadcceb0a29486786a5c4e30221405f7e9dbcdbfa1938577695b4d3f211304f6e8daccbea0928476685a4c3e201203f5e7d9cbbdaf91837567594b3d2f1102f4e6d8cabcae90827466584a3c2e1001f3e5d7c9bbad9f81736557493b2d1f00f2e4d6c8baac9e80726456483a2c1e0ff1e3d5c7b9ab9d8f71635547392b1d0ef0e2d4c6b8aa9c8e70625446382a1c0dffe1d3c5b7a99b8d7f61534537291b0cfee0d2c4b6a89a8c7e60524436281a0bfdefd1c3b5a7998b7d6f51433527190afceed0c2b4a6988a7c6e504234261809fbeddfc1b3a597897b6d5f4133251708faecdec0b2a496887a6c5e4032241607f9ebddcfb1a39587796b5d4f31231506f8eadcceb0a29486786a5c4e30221405f7e9dbcdbfa1938577695b4d3f211304f6e8daccbea0928476685a4c3e201203f5e7d9cbbdaf91837567594b3d2f1102f4e6d8cabcae90827466584a3c2e1001f3e5d7c9bbad9f81736557493b2d1f00f2e4d6c8baac9e80726456483a2c1e0ff1e3d5c7b9ab9d8f71635547392b1d0ef0e2d4c6b8aa9c8e70625446382a1c0dffe1d3c5b7a99b8d7f61534537291b0cfee0d2c4b6a89a8c7e60524436281a0bfdefd1c3b5a7998b7d6f51433527190afceed0c2b4a6988a7c6e504234261809fbeddfc1b3a597897b6d5f4133251708faecdec0b2a496887a6c5e4032241607f9ebddcfb1a39587796b5d4f31231506f8eadcceb0a29486786a5c4e30221405f7e9dbcdbfa1938577695b4d3f211304f6e8daccbea0928476685a4c3e201203f5e7d9cbbdaf91837567594b3d2f1102f4e6d8cabcae90827466584a3c2e1001f3e5d7c9bbad9f81736557493b2d1f00f2e4d6c8baac9e80726456483a2c1e0ff1e3d5c7b9ab9d8f71635547392b1d0ef0e2d4c6b8aa9c8e70625446382a1c0dffe1d3c5b7a99b8d7f61534537291b0cfee0d2c4b6a89a8c7e60524436281a0bfdefd1c3b5a7998b7d6f51433527190afceed0c2b4a6988a7c6e504234261809fbeddfc1b3a597897b6d5f4133251708faecdec0b2a496887a6c5e4032241607f9ebddcfb1a39587796b5d4f31231506f8eadcceb0a29486786a5c4e30221405f7e9dbcdbfa1938577695b4d3f211304f6e8daccbea0928476685a4c3e201203f5e7d9cbbdaf91837567594b3d2f1102f4e6d8cabcae90827466584a3c2e1001f3e5d7c9bbad9f81736557493b2d1f00f2e4d6c8baac9e80726456483a2c1e0ff1e3d5c7b9ab9d8f71635547392b1d0ef0e2d4c6b8aa9c8e70625446382a1c0dffe1d3c5b7a99b8d7f61534537291b0cfee0d2c4b6a89a8c7e60524436281a0bfdefd1c3b5a7998b7d6f51433527190afceed0c2b4a6988a7c6e504234261809fbeddfc1b3a597897b6d5f4133251708faecdec0b2a496887a6c5e4032241607f9ebddcfb1a39587796b5d4f31231506f8eadcceb0a29486786a5c4e30221405f7e9dbcdbfa1938577695b4d3f211304f6e8daccbea0928476685a4c3e201203f5e7d9cbbdaf91837567594b3d2f1102f4e6d8cabcae90827466584a3c2e1001f3e5d7c9bbad9f81736557493b2d1f00f2e4d6c8baac9e80726456483a2c1e0ff1e3d5c7b9ab9d8f71635547392b1d0ef0e2d4c6b8aa9c8e70625446382a1c0dffe1d3c5b7a99b8d7f61534537291b0cfee0d2c4b6a89a8c7e60524436281a0bfdefd1c3b5a7998b7d6f51433527190afceed0c2b4a6988a7c6e504234261809fbeddfc1b3a597897b6d5f4133251708faecdec0b2a496887a6c5e4032241607f9ebddcfb1a39587796b5d4f31231506f8eadcceb0a29486786a5c4e30221405f7e9dbcdbfa1938577695b4d3f211304f6e8daccbea0928476685a4c3e201203f5e7d9cbbdaf91837567594b3d2f1102f4e6d8cabcae90827466584a3c2e1001f3e5d7c9bbad9f81736557493b2d1f00f2e4d6c8baac9e80726456483a2c1e0ff1e3d5c7b9ab9d8f71635547392b1d0ef0e2d4c6b8aa9c8e70625446382a1c0dffe1d3c5b7a99b8d7f61534537291b0cfee0d2c4b6a89a8c7e60524436281a0bfdefd1c3b5a7998b7d6f51433527190afceed0c2b4a6988a7c6e504234261809fbeddfc1b3a597897b6d5f4133251708faecdec0b2a496887a6c5e4032241607f9ebddcfb1a39587796b5d4f31231506f8eadcceb0a29486786a5c4e30221405f7e9dbcdbfa1938577695b4d3f211304f6e8daccbea0928476685a4c3e201203f5e7d9cbbdaf91837567594b3d2f1102f4e6d8cabcae90827466584a3c2e1001f3e5d7c9bbad9f81736557493b2d1f00f2e4d6c8baac9e80726456483a2c1e0ff1e3d5c7b9ab9d8f71635547392b1d0ef0e2d4c6b8aa9c8e70625446382a1c0dffe1d3c5b7a99b8d7f61534537291b0cfee0d2c4b6a89a8c7e60524436281a0bfdefd1c3b5a7998b7d6f51433527190afceed0c2b4a6988a7c6e504234261809fbeddfc1b3a597897b6d5f4133251708faecdec0b2a496887a6c5e4032241607f9ebddcfb1a39587796b5d4f31231506f8eadcceb0a29486786a5c4e30221405f7e9dbcdbfa1938577695b4d3f211304f6e8daccbea0928476685a4c3e201203f5e7d9cbbdaf91837567594b3d2f1102f4e6d8cabcae90827466584a3c2e1", + "nonce": "868788898a8b8c8d8e8f909192939495969798999a9b9c9d", + "aad": "666c6970636173682d646d2d653265652d626c6f622d763197dfb79f2c2c44a9bd581b3b4fd30e7aee470d3b53062288886a976dd8ade15e79b5562e8fe654f94078b112e8a98ba7901f853ae695bed7e0e3910bad049664da29e95b02e00ffa15645775fb1d2ba222a1943395eea06b94e2c057b7be69d0a0a1a2a3a4a5a6a7a8a9aaabacadaeaf", + "blob": "868788898a8b8c8d8e8f909192939495969798999a9b9c9d8de7a55a21aad423d39573ec749c3fcaab95ab84f0ed8f922c704588e793e6eefe6bd04bc58cb391a1375e036e2a02709222b64c67642064fb2b8618526d2e0d88d6d082ac92ce764fdb69b9202c8d2c3de61abe75f1fa6f1a13da6727511628da7df9fc90cd2d73aefe7ef3f13411a1861312cc11efec0472221356fd4847feb695f7533561156d349fab096f76cca23488146d68fd8b9d65df3da49e2c463832ce2a7cdfdfb9b48449740e1019132c5a17624c1e049089319c26e8dca4c3f4c309ddafc18bc956ed4d23d78528b1153767b6f506bba5e5b547f2c85157f36ee54061bcbbaccf8973ad0a70933517f17117cd9bd269c3a9214c2421363d9c29e09e5e394ef182e17edd63709659f116838dbd52a5c8ab64d3ba57d2742b2a1e5651a91de516d8da34a23c29db3eacae26f60498ee24b15160abf881a50240d717b742491df7f46a51f403416abc7026727ebae98fd189b69f4d37ca8f18d577777bc28e50b4ef41a4c64dd5073cd8c6dee529c4d2b0d76c86bd19f82183c6b5a6054bd9133cb3ec6f0946e702cc28cc667aef95b84f9a355c9491b80afe51e56fe0d8a2fd3ba090e7989ddb3371aba4b7b9208e5126bd5a89505e1f6b15789e1e638488a0d11c52ed74c6c317bc18e13b796e6db4c62988c0af752d870065aa0cffca55e95b77ae6ba70442b707eb514bae50f494a2e3e6210d61f6660bf0652511baaf270cceecbe390d937bd5ab20eca727482be02dc32bb8d64e573ea8bff05c185aa926490c058c4427cef5573fa52c1bbc45fe79e4a3621ec9d07be1a42442d3c1b7e4b6759dc9f81a3f0ea3c1b14a85201ab9f677e7b5d1c0302a13287fb9b42336233e71e57dd834f2650790cf63f57e0050587093c3d8ff6f73abfa5ba543bc4923fec5deb0c4b5db66f346bead43935f9b390056874cbbe0afcf30fd88f0412a7b9a25b076f1f0f654ce9de4a8423c7b7ac75aceca99c34b3c41828c904020f96c1ae9f0cef83b3c9d7cf2a26d9ad4541692c4a7099ad92f2ea624868e0c591e59e62558e7f18306bcfd6178e026a871876f2a651f1515da1c67ed044bfc020851f26f64a92a9f9cb3b5d9d5f28b37927bca80895e7eea4d3b423581e78af04a0521491320db14b52904778968715ed0fe3d0e5bdabce24c8718c1d954684fea9823a5a7986a26b073f1a8a72534ac51e596acaee06f53dcb89d43ed5c52b02a4330ae7ef77a8879037a8b7034c21bc3b3e18148449c157e2903f46740fc5e6b6730eb759501b48a547f5ccfda40c77b0bc0ffe13f61bbb04e07f7129ea161e6788cc3da217709689fd7760cd394d7bea217fdce3fbcb453ea1b321c70c51f630338fb88f15efaff8634d039b02726d0759ed1f940d30e61445c0a23ad6d69c57f1e9856755a48a2f6276089be0c279cf7b82dbe309f4e69c6d82aeb47ad1d4a72b374e8b895ad2a2c5577886cf8d6bef51066c2d0a922caeced014e50daf06764668c0585e98acbcf1d941c054976606be27ef98794aa5ad54d4cd9c9605cfffa2d93b00dd5ba0a8fc3e0483fc8912727600a3e9ab70d4bd766afd671e8bb8c39680453a514ecf9b0209292ae58228acceee44108ec4aec1101d5109a1b2c3f4afb3266bc5ff1ce70b77a4289377a79890997c012f214ccb5d174ff2e65a881b7cb2c7147d3aa285fe75a9f7f8e4540a2f2e61166d967cce8223f9374d6e3ae53184abd68d30b5a15b7f2e8e4f5f3dbe76666aee335518bd9b05f9b911bc7babd7aa33c1ecb21732d66be507a362fdcee00777af78e04bbc55ad4819b7b007dade4a6bc32a2ba70f02f64fdbacbf93d69de3dfbde89aba0eb55e5dad361ec164659386af13aeb702271198b83503dcd67f299f4d714f3106ddab4763c5df0a520838f05a2b066bbe32b996939214309b3980dfabd903f86f861dba3495f7fa8b388c260b059e5bd26153a39710a44a71dd3aeaa1255eaf7b7be22ba0025919162bb74c9c4ea697a366341fb0f66a192385bf3327166082034a30dc4d8441dc6988afa655c933a922164a832d22021f50e9a01ffc9c66eeba6d8b1cb346422a969153a10caff36251adfe2f3f4f4ec047557b70b0498fc221a2434daeb81788bb3ee08b58d810c28daaaea0e4a09513152d3d0a20fc3e64e5ffbb0a02cdb9a6c6fffc2dc603c1e9a0f3ebf1b352bc17b7ecb76d101b22903aaa55bd1c70bbe69ef929ec9f10f0fc7ae2f6c103039d6523f7178434680b6d50078a705126d423ecc97de0955033a49e8e0131a0bc2c73c0ef1bd5f87b43a92288bea65c838e3bbf3c8fd8b91e39c8594b7f8ae5b6019200f82381fb23b59b2c875b3ceffeedf7ca40af5bbeb6aa388d3733b98f2c2e6db1913164b5d76d79d7aa9ebf7fb435195b87150f49e624b24bb52570ffbf205778670679e8c83180d193192ef0fd84911c0f626f8b59abfd994737140ef22ba79517cee3268c07d2253f67d05d5e6bc9fbaf77c0febee92c05ac28279822e82820e9df574db5456f38424bab9520f6526676de7df19878385c1105aeeb0c54512a8076272a136150e99e3e2800a953698cb73f6790050dae4352392700b772ae2ae22330f8735a017bee1c43b6a7001109f84c78d17ce2c5a023283b96cdc9b0e0c146930586045336c7b3b8e4630a838b3b202e9a61ef93e1a36afbc0f380b2043c8fdc066799c5f65be9ed1043ee5329e7ae92448fbab4e4a89c2e0b9dfa8b06f0e048fb536d372283ccc92cffdb94379975c120362e51313b01093f3be8f09a83bc601d306983426c97f3478028900cb97c884a8fb999ea197e3c3660cd7540f9ef128d5debc6da5179c92ffd505b96ebb073760920b9052b908fc012cc00ed4248c2e294544dfc1ad8a7c9cf4d27a8e00e488c443961656aeecd910f7e54ab0c1e38749fc4ba0def5fa9996c43b48caa389376efd0fd58ddb8000d666e62334b38109356e1129703afbd5bf73c4683cdb5587e045994a2065d579f396481f059d673eae1eb0c3cdb2fa77e2d43f12380b0d9210f4c1e77bc4fe7d4600e320d3cb9ea030f39788c05021f3e921c022863ca581d679745991d49c4064046a1c69c3406e75deae5265aaf7efa83625468cf1850747f4d4e3e89f6a386552b24a0e779fe7a44264aa6ef8bcc30c042ff6537a419df7a7fbc9790fc9b7e309cb486c201f37e159440de009b65a3a07203810434ce2f171aa3019bab617de5aa087caedd9515e0469bb35c592b733c4ec22a77666a491af42cffc247cfc9ef540e8d22cc9bc0aa62df7460af141b626e84bffaa2ae41df27254f33d1d8d3283a88f64a9e57ff10e283a74a652c29bc15647202c6c31254ba4e0de38312f8251b63238b270558bc239bbaebe057f062f08aab74c9a191ad57a2094ec6e3c21d70d8c74f8a349ec1455b3b30d86e70720bbc182a465020b65d3f78351f7d2ddaf7491467fa36e3babca96f316d0a498b20a91adcdb5cf978b60f2be7e35653404a6843b3703370e8613c4057f73de4953f25720aa08ba05a8c156e52edf73297d15844264b55f9380d6c82daf56ea1a45e60cba2cd3ac3c10e93b7d46f1fa8fa97aa24d22486209a0a98e3f25a17064e1890bb831c6454e012af6a327ad9dd29aeb9ff40d6d29e4aaeeec81488d4821c967d83418dd15608876f9aa9f96a8ca1fd1a4f6cdf905d1d27029dba339c2cf55368e7a3abb8ae4ba78ea408084bc90c1a5d1735a13906f569cca1d1cce28c37d861f10fe08aaf98af60ae01b0fa91b3e0d9d1cc87c9dfe21e3d9032e638ad14f3aa972c59d1bed654d02f1f6feda0942a5bab2755294e2d689df7685e31d1c2328c6b467f6d9fa21eb8d67cb06ec966d4d40070d6348ffa3b822502e7bbcfe9ae4a8c71fa884cc22c68cecdb0989996025f6a03cc4018ad4ebeb5c292895f07404874f9e93d9ea411f9e8f98bf1e93bf7585b67e54abdb8e4447f3148beaf17e33a3f6d2dedb5e198adb82550e6cb05baea48b2fbc4a0945d87e5d4624826b0bde5eb37ade8cd07a7c899d606897c3862197e32b7e731487656cfc0ff391b8e68e30bef29b8b72db825495db967a06914cba8b2e86e683850026b35fbda04df6c2d70bfbe34b59cd49e856825751b80c2b2261aaa7c4b9af34003132965bcf73b56667b76ce8b5325cc425d3e65182ba2a8fa708685220aa5903f8ba8d7cdcdae8cfd604aa58c5443be60a7f8fe9cf3be3aec5ee7d20a84a2906d7907d47f3ca45e6bb30c36607844207a825c58194c3d2cf6e8e7164fe1483747a2d5a143ffeb4d76dc8ca54f1b035bc739273c6ce2580cdebc58ae8253ace5d624fd0cd5bac9177fae8288ec3dc79ba86b52f53808cbde5163541572e11d84ff05457dc98400c6e3f9cf9ed26ee514e501177fdadb22828e07b1d2f4bb0f98c3d9ee9a8657089959618b8ba96ed5cfe96235fd415bb75ee2ba169973fe1bafa7c8ebc5c3fe05c0687ffb30bcaabedb00572ea5e85031657db1b83fd3f813b5d215574ccc14b975253c76e0486c5fc826d0d824739a09d9b39fcbf904bfc951f6c5a94b324fd6da278953158abed0caecb5ba657f4a3a41dd46ec1a2543af82be49b22c844cb103592d734a07ee3908c0a7ccf5a518fcb80fff57c0c79948c3fed5a25415fadcdc62ee37dc07397d678c9e41acf9bb2f54e36430df2fc29e9f6e8c8f71fec99df4b4d44d197dd3dbcb2f5d79526ff0f2c1b9e50035d9b1373886c5497a50ea90d87aa7e114b05db35f746c198a3381497ef100c0d26763cd93e2e1542352fe1568cfbce607d0977e276d703d61e90c7fe797e91e282b57aa352e72cda761ea568dfb30d526e58c006a31571e0b075ff3db231d7e41e9451b6a37539ce206464fb50345a9d774570845a082651dd84710134d9a7fd870622af71535b24fb32452ac9173fd11a937b117f3985e0b5bda3fa031c924e115ead72235a007f4798c4ddeb31ec13226f409dedcaf57b2d5835858461fcb407dc2210dfb7b682ad8620392be76993885a2307360e3fd9cae7badd3b07f1c60f6e773d9016ab3887ea6a1b1d3024ee4b27eadbf5924ce9bb91151f1377c601e96161da6f9a83489f2442dc6ead2b752efd45ea47a6d018ba3aa4f8707992d08e7457811284a546e6ea6ff3a3247bf6d1ae918ad1967f281360040313da36de664d853b45742918464dba600ac088a9088d3e9f1477268797c168f318d8c34a64977e668f7422516c57c1bfe2086212bc659fe582e4619fbfc14dc19463f389d670c0aac12d9df2c3112ec7924122bdeb887f16fcd002011eacdc2951af4d01bc8fa3b668e1679e5c96e23dd877be24d84a21e37f7e60e7e1f15eb19f8c0501f89205eb0b8f179e97a32c6125c8286a21b307480197d4d95ba3782a8039061ed68a9a55f7ea44ed77a998827d9b89814db8ed3dc23be8794b7246f3c824a5e72812bd583a45525312cc0aed306e0aa8846bfed2850dc112d8dbd3aaf832b309918c5ceace3102948ac4b4d200e4c42aab28288213c3ed8baac3c39b9139ab1bf12ab6c3cf2dd48095eaf8243d908093ce8e647ca289b17296dd46a880bf62fff5c083cf1879472e098e059dec47d5c6f851c15e53ecee6e3ebde177d220a1a80c60f6dd0e08fb91196f216057ee115444f4d09becdcb11b407de1d328324373d64acc88559adeafcec649e56d287a6c43a976ff0dc1f2337a60b9f18b949996b4ca0300617a57e62e4a3f07757640c965239af074656b1b1a262d965be346acbfa9e89069a66ab1021db79fc0d698502a2e1e11381bb181" + } + ], + "rejects": { + "invalidEdPublicKeys": [ + { + "name": "identity-order-1", + "edPublicKey": "0100000000000000000000000000000000000000000000000000000000000000", + "reason": "low-order point", + "ownSeed": "0102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f20", + "chatId": "97dfb79f2c2c44a9bd581b3b4fd30e7aee470d3b53062288886a976dd8ade15e" + }, + { + "name": "order-2", + "edPublicKey": "ecffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff7f", + "reason": "low-order point", + "ownSeed": "0102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f20", + "chatId": "97dfb79f2c2c44a9bd581b3b4fd30e7aee470d3b53062288886a976dd8ade15e" + }, + { + "name": "order-4-zero", + "edPublicKey": "0000000000000000000000000000000000000000000000000000000000000000", + "reason": "low-order point", + "ownSeed": "0102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f20", + "chatId": "97dfb79f2c2c44a9bd581b3b4fd30e7aee470d3b53062288886a976dd8ade15e" + }, + { + "name": "not-on-curve", + "edPublicKey": "0200000000000000000000000000000000000000000000000000000000000000", + "reason": "not a valid point", + "ownSeed": "0102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f20", + "chatId": "97dfb79f2c2c44a9bd581b3b4fd30e7aee470d3b53062288886a976dd8ade15e" + } + ], + "zeroSharedSecret": [ + { + "name": "peer-x25519-u-zero", + "x25519PrivateKey": "70788f1a0cea001a2631dae5d05dbd062008d5b30f50b9e29beb2a7822289044", + "peerX25519PublicKey": "0000000000000000000000000000000000000000000000000000000000000000" + } + ], + "decrypt": [ + { + "name": "tampered-tag", + "chatId": "97dfb79f2c2c44a9bd581b3b4fd30e7aee470d3b53062288886a976dd8ade15e", + "chatKey": "6c2005541fb82abe2f639ae77e3b146e7f0dd97a9b7d3b2dccef000c69e9df22", + "senderPublicKey": "79b5562e8fe654f94078b112e8a98ba7901f853ae695bed7e0e3910bad049664", + "recipientPublicKey": "da29e95b02e00ffa15645775fb1d2ba222a1943395eea06b94e2c057b7be69d0", + "nonce": "000102030405060708090a0b0c0d0e0f1011121314151617", + "ciphertext": "b81415e496a7c65aca660ab329de4a5aa66a90579b9fabf5af" + }, + { + "name": "tampered-body", + "chatId": "97dfb79f2c2c44a9bd581b3b4fd30e7aee470d3b53062288886a976dd8ade15e", + "chatKey": "6c2005541fb82abe2f639ae77e3b146e7f0dd97a9b7d3b2dccef000c69e9df22", + "senderPublicKey": "79b5562e8fe654f94078b112e8a98ba7901f853ae695bed7e0e3910bad049664", + "recipientPublicKey": "da29e95b02e00ffa15645775fb1d2ba222a1943395eea06b94e2c057b7be69d0", + "nonce": "000102030405060708090a0b0c0d0e0f1011121314151617", + "ciphertext": "b91415e496a7c65aca660ab329de4a5aa66a90579b9fabf5ae" + }, + { + "name": "swapped-sender-recipient", + "chatId": "97dfb79f2c2c44a9bd581b3b4fd30e7aee470d3b53062288886a976dd8ade15e", + "chatKey": "6c2005541fb82abe2f639ae77e3b146e7f0dd97a9b7d3b2dccef000c69e9df22", + "senderPublicKey": "da29e95b02e00ffa15645775fb1d2ba222a1943395eea06b94e2c057b7be69d0", + "recipientPublicKey": "79b5562e8fe654f94078b112e8a98ba7901f853ae695bed7e0e3910bad049664", + "nonce": "000102030405060708090a0b0c0d0e0f1011121314151617", + "ciphertext": "b81415e496a7c65aca660ab329de4a5aa66a90579b9fabf5ae" + }, + { + "name": "wrong-chat-id", + "chatId": "9027a70126863980b94e9b31ecc25b7c2a48723af73662f6c297939dd2ed6e0e", + "chatKey": "6c2005541fb82abe2f639ae77e3b146e7f0dd97a9b7d3b2dccef000c69e9df22", + "senderPublicKey": "79b5562e8fe654f94078b112e8a98ba7901f853ae695bed7e0e3910bad049664", + "recipientPublicKey": "da29e95b02e00ffa15645775fb1d2ba222a1943395eea06b94e2c057b7be69d0", + "nonce": "000102030405060708090a0b0c0d0e0f1011121314151617", + "ciphertext": "b81415e496a7c65aca660ab329de4a5aa66a90579b9fabf5ae" + }, + { + "name": "wrong-nonce", + "chatId": "97dfb79f2c2c44a9bd581b3b4fd30e7aee470d3b53062288886a976dd8ade15e", + "chatKey": "6c2005541fb82abe2f639ae77e3b146e7f0dd97a9b7d3b2dccef000c69e9df22", + "senderPublicKey": "79b5562e8fe654f94078b112e8a98ba7901f853ae695bed7e0e3910bad049664", + "recipientPublicKey": "da29e95b02e00ffa15645775fb1d2ba222a1943395eea06b94e2c057b7be69d0", + "nonce": "b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcc", + "ciphertext": "b81415e496a7c65aca660ab329de4a5aa66a90579b9fabf5ae" + }, + { + "name": "shorter-than-tag", + "chatId": "97dfb79f2c2c44a9bd581b3b4fd30e7aee470d3b53062288886a976dd8ade15e", + "chatKey": "6c2005541fb82abe2f639ae77e3b146e7f0dd97a9b7d3b2dccef000c69e9df22", + "senderPublicKey": "79b5562e8fe654f94078b112e8a98ba7901f853ae695bed7e0e3910bad049664", + "recipientPublicKey": "da29e95b02e00ffa15645775fb1d2ba222a1943395eea06b94e2c057b7be69d0", + "nonce": "000102030405060708090a0b0c0d0e0f1011121314151617", + "ciphertext": "000000000000000000000000000000" + } + ], + "decryptBlob": [ + { + "name": "tampered-tag", + "chatId": "97dfb79f2c2c44a9bd581b3b4fd30e7aee470d3b53062288886a976dd8ade15e", + "chatKey": "6c2005541fb82abe2f639ae77e3b146e7f0dd97a9b7d3b2dccef000c69e9df22", + "senderPublicKey": "79b5562e8fe654f94078b112e8a98ba7901f853ae695bed7e0e3910bad049664", + "recipientPublicKey": "da29e95b02e00ffa15645775fb1d2ba222a1943395eea06b94e2c057b7be69d0", + "blobId": "a0a1a2a3a4a5a6a7a8a9aaabacadaeaf", + "blob": "7f808182838485868788898a8b8c8d8e8f90919293949596860fbbbcc67390506bbf4ab45371f88340950f328b6579bacd333b2b2ebf1c38b09f22e213f4402f93812474672094caef33bb48a8f981789ebf22756d518253ac7cf4f44b165add56ab28206c0fda7636fb0f98f1a66172130b7a057f4ccda7f4e202151b5430779a1c79a6a954e59be0b8691d9b20b576f5df8257e6214bf6d433de712b10ddda50c09f059c7bda06ddbf04fef33fc23260e67ea05ee8dd4fd69c78c6277616ed895bec587fec5816d93e4da28648533347fd0c30b755dd5a5f703996b2f2fd4da6378889123223b06976fd823dd9513d" + }, + { + "name": "swapped-sender-recipient", + "chatId": "97dfb79f2c2c44a9bd581b3b4fd30e7aee470d3b53062288886a976dd8ade15e", + "chatKey": "6c2005541fb82abe2f639ae77e3b146e7f0dd97a9b7d3b2dccef000c69e9df22", + "senderPublicKey": "da29e95b02e00ffa15645775fb1d2ba222a1943395eea06b94e2c057b7be69d0", + "recipientPublicKey": "79b5562e8fe654f94078b112e8a98ba7901f853ae695bed7e0e3910bad049664", + "blobId": "a0a1a2a3a4a5a6a7a8a9aaabacadaeaf", + "blob": "7f808182838485868788898a8b8c8d8e8f90919293949596860fbbbcc67390506bbf4ab45371f88340950f328b6579bacd333b2b2ebf1c38b09f22e213f4402f93812474672094caef33bb48a8f981789ebf22756d518253ac7cf4f44b165add56ab28206c0fda7636fb0f98f1a66172130b7a057f4ccda7f4e202151b5430779a1c79a6a954e59be0b8691d9b20b576f5df8257e6214bf6d433de712b10ddda50c09f059c7bda06ddbf04fef33fc23260e67ea05ee8dd4fd69c78c6277616ed895bec587fec5816d93e4da28648533347fd0c30b755dd5a5f703996b2f2fd4da6378889123223b06976fd823dd9513c" + }, + { + "name": "wrong-blob-id", + "chatId": "97dfb79f2c2c44a9bd581b3b4fd30e7aee470d3b53062288886a976dd8ade15e", + "chatKey": "6c2005541fb82abe2f639ae77e3b146e7f0dd97a9b7d3b2dccef000c69e9df22", + "senderPublicKey": "79b5562e8fe654f94078b112e8a98ba7901f853ae695bed7e0e3910bad049664", + "recipientPublicKey": "da29e95b02e00ffa15645775fb1d2ba222a1943395eea06b94e2c057b7be69d0", + "blobId": "00000000000000000000000000000000", + "blob": "7f808182838485868788898a8b8c8d8e8f90919293949596860fbbbcc67390506bbf4ab45371f88340950f328b6579bacd333b2b2ebf1c38b09f22e213f4402f93812474672094caef33bb48a8f981789ebf22756d518253ac7cf4f44b165add56ab28206c0fda7636fb0f98f1a66172130b7a057f4ccda7f4e202151b5430779a1c79a6a954e59be0b8691d9b20b576f5df8257e6214bf6d433de712b10ddda50c09f059c7bda06ddbf04fef33fc23260e67ea05ee8dd4fd69c78c6277616ed895bec587fec5816d93e4da28648533347fd0c30b755dd5a5f703996b2f2fd4da6378889123223b06976fd823dd9513c" + }, + { + "name": "message-ciphertext-as-blob", + "chatId": "97dfb79f2c2c44a9bd581b3b4fd30e7aee470d3b53062288886a976dd8ade15e", + "chatKey": "6c2005541fb82abe2f639ae77e3b146e7f0dd97a9b7d3b2dccef000c69e9df22", + "senderPublicKey": "79b5562e8fe654f94078b112e8a98ba7901f853ae695bed7e0e3910bad049664", + "recipientPublicKey": "da29e95b02e00ffa15645775fb1d2ba222a1943395eea06b94e2c057b7be69d0", + "blobId": "a0a1a2a3a4a5a6a7a8a9aaabacadaeaf", + "blob": "000102030405060708090a0b0c0d0e0f1011121314151617b81415e496a7c65aca660ab329de4a5aa66a90579b9fabf5ae" + }, + { + "name": "shorter-than-nonce-and-tag", + "chatId": "97dfb79f2c2c44a9bd581b3b4fd30e7aee470d3b53062288886a976dd8ade15e", + "chatKey": "6c2005541fb82abe2f639ae77e3b146e7f0dd97a9b7d3b2dccef000c69e9df22", + "senderPublicKey": "79b5562e8fe654f94078b112e8a98ba7901f853ae695bed7e0e3910bad049664", + "recipientPublicKey": "da29e95b02e00ffa15645775fb1d2ba222a1943395eea06b94e2c057b7be69d0", + "blobId": "a0a1a2a3a4a5a6a7a8a9aaabacadaeaf", + "blob": "000000000000000000000000000000000000000000000000000000000000000000000000000000" + } + ], + "decryptBlobAsMessage": [ + { + "name": "blob-bytes-as-message", + "chatId": "97dfb79f2c2c44a9bd581b3b4fd30e7aee470d3b53062288886a976dd8ade15e", + "chatKey": "6c2005541fb82abe2f639ae77e3b146e7f0dd97a9b7d3b2dccef000c69e9df22", + "senderPublicKey": "79b5562e8fe654f94078b112e8a98ba7901f853ae695bed7e0e3910bad049664", + "recipientPublicKey": "da29e95b02e00ffa15645775fb1d2ba222a1943395eea06b94e2c057b7be69d0", + "nonce": "7f808182838485868788898a8b8c8d8e8f90919293949596", + "ciphertext": "860fbbbcc67390506bbf4ab45371f88340950f328b6579bacd333b2b2ebf1c38b09f22e213f4402f93812474672094caef33bb48a8f981789ebf22756d518253ac7cf4f44b165add56ab28206c0fda7636fb0f98f1a66172130b7a057f4ccda7f4e202151b5430779a1c79a6a954e59be0b8691d9b20b576f5df8257e6214bf6d433de712b10ddda50c09f059c7bda06ddbf04fef33fc23260e67ea05ee8dd4fd69c78c6277616ed895bec587fec5816d93e4da28648533347fd0c30b755dd5a5f703996b2f2fd4da6378889123223b06976fd823dd9513c" + } + ] + }, + "policy": { + "flipcashUserId": "70c4a3df54af439a88fea7de606d04cb", + "cases": [ + { + "name": "dm-flag-on", + "isDirectMessage": true, + "useE2ee": true, + "peerUserId": "5f0e1d2c3b4a49688776a5b4c3d2e1f0", + "shouldEncrypt": true + }, + { + "name": "dm-flag-off", + "isDirectMessage": true, + "useE2ee": false, + "peerUserId": "5f0e1d2c3b4a49688776a5b4c3d2e1f0", + "shouldEncrypt": false + }, + { + "name": "non-dm-flag-on", + "isDirectMessage": false, + "useE2ee": true, + "peerUserId": "5f0e1d2c3b4a49688776a5b4c3d2e1f0", + "shouldEncrypt": false + }, + { + "name": "non-dm-flag-off", + "isDirectMessage": false, + "useE2ee": false, + "peerUserId": "5f0e1d2c3b4a49688776a5b4c3d2e1f0", + "shouldEncrypt": false + }, + { + "name": "flipcash-dm-flag-on", + "isDirectMessage": true, + "useE2ee": true, + "peerUserId": "70c4a3df54af439a88fea7de606d04cb", + "shouldEncrypt": false + }, + { + "name": "flipcash-dm-flag-off", + "isDirectMessage": true, + "useE2ee": false, + "peerUserId": "70c4a3df54af439a88fea7de606d04cb", + "shouldEncrypt": false + }, + { + "name": "flipcash-non-dm-flag-on", + "isDirectMessage": false, + "useE2ee": true, + "peerUserId": "70c4a3df54af439a88fea7de606d04cb", + "shouldEncrypt": false + }, + { + "name": "flipcash-prefix-15-bytes-dm-flag-on", + "isDirectMessage": true, + "useE2ee": true, + "peerUserId": "70c4a3df54af439a88fea7de606d04", + "shouldEncrypt": true + }, + { + "name": "flipcash-plus-1-byte-dm-flag-on", + "isDirectMessage": true, + "useE2ee": true, + "peerUserId": "70c4a3df54af439a88fea7de606d04cb00", + "shouldEncrypt": true + }, + { + "name": "empty-peer-dm-flag-on", + "isDirectMessage": true, + "useE2ee": true, + "peerUserId": "", + "shouldEncrypt": true + } + ] + } +} diff --git a/settings.gradle.kts b/settings.gradle.kts index 50c10fea7d..8609b3df08 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -208,6 +208,7 @@ include( ":libs:datetime", ":libs:emojis", ":libs:encryption:base58", + ":libs:encryption:chat-cipher", ":libs:encryption:ed25519", ":libs:encryption:ed25519-native", ":libs:encryption:hmac", @@ -330,10 +331,13 @@ val kmpUnitTestModules = setOf( ":libs:reporting", ) // ed25519 and mnemonic excluded: both pull in the JNI-backed Ed25519Kmp Android actual for their -// host vector tests, which can't load on the Linux CI runner (see kmpUnitTestModules). +// host vector tests, which can't load on the Linux CI runner (see kmpUnitTestModules). chat-cipher +// has no host test for the same reason, plus libsodium's Android binding; its vectors run on the +// iOS simulator in the SharedCore tests workflow and as a device test. val noUnitTestModules = setOf( ":apps:flipcash:benchmark", ":kmp:shared-core", + ":libs:encryption:chat-cipher", ":libs:encryption:ed25519", ":libs:encryption:mnemonic", )