diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/relays/Relay.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/relays/Relay.kt index 662916c1f..32c01fdd6 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/relays/Relay.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/relays/Relay.kt @@ -275,9 +275,7 @@ class Relay( } fun resetEOSEStatuses() { - afterEOSEPerSubscription.keys.toList().forEach { - afterEOSEPerSubscription[it] = false - } + afterEOSEPerSubscription = LinkedHashMap(afterEOSEPerSubscription.size) } fun sendFilter(requestId: String) { diff --git a/benchmark/src/androidTest/java/com/vitorpamplona/amethyst/benchmark/CryptoBenchmark.kt b/benchmark/src/androidTest/java/com/vitorpamplona/amethyst/benchmark/CryptoBenchmark.kt index 371ed51cf..4c719cf1a 100644 --- a/benchmark/src/androidTest/java/com/vitorpamplona/amethyst/benchmark/CryptoBenchmark.kt +++ b/benchmark/src/androidTest/java/com/vitorpamplona/amethyst/benchmark/CryptoBenchmark.kt @@ -5,12 +5,6 @@ import androidx.benchmark.junit4.measureRepeated import androidx.test.ext.junit.runners.AndroidJUnit4 import com.vitorpamplona.quartz.crypto.CryptoUtils import com.vitorpamplona.quartz.crypto.KeyPair -import com.vitorpamplona.quartz.encoders.Bech32 -import com.vitorpamplona.quartz.encoders.Hex -import com.vitorpamplona.quartz.encoders.bechToBytes -import com.vitorpamplona.quartz.encoders.toNpub -import com.vitorpamplona.quartz.events.Event -import junit.framework.TestCase.assertEquals import junit.framework.TestCase.assertNotNull import org.junit.Rule import org.junit.Test @@ -38,7 +32,7 @@ class CryptoBenchmark { val keyPair2 = KeyPair() benchmarkRule.measureRepeated { - assertNotNull(CryptoUtils.getSharedSecretNIP44(keyPair1.privKey!!, keyPair2.pubKey)) + assertNotNull(CryptoUtils.getSharedSecretNIP44v1(keyPair1.privKey!!, keyPair2.pubKey)) } } @@ -58,7 +52,7 @@ class CryptoBenchmark { val keyPair2 = KeyPair() benchmarkRule.measureRepeated { - assertNotNull(CryptoUtils.computeSharedSecretNIP44(keyPair1.privKey!!, keyPair2.pubKey)) + assertNotNull(CryptoUtils.computeSharedSecretNIP44v1(keyPair1.privKey!!, keyPair2.pubKey)) } } diff --git a/benchmark/src/androidTest/java/com/vitorpamplona/amethyst/benchmark/GiftWrapBenchmark.kt b/benchmark/src/androidTest/java/com/vitorpamplona/amethyst/benchmark/GiftWrapBenchmark.kt index 8e45a0590..ade8bc777 100644 --- a/benchmark/src/androidTest/java/com/vitorpamplona/amethyst/benchmark/GiftWrapBenchmark.kt +++ b/benchmark/src/androidTest/java/com/vitorpamplona/amethyst/benchmark/GiftWrapBenchmark.kt @@ -54,7 +54,7 @@ class GiftWrapBenchmark { val countDownLatch2 = CountDownLatch(1) - Assert.assertEquals(expectedLength, events!!.wraps.map { it.toJson() }.joinToString("").length) + Assert.assertEquals(expectedLength, events!!.wraps.map { println("TEST ${it.toJson()}"); it.toJson() }.joinToString("").length) // Simulate Receiver events!!.wraps.forEach { diff --git a/benchmark/src/androidTest/java/com/vitorpamplona/amethyst/benchmark/GiftWrapReceivingBenchmark.kt b/benchmark/src/androidTest/java/com/vitorpamplona/amethyst/benchmark/GiftWrapReceivingBenchmark.kt index 2c7606bd6..f2782e428 100644 --- a/benchmark/src/androidTest/java/com/vitorpamplona/amethyst/benchmark/GiftWrapReceivingBenchmark.kt +++ b/benchmark/src/androidTest/java/com/vitorpamplona/amethyst/benchmark/GiftWrapReceivingBenchmark.kt @@ -4,15 +4,12 @@ import androidx.benchmark.junit4.BenchmarkRule import androidx.benchmark.junit4.measureRepeated import androidx.test.ext.junit.runners.AndroidJUnit4 import com.vitorpamplona.quartz.crypto.CryptoUtils -import com.vitorpamplona.quartz.encoders.toHexKey import com.vitorpamplona.quartz.crypto.KeyPair -import com.vitorpamplona.quartz.crypto.decodeNIP44 import com.vitorpamplona.quartz.encoders.hexToByteArray import com.vitorpamplona.quartz.events.ChatMessageEvent import com.vitorpamplona.quartz.events.Event import com.vitorpamplona.quartz.events.GiftWrapEvent import com.vitorpamplona.quartz.events.Gossip -import com.vitorpamplona.quartz.events.NIP24Factory import com.vitorpamplona.quartz.events.SealedGossipEvent import com.vitorpamplona.quartz.signers.NostrSigner import com.vitorpamplona.quartz.signers.NostrSignerInternal @@ -139,18 +136,6 @@ class GiftWrapReceivingBenchmark { } } - @Test - fun decodeWrapEvent() { - val sender = NostrSignerInternal(KeyPair()) - val receiver = NostrSignerInternal(KeyPair()) - - val wrap = createWrap(sender, receiver) - - benchmarkRule.measureRepeated { - assertNotNull(decodeNIP44(wrap.content)) - } - } - @Test fun decryptWrapEvent() { val sender = NostrSignerInternal(KeyPair()) @@ -158,10 +143,8 @@ class GiftWrapReceivingBenchmark { val wrap = createWrap(sender, receiver) - val toDecrypt = decodeNIP44(wrap.content) ?: return - benchmarkRule.measureRepeated { - assertNotNull(CryptoUtils.decryptNIP44(toDecrypt, sender.keyPair.privKey!!, wrap.pubKey.hexToByteArray())) + assertNotNull(CryptoUtils.decryptNIP44v1(wrap.content, sender.keyPair.privKey!!, wrap.pubKey.hexToByteArray())) } } @@ -172,26 +155,13 @@ class GiftWrapReceivingBenchmark { val wrap = createWrap(sender, receiver) - val toDecrypt = decodeNIP44(wrap.content) ?: return - val innerJson = CryptoUtils.decryptNIP44(toDecrypt, receiver.keyPair.privKey!!, wrap.pubKey.hexToByteArray()) + val innerJson = CryptoUtils.decryptNIP44v1(wrap.content, receiver.keyPair.privKey!!, wrap.pubKey.hexToByteArray()) benchmarkRule.measureRepeated { assertNotNull(innerJson?.let { Event.fromJson(it) }) } } - @Test - fun decodeSealEvent() { - val sender = NostrSignerInternal(KeyPair()) - val receiver = NostrSignerInternal(KeyPair()) - - val seal = createSeal(sender, receiver) - - benchmarkRule.measureRepeated { - assertNotNull(decodeNIP44(seal.content)) - } - } - @Test fun decryptSealedEvent() { val sender = NostrSignerInternal(KeyPair()) @@ -199,10 +169,8 @@ class GiftWrapReceivingBenchmark { val seal = createSeal(sender, receiver) - val toDecrypt = decodeNIP44(seal.content) ?: return - benchmarkRule.measureRepeated { - assertNotNull(CryptoUtils.decryptNIP44(toDecrypt, sender.keyPair.privKey!!, seal.pubKey.hexToByteArray())) + assertNotNull(CryptoUtils.decryptNIP44v1(seal.content, sender.keyPair.privKey!!, seal.pubKey.hexToByteArray())) } } @@ -213,8 +181,7 @@ class GiftWrapReceivingBenchmark { val seal = createSeal(sender, receiver) - val toDecrypt = decodeNIP44(seal.content) ?: return - val innerJson = CryptoUtils.decryptNIP44(toDecrypt, receiver.keyPair.privKey!!, seal.pubKey.hexToByteArray()) + val innerJson = CryptoUtils.decryptNIP44v1(seal.content, receiver.keyPair.privKey!!, seal.pubKey.hexToByteArray()) benchmarkRule.measureRepeated { assertNotNull(innerJson?.let { Gossip.fromJson(it) }) diff --git a/quartz/src/androidTest/assets/nip44.vectors.json b/quartz/src/androidTest/assets/nip44.vectors.json new file mode 100644 index 000000000..8fcabe8a1 --- /dev/null +++ b/quartz/src/androidTest/assets/nip44.vectors.json @@ -0,0 +1,648 @@ +{ + "v2": { + "valid": { + "get_conversation_key": [ + { + "sec1": "315e59ff51cb9209768cf7da80791ddcaae56ac9775eb25b6dee1234bc5d2268", + "pub2": "c2f9d9948dc8c7c38321e4b85c8558872eafa0641cd269db76848a6073e69133", + "conversation_key": "3dfef0ce2a4d80a25e7a328accf73448ef67096f65f79588e358d9a0eb9013f1" + }, + { + "sec1": "a1e37752c9fdc1273be53f68c5f74be7c8905728e8de75800b94262f9497c86e", + "pub2": "03bb7947065dde12ba991ea045132581d0954f042c84e06d8c00066e23c1a800", + "conversation_key": "4d14f36e81b8452128da64fe6f1eae873baae2f444b02c950b90e43553f2178b" + }, + { + "sec1": "98a5902fd67518a0c900f0fb62158f278f94a21d6f9d33d30cd3091195500311", + "pub2": "aae65c15f98e5e677b5050de82e3aba47a6fe49b3dab7863cf35d9478ba9f7d1", + "conversation_key": "9c00b769d5f54d02bf175b7284a1cbd28b6911b06cda6666b2243561ac96bad7" + }, + { + "sec1": "86ae5ac8034eb2542ce23ec2f84375655dab7f836836bbd3c54cefe9fdc9c19f", + "pub2": "59f90272378089d73f1339710c02e2be6db584e9cdbe86eed3578f0c67c23585", + "conversation_key": "19f934aafd3324e8415299b64df42049afaa051c71c98d0aa10e1081f2e3e2ba" + }, + { + "sec1": "2528c287fe822421bc0dc4c3615878eb98e8a8c31657616d08b29c00ce209e34", + "pub2": "f66ea16104c01a1c532e03f166c5370a22a5505753005a566366097150c6df60", + "conversation_key": "c833bbb292956c43366145326d53b955ffb5da4e4998a2d853611841903f5442" + }, + { + "sec1": "49808637b2d21129478041813aceb6f2c9d4929cd1303cdaf4fbdbd690905ff2", + "pub2": "74d2aab13e97827ea21baf253ad7e39b974bb2498cc747cdb168582a11847b65", + "conversation_key": "4bf304d3c8c4608864c0fe03890b90279328cd24a018ffa9eb8f8ccec06b505d" + }, + { + "sec1": "af67c382106242c5baabf856efdc0629cc1c5b4061f85b8ceaba52aa7e4b4082", + "pub2": "bdaf0001d63e7ec994fad736eab178ee3c2d7cfc925ae29f37d19224486db57b", + "conversation_key": "a3a575dd66d45e9379904047ebfb9a7873c471687d0535db00ef2daa24b391db" + }, + { + "sec1": "0e44e2d1db3c1717b05ffa0f08d102a09c554a1cbbf678ab158b259a44e682f1", + "pub2": "1ffa76c5cc7a836af6914b840483726207cb750889753d7499fb8b76aa8fe0de", + "conversation_key": "a39970a667b7f861f100e3827f4adbf6f464e2697686fe1a81aeda817d6b8bdf" + }, + { + "sec1": "5fc0070dbd0666dbddc21d788db04050b86ed8b456b080794c2a0c8e33287bb6", + "pub2": "31990752f296dd22e146c9e6f152a269d84b241cc95bb3ff8ec341628a54caf0", + "conversation_key": "72c21075f4b2349ce01a3e604e02a9ab9f07e35dd07eff746de348b4f3c6365e" + }, + { + "sec1": "1b7de0d64d9b12ddbb52ef217a3a7c47c4362ce7ea837d760dad58ab313cba64", + "pub2": "24383541dd8083b93d144b431679d70ef4eec10c98fceef1eff08b1d81d4b065", + "conversation_key": "dd152a76b44e63d1afd4dfff0785fa07b3e494a9e8401aba31ff925caeb8f5b1" + }, + { + "sec1": "df2f560e213ca5fb33b9ecde771c7c0cbd30f1cf43c2c24de54480069d9ab0af", + "pub2": "eeea26e552fc8b5e377acaa03e47daa2d7b0c787fac1e0774c9504d9094c430e", + "conversation_key": "770519e803b80f411c34aef59c3ca018608842ebf53909c48d35250bd9323af6" + }, + { + "sec1": "cffff919fcc07b8003fdc63bc8a00c0f5dc81022c1c927c62c597352190d95b9", + "pub2": "eb5c3cca1a968e26684e5b0eb733aecfc844f95a09ac4e126a9e58a4e4902f92", + "conversation_key": "46a14ee7e80e439ec75c66f04ad824b53a632b8409a29bbb7c192e43c00bb795" + }, + { + "sec1": "64ba5a685e443e881e9094647ddd32db14444bb21aa7986beeba3d1c4673ba0a", + "pub2": "50e6a4339fac1f3bf86f2401dd797af43ad45bbf58e0801a7877a3984c77c3c4", + "conversation_key": "968b9dbbfcede1664a4ca35a5d3379c064736e87aafbf0b5d114dff710b8a946" + }, + { + "sec1": "dd0c31ccce4ec8083f9b75dbf23cc2878e6d1b6baa17713841a2428f69dee91a", + "pub2": "b483e84c1339812bed25be55cff959778dfc6edde97ccd9e3649f442472c091b", + "conversation_key": "09024503c7bde07eb7865505891c1ea672bf2d9e25e18dd7a7cea6c69bf44b5d" + }, + { + "sec1": "af71313b0d95c41e968a172b33ba5ebd19d06cdf8a7a98df80ecf7af4f6f0358", + "pub2": "2a5c25266695b461ee2af927a6c44a3c598b8095b0557e9bd7f787067435bc7c", + "conversation_key": "fe5155b27c1c4b4e92a933edae23726a04802a7cc354a77ac273c85aa3c97a92" + }, + { + "sec1": "6636e8a389f75fe068a03b3edb3ea4a785e2768e3f73f48ffb1fc5e7cb7289dc", + "pub2": "514eb2064224b6a5829ea21b6e8f7d3ea15ff8e70e8555010f649eb6e09aec70", + "conversation_key": "ff7afacd4d1a6856d37ca5b546890e46e922b508639214991cf8048ddbe9745c" + }, + { + "sec1": "94b212f02a3cfb8ad147d52941d3f1dbe1753804458e6645af92c7b2ea791caa", + "pub2": "f0cac333231367a04b652a77ab4f8d658b94e86b5a8a0c472c5c7b0d4c6a40cc", + "conversation_key": "e292eaf873addfed0a457c6bd16c8effde33d6664265697f69f420ab16f6669b" + }, + { + "sec1": "aa61f9734e69ae88e5d4ced5aae881c96f0d7f16cca603d3bed9eec391136da6", + "pub2": "4303e5360a884c360221de8606b72dd316da49a37fe51e17ada4f35f671620a6", + "conversation_key": "8e7d44fd4767456df1fb61f134092a52fcd6836ebab3b00766e16732683ed848" + }, + { + "sec1": "5e914bdac54f3f8e2cba94ee898b33240019297b69e96e70c8a495943a72fc98", + "pub2": "5bd097924f606695c59f18ff8fd53c174adbafaaa71b3c0b4144a3e0a474b198", + "conversation_key": "f5a0aecf2984bf923c8cd5e7bb8be262d1a8353cb93959434b943a07cf5644bc" + }, + { + "sec1": "8b275067add6312ddee064bcdbeb9d17e88aa1df36f430b2cea5cc0413d8278a", + "pub2": "65bbbfca819c90c7579f7a82b750a18c858db1afbec8f35b3c1e0e7b5588e9b8", + "conversation_key": "2c565e7027eb46038c2263563d7af681697107e975e9914b799d425effd248d6" + }, + { + "sec1": "1ac848de312285f85e0f7ec208aac20142a1f453402af9b34ec2ec7a1f9c96fc", + "pub2": "45f7318fe96034d23ee3ddc25b77f275cc1dd329664dd51b89f89c4963868e41", + "conversation_key": "b56e970e5057a8fd929f8aad9248176b9af87819a708d9ddd56e41d1aec74088" + }, + { + "sec1": "295a1cf621de401783d29d0e89036aa1c62d13d9ad307161b4ceb535ba1b40e6", + "pub2": "840115ddc7f1034d3b21d8e2103f6cb5ab0b63cf613f4ea6e61ae3d016715cdd", + "conversation_key": "b4ee9c0b9b9fef88975773394f0a6f981ca016076143a1bb575b9ff46e804753" + }, + { + "sec1": "a28eed0fe977893856ab9667e06ace39f03abbcdb845c329a1981be438ba565d", + "pub2": "b0f38b950a5013eba5ab4237f9ed29204a59f3625c71b7e210fec565edfa288c", + "conversation_key": "9d3a802b45bc5aeeb3b303e8e18a92ddd353375710a31600d7f5fff8f3a7285b" + }, + { + "sec1": "7ab65af72a478c05f5c651bdc4876c74b63d20d04cdbf71741e46978797cd5a4", + "pub2": "f1112159161b568a9cb8c9dd6430b526c4204bcc8ce07464b0845b04c041beda", + "conversation_key": "943884cddaca5a3fef355e9e7f08a3019b0b66aa63ec90278b0f9fdb64821e79" + }, + { + "sec1": "95c79a7b75ba40f2229e85756884c138916f9d103fc8f18acc0877a7cceac9fe", + "pub2": "cad76bcbd31ca7bbda184d20cc42f725ed0bb105b13580c41330e03023f0ffb3", + "conversation_key": "81c0832a669eea13b4247c40be51ccfd15bb63fcd1bba5b4530ce0e2632f301b" + }, + { + "sec1": "baf55cc2febd4d980b4b393972dfc1acf49541e336b56d33d429bce44fa12ec9", + "pub2": "0c31cf87fe565766089b64b39460ebbfdedd4a2bc8379be73ad3c0718c912e18", + "conversation_key": "37e2344da9ecdf60ae2205d81e89d34b280b0a3f111171af7e4391ded93b8ea6" + }, + { + "sec1": "6eeec45acd2ed31693c5256026abf9f072f01c4abb61f51cf64e6956b6dc8907", + "pub2": "e501b34ed11f13d816748c0369b0c728e540df3755bab59ed3327339e16ff828", + "conversation_key": "afaa141b522ddb27bb880d768903a7f618bb8b6357728cae7fb03af639b946e6" + }, + { + "sec1": "261a076a9702af1647fb343c55b3f9a4f1096273002287df0015ba81ce5294df", + "pub2": "b2777c863878893ae100fb740c8fab4bebd2bf7be78c761a75593670380a6112", + "conversation_key": "76f8d2853de0734e51189ced523c09427c3e46338b9522cd6f74ef5e5b475c74" + }, + { + "sec1": "ed3ec71ca406552ea41faec53e19f44b8f90575eda4b7e96380f9cc73c26d6f3", + "pub2": "86425951e61f94b62e20cae24184b42e8e17afcf55bafa58645efd0172624fae", + "conversation_key": "f7ffc520a3a0e9e9b3c0967325c9bf12707f8e7a03f28b6cd69ae92cf33f7036" + }, + { + "sec1": "5a788fc43378d1303ac78639c59a58cb88b08b3859df33193e63a5a3801c722e", + "pub2": "a8cba2f87657d229db69bee07850fd6f7a2ed070171a06d006ec3a8ac562cf70", + "conversation_key": "7d705a27feeedf78b5c07283362f8e361760d3e9f78adab83e3ae5ce7aeb6409" + }, + { + "sec1": "63bffa986e382b0ac8ccc1aa93d18a7aa445116478be6f2453bad1f2d3af2344", + "pub2": "b895c70a83e782c1cf84af558d1038e6b211c6f84ede60408f519a293201031d", + "conversation_key": "3a3b8f00d4987fc6711d9be64d9c59cf9a709c6c6481c2cde404bcc7a28f174e" + }, + { + "sec1": "e4a8bcacbf445fd3721792b939ff58e691cdcba6a8ba67ac3467b45567a03e5c", + "pub2": "b54053189e8c9252c6950059c783edb10675d06d20c7b342f73ec9fa6ed39c9d", + "conversation_key": "7b3933b4ef8189d347169c7955589fc1cfc01da5239591a08a183ff6694c44ad" + }, + { + "sec1": "fffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364139", + "pub2": "0000000000000000000000000000000000000000000000000000000000000002", + "conversation_key": "8b6392dbf2ec6a2b2d5b1477fc2be84d63ef254b667cadd31bd3f444c44ae6ba", + "note": "sec1 = n-2, pub2: random, 0x02" + }, + { + "sec1": "0000000000000000000000000000000000000000000000000000000000000002", + "pub2": "1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdeb", + "conversation_key": "be234f46f60a250bef52a5ee34c758800c4ca8e5030bf4cc1a31d37ba2104d43", + "note": "sec1 = 2, pub2: rand" + }, + { + "sec1": "0000000000000000000000000000000000000000000000000000000000000001", + "pub2": "79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798", + "conversation_key": "3b4610cb7189beb9cc29eb3716ecc6102f1247e8f3101a03a1787d8908aeb54e", + "note": "sec1 == pub2" + } + ], + "get_message_keys": { + "conversation_key": "a1a3d60f3470a8612633924e91febf96dc5366ce130f658b1f0fc652c20b3b54", + "keys": [ + { + "nonce": "e1e6f880560d6d149ed83dcc7e5861ee62a5ee051f7fde9975fe5d25d2a02d72", + "chacha_key": "f145f3bed47cb70dbeaac07f3a3fe683e822b3715edb7c4fe310829014ce7d76", + "chacha_nonce": "c4ad129bb01180c0933a160c", + "hmac_key": "027c1db445f05e2eee864a0975b0ddef5b7110583c8c192de3732571ca5838c4" + }, + { + "nonce": "e1d6d28c46de60168b43d79dacc519698512ec35e8ccb12640fc8e9f26121101", + "chacha_key": "e35b88f8d4a8f1606c5082f7a64b100e5d85fcdb2e62aeafbec03fb9e860ad92", + "chacha_nonce": "22925e920cee4a50a478be90", + "hmac_key": "46a7c55d4283cb0df1d5e29540be67abfe709e3b2e14b7bf9976e6df994ded30" + }, + { + "nonce": "cfc13bef512ac9c15951ab00030dfaf2626fdca638dedb35f2993a9eeb85d650", + "chacha_key": "020783eb35fdf5b80ef8c75377f4e937efb26bcbad0e61b4190e39939860c4bf", + "chacha_nonce": "d3594987af769a52904656ac", + "hmac_key": "237ec0ccb6ebd53d179fa8fd319e092acff599ef174c1fdafd499ef2b8dee745" + }, + { + "nonce": "ea6eb84cac23c5c1607c334e8bdf66f7977a7e374052327ec28c6906cbe25967", + "chacha_key": "ff68db24b34fa62c78ac5ffeeaf19533afaedf651fb6a08384e46787f6ce94be", + "chacha_nonce": "50bb859aa2dde938cc49ec7a", + "hmac_key": "06ff32e1f7b29753a727d7927b25c2dd175aca47751462d37a2039023ec6b5a6" + }, + { + "nonce": "8c2e1dd3792802f1f9f7842e0323e5d52ad7472daf360f26e15f97290173605d", + "chacha_key": "2f9daeda8683fdeede81adac247c63cc7671fa817a1fd47352e95d9487989d8b", + "chacha_nonce": "400224ba67fc2f1b76736916", + "hmac_key": "465c05302aeeb514e41c13ed6405297e261048cfb75a6f851ffa5b445b746e4b" + }, + { + "nonce": "05c28bf3d834fa4af8143bf5201a856fa5fac1a3aee58f4c93a764fc2f722367", + "chacha_key": "1e3d45777025a035be566d80fd580def73ed6f7c043faec2c8c1c690ad31c110", + "chacha_nonce": "021905b1ea3afc17cb9bf96f", + "hmac_key": "74a6e481a89dcd130aaeb21060d7ec97ad30f0007d2cae7b1b11256cc70dfb81" + }, + { + "nonce": "5e043fb153227866e75a06d60185851bc90273bfb93342f6632a728e18a07a17", + "chacha_key": "1ea72c9293841e7737c71567d8120145a58991aaa1c436ef77bf7adb83f882f1", + "chacha_nonce": "72f69a5a5f795465cee59da8", + "hmac_key": "e9daa1a1e9a266ecaa14e970a84bce3fbbf329079bbccda626582b4e66a0d4c9" + }, + { + "nonce": "7be7338eaf06a87e274244847fe7a97f5c6a91f44adc18fcc3e411ad6f786dbf", + "chacha_key": "881e7968a1f0c2c80742ee03cd49ea587e13f22699730f1075ade01931582bf6", + "chacha_nonce": "6e69be92d61c04a276021565", + "hmac_key": "901afe79e74b19967c8829af23617d7d0ffbf1b57190c096855c6a03523a971b" + }, + { + "nonce": "94571c8d590905bad7becd892832b472f2aa5212894b6ce96e5ba719c178d976", + "chacha_key": "f80873dd48466cb12d46364a97b8705c01b9b4230cb3ec3415a6b9551dc42eef", + "chacha_nonce": "3dda53569cfcb7fac1805c35", + "hmac_key": "e9fc264345e2839a181affebc27d2f528756e66a5f87b04bf6c5f1997047051e" + }, + { + "nonce": "13a6ee974b1fd759135a2c2010e3cdda47081c78e771125e4f0c382f0284a8cb", + "chacha_key": "bc5fb403b0bed0d84cf1db872b6522072aece00363178c98ad52178d805fca85", + "chacha_nonce": "65064239186e50304cc0f156", + "hmac_key": "e872d320dde4ed3487958a8e43b48aabd3ced92bc24bb8ff1ccb57b590d9701a" + }, + { + "nonce": "082fecdb85f358367b049b08be0e82627ae1d8edb0f27327ccb593aa2613b814", + "chacha_key": "1fbdb1cf6f6ea816349baf697932b36107803de98fcd805ebe9849b8ad0e6a45", + "chacha_nonce": "2e605e1d825a3eaeb613db9c", + "hmac_key": "fae910f591cf3c7eb538c598583abad33bc0a03085a96ca4ea3a08baf17c0eec" + }, + { + "nonce": "4c19020c74932c30ec6b2d8cd0d5bb80bd0fc87da3d8b4859d2fb003810afd03", + "chacha_key": "1ab9905a0189e01cda82f843d226a82a03c4f5b6dbea9b22eb9bc953ba1370d4", + "chacha_nonce": "cbb2530ea653766e5a37a83a", + "hmac_key": "267f68acac01ac7b34b675e36c2cef5e7b7a6b697214add62a491bedd6efc178" + }, + { + "nonce": "67723a3381497b149ce24814eddd10c4c41a1e37e75af161930e6b9601afd0ff", + "chacha_key": "9ecbd25e7e2e6c97b8c27d376dcc8c5679da96578557e4e21dba3a7ef4e4ac07", + "chacha_nonce": "ef649fcf335583e8d45e3c2e", + "hmac_key": "04dbbd812fa8226fdb45924c521a62e3d40a9e2b5806c1501efdeba75b006bf1" + }, + { + "nonce": "42063fe80b093e8619b1610972b4c3ab9e76c14fd908e642cd4997cafb30f36c", + "chacha_key": "211c66531bbcc0efcdd0130f9f1ebc12a769105eb39608994bcb188fa6a73a4a", + "chacha_nonce": "67803605a7e5010d0f63f8c8", + "hmac_key": "e840e4e8921b57647369d121c5a19310648105dbdd008200ebf0d3b668704ff8" + }, + { + "nonce": "b5ac382a4be7ac03b554fe5f3043577b47ea2cd7cfc7e9ca010b1ffbb5cf1a58", + "chacha_key": "b3b5f14f10074244ee42a3837a54309f33981c7232a8b16921e815e1f7d1bb77", + "chacha_nonce": "4e62a0073087ed808be62469", + "hmac_key": "c8efa10230b5ea11633816c1230ca05fa602ace80a7598916d83bae3d3d2ccd7" + }, + { + "nonce": "e9d1eba47dd7e6c1532dc782ff63125db83042bb32841db7eeafd528f3ea7af9", + "chacha_key": "54241f68dc2e50e1db79e892c7c7a471856beeb8d51b7f4d16f16ab0645d2f1a", + "chacha_nonce": "a963ed7dc29b7b1046820a1d", + "hmac_key": "aba215c8634530dc21c70ddb3b3ee4291e0fa5fa79be0f85863747bde281c8b2" + }, + { + "nonce": "a94ecf8efeee9d7068de730fad8daf96694acb70901d762de39fa8a5039c3c49", + "chacha_key": "c0565e9e201d2381a2368d7ffe60f555223874610d3d91fbbdf3076f7b1374dd", + "chacha_nonce": "329bb3024461e84b2e1c489b", + "hmac_key": "ac42445491f092481ce4fa33b1f2274700032db64e3a15014fbe8c28550f2fec" + }, + { + "nonce": "533605ea214e70c25e9a22f792f4b78b9f83a18ab2103687c8a0075919eaaa53", + "chacha_key": "ab35a5e1e54d693ff023db8500d8d4e79ad8878c744e0eaec691e96e141d2325", + "chacha_nonce": "653d759042b85194d4d8c0a7", + "hmac_key": "b43628e37ba3c31ce80576f0a1f26d3a7c9361d29bb227433b66f49d44f167ba" + }, + { + "nonce": "7f38df30ceea1577cb60b355b4f5567ff4130c49e84fed34d779b764a9cc184c", + "chacha_key": "a37d7f211b84a551a127ff40908974eb78415395d4f6f40324428e850e8c42a3", + "chacha_nonce": "b822e2c959df32b3cb772a7c", + "hmac_key": "1ba31764f01f69b5c89ded2d7c95828e8052c55f5d36f1cd535510d61ba77420" + }, + { + "nonce": "11b37f9dbc4d0185d1c26d5f4ed98637d7c9701fffa65a65839fa4126573a4e5", + "chacha_key": "964f38d3a31158a5bfd28481247b18dd6e44d69f30ba2a40f6120c6d21d8a6ba", + "chacha_nonce": "5f72c5b87c590bcd0f93b305", + "hmac_key": "2fc4553e7cedc47f29690439890f9f19c1077ef3e9eaeef473d0711e04448918" + }, + { + "nonce": "8be790aa483d4cdd843189f71f135b3ec7e31f381312c8fe9f177aab2a48eafa", + "chacha_key": "95c8c74d633721a131316309cf6daf0804d59eaa90ea998fc35bac3d2fbb7a94", + "chacha_nonce": "409a7654c0e4bf8c2c6489be", + "hmac_key": "21bb0b06eb2b460f8ab075f497efa9a01c9cf9146f1e3986c3bf9da5689b6dc4" + }, + { + "nonce": "19fd2a718ea084827d6bd73f509229ddf856732108b59fc01819f611419fd140", + "chacha_key": "cc6714b9f5616c66143424e1413d520dae03b1a4bd202b82b0a89b0727f5cdc8", + "chacha_nonce": "1b7fd2534f015a8f795d8f32", + "hmac_key": "2bef39c4ce5c3c59b817e86351373d1554c98bc131c7e461ed19d96cfd6399a0" + }, + { + "nonce": "3c2acd893952b2f6d07d8aea76f545ca45961a93fe5757f6a5a80811d5e0255d", + "chacha_key": "c8de6c878cb469278d0af894bc181deb6194053f73da5014c2b5d2c8db6f2056", + "chacha_nonce": "6ffe4f1971b904a1b1a81b99", + "hmac_key": "df1cd69dd3646fca15594284744d4211d70e7d8472e545d276421fbb79559fd4" + }, + { + "nonce": "7dbea4cead9ac91d4137f1c0a6eebb6ba0d1fb2cc46d829fbc75f8d86aca6301", + "chacha_key": "c8e030f6aa680c3d0b597da9c92bb77c21c4285dd620c5889f9beba7446446b0", + "chacha_nonce": "a9b5a67d081d3b42e737d16f", + "hmac_key": "355a85f551bc3cce9a14461aa60994742c9bbb1c81a59ca102dc64e61726ab8e" + }, + { + "nonce": "45422e676cdae5f1071d3647d7a5f1f5adafb832668a578228aa1155a491f2f3", + "chacha_key": "758437245f03a88e2c6a32807edfabff51a91c81ca2f389b0b46f2c97119ea90", + "chacha_nonce": "263830a065af33d9c6c5aa1f", + "hmac_key": "7c581cf3489e2de203a95106bfc0de3d4032e9d5b92b2b61fb444acd99037e17" + }, + { + "nonce": "babc0c03fad24107ad60678751f5db2678041ff0d28671ede8d65bdf7aa407e9", + "chacha_key": "bd68a28bd48d9ffa3602db72c75662ac2848a0047a313d2ae2d6bc1ac153d7e9", + "chacha_nonce": "d0f9d2a1ace6c758f594ffdd", + "hmac_key": "eb435e3a642adfc9d59813051606fc21f81641afd58ea6641e2f5a9f123bb50a" + }, + { + "nonce": "7a1b8aac37d0d20b160291fad124ab697cfca53f82e326d78fef89b4b0ea8f83", + "chacha_key": "9e97875b651a1d30d17d086d1e846778b7faad6fcbc12e08b3365d700f62e4fe", + "chacha_nonce": "ccdaad5b3b7645be430992eb", + "hmac_key": "6f2f55cf35174d75752f63c06cc7cbc8441759b142999ed2d5a6d09d263e1fc4" + }, + { + "nonce": "8370e4e32d7e680a83862cab0da6136ef607014d043e64cdf5ecc0c4e20b3d9a", + "chacha_key": "1472bed5d19db9c546106de946e0649cd83cc9d4a66b087a65906e348dcf92e2", + "chacha_nonce": "ed02dece5fc3a186f123420b", + "hmac_key": "7b3f7739f49d30c6205a46b174f984bb6a9fc38e5ccfacef2dac04fcbd3b184e" + }, + { + "nonce": "9f1c5e8a29cd5677513c2e3a816551d6833ee54991eb3f00d5b68096fc8f0183", + "chacha_key": "5e1a7544e4d4dafe55941fcbdf326f19b0ca37fc49c4d47e9eec7fb68cde4975", + "chacha_nonce": "7d9acb0fdc174e3c220f40de", + "hmac_key": "e265ab116fbbb86b2aefc089a0986a0f5b77eda50c7410404ad3b4f3f385c7a7" + }, + { + "nonce": "c385aa1c37c2bfd5cc35fcdbdf601034d39195e1cabff664ceb2b787c15d0225", + "chacha_key": "06bf4e60677a13e54c4a38ab824d2ef79da22b690da2b82d0aa3e39a14ca7bdd", + "chacha_nonce": "26b450612ca5e905b937e147", + "hmac_key": "22208152be2b1f5f75e6bfcc1f87763d48bb7a74da1be3d102096f257207f8b3" + }, + { + "nonce": "3ff73528f88a50f9d35c0ddba4560bacee5b0462d0f4cb6e91caf41847040ce4", + "chacha_key": "850c8a17a23aa761d279d9901015b2bbdfdff00adbf6bc5cf22bd44d24ecabc9", + "chacha_nonce": "4a296a1fb0048e5020d3b129", + "hmac_key": "b1bf49a533c4da9b1d629b7ff30882e12d37d49c19abd7b01b7807d75ee13806" + }, + { + "nonce": "2dcf39b9d4c52f1cb9db2d516c43a7c6c3b8c401f6a4ac8f131a9e1059957036", + "chacha_key": "17f8057e6156ba7cc5310d01eda8c40f9aa388f9fd1712deb9511f13ecc37d27", + "chacha_nonce": "a8188daff807a1182200b39d", + "hmac_key": "47b89da97f68d389867b5d8a2d7ba55715a30e3d88a3cc11f3646bc2af5580ef" + } + ] + }, + "calc_padded_len": [ + [16, 32], + [32, 32], + [33, 64], + [37, 64], + [45, 64], + [49, 64], + [64, 64], + [65, 96], + [100, 128], + [111, 128], + [200, 224], + [250, 256], + [320, 320], + [383, 384], + [384, 384], + [400, 448], + [500, 512], + [512, 512], + [515, 640], + [700, 768], + [800, 896], + [900, 1024], + [1020, 1024], + [65536, 65536] + ], + "encrypt_decrypt": [ + { + "sec1": "0000000000000000000000000000000000000000000000000000000000000001", + "sec2": "0000000000000000000000000000000000000000000000000000000000000002", + "conversation_key": "c41c775356fd92eadc63ff5a0dc1da211b268cbea22316767095b2871ea1412d", + "nonce": "0000000000000000000000000000000000000000000000000000000000000001", + "plaintext": "a", + "payload": "AgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABee0G5VSK0/9YypIObAtDKfYEAjD35uVkHyB0F4DwrcNaCXlCWZKaArsGrY6M9wnuTMxWfp1RTN9Xga8no+kF5Vsb" + }, + { + "sec1": "0000000000000000000000000000000000000000000000000000000000000002", + "sec2": "0000000000000000000000000000000000000000000000000000000000000001", + "conversation_key": "c41c775356fd92eadc63ff5a0dc1da211b268cbea22316767095b2871ea1412d", + "nonce": "f00000000000000000000000000000f00000000000000000000000000000000f", + "plaintext": "🍕🫃", + "payload": "AvAAAAAAAAAAAAAAAAAAAPAAAAAAAAAAAAAAAAAAAAAPSKSK6is9ngkX2+cSq85Th16oRTISAOfhStnixqZziKMDvB0QQzgFZdjLTPicCJaV8nDITO+QfaQ61+KbWQIOO2Yj" + }, + { + "sec1": "5c0c523f52a5b6fad39ed2403092df8cebc36318b39383bca6c00808626fab3a", + "sec2": "4b22aa260e4acb7021e32f38a6cdf4b673c6a277755bfce287e370c924dc936d", + "conversation_key": "3e2b52a63be47d34fe0a80e34e73d436d6963bc8f39827f327057a9986c20a45", + "nonce": "b635236c42db20f021bb8d1cdff5ca75dd1a0cc72ea742ad750f33010b24f73b", + "plaintext": "表ポあA鷗ŒéB逍Üߪąñ丂㐀𠀀", + "payload": "ArY1I2xC2yDwIbuNHN/1ynXdGgzHLqdCrXUPMwELJPc7s7JqlCMJBAIIjfkpHReBPXeoMCyuClwgbT419jUWU1PwaNl4FEQYKCDKVJz+97Mp3K+Q2YGa77B6gpxB/lr1QgoqpDf7wDVrDmOqGoiPjWDqy8KzLueKDcm9BVP8xeTJIxs=" + }, + { + "sec1": "8f40e50a84a7462e2b8d24c28898ef1f23359fff50d8c509e6fb7ce06e142f9c", + "sec2": "b9b0a1e9cc20100c5faa3bbe2777303d25950616c4c6a3fa2e3e046f936ec2ba", + "conversation_key": "d5a2f879123145a4b291d767428870f5a8d9e5007193321795b40183d4ab8c2b", + "nonce": "b20989adc3ddc41cd2c435952c0d59a91315d8c5218d5040573fc3749543acaf", + "plaintext": "ability🤝的 ȺȾ", + "payload": "ArIJia3D3cQc0sQ1lSwNWakTFdjFIY1QQFc/w3SVQ6yvbG2S0x4Yu86QGwPTy7mP3961I1XqB6SFFTzqDZZavhxoWMj7mEVGMQIsh2RLWI5EYQaQDIePSnXPlzf7CIt+voTD" + }, + { + "sec1": "875adb475056aec0b4809bd2db9aa00cff53a649e7b59d8edcbf4e6330b0995c", + "sec2": "9c05781112d5b0a2a7148a222e50e0bd891d6b60c5483f03456e982185944aae", + "conversation_key": "3b15c977e20bfe4b8482991274635edd94f366595b1a3d2993515705ca3cedb8", + "nonce": "8d4442713eb9d4791175cb040d98d6fc5be8864d6ec2f89cf0895a2b2b72d1b1", + "plaintext": "pepper👀їжак", + "payload": "Ao1EQnE+udR5EXXLBA2Y1vxb6IZNbsL4nPCJWisrctGxY3AduCS+jTUgAAnfvKafkmpy15+i9YMwCdccisRa8SvzW671T2JO4LFSPX31K4kYUKelSAdSPwe9NwO6LhOsnoJ+" + }, + { + "sec1": "eba1687cab6a3101bfc68fd70f214aa4cc059e9ec1b79fdb9ad0a0a4e259829f", + "sec2": "dff20d262bef9dfd94666548f556393085e6ea421c8af86e9d333fa8747e94b3", + "conversation_key": "4f1538411098cf11c8af216836444787c462d47f97287f46cf7edb2c4915b8a5", + "nonce": "2180b52ae645fcf9f5080d81b1f0b5d6f2cd77ff3c986882bb549158462f3407", + "plaintext": "( ͡° ͜ʖ ͡°)", + "payload": "AiGAtSrmRfz59QgNgbHwtdbyzXf/PJhogrtUkVhGLzQHv4qhKQwnFQ54OjVMgqCea/Vj0YqBSdhqNR777TJ4zIUk7R0fnizp6l1zwgzWv7+ee6u+0/89KIjY5q1wu6inyuiv" + }, + { + "sec1": "d5633530f5bcfebceb5584cfbbf718a30df0751b729dd9a789b9f30c0587d74e", + "sec2": "b74e6a341fb134127272b795a08b59250e5fa45a82a2eb4095e4ce9ed5f5e214", + "conversation_key": "75fe686d21a035f0c7cd70da64ba307936e5ca0b20710496a6b6b5f573377bdd", + "nonce": "e4cd5f7ce4eea024bc71b17ad456a986a74ac426c2c62b0a15eb5c5c8f888b68", + "plaintext": "مُنَاقَشَةُ سُبُلِ اِسْتِخْدَامِ اللُّغَةِ فِي النُّظُمِ الْقَائِمَةِ وَفِيم يَخُصَّ التَّطْبِيقَاتُ الْحاسُوبِيَّةُ،", + "payload": "AuTNX3zk7qAkvHGxetRWqYanSsQmwsYrChXrXFyPiItoIBsWu1CB+sStla2M4VeANASHxM78i1CfHQQH1YbBy24Tng7emYW44ol6QkFD6D8Zq7QPl+8L1c47lx8RoODEQMvNCbOk5ffUV3/AhONHBXnffrI+0025c+uRGzfqpYki4lBqm9iYU+k3Tvjczq9wU0mkVDEaM34WiQi30MfkJdRbeeYaq6kNvGPunLb3xdjjs5DL720d61Flc5ZfoZm+CBhADy9D9XiVZYLKAlkijALJur9dATYKci6OBOoc2SJS2Clai5hOVzR0yVeyHRgRfH9aLSlWW5dXcUxTo7qqRjNf8W5+J4jF4gNQp5f5d0YA4vPAzjBwSP/5bGzNDslKfcAH" + }, + { + "sec1": "d5633530f5bcfebceb5584cfbbf718a30df0751b729dd9a789b9f30c0587d74e", + "sec2": "b74e6a341fb134127272b795a08b59250e5fa45a82a2eb4095e4ce9ed5f5e214", + "conversation_key": "75fe686d21a035f0c7cd70da64ba307936e5ca0b20710496a6b6b5f573377bdd", + "nonce": "38d1ca0abef9e5f564e89761a86cee04574b6825d3ef2063b10ad75899e4b023", + "plaintext": "الكل في المجمو عة (5)", + "payload": "AjjRygq++eX1ZOiXYahs7gRXS2gl0+8gY7EK11iZ5LAjbOTrlfrxak5Lki42v2jMPpLSicy8eHjsWkkMtF0i925vOaKG/ZkMHh9ccQBdfTvgEGKzztedqDCAWb5TP1YwU1PsWaiiqG3+WgVvJiO4lUdMHXL7+zKKx8bgDtowzz4QAwI=" + }, + { + "sec1": "d5633530f5bcfebceb5584cfbbf718a30df0751b729dd9a789b9f30c0587d74e", + "sec2": "b74e6a341fb134127272b795a08b59250e5fa45a82a2eb4095e4ce9ed5f5e214", + "conversation_key": "75fe686d21a035f0c7cd70da64ba307936e5ca0b20710496a6b6b5f573377bdd", + "nonce": "4f1a31909f3483a9e69c8549a55bbc9af25fa5bbecf7bd32d9896f83ef2e12e0", + "plaintext": "𝖑𝖆𝖟𝖞 社會科學院語學研究所", + "payload": "Ak8aMZCfNIOp5pyFSaVbvJryX6W77Pe9MtmJb4PvLhLgh/TsxPLFSANcT67EC1t/qxjru5ZoADjKVEt2ejdx+xGvH49mcdfbc+l+L7gJtkH7GLKpE9pQNQWNHMAmj043PAXJZ++fiJObMRR2mye5VHEANzZWkZXMrXF7YjuG10S1pOU=" + }, + { + "sec1": "d5633530f5bcfebceb5584cfbbf718a30df0751b729dd9a789b9f30c0587d74e", + "sec2": "b74e6a341fb134127272b795a08b59250e5fa45a82a2eb4095e4ce9ed5f5e214", + "conversation_key": "75fe686d21a035f0c7cd70da64ba307936e5ca0b20710496a6b6b5f573377bdd", + "nonce": "a3e219242d85465e70adcd640b564b3feff57d2ef8745d5e7a0663b2dccceb54", + "plaintext": "🙈 🙉 🙊 0️⃣ 1️⃣ 2️⃣ 3️⃣ 4️⃣ 5️⃣ 6️⃣ 7️⃣ 8️⃣ 9️⃣ 🔟 Powerلُلُصّبُلُلصّبُررً ॣ ॣh ॣ ॣ冗", + "payload": "AqPiGSQthUZecK3NZAtWSz/v9X0u+HRdXnoGY7LczOtUf05aMF89q1FLwJvaFJYICZoMYgRJHFLwPiOHce7fuAc40kX0wXJvipyBJ9HzCOj7CgtnC1/cmPCHR3s5AIORmroBWglm1LiFMohv1FSPEbaBD51VXxJa4JyWpYhreSOEjn1wd0lMKC9b+osV2N2tpbs+rbpQem2tRen3sWflmCqjkG5VOVwRErCuXuPb5+hYwd8BoZbfCrsiAVLd7YT44dRtKNBx6rkabWfddKSLtreHLDysOhQUVOp/XkE7OzSkWl6sky0Hva6qJJ/V726hMlomvcLHjE41iKmW2CpcZfOedg==" + } + ], + "encrypt_decrypt_long_msg": [ + { + "conversation_key": "8fc262099ce0d0bb9b89bac05bb9e04f9bc0090acc181fef6840ccee470371ed", + "nonce": "326bcb2c943cd6bb717588c9e5a7e738edf6ed14ec5f5344caa6ef56f0b9cff7", + "pattern": "x", + "repeat": 65535, + "plaintext_sha256": "09ab7495d3e61a76f0deb12cb0306f0696cbb17ffc12131368c7a939f12f56d3", + "payload_sha256": "90714492225faba06310bff2f249ebdc2a5e609d65a629f1c87f2d4ffc55330a" + }, + { + "conversation_key": "56adbe3720339363ab9c3b8526ffce9fd77600927488bfc4b59f7a68ffe5eae0", + "nonce": "ad68da81833c2a8ff609c3d2c0335fd44fe5954f85bb580c6a8d467aa9fc5dd0", + "pattern": "!", + "repeat": 65535, + "plaintext_sha256": "6af297793b72ae092c422e552c3bb3cbc310da274bd1cf9e31023a7fe4a2d75e", + "payload_sha256": "8013e45a109fad3362133132b460a2d5bce235fe71c8b8f4014793fb52a49844" + }, + { + "conversation_key": "7fc540779979e472bb8d12480b443d1e5eb1098eae546ef2390bee499bbf46be", + "nonce": "34905e82105c20de9a2f6cd385a0d541e6bcc10601d12481ff3a7575dc622033", + "pattern": "🦄", + "repeat": 16383, + "plaintext_sha256": "a249558d161b77297bc0cb311dde7d77190f6571b25c7e4429cd19044634a61f", + "payload_sha256": "b3348422471da1f3c59d79acfe2fe103f3cd24488109e5b18734cdb5953afd15" + } + ] + }, + "invalid": { + "encrypt_msg_lengths": [0, 65536, 100000, 10000000], + "get_conversation_key": [ + { + "sec1": "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", + "pub2": "1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef", + "note": "sec1 higher than curve.n" + }, + { + "sec1": "0000000000000000000000000000000000000000000000000000000000000000", + "pub2": "1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef", + "note": "sec1 is 0" + }, + { + "sec1": "fffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364139", + "pub2": "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", + "note": "pub2 is invalid, no sqrt, all-ff" + }, + { + "sec1": "fffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141", + "pub2": "1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef", + "note": "sec1 == curve.n" + }, + { + "sec1": "0000000000000000000000000000000000000000000000000000000000000002", + "pub2": "1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef", + "note": "pub2 is invalid, no sqrt" + }, + { + "sec1": "0102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f20", + "pub2": "0000000000000000000000000000000000000000000000000000000000000000", + "note": "pub2 is point of order 3 on twist" + }, + { + "sec1": "0102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f20", + "pub2": "eb1f7200aecaa86682376fb1c13cd12b732221e774f553b0a0857f88fa20f86d", + "note": "pub2 is point of order 13 on twist" + }, + { + "sec1": "0102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f20", + "pub2": "709858a4c121e4a84eb59c0ded0261093c71e8ca29efeef21a6161c447bcaf9f", + "note": "pub2 is point of order 3319 on twist" + } + ], + "decrypt": [ + { + "conversation_key": "ca2527a037347b91bea0c8a30fc8d9600ffd81ec00038671e3a0f0cb0fc9f642", + "nonce": "daaea5ca345b268e5b62060ca72c870c48f713bc1e00ff3fc0ddb78e826f10db", + "plaintext": "n o b l e", + "payload": "#Atqupco0WyaOW2IGDKcshwxI9xO8HgD/P8Ddt46CbxDbrhdG8VmJdU0MIDf06CUvEvdnr1cp1fiMtlM/GrE92xAc1K5odTpCzUB+mjXgbaqtntBUbTToSUoT0ovrlPwzGjyp", + "note": "unknown encryption version" + }, + { + "conversation_key": "36f04e558af246352dcf73b692fbd3646a2207bd8abd4b1cd26b234db84d9481", + "nonce": "ad408d4be8616dc84bb0bf046454a2a102edac937c35209c43cd7964c5feb781", + "plaintext": "⚠️", + "payload": "AK1AjUvoYW3IS7C/BGRUoqEC7ayTfDUgnEPNeWTF/reBZFaha6EAIRueE9D1B1RuoiuFScC0Q94yjIuxZD3JStQtE8JMNacWFs9rlYP+ZydtHhRucp+lxfdvFlaGV/sQlqZz", + "note": "unknown encryption version 0" + }, + { + "conversation_key": "ca2527a037347b91bea0c8a30fc8d9600ffd81ec00038671e3a0f0cb0fc9f642", + "nonce": "daaea5ca345b268e5b62060ca72c870c48f713bc1e00ff3fc0ddb78e826f10db", + "plaintext": "n o s t r", + "payload": "Atфupco0WyaOW2IGDKcshwxI9xO8HgD/P8Ddt46CbxDbrhdG8VmJZE0UICD06CUvEvdnr1cp1fiMtlM/GrE92xAc1EwsVCQEgWEu2gsHUVf4JAa3TpgkmFc3TWsax0v6n/Wq", + "note": "invalid base64" + }, + { + "conversation_key": "cff7bd6a3e29a450fd27f6c125d5edeb0987c475fd1e8d97591e0d4d8a89763c", + "nonce": "09ff97750b084012e15ecb84614ce88180d7b8ec0d468508a86b6d70c0361a25", + "plaintext": "¯\\_(ツ)_/¯", + "payload": "Agn/l3ULCEAS4V7LhGFM6IGA17jsDUaFCKhrbXDANholyySBfeh+EN8wNB9gaLlg4j6wdBYh+3oK+mnxWu3NKRbSvQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA", + "note": "invalid MAC" + }, + { + "conversation_key": "cfcc9cf682dfb00b11357f65bdc45e29156b69db424d20b3596919074f5bf957", + "nonce": "65b14b0b949aaa7d52c417eb753b390e8ad6d84b23af4bec6d9bfa3e03a08af4", + "plaintext": "🥎", + "payload": "AmWxSwuUmqp9UsQX63U7OQ6K1thLI69L7G2b+j4DoIr0oRWQ8avl4OLqWZiTJ10vIgKrNqjoaX+fNhE9RqmR5g0f6BtUg1ijFMz71MO1D4lQLQfW7+UHva8PGYgQ1QpHlKgR", + "note": "invalid MAC" + }, + { + "conversation_key": "5254827d29177622d40a7b67cad014fe7137700c3c523903ebbe3e1b74d40214", + "nonce": "7ab65dbb8bbc2b8e35cafb5745314e1f050325a864d11d0475ef75b3660d91c1", + "plaintext": "elliptic-curve cryptography", + "payload": "Anq2XbuLvCuONcr7V0UxTh8FAyWoZNEdBHXvdbNmDZHB573MI7R7rrTYftpqmvUpahmBC2sngmI14/L0HjOZ7lWGJlzdh6luiOnGPc46cGxf08MRC4CIuxx3i2Lm0KqgJ7vA", + "note": "invalid padding" + }, + { + "conversation_key": "fea39aca9aa8340c3a78ae1f0902aa7e726946e4efcd7783379df8096029c496", + "nonce": "7d4283e3b54c885d6afee881f48e62f0a3f5d7a9e1cb71ccab594a7882c39330", + "plaintext": "noble", + "payload": "An1Cg+O1TIhdav7ogfSOYvCj9dep4ctxzKtZSniCw5MwRrrPJFyAQYZh5VpjC2QYzny5LIQ9v9lhqmZR4WBYRNJ0ognHVNMwiFV1SHpvUFT8HHZN/m/QarflbvDHAtO6pY16", + "note": "invalid padding" + }, + { + "conversation_key": "0c4cffb7a6f7e706ec94b2e879f1fc54ff8de38d8db87e11787694d5392d5b3f", + "nonce": "6f9fd72667c273acd23ca6653711a708434474dd9eb15c3edb01ce9a95743e9b", + "plaintext": "censorship-resistant and global social network", + "payload": "Am+f1yZnwnOs0jymZTcRpwhDRHTdnrFcPtsBzpqVdD6b2NZDaNm/TPkZGr75kbB6tCSoq7YRcbPiNfJXNch3Tf+o9+zZTMxwjgX/nm3yDKR2kHQMBhVleCB9uPuljl40AJ8kXRD0gjw+aYRJFUMK9gCETZAjjmrsCM+nGRZ1FfNsHr6Z", + "note": "invalid padding" + }, + { + "conversation_key": "5cd2d13b9e355aeb2452afbd3786870dbeecb9d355b12cb0a3b6e9da5744cd35", + "nonce": "b60036976a1ada277b948fd4caa065304b96964742b89d26f26a25263a5060bd", + "plaintext": "0", + "payload": "", + "note": "invalid payload length: 0" + }, + { + "conversation_key": "d61d3f09c7dfe1c0be91af7109b60a7d9d498920c90cbba1e137320fdd938853", + "nonce": "1a29d02c8b4527745a2ccb38bfa45655deb37bc338ab9289d756354cea1fd07c", + "plaintext": "1", + "payload": "Ag==", + "note": "invalid payload length: 4" + }, + { + "conversation_key": "873bb0fc665eb950a8e7d5971965539f6ebd645c83c08cd6a85aafbad0f0bc47", + "nonce": "c826d3c38e765ab8cc42060116cd1464b2a6ce01d33deba5dedfb48615306d4a", + "plaintext": "2", + "payload": "AqxgToSh3H7iLYRJjoWAM+vSv/Y1mgNlm6OWWjOYUClrFF8=", + "note": "invalid payload length: 48" + }, + { + "conversation_key": "9f2fef8f5401ac33f74641b568a7a30bb19409c76ffdc5eae2db6b39d2617fbe", + "nonce": "9ff6484642545221624eaac7b9ea27133a4cc2356682a6033aceeef043549861", + "plaintext": "3", + "payload": "Ap/2SEZCVFIhYk6qx7nqJxM6TMI1ZoKmAzrO7vBDVJhhuZXWiM20i/tIsbjT0KxkJs2MZjh1oXNYMO9ggfk7i47WQA==", + "note": "invalid payload length: 92" + } + ] + } + } +} \ No newline at end of file diff --git a/quartz/src/androidTest/java/com/vitorpamplona/quartz/CryptoUtilsTest.kt b/quartz/src/androidTest/java/com/vitorpamplona/quartz/CryptoUtilsTest.kt index 352c96ba3..2ebdc9fd3 100644 --- a/quartz/src/androidTest/java/com/vitorpamplona/quartz/CryptoUtilsTest.kt +++ b/quartz/src/androidTest/java/com/vitorpamplona/quartz/CryptoUtilsTest.kt @@ -26,7 +26,7 @@ class CryptoUtilsTest { val privateKey = "f410f88bcec6cbfda04d6a273c7b1dd8bba144cd45b71e87109cfa11dd7ed561" val publicKey = "765cd7cf91d3ad07423d114d5a39c61d52b2cdbc18ba055ddbbeec71fbe2aa2f" - val key = CryptoUtils.getSharedSecretNIP44(privateKey = privateKey.hexToByteArray(), pubKey = publicKey.hexToByteArray()) + val key = CryptoUtils.getSharedSecretNIP44v1(privateKey = privateKey.hexToByteArray(), pubKey = publicKey.hexToByteArray()) assertEquals("577c966f499dddd8e8dcc34e8f352e283cc177e53ae372794947e0b8ede7cfd8", key.toHexKey()) } @@ -36,8 +36,8 @@ class CryptoUtilsTest { val sender = KeyPair() val receiver = KeyPair() - val sharedSecret1 = CryptoUtils.getSharedSecretNIP44(sender.privKey!!, receiver.pubKey) - val sharedSecret2 = CryptoUtils.getSharedSecretNIP44(receiver.privKey!!, sender.pubKey) + val sharedSecret1 = CryptoUtils.getSharedSecretNIP44v1(sender.privKey!!, receiver.pubKey) + val sharedSecret2 = CryptoUtils.getSharedSecretNIP44v1(receiver.privKey!!, sender.pubKey) assertEquals(sharedSecret1.toHexKey(), sharedSecret2.toHexKey()) @@ -61,28 +61,16 @@ class CryptoUtilsTest { assertEquals(msg, decrypted) } + @Test - fun encryptDecryptNIP4WithJsonSchemaTest() { + fun encryptDecryptNIP44v1Test() { val msg = "Hi" val privateKey = CryptoUtils.privkeyCreate() val publicKey = CryptoUtils.pubkeyCreate(privateKey) - val encrypted = CryptoUtils.encryptNIP04Json(msg, privateKey, publicKey) - val decrypted = CryptoUtils.decryptNIP04(encrypted, privateKey, publicKey) - - assertEquals(msg, decrypted) - } - - @Test - fun encryptDecryptNIP44Test() { - val msg = "Hi" - - val privateKey = CryptoUtils.privkeyCreate() - val publicKey = CryptoUtils.pubkeyCreate(privateKey) - - val encrypted = CryptoUtils.encryptNIP44(msg, privateKey, publicKey) - val decrypted = CryptoUtils.decryptNIP44(encrypted, privateKey, publicKey) + val encrypted = CryptoUtils.encryptNIP44v1(msg, privateKey, publicKey) + val decrypted = CryptoUtils.decryptNIP44v1(encrypted, privateKey, publicKey) assertEquals(msg, decrypted) } @@ -101,15 +89,15 @@ class CryptoUtilsTest { } @Test - fun encryptSharedSecretDecryptNIP44Test() { + fun encryptSharedSecretDecryptNIP44v1Test() { val msg = "Hi" val privateKey = CryptoUtils.privkeyCreate() val publicKey = CryptoUtils.pubkeyCreate(privateKey) - val sharedSecret = CryptoUtils.getSharedSecretNIP44(privateKey, publicKey) + val sharedSecret = CryptoUtils.getSharedSecretNIP44v1(privateKey, publicKey) - val encrypted = CryptoUtils.encryptNIP44(msg, sharedSecret) - val decrypted = CryptoUtils.decryptNIP44(encrypted, sharedSecret) + val encrypted = CryptoUtils.encryptNIP44v1(msg, sharedSecret) + val decrypted = CryptoUtils.decryptNIP44v1(encrypted, sharedSecret) assertEquals(msg, decrypted) } diff --git a/quartz/src/androidTest/java/com/vitorpamplona/quartz/NIP44v2Test.kt b/quartz/src/androidTest/java/com/vitorpamplona/quartz/NIP44v2Test.kt new file mode 100644 index 000000000..6f3cfad96 --- /dev/null +++ b/quartz/src/androidTest/java/com/vitorpamplona/quartz/NIP44v2Test.kt @@ -0,0 +1,202 @@ +package com.vitorpamplona.quartz; + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation +import com.fasterxml.jackson.annotation.JsonProperty +import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper +import com.vitorpamplona.quartz.crypto.Nip44v2 +import com.vitorpamplona.quartz.encoders.hexToByteArray +import com.vitorpamplona.quartz.encoders.toHexKey +import fr.acinq.secp256k1.Secp256k1 +import junit.framework.TestCase.assertEquals +import junit.framework.TestCase.assertNotNull +import junit.framework.TestCase.assertNull +import junit.framework.TestCase.fail +import org.junit.Test +import org.junit.runner.RunWith +import java.security.MessageDigest +import java.security.SecureRandom + + +@RunWith(AndroidJUnit4::class) +public class NIP44v2Test { + val vectors: VectorFile = jacksonObjectMapper().readValue( + getInstrumentation().context.assets.open("nip44.vectors.json"), + VectorFile::class.java + ) + + val random = SecureRandom() + val nip44v2 = Nip44v2(Secp256k1.get(), random) + + @Test + fun conversationKeyTest() { + for (v in vectors.v2?.valid?.getConversationKey!!) { + val conversationKey = nip44v2.getConversationKey(v.sec1!!.hexToByteArray(), v.pub2!!.hexToByteArray()) + + assertEquals(v.conversationKey, conversationKey.toHexKey()) + } + } + + @Test + fun paddingTest() { + for (v in vectors.v2?.valid?.calcPaddedLen!!) { + val actual = nip44v2.calcPaddedLen(v[0]) + assertEquals(v[1], actual) + } + } + + @Test + fun encryptDecryptTest() { + for (v in vectors.v2?.valid?.encryptDecrypt!!) { + val pub2 = com.vitorpamplona.quartz.crypto.KeyPair(v.sec2!!.hexToByteArray()) + val conversationKey = nip44v2.getConversationKey(v.sec1!!.hexToByteArray(), pub2.pubKey) + assertEquals(v.conversationKey, conversationKey.toHexKey()) + + val ciphertext = nip44v2.encryptWithNonce( + v.plaintext!!, + conversationKey, + v.nonce!!.hexToByteArray() + ).encodePayload() + + assertEquals(v.payload, ciphertext) + + val decrypted = nip44v2.decrypt(v.payload!!, conversationKey) + assertEquals(v.plaintext, decrypted) + } + } + + @Test + fun encryptDecryptLongTest() { + for (v in vectors.v2?.valid?.encryptDecryptLongMsg!!) { + val conversationKey = v.conversationKey!!.hexToByteArray() + val plaintext = v.pattern!!.repeat(v.repeat!!) + + assertEquals(v.plaintextSha256, sha256Hex(plaintext.toByteArray(Charsets.UTF_8))) + + val ciphertext = nip44v2.encryptWithNonce( + plaintext, + conversationKey, + v.nonce!!.hexToByteArray() + ).encodePayload() + + assertEquals(v.payloadSha256, sha256Hex(ciphertext.toByteArray(Charsets.UTF_8))) + + val decrypted = nip44v2.decrypt(ciphertext, conversationKey) + + assertEquals(plaintext, decrypted) + } + } + + @Test + fun invalidMessageLenghts() { + for (v in vectors.v2?.invalid?.encryptMsgLengths!!) { + val key = ByteArray(32) + random.nextBytes(key) + try { + nip44v2.encrypt("a".repeat(v), key) + fail("Should Throw for ${v}") + } catch (e: Exception) { + assertNotNull(e) + } + } + } + + @Test + fun invalidDecrypt() { + for (v in vectors.v2?.invalid?.decrypt!!) { + try { + val result = nip44v2.decrypt(v.payload!!, v.conversationKey!!.hexToByteArray()) + assertNull(result) + //fail("Should Throw for ${v.note}") + } catch (e: Exception) { + assertNotNull(e) + } + } + } + + @Test + fun invalidConversationKey() { + for (v in vectors.v2?.invalid?.getConversationKey!!) { + try { + nip44v2.getConversationKey(v.sec1!!.hexToByteArray(), v.pub2!!.hexToByteArray()) + fail("Should Throw for ${v.note}") + } catch (e: Exception) { + assertNotNull(e) + } + } + } + + fun sha256Hex(data: ByteArray): String { + // Creates a new buffer every time + return MessageDigest.getInstance("SHA-256").digest(data).toHexKey() + } +} + +data class VectorFile ( + val v2 : V2? = V2() +) + +data class V2 ( + val valid : Valid? = Valid(), + val invalid : Invalid? = Invalid() +) + +data class Valid ( + @JsonProperty("get_conversation_key" ) val getConversationKey : ArrayList = arrayListOf(), + @JsonProperty("get_message_keys" ) val getMessageKeys : GetMessageKeys? = GetMessageKeys(), + @JsonProperty("calc_padded_len" ) val calcPaddedLen : ArrayList> = arrayListOf(), + @JsonProperty("encrypt_decrypt" ) val encryptDecrypt : ArrayList = arrayListOf(), + @JsonProperty("encrypt_decrypt_long_msg" ) val encryptDecryptLongMsg : ArrayList = arrayListOf() +) + +data class Invalid ( + @JsonProperty("encrypt_msg_lengths" ) val encryptMsgLengths : ArrayList = arrayListOf(), + @JsonProperty("get_conversation_key" ) val getConversationKey : ArrayList = arrayListOf(), + @JsonProperty("decrypt" ) val decrypt : ArrayList = arrayListOf() +) + +data class GetConversationKey ( + val sec1 : String? = null, + val pub2 : String? = null, + val note : String? = null, + @JsonProperty("conversation_key" ) val conversationKey : String? = null +) + +data class GetMessageKeys ( + @JsonProperty("conversation_key" ) val conversationKey : String? = null, + val keys : ArrayList = arrayListOf() +) + +data class Keys ( + @JsonProperty("nonce" ) val nonce : String? = null, + @JsonProperty("chacha_key" ) val chachaKey : String? = null, + @JsonProperty("chacha_nonce" ) val chachaNonce : String? = null, + @JsonProperty("hmac_key" ) val hmacKey : String? = null +) + +data class EncryptDecrypt ( + val sec1 : String? = null, + val sec2 : String? = null, + @JsonProperty("conversation_key" ) val conversationKey : String? = null, + val nonce : String? = null, + val plaintext : String? = null, + val payload : String? = null +) + +data class EncryptDecryptLongMsg ( + @JsonProperty("conversation_key" ) val conversationKey : String? = null, + val nonce : String? = null, + val pattern : String? = null, + val repeat : Int? = null, + @JsonProperty("plaintext_sha256" ) val plaintextSha256 : String? = null, + @JsonProperty("payload_sha256" ) val payloadSha256 : String? = null +) + +data class Decrypt ( + @JsonProperty("conversation_key" ) val conversationKey : String? = null, + val nonce : String? = null, + val plaintext : String? = null, + val payload : String? = null, + val note : String? = null +) + diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/crypto/CryptoUtils.kt b/quartz/src/main/java/com/vitorpamplona/quartz/crypto/CryptoUtils.kt index ccf3f0a34..54b7fbfaa 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/crypto/CryptoUtils.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/crypto/CryptoUtils.kt @@ -1,32 +1,26 @@ package com.vitorpamplona.quartz.crypto import android.util.Log -import android.util.LruCache -import com.goterl.lazysodium.SodiumAndroid -import com.goterl.lazysodium.utils.Key -import com.vitorpamplona.quartz.encoders.Hex +import com.vitorpamplona.quartz.encoders.hexToByteArray import com.vitorpamplona.quartz.events.Event import fr.acinq.secp256k1.Secp256k1 import java.security.MessageDigest import java.security.SecureRandom import java.util.Base64 -import javax.crypto.Cipher -import javax.crypto.spec.IvParameterSpec -import javax.crypto.spec.SecretKeySpec object CryptoUtils { - private val sharedKeyCache04 = LruCache(200) - private val sharedKeyCache44 = LruCache(200) - private val secp256k1 = Secp256k1.get() - private val libSodium = SodiumAndroid() private val random = SecureRandom() - private val h02 = Hex.decode("02") + + private val nip04 = Nip04(secp256k1, random) + private val nip44v1 = Nip44v1(secp256k1, random) + private val nip44v2 = Nip44v2(secp256k1, random) fun clearCache() { - sharedKeyCache04.evictAll() - sharedKeyCache44.evictAll() + nip04.clearCache() + nip44v1.clearCache() + nip44v2.clearCache() } fun randomInt(bound: Int): Int { @@ -63,242 +57,174 @@ object CryptoUtils { return MessageDigest.getInstance("SHA-256").digest(data) } + /** + * NIP 04 Utils + */ fun encryptNIP04(msg: String, privateKey: ByteArray, pubKey: ByteArray): String { - val info = encryptNIP04(msg, getSharedSecretNIP04(privateKey, pubKey)) - val encryptionInfo = EncryptedInfoString( - v = info.v, - nonce = Base64.getEncoder().encodeToString(info.nonce), - ciphertext = Base64.getEncoder().encodeToString(info.ciphertext) - ) - return "${encryptionInfo.ciphertext}?iv=${encryptionInfo.nonce}" + return nip04.encrypt(msg, privateKey, pubKey) } - fun encryptNIP04Json(msg: String, privateKey: ByteArray, pubKey: ByteArray): EncryptedInfo { - return encryptNIP04(msg, getSharedSecretNIP04(privateKey, pubKey)) - } - - fun encryptNIP04(msg: String, sharedSecret: ByteArray): EncryptedInfo { - val iv = ByteArray(16) - random.nextBytes(iv) - val cipher = Cipher.getInstance("AES/CBC/PKCS5Padding") - cipher.init(Cipher.ENCRYPT_MODE, SecretKeySpec(sharedSecret, "AES"), IvParameterSpec(iv)) - //val ivBase64 = Base64.getEncoder().encodeToString(iv) - val encryptedMsg = cipher.doFinal(msg.toByteArray()) - //val encryptedMsgBase64 = Base64.getEncoder().encodeToString(encryptedMsg) - return EncryptedInfo(encryptedMsg, iv, Nip44Version.NIP04.versionCode) + fun encryptNIP04(msg: String, sharedSecret: ByteArray): Nip04.EncryptedInfo { + return nip04.encrypt(msg, sharedSecret) } fun decryptNIP04(msg: String, privateKey: ByteArray, pubKey: ByteArray): String { - val sharedSecret = getSharedSecretNIP04(privateKey, pubKey) - return decryptNIP04(msg, sharedSecret) + return nip04.decrypt(msg, privateKey, pubKey) } - fun decryptNIP04(encryptedInfo: EncryptedInfo, privateKey: ByteArray, pubKey: ByteArray): String { - val sharedSecret = getSharedSecretNIP04(privateKey, pubKey) - return decryptNIP04(encryptedInfo.ciphertext, encryptedInfo.nonce, sharedSecret) + fun decryptNIP04(encryptedInfo: Nip04.EncryptedInfo, privateKey: ByteArray, pubKey: ByteArray): String { + return nip04.decrypt(encryptedInfo, privateKey, pubKey) } fun decryptNIP04(msg: String, sharedSecret: ByteArray): String { - val parts = msg.split("?iv=") - return decryptNIP04(parts[0], parts[1], sharedSecret) + return nip04.decrypt(msg, sharedSecret) } private fun decryptNIP04(cipher: String, nonce: String, sharedSecret: ByteArray): String { - val iv = Base64.getDecoder().decode(nonce) - val encryptedMsg = Base64.getDecoder().decode(cipher) - return decryptNIP04(encryptedMsg, iv, sharedSecret) + return nip04.decrypt(cipher, nonce, sharedSecret) } private fun decryptNIP04(encryptedMsg: ByteArray, iv: ByteArray, sharedSecret: ByteArray): String { - val cipher = Cipher.getInstance("AES/CBC/PKCS5Padding") - cipher.init(Cipher.DECRYPT_MODE, SecretKeySpec(sharedSecret, "AES"), IvParameterSpec(iv)) - return String(cipher.doFinal(encryptedMsg)) + return nip04.decrypt(encryptedMsg, iv, sharedSecret) } - fun encryptNIP44(msg: String, privateKey: ByteArray, pubKey: ByteArray): EncryptedInfo { - val sharedSecret = getSharedSecretNIP44(privateKey, pubKey) - return encryptNIP44(msg, sharedSecret) - } - - fun encryptNIP44(msg: String, sharedSecret: ByteArray): EncryptedInfo { - val nonce = ByteArray(24) - random.nextBytes(nonce) - - val cipher = cryptoStreamXChaCha20Xor( - libSodium = libSodium, - messageBytes = msg.toByteArray(),//compress(msg), - nonce = nonce, - key = Key.fromBytes(sharedSecret) - ) - - return EncryptedInfo( - ciphertext = cipher ?: ByteArray(0), - nonce = nonce, - v = Nip44Version.NIP44.versionCode - ) - } - - fun decryptNIP44(encryptedInfo: EncryptedInfo, privateKey: ByteArray, pubKey: ByteArray): String? { - val sharedSecret = getSharedSecretNIP44(privateKey, pubKey) - return decryptNIP44(encryptedInfo, sharedSecret) - } - - fun decryptNIP44(encryptedInfo: EncryptedInfo, sharedSecret: ByteArray): String? { - return cryptoStreamXChaCha20Xor( - libSodium = libSodium, - messageBytes = encryptedInfo.ciphertext, - nonce = encryptedInfo.nonce, - key = Key.fromBytes(sharedSecret) - )?.decodeToString() //?.let { decompress(it) } - } - - /** - * @return 32B shared secret - */ fun getSharedSecretNIP04(privateKey: ByteArray, pubKey: ByteArray): ByteArray { - val hash = combinedHashCode(privateKey, pubKey) - val preComputed = sharedKeyCache04[hash] - if (preComputed != null) return preComputed + return nip04.getSharedSecret(privateKey, pubKey) + } - val computed = computeSharedSecretNIP04(privateKey, pubKey) - sharedKeyCache04.put(hash, computed) - return computed + fun computeSharedSecretNIP04(privateKey: ByteArray, pubKey: ByteArray): ByteArray { + return nip04.computeSharedSecret(privateKey, pubKey) + } + + + /** + * NIP 44v1 Utils + */ + + fun encryptNIP44v1(msg: String, privateKey: ByteArray, pubKey: ByteArray): Nip44v1.EncryptedInfo { + return nip44v1.encrypt(msg, privateKey, pubKey) + } + + fun encryptNIP44v1(msg: String, sharedSecret: ByteArray): Nip44v1.EncryptedInfo { + return nip44v1.encrypt(msg, sharedSecret) + } + + fun decryptNIP44v1(encryptedInfo: Nip44v1.EncryptedInfo, privateKey: ByteArray, pubKey: ByteArray): String? { + return nip44v1.decrypt(encryptedInfo, privateKey, pubKey) + } + + fun decryptNIP44v1(encryptedInfo: String, privateKey: ByteArray, pubKey: ByteArray): String? { + return nip44v1.decrypt(encryptedInfo, privateKey, pubKey) + } + + fun decryptNIP44v1(encryptedInfo: Nip44v1.EncryptedInfo, sharedSecret: ByteArray): String? { + return nip44v1.decrypt(encryptedInfo, sharedSecret) + } + + fun getSharedSecretNIP44v1(privateKey: ByteArray, pubKey: ByteArray): ByteArray { + return nip44v1.getSharedSecret(privateKey, pubKey) + } + + fun computeSharedSecretNIP44v1(privateKey: ByteArray, pubKey: ByteArray): ByteArray { + return nip44v1.computeSharedSecret(privateKey, pubKey) } /** - * @return 32B shared secret + * NIP 44v2 Utils */ - fun computeSharedSecretNIP04(privateKey: ByteArray, pubKey: ByteArray): ByteArray = - secp256k1.pubKeyTweakMul(h02 + pubKey, privateKey).copyOfRange(1, 33) - /** - * @return 32B shared secret - */ - fun getSharedSecretNIP44(privateKey: ByteArray, pubKey: ByteArray): ByteArray { - val hash = combinedHashCode(privateKey, pubKey) - val preComputed = sharedKeyCache44[hash] - if (preComputed != null) return preComputed - - val computed = computeSharedSecretNIP44(privateKey, pubKey) - sharedKeyCache44.put(hash, computed) - return computed + fun encryptNIP44v2(msg: String, privateKey: ByteArray, pubKey: ByteArray): Nip44v2.EncryptedInfo { + return nip44v2.encrypt(msg, privateKey, pubKey) } - /** - * @return 32B shared secret - */ - fun computeSharedSecretNIP44(privateKey: ByteArray, pubKey: ByteArray): ByteArray = - sha256(secp256k1.pubKeyTweakMul(h02 + pubKey, privateKey).copyOfRange(1, 33)) -} - -data class EncryptedInfo(val ciphertext: ByteArray, val nonce: ByteArray, val v: Int) -data class EncryptedInfoString(val ciphertext: String, val nonce: String, val v: Int) - -enum class Nip44Version(val versionCode: Int) { - NIP04(0), - NIP44(1) -} - - -fun encodeNIP44(info: EncryptedInfo): String { - return encodeByteArray(info) -} - -fun decodeNIP44(str: String): EncryptedInfo? { - if (str.isEmpty()) return null - return if (str[0] == '{') { - decodeJackson(str) - } else { - decodeByteArray(str) + fun encryptNIP44v2(msg: String, sharedSecret: ByteArray): Nip44v2.EncryptedInfo { + return nip44v2.encrypt(msg, sharedSecret) } -} -fun encodeByteArray(info: EncryptedInfo): String { - return Base64.getEncoder().encodeToString(byteArrayOf(info.v.toByte()) + info.nonce + info.ciphertext) -} - -fun decodeByteArray(base64: String): EncryptedInfo? { - return try { - val byteArray = Base64.getDecoder().decode(base64) - return EncryptedInfo( - v = byteArray[0].toInt(), - nonce = byteArray.copyOfRange(1, 25), - ciphertext = byteArray.copyOfRange(25, byteArray.size) - ) - } catch (e: Exception) { - Log.w("CryptoUtils", "Unable to Parse encrypted payload: ${base64}") - null + fun decryptNIP44v2(encryptedInfo: Nip44v2.EncryptedInfo, privateKey: ByteArray, pubKey: ByteArray): String? { + return nip44v2.decrypt(encryptedInfo, privateKey, pubKey) } -} -fun encodeJackson(info: EncryptedInfo): String { - return Event.mapper.writeValueAsString( - EncryptedInfoString( - v = info.v, - nonce = Base64.getEncoder().encodeToString(info.nonce), - ciphertext = Base64.getEncoder().encodeToString(info.ciphertext) - ) - ) -} + fun decryptNIP44v2(encryptedInfo: String, privateKey: ByteArray, pubKey: ByteArray): String? { + return nip44v2.decrypt(encryptedInfo, privateKey, pubKey) + } -fun decodeJackson(json: String): EncryptedInfo { - val info = Event.mapper.readValue(json, EncryptedInfoString::class.java) - return EncryptedInfo( - v = info.v, - nonce = Base64.getDecoder().decode(info.nonce), - ciphertext = Base64.getDecoder().decode(info.ciphertext) - ) -} + fun decryptNIP44v2(encryptedInfo: Nip44v2.EncryptedInfo, sharedSecret: ByteArray): String? { + return nip44v2.decrypt(encryptedInfo, sharedSecret) + } -fun combinedHashCode(a: ByteArray, b: ByteArray): Int { - var result = 1 - for (element in a) result = 31 * result + element - for (element in b) result = 31 * result + element - return result -} + fun getSharedSecretNIP44v2(privateKey: ByteArray, pubKey: ByteArray): ByteArray { + return nip44v2.getConversationKey(privateKey, pubKey) + } -/* -OLD Versions used for the Benchmark + fun computeSharedSecretNIP44v2(privateKey: ByteArray, pubKey: ByteArray): ByteArray { + return nip44v2.computeConversationKey(privateKey, pubKey) + } -fun encodeKotlin(info: EncryptedInfo): String { - return Json.encodeToString( - EncryptedInfoString( - v = info.v, - nonce = Base64.getEncoder().encodeToString(info.nonce), - ciphertext = Base64.getEncoder().encodeToString(info.ciphertext) - ) - ) -} + fun decryptNIP44(payload: String, privateKey: ByteArray, pubKey: ByteArray): String? { + if (payload.isEmpty()) return null + return if (payload[0] == '{') { + decryptNIP44FromJackson(payload, privateKey, pubKey) + } else { + decryptNIP44FromBase64(payload, privateKey, pubKey) + } + } -fun decodeKotlin(json: String): EncryptedInfo { - val info = Json.decodeFromString(json) - return EncryptedInfo( - v = info.v, - nonce = Base64.getDecoder().decode(info.nonce), - ciphertext = Base64.getDecoder().decode(info.ciphertext) - ) -} + data class EncryptedInfoString(val ciphertext: String, val nonce: String, val v: Int, val mac: String?) -fun encodeCSV(info: EncryptedInfo): String { - return "${info.v},${Base64.getEncoder().encodeToString(info.nonce)},${Base64.getEncoder().encodeToString(info.ciphertext)}" -} + fun decryptNIP44FromJackson(json: String, privateKey: ByteArray, pubKey: ByteArray): String? { + return try { + val info = Event.mapper.readValue(json, EncryptedInfoString::class.java) -fun decodeCSV(base64: String): EncryptedInfo { - val parts = base64.split(",") - return EncryptedInfo( - v = parts[0].toInt(), - nonce = Base64.getDecoder().decode(parts[1]), - ciphertext = Base64.getDecoder().decode(parts[2]) - ) -} + when (info.v) { + Nip04.EncryptedInfo.v -> { + val encryptedInfo = Nip04.EncryptedInfo( + ciphertext = Base64.getDecoder().decode(info.ciphertext), + nonce = Base64.getDecoder().decode(info.nonce) + ) + decryptNIP04(encryptedInfo, privateKey, pubKey) + } + Nip44v1.EncryptedInfo.v -> { + val encryptedInfo = Nip44v1.EncryptedInfo( + ciphertext = Base64.getDecoder().decode(info.ciphertext), + nonce = Base64.getDecoder().decode(info.nonce) + ) + decryptNIP44v1(encryptedInfo, privateKey, pubKey) + } + Nip44v2.EncryptedInfo.v -> { + val encryptedInfo = Nip44v2.EncryptedInfo( + ciphertext = Base64.getDecoder().decode(info.ciphertext), + nonce = Base64.getDecoder().decode(info.nonce), + mac = Base64.getDecoder().decode(info.mac) + ) + decryptNIP44v2(encryptedInfo, privateKey, pubKey) + } + else -> null + } + } catch (e: Exception) { + Log.e("CryptoUtils", "Could not identify the version for NIP44 payload ${json}") + e.printStackTrace() + null + } + } + fun decryptNIP44FromBase64(payload: String, privateKey: ByteArray, pubKey: ByteArray): String? { + if (payload.isEmpty()) return null -fun compress(input: String): ByteArray { - return DeflaterInputStream(input.toByteArray().inputStream()).readBytes() -} + return try { + val byteArray = Base64.getDecoder().decode(payload) -fun decompress(inputBytes: ByteArray): String { - return InflaterInputStream(inputBytes.inputStream()).bufferedReader().use { it.readText() } -} + when (byteArray[0].toInt()) { + Nip04.EncryptedInfo.v -> decryptNIP04(payload, privateKey, pubKey) + Nip44v1.EncryptedInfo.v -> decryptNIP44v1(payload, privateKey, pubKey) + Nip44v2.EncryptedInfo.v -> decryptNIP44v2(payload, privateKey, pubKey) + else -> null + } + } catch (e: Exception) { + Log.e("CryptoUtils", "Could not identify the version for NIP44 payload ${payload}") + e.printStackTrace() + null + } + } -*/ \ No newline at end of file +} \ No newline at end of file diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/crypto/Hkdf.kt b/quartz/src/main/java/com/vitorpamplona/quartz/crypto/Hkdf.kt new file mode 100644 index 000000000..f0bded67d --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/crypto/Hkdf.kt @@ -0,0 +1,37 @@ +package com.vitorpamplona.quartz.crypto + +import java.nio.ByteBuffer +import javax.crypto.Mac +import javax.crypto.spec.SecretKeySpec + +class Hkdf(val algorithm: String = "HmacSHA256", val hashLen: Int = 32) { + fun extract(key: ByteArray, salt: ByteArray): ByteArray { + val mac = Mac.getInstance(algorithm) + mac.init(SecretKeySpec(salt, algorithm)) + return mac.doFinal(key) + } + + fun expand(key: ByteArray, nonce: ByteArray, outputLength: Int): ByteArray { + check(key.size == hashLen) + check(nonce.size == hashLen) + + val n = if (outputLength % hashLen == 0) outputLength / hashLen else outputLength / hashLen + 1 + var hashRound = ByteArray(0) + val generatedBytes = ByteBuffer.allocate(Math.multiplyExact(n, hashLen)) + val mac = Mac.getInstance(algorithm) + mac.init(SecretKeySpec(key, algorithm)) + for (roundNum in 1..n) { + mac.reset() + val t = ByteBuffer.allocate(hashRound.size + nonce.size + 1) + t.put(hashRound) + t.put(nonce) + t.put(roundNum.toByte()) + hashRound = mac.doFinal(t.array()) + generatedBytes.put(hashRound) + } + val result = ByteArray(outputLength) + generatedBytes.rewind() + generatedBytes[result, 0, outputLength] + return result + } +} \ No newline at end of file diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/crypto/Nip04.kt b/quartz/src/main/java/com/vitorpamplona/quartz/crypto/Nip04.kt new file mode 100644 index 000000000..f6a85c812 --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/crypto/Nip04.kt @@ -0,0 +1,130 @@ +package com.vitorpamplona.quartz.crypto + +import android.util.Log +import android.util.LruCache +import com.vitorpamplona.quartz.encoders.Hex +import fr.acinq.secp256k1.Secp256k1 +import java.security.MessageDigest +import java.security.SecureRandom +import java.util.Base64 +import javax.crypto.Cipher +import javax.crypto.spec.IvParameterSpec +import javax.crypto.spec.SecretKeySpec + +class Nip04(val secp256k1: Secp256k1, val random: SecureRandom) { + private val sharedKeyCache = SharedKeyCache() + private val h02 = Hex.decode("02") + + fun clearCache() { + sharedKeyCache.clearCache() + } + + fun encrypt(msg: String, privateKey: ByteArray, pubKey: ByteArray): String { + return encrypt(msg, getSharedSecret(privateKey, pubKey)).encodeToNIP04() + } + + fun encrypt(msg: String, sharedSecret: ByteArray): EncryptedInfo { + val iv = ByteArray(16) + random.nextBytes(iv) + + val cipher = Cipher.getInstance("AES/CBC/PKCS5Padding") + cipher.init(Cipher.ENCRYPT_MODE, SecretKeySpec(sharedSecret, "AES"), IvParameterSpec(iv)) + //val ivBase64 = Base64.getEncoder().encodeToString(iv) + val encryptedMsg = cipher.doFinal(msg.toByteArray()) + //val encryptedMsgBase64 = Base64.getEncoder().encodeToString(encryptedMsg) + return EncryptedInfo(encryptedMsg, iv) + } + + fun decrypt(msg: String, privateKey: ByteArray, pubKey: ByteArray): String { + val sharedSecret = getSharedSecret(privateKey, pubKey) + return decrypt(msg, sharedSecret) + } + + fun decrypt(encryptedInfo: EncryptedInfo, privateKey: ByteArray, pubKey: ByteArray): String { + val sharedSecret = getSharedSecret(privateKey, pubKey) + return decrypt(encryptedInfo.ciphertext, encryptedInfo.nonce, sharedSecret) + } + + fun decrypt(msg: String, sharedSecret: ByteArray): String { + val decoded = EncryptedInfo.decodeFromNIP04(msg) + check(decoded != null) { + "Unable to decode msg $msg as NIP04" + } + return decrypt(decoded.ciphertext, decoded.nonce, sharedSecret) + } + + fun decrypt(cipher: String, nonce: String, sharedSecret: ByteArray): String { + val iv = Base64.getDecoder().decode(nonce) + val encryptedMsg = Base64.getDecoder().decode(cipher) + return decrypt(encryptedMsg, iv, sharedSecret) + } + + fun decrypt(encryptedMsg: ByteArray, iv: ByteArray, sharedSecret: ByteArray): String { + val cipher = Cipher.getInstance("AES/CBC/PKCS5Padding") + cipher.init(Cipher.DECRYPT_MODE, SecretKeySpec(sharedSecret, "AES"), IvParameterSpec(iv)) + return String(cipher.doFinal(encryptedMsg)) + } + + fun getSharedSecret(privateKey: ByteArray, pubKey: ByteArray): ByteArray { + val preComputed = sharedKeyCache.get(privateKey, pubKey) + if (preComputed != null) return preComputed + + val computed = computeSharedSecret(privateKey, pubKey) + sharedKeyCache.add(privateKey, pubKey, computed) + return computed + } + + /** + * @return 32B shared secret + */ + fun computeSharedSecret(privateKey: ByteArray, pubKey: ByteArray): ByteArray = + secp256k1.pubKeyTweakMul(h02 + pubKey, privateKey).copyOfRange(1, 33) + + class EncryptedInfo( + val ciphertext: ByteArray, + val nonce: ByteArray + ) { + companion object { + const val v: Int = 0 + + fun decodePayload(payload: String): EncryptedInfo? { + return try { + val byteArray = Base64.getDecoder().decode(payload) + check(byteArray[0].toInt() == Nip44v1.EncryptedInfo.v) + return EncryptedInfo( + nonce = byteArray.copyOfRange(1, 25), + ciphertext = byteArray.copyOfRange(25, byteArray.size) + ) + } catch (e: Exception) { + Log.w("NIP04", "Unable to Parse encrypted payload: ${payload}") + null + } + } + + fun decodeFromNIP04(payload: String): EncryptedInfo? { + return try { + val parts = payload.split("?iv=") + EncryptedInfo( + ciphertext = Base64.getDecoder().decode(parts[0]), + nonce = Base64.getDecoder().decode(parts[1]) + ) + } catch (e: Exception) { + Log.w("NIP04", "Unable to Parse encrypted payload: ${payload}") + null + } + } + } + + fun encodePayload(): String { + return Base64.getEncoder().encodeToString( + byteArrayOf(v.toByte()) + nonce + ciphertext + ) + } + + fun encodeToNIP04(): String { + val nonce = Base64.getEncoder().encodeToString(nonce) + val ciphertext = Base64.getEncoder().encodeToString(ciphertext) + return "${ciphertext}?iv=${nonce}" + } + } +} \ No newline at end of file diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/crypto/Nip44v1.kt b/quartz/src/main/java/com/vitorpamplona/quartz/crypto/Nip44v1.kt new file mode 100644 index 000000000..4be632923 --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/crypto/Nip44v1.kt @@ -0,0 +1,117 @@ +package com.vitorpamplona.quartz.crypto + +import android.util.Log +import com.goterl.lazysodium.SodiumAndroid +import com.goterl.lazysodium.utils.Key +import com.vitorpamplona.quartz.encoders.Hex +import fr.acinq.secp256k1.Secp256k1 +import java.security.MessageDigest +import java.security.SecureRandom +import java.util.Base64 + +class Nip44v1(val secp256k1: Secp256k1, val random: SecureRandom) { + private val sharedKeyCache = SharedKeyCache() + private val h02 = Hex.decode("02") + private val libSodium = SodiumAndroid() + + fun clearCache() { + sharedKeyCache.clearCache() + } + + fun encrypt(msg: String, privateKey: ByteArray, pubKey: ByteArray): EncryptedInfo { + val sharedSecret = getSharedSecret(privateKey, pubKey) + return encrypt(msg, sharedSecret) + } + + fun encrypt(msg: String, sharedSecret: ByteArray): EncryptedInfo { + val nonce = ByteArray(24) + random.nextBytes(nonce) + + val cipher = cryptoStreamXChaCha20Xor( + libSodium = libSodium, + messageBytes = msg.toByteArray(), + nonce = nonce, + key = Key.fromBytes(sharedSecret) + ) + + return EncryptedInfo( + ciphertext = cipher ?: ByteArray(0), + nonce = nonce, + ) + } + + fun decrypt(payload: String, privateKey: ByteArray, pubKey: ByteArray): String? { + val sharedSecret = getSharedSecret(privateKey, pubKey) + return decrypt(payload, sharedSecret) + } + + fun decrypt(encryptedInfo: EncryptedInfo, privateKey: ByteArray, pubKey: ByteArray): String? { + val sharedSecret = getSharedSecret(privateKey, pubKey) + return decrypt(encryptedInfo, sharedSecret) + } + + fun decrypt(payload: String, sharedSecret: ByteArray): String? { + val encryptedInfo = EncryptedInfo.decodePayload(payload) ?: return null + return decrypt(encryptedInfo, sharedSecret) + } + + fun decrypt(encryptedInfo: EncryptedInfo, sharedSecret: ByteArray): String? { + return cryptoStreamXChaCha20Xor( + libSodium = libSodium, + messageBytes = encryptedInfo.ciphertext, + nonce = encryptedInfo.nonce, + key = Key.fromBytes(sharedSecret) + )?.decodeToString() + } + + fun getSharedSecret(privateKey: ByteArray, pubKey: ByteArray): ByteArray { + val preComputed = sharedKeyCache.get(privateKey, pubKey) + if (preComputed != null) return preComputed + + val computed = computeSharedSecret(privateKey, pubKey) + sharedKeyCache.add(privateKey, pubKey, computed) + return computed + } + + /** + * @return 32B shared secret + */ + fun computeSharedSecret(privateKey: ByteArray, pubKey: ByteArray): ByteArray = + sha256( + secp256k1.pubKeyTweakMul(h02 + pubKey, privateKey).copyOfRange(1, 33) + ) + + fun sha256(data: ByteArray): ByteArray { + // Creates a new buffer every time + return MessageDigest.getInstance("SHA-256").digest(data) + } + + class EncryptedInfo( + val ciphertext: ByteArray, + val nonce: ByteArray + ) { + companion object { + const val v: Int = 1 + + fun decodePayload(payload: String): EncryptedInfo? { + return try { + val byteArray = Base64.getDecoder().decode(payload) + check(byteArray[0].toInt() == v) + return EncryptedInfo( + nonce = byteArray.copyOfRange(1, 25), + ciphertext = byteArray.copyOfRange(25, byteArray.size) + ) + } catch (e: Exception) { + Log.w("NIP44v1", "Unable to Parse encrypted payload: ${payload}") + null + } + } + } + + fun encodePayload(): String { + return Base64.getEncoder().encodeToString( + byteArrayOf(v.toByte()) + nonce + ciphertext + ) + } + } +} \ No newline at end of file diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/crypto/Nip44v2.kt b/quartz/src/main/java/com/vitorpamplona/quartz/crypto/Nip44v2.kt new file mode 100644 index 000000000..3ba7cb6e2 --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/crypto/Nip44v2.kt @@ -0,0 +1,223 @@ +package com.vitorpamplona.quartz.crypto + +import android.util.Log +import com.goterl.lazysodium.LazySodiumAndroid +import com.goterl.lazysodium.SodiumAndroid +import com.vitorpamplona.quartz.encoders.Hex +import com.vitorpamplona.quartz.encoders.toHexKey +import fr.acinq.secp256k1.Secp256k1 +import java.nio.ByteBuffer +import java.nio.ByteOrder +import java.security.SecureRandom +import java.util.Base64 +import kotlin.experimental.and +import kotlin.math.floor +import kotlin.math.log2 + + +class Nip44v2(val secp256k1: Secp256k1, val random: SecureRandom) { + private val sharedKeyCache = SharedKeyCache() + + private val libSodium = SodiumAndroid() + private val lazySodium = LazySodiumAndroid(libSodium) + private val hkdf = Hkdf() + + private val h02 = Hex.decode("02") + private val hashLength = 32 + + private val minPlaintextSize: Int = 0x0001 // 1b msg => padded to 32b + private val maxPlaintextSize: Int = 0xffff // 65535 (64kb-1) => padded to 64kb + + fun clearCache() { + sharedKeyCache.clearCache() + } + + fun encrypt(msg: String, privateKey: ByteArray, pubKey: ByteArray): EncryptedInfo { + return encrypt(msg, getConversationKey(privateKey, pubKey)) + } + + fun encrypt(plaintext: String, conversationKey: ByteArray): EncryptedInfo { + val nonce = ByteArray(hashLength) + random.nextBytes(nonce) + return encryptWithNonce(plaintext, conversationKey, nonce) + } + + fun encryptWithNonce(plaintext: String, conversationKey: ByteArray, nonce: ByteArray): EncryptedInfo { + val messageKeys = getMessageKeys(conversationKey, nonce) + val padded = pad(plaintext) + + val ciphertext = ByteArray(padded.size) + + lazySodium.cryptoStreamChaCha20IetfXor( + ciphertext, padded, padded.size.toLong(), messageKeys.chachaNonce, messageKeys.chachaKey + ) + + val mac = hmacAad(messageKeys.hmacKey, ciphertext, nonce) + + return EncryptedInfo( + nonce = nonce, + ciphertext = ciphertext, + mac = mac + ) + } + + + fun decrypt(payload: String, privateKey: ByteArray, pubKey: ByteArray): String? { + return decrypt(payload, getConversationKey(privateKey, pubKey)) + } + + fun decrypt(decoded: EncryptedInfo, privateKey: ByteArray, pubKey: ByteArray): String? { + return decrypt(decoded, getConversationKey(privateKey, pubKey)) + } + + fun decrypt(payload: String, conversationKey: ByteArray): String? { + val decoded = EncryptedInfo.decodePayload(payload) ?: return null + return decrypt(decoded, conversationKey) + } + + fun decrypt(decoded: EncryptedInfo, conversationKey: ByteArray): String? { + val messageKey = getMessageKeys(conversationKey, decoded.nonce) + val calculatedMac = hmacAad(messageKey.hmacKey, decoded.ciphertext, decoded.nonce) + + check(calculatedMac.contentEquals(decoded.mac)) { + "Invalid Mac: Calculated ${calculatedMac.toHexKey()}, decoded: ${decoded.mac.toHexKey()}" + } + + val mLen = decoded.ciphertext.size.toLong() + val padded = ByteArray(decoded.ciphertext.size) + + lazySodium.cryptoStreamChaCha20IetfXor( + padded, decoded.ciphertext, mLen, messageKey.chachaNonce, messageKey.chachaKey + ) + + return unpad(padded) + } + + fun getConversationKey(privateKey: ByteArray, pubKey: ByteArray): ByteArray { + val preComputed = sharedKeyCache.get(privateKey, pubKey) + if (preComputed != null) return preComputed + + val computed = computeConversationKey(privateKey, pubKey) + sharedKeyCache.add(privateKey, pubKey, computed) + return computed + } + + fun calcPaddedLen(len: Int): Int { + check(len > 0) { + "expected positive integer" + } + if (len <= 32) return 32 + val nextPower = 1 shl (floor(log2(len - 1f)) + 1).toInt() + val chunk = if (nextPower <= 256) 32 else nextPower / 8 + return chunk * (floor((len - 1f) / chunk).toInt() + 1) + } + + fun pad(plaintext: String): ByteArray { + val unpadded = plaintext.toByteArray(Charsets.UTF_8) + val unpaddedLen = unpadded.size + + check(unpaddedLen > 0) { + "Message is empty ($unpaddedLen): $plaintext" + } + + check(unpaddedLen <= maxPlaintextSize) { + "Message is too long ($unpaddedLen): $plaintext" + } + + val prefix = ByteBuffer.allocate(2).order(ByteOrder.BIG_ENDIAN).putShort(unpaddedLen.toShort()).array() + val suffix = ByteArray(calcPaddedLen(unpaddedLen) - unpaddedLen) + return ByteBuffer.wrap(prefix + unpadded + suffix).array() + } + + private fun bytesToInt(byte1: Byte, byte2: Byte, bigEndian: Boolean): Int { + return if (bigEndian) + (byte1.toInt() and 0xFF shl 8 or (byte2.toInt() and 0xFF)) + else + (byte2.toInt() and 0xFF shl 8 or (byte1.toInt() and 0xFF)) + } + + fun unpad(padded: ByteArray): String { + val unpaddedLen: Int = bytesToInt(padded[0], padded[1], true) + val unpadded = padded.sliceArray(2 until 2 + unpaddedLen) + + check( + unpaddedLen in minPlaintextSize..maxPlaintextSize + && unpadded.size == unpaddedLen + && padded.size == 2 + calcPaddedLen(unpaddedLen)) { + "invalid padding ${unpadded.size} != $unpaddedLen" + } + + return unpadded.decodeToString() + } + + fun hmacAad(key: ByteArray, message: ByteArray, aad: ByteArray): ByteArray { + check (aad.size == hashLength) { + "AAD associated data must be 32 bytes, but it was ${aad.size} bytes" + } + + return hkdf.extract(aad + message, key) + } + + + fun getMessageKeys(conversationKey: ByteArray, nonce: ByteArray): MessageKey { + val keys = hkdf.expand(conversationKey, nonce, 76) + return MessageKey( + chachaKey = keys.copyOfRange(0, 32), + chachaNonce = keys.copyOfRange(32, 44), + hmacKey = keys.copyOfRange(44, 76), + ) + } + + class MessageKey( + val chachaKey: ByteArray, + val chachaNonce: ByteArray, + val hmacKey: ByteArray + ) + + /** + * @return 32B shared secret + */ + fun computeConversationKey(privateKey: ByteArray, pubKey: ByteArray): ByteArray { + val sharedX = secp256k1.pubKeyTweakMul(h02 + pubKey, privateKey).copyOfRange(1, 33) + return hkdf.extract(sharedX, "nip44-v2".toByteArray(Charsets.UTF_8)) + } + + class EncryptedInfo( + val nonce: ByteArray, + val ciphertext: ByteArray, + val mac: ByteArray + ) { + companion object { + const val v: Int = 2 + + fun decodePayload(payload: String): EncryptedInfo? { + check(payload.length >= 132 || payload.length <= 87472) { + "Invalid payload length ${payload.length} for ${payload}" + } + check(payload[0] != '#') { + "Unknown encryption version ${payload.get(0)}" + } + + return try { + val byteArray = Base64.getDecoder().decode(payload) + check(byteArray[0].toInt() == v) + return EncryptedInfo( + nonce = byteArray.copyOfRange(1, 33), + ciphertext = byteArray.copyOfRange(33, byteArray.size-32), + mac = byteArray.copyOfRange(byteArray.size-32, byteArray.size) + ) + } catch (e: Exception) { + Log.w("NIP44v2", "Unable to Parse encrypted payload: $payload") + null + } + } + } + + + fun encodePayload(): String { + return Base64.getEncoder().encodeToString( + byteArrayOf(v.toByte()) + nonce + ciphertext + mac + ) + } + } +} \ No newline at end of file diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/crypto/SharedKeyCache.kt b/quartz/src/main/java/com/vitorpamplona/quartz/crypto/SharedKeyCache.kt new file mode 100644 index 000000000..3014f8234 --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/crypto/SharedKeyCache.kt @@ -0,0 +1,26 @@ +package com.vitorpamplona.quartz.crypto + +import android.util.LruCache + +class SharedKeyCache { + private val sharedKeyCache = LruCache(200) + + fun clearCache() { + sharedKeyCache.evictAll() + } + + fun combinedHashCode(a: ByteArray, b: ByteArray): Int { + var result = 1 + for (element in a) result = 31 * result + element + for (element in b) result = 31 * result + element + return result + } + + fun get(privateKey: ByteArray, pubKey: ByteArray): ByteArray? { + return sharedKeyCache[combinedHashCode(privateKey, pubKey)] + } + + fun add(privateKey: ByteArray, pubKey: ByteArray, secret: ByteArray) { + sharedKeyCache.put(combinedHashCode(privateKey, pubKey), secret) + } +} \ No newline at end of file diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/events/ChatMessageEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/events/ChatMessageEvent.kt index a5b418453..7ee3e29c5 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/events/ChatMessageEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/events/ChatMessageEvent.kt @@ -90,7 +90,7 @@ class ChatMessageEvent( subject?.let { tags.add(arrayOf("subject", it)) } - tags.add(arrayOf("alt", alt)) + //tags.add(arrayOf("alt", alt)) signer.sign(createdAt, kind, tags.toTypedArray(), msg, onReady) } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/events/GiftWrapEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/events/GiftWrapEvent.kt index 9c36291da..daed2b25d 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/events/GiftWrapEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/events/GiftWrapEvent.kt @@ -1,15 +1,8 @@ package com.vitorpamplona.quartz.events -import android.util.Log import androidx.compose.runtime.Immutable import com.vitorpamplona.quartz.utils.TimeUtils -import com.vitorpamplona.quartz.encoders.hexToByteArray -import com.vitorpamplona.quartz.encoders.toHexKey -import com.vitorpamplona.quartz.crypto.CryptoUtils import com.vitorpamplona.quartz.crypto.KeyPair -import com.vitorpamplona.quartz.crypto.Nip44Version -import com.vitorpamplona.quartz.crypto.decodeNIP44 -import com.vitorpamplona.quartz.crypto.encodeNIP44 import com.vitorpamplona.quartz.encoders.HexKey import com.vitorpamplona.quartz.signers.NostrSigner import com.vitorpamplona.quartz.signers.NostrSignerInternal @@ -31,7 +24,6 @@ class GiftWrapEvent( onReady(it) return } - unwrap(signer) { gift -> if (gift is WrappedEvent) { gift.host = this @@ -72,7 +64,7 @@ class GiftWrapEvent( ) { val signer = NostrSignerInternal(KeyPair()) // GiftWrap is always a random key val serializedContent = toJson(event) - val tags = arrayOf(arrayOf("p", recipientPubKey), arrayOf("alt", alt)) + val tags = arrayOf(arrayOf("p", recipientPubKey)) signer.nip44Encrypt(serializedContent, recipientPubKey) { signer.sign(createdAt, kind, tags, it, onReady) diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/events/SealedGossipEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/events/SealedGossipEvent.kt index a4da19974..bffc87fe0 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/events/SealedGossipEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/events/SealedGossipEvent.kt @@ -4,15 +4,9 @@ import android.util.Log import androidx.compose.runtime.Immutable import com.fasterxml.jackson.annotation.JsonProperty import com.vitorpamplona.quartz.utils.TimeUtils -import com.vitorpamplona.quartz.encoders.hexToByteArray import com.vitorpamplona.quartz.encoders.toHexKey -import com.vitorpamplona.quartz.crypto.CryptoUtils -import com.vitorpamplona.quartz.crypto.Nip44Version -import com.vitorpamplona.quartz.crypto.decodeNIP44 -import com.vitorpamplona.quartz.crypto.encodeNIP44 import com.vitorpamplona.quartz.encoders.HexKey import com.vitorpamplona.quartz.signers.NostrSigner -import java.util.UUID @Immutable class SealedGossipEvent( diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/signers/NostrSignerInternal.kt b/quartz/src/main/java/com/vitorpamplona/quartz/signers/NostrSignerInternal.kt index bb9ce4bed..c9799cdaa 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/signers/NostrSignerInternal.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/signers/NostrSignerInternal.kt @@ -3,19 +3,13 @@ package com.vitorpamplona.quartz.signers import android.util.Log import com.vitorpamplona.quartz.crypto.CryptoUtils import com.vitorpamplona.quartz.crypto.KeyPair -import com.vitorpamplona.quartz.crypto.Nip44Version -import com.vitorpamplona.quartz.crypto.decodeNIP44 -import com.vitorpamplona.quartz.crypto.encodeNIP44 import com.vitorpamplona.quartz.encoders.HexKey import com.vitorpamplona.quartz.encoders.hexToByteArray import com.vitorpamplona.quartz.encoders.toHexKey -import com.vitorpamplona.quartz.events.AdvertisedRelayListEvent import com.vitorpamplona.quartz.events.Event import com.vitorpamplona.quartz.events.EventFactory import com.vitorpamplona.quartz.events.LnZapPrivateEvent import com.vitorpamplona.quartz.events.LnZapRequestEvent -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.withContext class NostrSignerInternal(val keyPair: KeyPair): NostrSigner(keyPair.pubKey.toHexKey()) { override fun sign( @@ -94,28 +88,23 @@ class NostrSignerInternal(val keyPair: KeyPair): NostrSigner(keyPair.pubKey.toHe override fun nip44Encrypt(decryptedContent: String, toPublicKey: HexKey, onReady: (String)-> Unit) { if (keyPair.privKey == null) return - val sharedSecret = CryptoUtils.getSharedSecretNIP44(keyPair.privKey, toPublicKey.hexToByteArray()) - onReady( - encodeNIP44( - CryptoUtils.encryptNIP44( - decryptedContent, - sharedSecret - ) - ) + CryptoUtils.encryptNIP44v2( + decryptedContent, + keyPair.privKey, + toPublicKey.hexToByteArray() + ).encodePayload() ) } override fun nip44Decrypt(encryptedContent: String, fromPublicKey: HexKey, onReady: (String)-> Unit) { if (keyPair.privKey == null) return - val toDecrypt = decodeNIP44(encryptedContent) ?: return - - when (toDecrypt.v) { - Nip44Version.NIP04.versionCode -> CryptoUtils.decryptNIP04(toDecrypt, keyPair.privKey, fromPublicKey.hexToByteArray()) - Nip44Version.NIP44.versionCode -> CryptoUtils.decryptNIP44(toDecrypt, keyPair.privKey, fromPublicKey.hexToByteArray()) - else -> null - }?.let { + CryptoUtils.decryptNIP44( + payload = encryptedContent, + privateKey = keyPair.privKey, + pubKey = fromPublicKey.hexToByteArray() + )?.let { onReady(it) } }