- Migrates Quartz to a KMP project

- Converts OpenTimestamps from java to Kotlin
- Moves OTS OkHttp setup to Quartz
This commit is contained in:
Vitor Pamplona
2025-08-27 17:32:39 -04:00
parent d2214889b6
commit 442f75fc41
979 changed files with 6285 additions and 5706 deletions
@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.INTERNET"/>
</manifest>
@@ -0,0 +1,124 @@
{
"english": [
[
"00000000000000000000000000000000",
"abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about",
"c55257c360c07c72029aebc1b53c05ed0362ada38ead3e3e9efa3708e53495531f09a6987599d18264c1e1c92f2cf141630c7a3c4ab7c81b2f001698e7463b04"
],
[
"7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f",
"legal winner thank year wave sausage worth useful legal winner thank yellow",
"2e8905819b8723fe2c1d161860e5ee1830318dbf49a83bd451cfb8440c28bd6fa457fe1296106559a3c80937a1c1069be3a3a5bd381ee6260e8d9739fce1f607"
],
[
"80808080808080808080808080808080",
"letter advice cage absurd amount doctor acoustic avoid letter advice cage above",
"d71de856f81a8acc65e6fc851a38d4d7ec216fd0796d0a6827a3ad6ed5511a30fa280f12eb2e47ed2ac03b5c462a0358d18d69fe4f985ec81778c1b370b652a8"
],
[
"ffffffffffffffffffffffffffffffff",
"zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo wrong",
"ac27495480225222079d7be181583751e86f571027b0497b5b5d11218e0a8a13332572917f0f8e5a589620c6f15b11c61dee327651a14c34e18231052e48c069"
],
[
"000000000000000000000000000000000000000000000000",
"abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon agent",
"035895f2f481b1b0f01fcf8c289c794660b289981a78f8106447707fdd9666ca06da5a9a565181599b79f53b844d8a71dd9f439c52a3d7b3e8a79c906ac845fa"
],
[
"7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f",
"legal winner thank year wave sausage worth useful legal winner thank year wave sausage worth useful legal will",
"f2b94508732bcbacbcc020faefecfc89feafa6649a5491b8c952cede496c214a0c7b3c392d168748f2d4a612bada0753b52a1c7ac53c1e93abd5c6320b9e95dd"
],
[
"808080808080808080808080808080808080808080808080",
"letter advice cage absurd amount doctor acoustic avoid letter advice cage absurd amount doctor acoustic avoid letter always",
"107d7c02a5aa6f38c58083ff74f04c607c2d2c0ecc55501dadd72d025b751bc27fe913ffb796f841c49b1d33b610cf0e91d3aa239027f5e99fe4ce9e5088cd65"
],
[
"ffffffffffffffffffffffffffffffffffffffffffffffff",
"zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo when",
"0cd6e5d827bb62eb8fc1e262254223817fd068a74b5b449cc2f667c3f1f985a76379b43348d952e2265b4cd129090758b3e3c2c49103b5051aac2eaeb890a528"
],
[
"0000000000000000000000000000000000000000000000000000000000000000",
"abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon art",
"bda85446c68413707090a52022edd26a1c9462295029f2e60cd7c4f2bbd3097170af7a4d73245cafa9c3cca8d561a7c3de6f5d4a10be8ed2a5e608d68f92fcc8"
],
[
"7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f",
"legal winner thank year wave sausage worth useful legal winner thank year wave sausage worth useful legal winner thank year wave sausage worth title",
"bc09fca1804f7e69da93c2f2028eb238c227f2e9dda30cd63699232578480a4021b146ad717fbb7e451ce9eb835f43620bf5c514db0f8add49f5d121449d3e87"
],
[
"8080808080808080808080808080808080808080808080808080808080808080",
"letter advice cage absurd amount doctor acoustic avoid letter advice cage absurd amount doctor acoustic avoid letter advice cage absurd amount doctor acoustic bless",
"c0c519bd0e91a2ed54357d9d1ebef6f5af218a153624cf4f2da911a0ed8f7a09e2ef61af0aca007096df430022f7a2b6fb91661a9589097069720d015e4e982f"
],
[
"ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
"zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo vote",
"dd48c104698c30cfe2b6142103248622fb7bb0ff692eebb00089b32d22484e1613912f0a5b694407be899ffd31ed3992c456cdf60f5d4564b8ba3f05a69890ad"
],
[
"77c2b00716cec7213839159e404db50d",
"jelly better achieve collect unaware mountain thought cargo oxygen act hood bridge",
"b5b6d0127db1a9d2226af0c3346031d77af31e918dba64287a1b44b8ebf63cdd52676f672a290aae502472cf2d602c051f3e6f18055e84e4c43897fc4e51a6ff"
],
[
"b63a9c59a6e641f288ebc103017f1da9f8290b3da6bdef7b",
"renew stay biology evidence goat welcome casual join adapt armor shuffle fault little machine walk stumble urge swap",
"9248d83e06f4cd98debf5b6f010542760df925ce46cf38a1bdb4e4de7d21f5c39366941c69e1bdbf2966e0f6e6dbece898a0e2f0a4c2b3e640953dfe8b7bbdc5"
],
[
"3e141609b97933b66a060dcddc71fad1d91677db872031e85f4c015c5e7e8982",
"dignity pass list indicate nasty swamp pool script soccer toe leaf photo multiply desk host tomato cradle drill spread actor shine dismiss champion exotic",
"ff7f3184df8696d8bef94b6c03114dbee0ef89ff938712301d27ed8336ca89ef9635da20af07d4175f2bf5f3de130f39c9d9e8dd0472489c19b1a020a940da67"
],
[
"0460ef47585604c5660618db2e6a7e7f",
"afford alter spike radar gate glance object seek swamp infant panel yellow",
"65f93a9f36b6c85cbe634ffc1f99f2b82cbb10b31edc7f087b4f6cb9e976e9faf76ff41f8f27c99afdf38f7a303ba1136ee48a4c1e7fcd3dba7aa876113a36e4"
],
[
"72f60ebac5dd8add8d2a25a797102c3ce21bc029c200076f",
"indicate race push merry suffer human cruise dwarf pole review arch keep canvas theme poem divorce alter left",
"3bbf9daa0dfad8229786ace5ddb4e00fa98a044ae4c4975ffd5e094dba9e0bb289349dbe2091761f30f382d4e35c4a670ee8ab50758d2c55881be69e327117ba"
],
[
"2c85efc7f24ee4573d2b81a6ec66cee209b2dcbd09d8eddc51e0215b0b68e416",
"clutch control vehicle tonight unusual clog visa ice plunge glimpse recipe series open hour vintage deposit universe tip job dress radar refuse motion taste",
"fe908f96f46668b2d5b37d82f558c77ed0d69dd0e7e043a5b0511c48c2f1064694a956f86360c93dd04052a8899497ce9e985ebe0c8c52b955e6ae86d4ff4449"
],
[
"eaebabb2383351fd31d703840b32e9e2",
"turtle front uncle idea crush write shrug there lottery flower risk shell",
"bdfb76a0759f301b0b899a1e3985227e53b3f51e67e3f2a65363caedf3e32fde42a66c404f18d7b05818c95ef3ca1e5146646856c461c073169467511680876c"
],
[
"7ac45cfe7722ee6c7ba84fbc2d5bd61b45cb2fe5eb65aa78",
"kiss carry display unusual confirm curtain upgrade antique rotate hello void custom frequent obey nut hole price segment",
"ed56ff6c833c07982eb7119a8f48fd363c4a9b1601cd2de736b01045c5eb8ab4f57b079403485d1c4924f0790dc10a971763337cb9f9c62226f64fff26397c79"
],
[
"4fa1a8bc3e6d80ee1316050e862c1812031493212b7ec3f3bb1b08f168cabeef",
"exile ask congress lamp submit jacket era scheme attend cousin alcohol catch course end lucky hurt sentence oven short ball bird grab wing top",
"095ee6f817b4c2cb30a5a797360a81a40ab0f9a4e25ecd672a3f58a0b5ba0687c096a6b14d2c0deb3bdefce4f61d01ae07417d502429352e27695163f7447a8c"
],
[
"18ab19a9f54a9274f03e5209a2ac8a91",
"board flee heavy tunnel powder denial science ski answer betray cargo cat",
"6eff1bb21562918509c73cb990260db07c0ce34ff0e3cc4a8cb3276129fbcb300bddfe005831350efd633909f476c45c88253276d9fd0df6ef48609e8bb7dca8"
],
[
"18a2e1d81b8ecfb2a333adcb0c17a5b9eb76cc5d05db91a4",
"board blade invite damage undo sun mimic interest slam gaze truly inherit resist great inject rocket museum chief",
"f84521c777a13b61564234bf8f8b62b3afce27fc4062b51bb5e62bdfecb23864ee6ecf07c1d5a97c0834307c5c852d8ceb88e7c97923c0a3b496bedd4e5f88a9"
],
[
"15da872c95a13dd738fbf50e427583ad61f18fd99f628c417a61cf8343c90419",
"beyond stage sleep clip because twist token leaf atom beauty genius food business side grid unable middle armed observe pair crouch tonight away coconut",
"b15509eaa2d09d3efd3e006ef42151b30367dc6e3aa5e44caba3fe4d3e352e65101fbdb86a96776b91946ff06f8eac594dc6ee1d3e82a42dfe1b40fef6bcc3fd"
]
]
}
File diff suppressed because one or more lines are too long
@@ -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"
}
]
}
}
}
File diff suppressed because one or more lines are too long
Binary file not shown.
File diff suppressed because it is too large Load Diff
Binary file not shown.
@@ -0,0 +1,79 @@
/**
* Copyright (c) 2025 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
* Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.quartz
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation
import com.fasterxml.jackson.module.kotlin.readValue
import com.vitorpamplona.quartz.nip01Core.core.Event
import com.vitorpamplona.quartz.nip01Core.jackson.JsonMapper
import com.vitorpamplona.quartz.nip01Core.verify
import junit.framework.TestCase.assertEquals
import junit.framework.TestCase.assertTrue
import kotlinx.coroutines.runBlocking
import org.junit.Test
import org.junit.runner.RunWith
import java.io.InputStreamReader
import java.util.zip.GZIPInputStream
@RunWith(AndroidJUnit4::class)
class LargeDBSignatureCheck {
@Test
fun insertDatabaseSample() =
runBlocking {
val fullDBInputStream = getInstrumentation().context.assets.open("nostr_vitor_short.json")
val eventArray =
JsonMapper.mapper.readValue<ArrayList<Event>>(
InputStreamReader(fullDBInputStream),
) as List<Event>
var counter = 0
eventArray.forEach {
assertTrue(it.verify())
counter++
}
assertEquals(eventArray.size, counter)
}
@Test
fun insertStartupDatabase() =
runBlocking {
// This file includes duplicates
val fullDBInputStream = getInstrumentation().context.assets.open("nostr_vitor_startup_data.json")
val eventArray =
JsonMapper.mapper.readValue<ArrayList<Event>>(
GZIPInputStream(fullDBInputStream),
) as List<Event>
var counter = 0
eventArray.forEach {
if (it.sig != "") {
assertTrue(it.verify())
}
counter++
}
assertEquals(eventArray.size, counter)
}
}
@@ -0,0 +1,144 @@
/**
* Copyright (c) 2025 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
* Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.quartz.bloom
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.vitorpamplona.quartz.nip01Core.core.HexKey
import com.vitorpamplona.quartz.nip01Core.core.hexToByteArray
import com.vitorpamplona.quartz.nip01Core.crypto.Nip01
import com.vitorpamplona.quartz.utils.RandomInstance
import com.vitorpamplona.quartz.utils.sha256.Sha256Hasher
import junit.framework.TestCase.assertEquals
import junit.framework.TestCase.assertFalse
import junit.framework.TestCase.assertTrue
import org.junit.Test
import org.junit.runner.RunWith
import java.math.BigInteger
import java.util.Base64
import java.util.BitSet
import java.util.concurrent.locks.ReentrantReadWriteLock
import kotlin.concurrent.read
import kotlin.concurrent.write
class BloomFilter(
private val size: Int,
private val rounds: Int,
private val bits: BitSet = BitSet(size),
private val salt: ByteArray = RandomInstance.bytes(8),
) {
private val hash = Sha256Hasher()
private val lock = ReentrantReadWriteLock()
fun add(value: HexKey) = add(value.hexToByteArray())
fun print() =
buildString {
for (seed in 0 until bits.size()) {
append(if (bits.get(seed)) "1" else "0")
}
}
fun add(value: ByteArray) {
lock.write {
for (seed in 0 until rounds) {
bits.set(hash(seed, value))
}
}
}
fun mightContains(value: HexKey): Boolean = mightContains(value.hexToByteArray())
fun mightContains(value: ByteArray): Boolean {
lock.read {
for (seed in 0 until rounds) {
if (!bits.get(hash(seed, value))) {
return false
}
}
}
return true
}
fun encode() = "$size:$rounds:${Base64.getEncoder().encodeToString(bits.toByteArray())}:${Base64.getEncoder().encodeToString(salt)}"
fun hash(
seed: Int,
value: ByteArray,
) = BigInteger(1, hash.hash(value + salt + seed.toByte()))
.remainder(BigInteger.valueOf(size.toLong()))
.toInt()
companion object {
fun decode(encodedStr: String): BloomFilter {
val (sizeStr, roundsStr, filterB64, saltB64) = encodedStr.split(":")
val bitSet = BitSet.valueOf(Base64.getDecoder().decode(filterB64))
val salt = Base64.getDecoder().decode(saltB64)
return BloomFilter(sizeStr.toInt(), roundsStr.toInt(), bitSet, salt)
}
}
}
@RunWith(AndroidJUnit4::class)
class BloomFilterTest {
val testEncoded = "100:10:AAAkAQANcYQFCQoB:hZkZYqqdxcE="
val testInBinary = "00000000000000000010010010000000000000001011000010001110001000011010000010010000010100001000000000000000000000000000000000000000"
@Test
fun testCreate() {
val bloomFilter = BloomFilter(100, 10, salt = Base64.getDecoder().decode("hZkZYqqdxcE="))
bloomFilter.add("ca29c211f1c72d5b6622268ff43d2288ea2b2cb5b9aa196ff9f1704fc914b71b")
bloomFilter.add("460c25e682fda7832b52d1f22d3d22b3176d972f60dcdc3212ed8c92ef85065c")
assertEquals(testEncoded, bloomFilter.encode())
assertEquals(testInBinary, bloomFilter.print())
assertTrue(bloomFilter.mightContains("ca29c211f1c72d5b6622268ff43d2288ea2b2cb5b9aa196ff9f1704fc914b71b"))
assertTrue(bloomFilter.mightContains("460c25e682fda7832b52d1f22d3d22b3176d972f60dcdc3212ed8c92ef85065c"))
assertFalse(bloomFilter.mightContains("560c25e682fda7832b52d1f22d3d22b3176d972f60dcdc3212ed8c92ef85065c"))
}
@Test
fun testDecoding() {
val bloomFilter = BloomFilter.decode(testEncoded)
assertEquals(testEncoded, bloomFilter.encode())
assertEquals(testInBinary, bloomFilter.print())
assertTrue(bloomFilter.mightContains("ca29c211f1c72d5b6622268ff43d2288ea2b2cb5b9aa196ff9f1704fc914b71b"))
assertTrue(bloomFilter.mightContains("460c25e682fda7832b52d1f22d3d22b3176d972f60dcdc3212ed8c92ef85065c"))
assertFalse(bloomFilter.mightContains("560c25e682fda7832b52d1f22d3d22b3176d972f60dcdc3212ed8c92ef85065c"))
}
@Test
fun runProb() {
val bloomFilter = BloomFilter.decode(testEncoded)
var failureCounter = 0
for (seed in 0..1000000) {
if (bloomFilter.mightContains(Nip01.pubKeyCreate(Nip01.privKeyCreate()))) {
failureCounter++
}
}
assertEquals(0, failureCounter)
}
}
@@ -0,0 +1,45 @@
/**
* Copyright (c) 2025 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
* Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.quartz.lightning
import androidx.test.ext.junit.runners.AndroidJUnit4
import org.junit.Assert
import org.junit.Test
import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
class LnInvoiceUtilTest {
@Test
fun test100KAmountCalculation() {
val bolt11 =
"lnbc1m1pjt9u0qsp553q90pj5mafzv20w45eqavned9tgwhl4q99n9s5ppcw24nzw3zeqpp5002kd3ktym67du86kj665fgaev7ka8ys7j5yz5fg686lr5e2gfkshp5dkk27nnuax05az3pk2r6ytxtvwn5j4xzsq9ajprhc7crjkmgvr3qxqyjw5qcqpjrzjqtzxvfsuxe4l92pf97tt4rcgpy2xalkmlwexh899wqxf83l8nwv4xzh0gvqq89qqqqqqqqlgqqqqq0gqvs9qxpqysgqx5mz04wd7kqu5zhhel9enr036hjrp4gga0nz084p2asjl36a0zmrk6mhqa249zsgqref2rlvhffm73u7rxgr47gden6rugup4ksvpzsqvds4pz"
// Context of the app under test.
Assert.assertEquals(100000, LnInvoiceUtil.getAmountInSats(bolt11).toLong())
}
@Test
fun test100GAmountCalculation() {
val bolt11 =
"lnbc1000000000000000p1pjtxqf0pp5myqxhcufqy56elfsg9dd4dthnqptusnnpwnul7u86l95xzjgqd8shp5gueg34sgm3u3nxqjqyunvvqdu0pr6jz6mwh4ew4886f2lpf4cmrqxqztgsp5w0cdfd45dfnqwex5gn85x7fru3jcrxhlcx3enx835477m3gdfcuq9qyyssqelrcmm7p9qazgjuxtdg7sd8nq5cscl2tratjlclt5rk5mc7uq2lphq3r2a43j5ua4leakc4emq8yp2yxdnzvzszpw6u2afac0kgl7hspfj67ta"
// Context of the app under test.
Assert.assertEquals(100000000000, LnInvoiceUtil.getAmountInSats(bolt11).toLong())
}
}
@@ -0,0 +1,61 @@
/**
* Copyright (c) 2025 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
* Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.quartz.nip01Core
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.vitorpamplona.quartz.nip01Core.jackson.JsonMapper
import org.junit.Test
import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
class EventSigCheck {
val payload1 =
"[\"EVENT\",\"40b9\",{\"id\":\"48a72b485d38338627ec9d427583551f9af4f016c739b8ec0d6313540a8b12cf\"," +
"\"kind\":1,\"pubkey\":\"3d842afecd5e293f28b6627933704a3fb8ce153aa91d790ab11f6a752d44a42d\"," +
"\"created_at\":1677940007,\"content\":" +
"\"I got asked about follower count again today. Why does my follower count go down when " +
"I delete public relays (in our list) and replace them with filter.nostr.wine? \\n\\nIll " +
"give you one final explanation to rule them all. First, lets go over how clients calculate " +
"your follower count.\\n\\n1. Your client sends a request to all your connected relays asking " +
"for accounts who follow you\\n2. Relays answer back with the events requested\\n3. The client " +
"aggregates the event total and displays it\\n\\nEach relay has a set limit on how many stored " +
"events it will return per request. For some relays its 500, others 1000, some as high as 5000. " +
"Lets say for simplicity that all your public relays use 500 as their limit. If you ask 10 " +
"relays for your followers the max possible answer you can get is 5000. That wont change if " +
"you have 20,000 followers or 100,000. You may get back a “different” 5000 each time, but youll " +
"still cap out at 5000 because that is the most events your client will receive.\u2028\u2028Our " +
"limit on filter.nostr.wine is 2000 events. If you replace 10 public relays with only " +
"filter.nostr.wine, the MOST followers you will ever get back from our filter relay is 2000. " +
"That doesnt mean you only have 2000 followers or that your reach is reduced in any way.\\n\\nAs " +
"long as you are writing to and reading from the same public relays, neither your reach nor any " +
"content was lost. That concludes my TED talk. I hope you all have a fantastic day and weekend.\"," +
"\"tags\":[],\"sig\":\"dcaf8ab98bb9179017b35bd814092850d1062b26c263dff89fb1ae8c019a324139d1729012d" +
"9d05ff0a517f76b1117d869b2cc7d36bea8aa5f4b94c5e2548aa8\"}]"
@Test
fun testUnicode2028and2029ShouldNotBeEscaped() {
val msg = JsonMapper.mapper.readTree(payload1)
val event = JsonMapper.fromJson(msg[2])
// Should pass
event.checkSignature()
}
}
@@ -0,0 +1,111 @@
/**
* Copyright (c) 2025 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
* Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.quartz.nip01Core
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.vitorpamplona.quartz.nip01Core.core.hexToByteArray
import com.vitorpamplona.quartz.nip01Core.core.toHexKey
import com.vitorpamplona.quartz.nip01Core.crypto.Nip01
import com.vitorpamplona.quartz.utils.Secp256k1Instance
import com.vitorpamplona.quartz.utils.sha256.sha256
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNotEquals
import org.junit.Assert.assertTrue
import org.junit.Test
import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
class Nip01Test {
private val privateKey = "f410f88bcec6cbfda04d6a273c7b1dd8bba144cd45b71e87109cfa11dd7ed561".hexToByteArray()
@Test
fun testGetPublicFromPrivateKey() {
assertEquals(
"7d4b8806f1fd713c287235411bf95aa81b7242ead892733ec84b3f2719845be6",
Nip01.pubKeyCreate(privateKey).toHexKey(),
)
}
@Test
fun testGetPublicCompressedWith2() {
val key = "e6159851715b4aa6190c22b899b0c792847de0a4435ac5b678f35738351c43b0".hexToByteArray()
assertEquals(
"029fa4ce8c87ca546b196e6518db80a6780e1bd5552b61f9f17bafee5d4e34e09b",
Secp256k1Instance.compressedPubKeyFor(key).toHexKey(),
)
}
@Test
fun testGetPublicCompressedWith3() {
val key = "65f039136f8da8d3e87b4818746b53318d5481e24b2673f162815144223a0b5a".hexToByteArray()
assertEquals(
"033dcef7585efbdb68747d919152bd481e21f5e952aaaef5a19604fbd096a93dd5",
Secp256k1Instance.compressedPubKeyFor(key).toHexKey(),
)
}
@Test
fun testDeterministicSign() {
assertEquals(
"1484d0e0bd62165e822e31f1f4cc8e1ce8e20c30a060e24fb0ecd7baf7c624f661fb7a3e4f0ddb43018e5f0b4892c929af64d8b7a86021aa081ec8231e3dfa37",
Nip01.sign(sha256("Test".toByteArray()), privateKey, null).toHexKey(),
)
}
@Test
fun testSha256() {
assertEquals(
"532eaabd9574880dbf76b9b8cc00832c20a6ec113d682299550d7a6e0f345e25",
sha256("Test".toByteArray()).toHexKey(),
)
}
@Test
fun testDeterministicVerify() {
assertTrue(
Nip01.verify(
"1484d0e0bd62165e822e31f1f4cc8e1ce8e20c30a060e24fb0ecd7baf7c624f661fb7a3e4f0ddb43018e5f0b4892c929af64d8b7a86021aa081ec8231e3dfa37".hexToByteArray(),
sha256("Test".toByteArray()),
Nip01.pubKeyCreate(privateKey),
),
)
}
@Test
fun testNonDeterministicSign() {
assertNotEquals(
"1484d0e0bd62165e822e31f1f4cc8e1ce8e20c30a060e24fb0ecd7baf7c624f661fb7a3e4f0ddb43018e5f0b4892c929af64d8b7a86021aa081ec8231e3dfa37",
Nip01.sign(sha256("Test".toByteArray()), privateKey).toHexKey(),
)
}
@Test
fun testNonDeterministicSignVerify() {
val signature = Nip01.sign(sha256("Test".toByteArray()), privateKey)
assertTrue(
Nip01.verify(
signature,
sha256("Test".toByteArray()),
Nip01.pubKeyCreate(privateKey),
),
)
}
}
@@ -0,0 +1,39 @@
/**
* Copyright (c) 2025 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
* Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.quartz.nip01Core.crypto
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.vitorpamplona.quartz.nip01Core.core.hexToByteArray
import com.vitorpamplona.quartz.nip01Core.core.toHexKey
import org.junit.Assert.assertEquals
import org.junit.Test
import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
class Nip01CryptoTest {
@Test
fun testGetPublicFromPrivateKey() {
val privateKey =
"f410f88bcec6cbfda04d6a273c7b1dd8bba144cd45b71e87109cfa11dd7ed561".hexToByteArray()
val publicKey = Nip01.pubKeyCreate(privateKey).toHexKey()
assertEquals("7d4b8806f1fd713c287235411bf95aa81b7242ead892733ec84b3f2719845be6", publicKey)
}
}
@@ -0,0 +1,104 @@
/**
* Copyright (c) 2025 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
* Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.quartz.nip01Core.store.sqlite
import android.content.Context
import android.database.sqlite.SQLiteConstraintException
import androidx.test.core.app.ApplicationProvider
import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter
import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerSync
import com.vitorpamplona.quartz.nip23LongContent.LongTextNoteEvent
import com.vitorpamplona.quartz.utils.TimeUtils
import junit.framework.TestCase
import junit.framework.TestCase.fail
import org.junit.After
import org.junit.Before
import org.junit.Test
class AddressableTest {
private lateinit var db: EventStore
val signer = NostrSignerSync()
@Before
fun setup() {
val context = ApplicationProvider.getApplicationContext<Context>()
db = EventStore(context, "test.db", relayUrl = "testUrl")
}
@After
fun tearDown() {
db.close()
}
@Test
fun testReplacingAddressables() {
val time = TimeUtils.now()
val version1 = signer.sign(LongTextNoteEvent.build("my cool blog, version 1", "title", dTag = "my-cool-blog", createdAt = time))
val version2 = signer.sign(LongTextNoteEvent.build("my cool blog, version 2", "title", dTag = "my-cool-blog", createdAt = time + 1))
val version3 = signer.sign(LongTextNoteEvent.build("my cool blog, version 3", "title", dTag = "my-cool-blog", createdAt = time + 2))
db.insert(version1)
db.assertQuery(version1, Filter(ids = listOf(version1.id)))
db.insert(version2)
db.assertQuery(null, Filter(ids = listOf(version1.id)))
db.assertQuery(version2, Filter(ids = listOf(version2.id)))
db.insert(version3)
db.assertQuery(null, Filter(ids = listOf(version1.id)))
db.assertQuery(null, Filter(ids = listOf(version2.id)))
db.assertQuery(version3, Filter(ids = listOf(version3.id)))
}
@Test
fun testBlockingOldAddressables() {
val time = TimeUtils.now()
val version1 = signer.sign(LongTextNoteEvent.build("my cool blog, version 1", "title", dTag = "my-cool-blog", createdAt = time))
val version2 = signer.sign(LongTextNoteEvent.build("my cool blog, version 2", "title", dTag = "my-cool-blog", createdAt = time + 1))
val version3 = signer.sign(LongTextNoteEvent.build("my cool blog, version 3", "title", dTag = "my-cool-blog", createdAt = time + 2))
db.insert(version3)
db.assertQuery(version3, Filter(ids = listOf(version3.id)))
try {
db.insert(version2)
fail("It should not allow inserting an older version")
} catch (e: Exception) {
TestCase.assertTrue(e is SQLiteConstraintException)
}
try {
db.insert(version1)
fail("It should not allow inserting an older version")
} catch (e: Exception) {
TestCase.assertTrue(e is SQLiteConstraintException)
}
db.assertQuery(version3, Filter(ids = listOf(version3.id)))
db.assertQuery(null, Filter(ids = listOf(version2.id)))
db.assertQuery(null, Filter(ids = listOf(version1.id)))
}
}
@@ -0,0 +1,83 @@
/**
* Copyright (c) 2025 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
* Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.quartz.nip01Core.store.sqlite
import com.vitorpamplona.quartz.nip01Core.core.Event
import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter
import junit.framework.TestCase
fun EventStore.assertQuery(
expected: Event?,
filter: Filter,
) {
val queryResult = query(filter)
val countResult = count(filter)
if (expected == null) {
TestCase.assertEquals(0, queryResult.size)
TestCase.assertEquals(0, countResult)
} else {
TestCase.assertEquals(1, queryResult.size)
TestCase.assertEquals(1, countResult)
TestCase.assertEquals(expected.toJson(), queryResult.first().toJson())
}
}
fun EventStore.assertQuery(
expected: List<Event>,
filter: Filter,
) {
val queryResult = query(filter)
val countResult = count(filter)
TestCase.assertEquals(expected.size, queryResult.size)
TestCase.assertEquals(expected.size, countResult)
expected.forEachIndexed { index, event ->
TestCase.assertEquals(event.toJson(), queryResult[index].toJson())
}
}
fun SQLiteEventStore.assertQuery(
expected: Event?,
filter: Filter,
) {
val queryResult = query(filter)
val countResult = count(filter)
if (expected == null) {
TestCase.assertEquals(0, queryResult.size)
TestCase.assertEquals(0, countResult)
} else {
TestCase.assertEquals(1, queryResult.size)
TestCase.assertEquals(1, countResult)
TestCase.assertEquals(expected.toJson(), queryResult.first().toJson())
}
}
fun SQLiteEventStore.assertQuery(
expected: List<Event>,
filter: Filter,
) {
val queryResult = query(filter)
val countResult = count(filter)
TestCase.assertEquals(expected.size, queryResult.size)
TestCase.assertEquals(expected.size, countResult)
expected.forEachIndexed { index, event ->
TestCase.assertEquals(event.toJson(), queryResult[index].toJson())
}
}
@@ -0,0 +1,207 @@
/**
* Copyright (c) 2025 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
* Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.quartz.nip01Core.store.sqlite
import android.content.Context
import androidx.test.core.app.ApplicationProvider
import com.vitorpamplona.quartz.nip01Core.metadata.MetadataEvent
import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter
import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerSync
import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent
import com.vitorpamplona.quartz.nip22Comments.CommentEvent
import junit.framework.TestCase.assertEquals
import org.junit.After
import org.junit.Before
import org.junit.Test
class BasicTest {
private lateinit var db: SQLiteEventStore
val signer = NostrSignerSync()
companion object Companion {
val profile =
MetadataEvent(
id = "490d7439e530423f2540d4f2bdb73a0a2935f3df9e1f2a6f699a140c7db311fe",
pubKey = "70a9b3c312a6b83e476739bd29d60ca700da1d5b982cbca87b5f3d27d4038d67",
createdAt = 1740669816,
tags =
arrayOf(
arrayOf("alt", "User profile for Vitor"),
arrayOf("name", "Vitor"),
),
content = "{\"name\":\"Vitor\"}",
sig = "977a6152199f17d103d8d56736ed1b7767054464cf9423d017c01c8cdd2344698f0a5e13da8dff98d01bb1f798837e3b6271e1fd1cac861bb90686f622ae6ef4",
)
val comment =
CommentEvent(
id = "fecb2ecf61a1433d417a784d10bd1e8ec19a916170a53ca8fb3a15fc666a6592",
pubKey = "f8ff11c7a7d3478355d3b4d174e5a473797a906ea4aa61aa9b6bc0652c1ea17a",
createdAt = 1747753115,
tags =
arrayOf(
arrayOf("alt", "Reply to geo:drt3n"),
arrayOf("I", "geo:drt3n"),
arrayOf("I", "geo:drt3"),
arrayOf("I", "geo:drt"),
arrayOf("I", "geo:dr"),
arrayOf("I", "geo:d"),
arrayOf("K", "geo"),
arrayOf("i", "geo:drt3n"),
arrayOf("i", "geo:drt3"),
arrayOf("i", "geo:drt"),
arrayOf("i", "geo:dr"),
arrayOf("i", "geo:d"),
arrayOf("k", "geo"),
),
content = "testing",
sig = "12070e663272f1227c639fb834eb2122fc7bb995f4c49e55ebb1dfe2135ef7347d44810bacd2e64fd26b8826fd47d2800ce6c3d3b579bb3afe39088ffd4faa60",
)
}
@Before
fun setup() {
val context = ApplicationProvider.getApplicationContext<Context>()
db = SQLiteEventStore(context, null)
}
@After
fun tearDown() {
db.close()
}
@Test
fun testInsertDeleteEvent() {
val note = signer.sign(TextNoteEvent.build("test1"))
db.insertEvent(note)
db.assertQuery(note, Filter(ids = listOf(note.id)))
db.delete(note.id)
db.assertQuery(null, Filter(ids = listOf(note.id)))
db.insertEvent(note)
db.assertQuery(note, Filter(ids = listOf(note.id)))
}
@Test
fun testEmptyFilter() {
val note1 = signer.sign(TextNoteEvent.build("test1", createdAt = 1))
val note2 = signer.sign(TextNoteEvent.build("test2", createdAt = 2))
db.insertEvent(note1)
db.assertQuery(note1, Filter())
db.insertEvent(note2)
db.assertQuery(listOf(note2, note1), Filter())
}
@Test
fun testLimitFilter() {
val note1 = signer.sign(TextNoteEvent.build("test1", createdAt = 1))
val note2 = signer.sign(TextNoteEvent.build("test2", createdAt = 2))
val note3 = signer.sign(TextNoteEvent.build("test3", createdAt = 3))
val note4 = signer.sign(TextNoteEvent.build("test4", createdAt = 4))
db.insertEvent(note1)
db.assertQuery(note1, Filter(limit = 1))
db.insertEvent(note2)
db.insertEvent(note3)
db.insertEvent(note4)
db.assertQuery(listOf(note4), Filter(limit = 1))
}
@Test
fun testPubkeyTag() {
db.insertEvent(comment)
db.insertEvent(profile)
db.assertQuery(
comment,
Filter(authors = listOf(comment.pubKey), tags = mapOf("I" to listOf("geo:drt3n"))),
)
}
@Test
fun testTagOnly() {
db.insertEvent(comment)
db.insertEvent(profile)
db.assertQuery(comment, Filter(tags = mapOf("I" to listOf("geo:drt3n"))))
}
@Test
fun testTagWithSinceOnly() {
db.insertEvent(comment)
db.insertEvent(profile)
db.assertQuery(
comment,
Filter(tags = mapOf("I" to listOf("geo:drt3n")), since = comment.createdAt - 1),
)
db.assertQuery(
comment,
Filter(tags = mapOf("I" to listOf("geo:drt3n")), since = comment.createdAt),
)
db.assertQuery(
null,
Filter(tags = mapOf("I" to listOf("geo:drt3n")), since = comment.createdAt + 1),
)
}
@Test
fun testTagWithUntilOnly() {
db.insertEvent(comment)
db.insertEvent(profile)
db.assertQuery(
null,
Filter(tags = mapOf("I" to listOf("geo:drt3n")), until = comment.createdAt - 1),
)
db.assertQuery(
comment,
Filter(tags = mapOf("I" to listOf("geo:drt3n")), until = comment.createdAt),
)
db.assertQuery(
comment,
Filter(tags = mapOf("I" to listOf("geo:drt3n")), until = comment.createdAt + 1),
)
}
@Test
fun testTagWithUntilOnlyEmitting() {
db.insertEvent(comment)
db.insertEvent(profile)
db.query(Filter(tags = mapOf("I" to listOf("geo:drt3n")))) { event ->
assertEquals(comment.toJson(), event.toJson())
}
}
}
@@ -0,0 +1,173 @@
/**
* Copyright (c) 2025 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
* Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.quartz.nip01Core.store.sqlite
import android.content.Context
import android.database.sqlite.SQLiteConstraintException
import androidx.test.core.app.ApplicationProvider
import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter
import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerSync
import com.vitorpamplona.quartz.nip09Deletions.DeletionEvent
import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent
import com.vitorpamplona.quartz.nip23LongContent.LongTextNoteEvent
import com.vitorpamplona.quartz.utils.TimeUtils
import junit.framework.TestCase.fail
import org.junit.After
import org.junit.Assert.assertEquals
import org.junit.Before
import org.junit.Test
class DeletionTest {
private lateinit var db: EventStore
val signer = NostrSignerSync()
@Before
fun setup() {
val context = ApplicationProvider.getApplicationContext<Context>()
db = EventStore(context, null)
}
@After
fun tearDown() {
db.close()
}
@Test
fun testInsertDeleteEvent() {
val note1 = signer.sign(TextNoteEvent.build("test1"))
val note2 = signer.sign(TextNoteEvent.build("test2"))
val note3 = signer.sign(TextNoteEvent.build("test3"))
db.insert(note1)
db.insert(note2)
db.insert(note3)
db.assertQuery(note1, Filter(ids = listOf(note1.id)))
db.assertQuery(note2, Filter(ids = listOf(note2.id)))
db.assertQuery(note3, Filter(ids = listOf(note3.id)))
val deletion = signer.sign(DeletionEvent.build(listOf(note1)))
db.insert(deletion)
db.assertQuery(deletion, Filter(ids = listOf(deletion.id)))
db.assertQuery(null, Filter(ids = listOf(note1.id)))
db.assertQuery(note2, Filter(ids = listOf(note2.id)))
db.assertQuery(note3, Filter(ids = listOf(note3.id)))
// trying to insert again should fail.
try {
db.insert(note1)
fail("Should not be able to insert a deleted event")
} catch (e: SQLiteConstraintException) {
assertEquals("blocked: a deletion event for this event id exists (code 1811 SQLITE_CONSTRAINT_TRIGGER)", e.message)
}
db.assertQuery(deletion, Filter(ids = listOf(deletion.id)))
db.assertQuery(null, Filter(ids = listOf(note1.id)))
db.assertQuery(note2, Filter(ids = listOf(note2.id)))
db.assertQuery(note3, Filter(ids = listOf(note3.id)))
}
@Test
fun testInsertDeleteEventOfAddressable() {
val time = TimeUtils.now()
val note1 = signer.sign(LongTextNoteEvent.build("my cool blog, version 1", "title", dTag = "my-cool-blog", createdAt = time))
val note2 = signer.sign(LongTextNoteEvent.build("my cool blog, version 2", "title", dTag = "my-cool-blog", createdAt = time + 1))
val note3 = signer.sign(LongTextNoteEvent.build("my cool blog, version 3", "title", dTag = "my-cool-blog", createdAt = time + 2))
db.insert(note1)
db.assertQuery(note1, Filter(ids = listOf(note1.id)))
db.insert(note2)
db.assertQuery(null, Filter(ids = listOf(note1.id)))
db.assertQuery(note2, Filter(ids = listOf(note2.id)))
db.insert(note3)
db.assertQuery(null, Filter(ids = listOf(note1.id)))
db.assertQuery(null, Filter(ids = listOf(note2.id)))
db.assertQuery(note3, Filter(ids = listOf(note3.id)))
val deletion = signer.sign(DeletionEvent.build(listOf(note1)))
db.insert(deletion)
db.assertQuery(deletion, Filter(ids = listOf(deletion.id)))
db.assertQuery(null, Filter(ids = listOf(note1.id)))
db.assertQuery(null, Filter(ids = listOf(note2.id)))
db.assertQuery(null, Filter(ids = listOf(note3.id)))
// trying to insert again should fail.
try {
db.insert(note1)
fail("Should not be able to insert a deleted event")
} catch (e: SQLiteConstraintException) {
assertEquals("blocked: a deletion event for this event id exists (code 1811 SQLITE_CONSTRAINT_TRIGGER)", e.message)
}
db.assertQuery(deletion, Filter(ids = listOf(deletion.id)))
db.assertQuery(null, Filter(ids = listOf(note1.id)))
db.assertQuery(null, Filter(ids = listOf(note2.id)))
db.assertQuery(null, Filter(ids = listOf(note3.id)))
}
@Test
fun testInsertDeleteEventOfAddressable2() {
val time = TimeUtils.now()
val note1 = signer.sign(LongTextNoteEvent.build("my cool blog, version 1", "title", dTag = "my-cool-blog", createdAt = time))
val note2 = signer.sign(LongTextNoteEvent.build("my cool blog, version 2", "title", dTag = "my-cool-blog", createdAt = time + 1))
val note3 = signer.sign(LongTextNoteEvent.build("my cool blog, version 3", "title", dTag = "my-cool-blog", createdAt = time + 2))
db.insert(note1)
db.insert(note2)
db.insert(note3)
db.assertQuery(null, Filter(ids = listOf(note1.id)))
db.assertQuery(null, Filter(ids = listOf(note2.id)))
db.assertQuery(note3, Filter(ids = listOf(note3.id)))
val deletion = signer.sign(DeletionEvent.buildAddressOnly(listOf(note1)))
db.insert(deletion)
db.assertQuery(deletion, Filter(ids = listOf(deletion.id)))
db.assertQuery(null, Filter(ids = listOf(note1.id)))
db.assertQuery(null, Filter(ids = listOf(note2.id)))
db.assertQuery(null, Filter(ids = listOf(note3.id)))
// trying to insert again should fail.
try {
db.insert(note1)
fail("Should not be able to insert a deleted event")
} catch (e: SQLiteConstraintException) {
assertEquals("blocked: a deletion event for this address exists (code 1811 SQLITE_CONSTRAINT_TRIGGER)", e.message)
}
db.assertQuery(deletion, Filter(ids = listOf(deletion.id)))
db.assertQuery(null, Filter(ids = listOf(note1.id)))
db.assertQuery(null, Filter(ids = listOf(note2.id)))
db.assertQuery(null, Filter(ids = listOf(note3.id)))
}
}
@@ -0,0 +1,103 @@
/**
* Copyright (c) 2025 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
* Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.quartz.nip01Core.store.sqlite
import android.content.Context
import android.database.sqlite.SQLiteConstraintException
import androidx.test.core.app.ApplicationProvider
import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter
import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerSync
import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent
import com.vitorpamplona.quartz.nip40Expiration.expiration
import com.vitorpamplona.quartz.utils.TimeUtils
import junit.framework.TestCase.assertTrue
import junit.framework.TestCase.fail
import org.junit.After
import org.junit.Before
import org.junit.Test
class ExpirationTest {
private lateinit var db: EventStore
val signer = NostrSignerSync()
@Before
fun setup() {
val context = ApplicationProvider.getApplicationContext<Context>()
db = EventStore(context, null, relayUrl = "testUrl")
}
@After
fun tearDown() {
db.close()
}
@Test
fun testDeletingExpiredEvents() {
val time = TimeUtils.now()
val noteSafe =
signer.sign(
TextNoteEvent.build("test1", createdAt = time + 1) {
expiration(time + 100)
},
)
db.insert(noteSafe)
val noteToExpire =
signer.sign(
TextNoteEvent.build("test1", createdAt = time + 1) {
expiration(time + 1)
},
)
db.insert(noteToExpire)
db.assertQuery(noteToExpire, Filter(ids = listOf(noteToExpire.id)))
Thread.sleep(2000)
db.deleteExpiredEvents()
db.assertQuery(null, Filter(ids = listOf(noteToExpire.id)))
db.assertQuery(noteSafe, Filter(ids = listOf(noteSafe.id)))
}
@Test
fun testInsertingExpiredEvents() {
val time = TimeUtils.now()
val note1 =
signer.sign(
TextNoteEvent.build("test1", createdAt = time - 12) {
expiration(time - 10)
},
)
try {
db.insert(note1)
fail("Should not be able to insert expired events")
} catch (e: Exception) {
assertTrue(e is SQLiteConstraintException)
}
}
}
@@ -0,0 +1,104 @@
/**
* Copyright (c) 2025 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
* Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.quartz.nip01Core.store.sqlite
import android.content.Context
import android.database.sqlite.SQLiteException
import android.util.Log
import androidx.test.core.app.ApplicationProvider
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation
import com.fasterxml.jackson.module.kotlin.readValue
import com.vitorpamplona.quartz.nip01Core.core.Event
import com.vitorpamplona.quartz.nip01Core.jackson.JsonMapper
import com.vitorpamplona.quartz.nip40Expiration.isExpired
import org.junit.After
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import java.util.zip.GZIPInputStream
import kotlin.system.measureTimeMillis
@RunWith(AndroidJUnit4::class)
class LargeDBTests {
companion object {
fun getEventDB(): List<Event> {
// This file includes duplicates
val fullDBInputStream = getInstrumentation().context.assets.open("nostr_vitor_startup_data.json")
return JsonMapper.mapper.readValue<ArrayList<Event>>(
GZIPInputStream(fullDBInputStream),
)
}
val events = getEventDB().distinctBy { it.id }.filter { !it.isExpired() }.sortedBy { it.createdAt }
}
private lateinit var db: EventStore
@Before
fun setup() {
val context = ApplicationProvider.getApplicationContext<Context>()
db = EventStore(context, "largeDBTest.db")
db.store.clearDB()
}
@After
fun tearDown() {
db.store.clearDB()
db.close()
}
@Test
fun insertHeavyEvent() {
events.first { it.id == "3f34b8cb682307ec11753de4669ce8948e95fd6fb360d79136446c5547fd235e" }.let { event ->
println(event.toJson())
try {
val measure =
measureTimeMillis {
db.insert(event)
}
if (measure > 1) {
println("Inserted event ${event.id} of kind ${event.kind} in $measure ms")
}
} catch (e: SQLiteException) {
Log.w("LargeDBTests", "Error inserting event: ${e.message} for event: ${event.toJson()}")
}
}
}
@Test
fun insertDatabase() {
events.forEach { event ->
try {
val measure =
measureTimeMillis {
db.insert(event)
}
if (measure > 1) {
println("Inserted event ${event.id} of kind ${event.kind} in $measure ms")
}
} catch (e: SQLiteException) {
Log.w("LargeDBTests", "Error inserting event: ${e.message} for event: ${event.toJson()}")
}
}
}
}
@@ -0,0 +1,104 @@
/**
* Copyright (c) 2025 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
* Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.quartz.nip01Core.store.sqlite
import android.content.Context
import android.database.sqlite.SQLiteConstraintException
import androidx.test.core.app.ApplicationProvider
import com.vitorpamplona.quartz.nip01Core.metadata.MetadataEvent
import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter
import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerSync
import com.vitorpamplona.quartz.utils.TimeUtils
import junit.framework.TestCase
import junit.framework.TestCase.fail
import org.junit.After
import org.junit.Before
import org.junit.Test
class ReplaceableTest {
private lateinit var db: EventStore
val signer = NostrSignerSync()
@Before
fun setup() {
val context = ApplicationProvider.getApplicationContext<Context>()
db = EventStore(context, null, relayUrl = "testUrl")
}
@After
fun tearDown() {
db.close()
}
@Test
fun testReplacing() {
val time = TimeUtils.now()
val version1 = signer.sign(MetadataEvent.createNew("Vitor 1", createdAt = time))
val version2 = signer.sign(MetadataEvent.createNew("Vitor 2", createdAt = time + 1))
val version3 = signer.sign(MetadataEvent.createNew("Vitor 3", createdAt = time + 2))
db.insert(version1)
db.assertQuery(version1, Filter(ids = listOf(version1.id)))
db.insert(version2)
db.assertQuery(null, Filter(ids = listOf(version1.id)))
db.assertQuery(version2, Filter(ids = listOf(version2.id)))
db.insert(version3)
db.assertQuery(null, Filter(ids = listOf(version1.id)))
db.assertQuery(null, Filter(ids = listOf(version2.id)))
db.assertQuery(version3, Filter(ids = listOf(version3.id)))
}
@Test
fun testBlockingOldVersions() {
val time = TimeUtils.now()
val version1 = signer.sign(MetadataEvent.createNew("Vitor 1", createdAt = time))
val version2 = signer.sign(MetadataEvent.createNew("Vitor 2", createdAt = time + 1))
val version3 = signer.sign(MetadataEvent.createNew("Vitor 3", createdAt = time + 2))
db.insert(version3)
db.assertQuery(version3, Filter(ids = listOf(version3.id)))
try {
db.insert(version2)
fail("It should not allow inserting an older version")
} catch (e: Exception) {
TestCase.assertTrue(e is SQLiteConstraintException)
}
try {
db.insert(version1)
fail("It should not allow inserting an older version")
} catch (e: Exception) {
TestCase.assertTrue(e is SQLiteConstraintException)
}
db.assertQuery(version3, Filter(ids = listOf(version3.id)))
db.assertQuery(null, Filter(ids = listOf(version2.id)))
db.assertQuery(null, Filter(ids = listOf(version1.id)))
}
}
@@ -0,0 +1,90 @@
/**
* Copyright (c) 2025 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
* Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.quartz.nip01Core.store.sqlite
import android.content.Context
import android.database.sqlite.SQLiteConstraintException
import androidx.test.core.app.ApplicationProvider
import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter
import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerSync
import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent
import com.vitorpamplona.quartz.nip62RequestToVanish.RequestToVanishEvent
import com.vitorpamplona.quartz.utils.TimeUtils
import junit.framework.TestCase.fail
import org.junit.After
import org.junit.Assert.assertEquals
import org.junit.Before
import org.junit.Test
class RightToVanishTest {
private lateinit var db: EventStore
val signer = NostrSignerSync()
@Before
fun setup() {
val context = ApplicationProvider.getApplicationContext<Context>()
db = EventStore(context, null, relayUrl = "testUrl")
}
@After
fun tearDown() {
db.close()
}
@Test
fun testInsertDeleteEvent() {
val time = TimeUtils.now()
val note1 = signer.sign(TextNoteEvent.build("test1", createdAt = time))
val note2 = signer.sign(TextNoteEvent.build("test2", createdAt = time + 1))
val note3 = signer.sign(TextNoteEvent.build("test3", createdAt = time + 2))
db.insert(note1)
db.insert(note2)
db.insert(note3)
db.assertQuery(note1, Filter(ids = listOf(note1.id)))
db.assertQuery(note2, Filter(ids = listOf(note2.id)))
db.assertQuery(note3, Filter(ids = listOf(note3.id)))
val vanish = signer.sign(RequestToVanishEvent.build("testUrl", createdAt = time + 2))
db.insert(vanish)
db.assertQuery(vanish, Filter(ids = listOf(vanish.id)))
db.assertQuery(null, Filter(ids = listOf(note1.id)))
db.assertQuery(null, Filter(ids = listOf(note2.id)))
db.assertQuery(note3, Filter(ids = listOf(note3.id)))
// trying to insert again should fail.
try {
db.insert(note1)
fail("Should not be able to insert a deleted event")
} catch (e: SQLiteConstraintException) {
assertEquals("blocked: a request to vanish event exists (code 1811 SQLITE_CONSTRAINT_TRIGGER)", e.message)
}
db.assertQuery(vanish, Filter(ids = listOf(vanish.id)))
db.assertQuery(null, Filter(ids = listOf(note1.id)))
db.assertQuery(null, Filter(ids = listOf(note2.id)))
db.assertQuery(note3, Filter(ids = listOf(note3.id)))
}
}
@@ -0,0 +1,108 @@
/**
* Copyright (c) 2025 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
* Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.quartz.nip01Core.store.sqlite
import android.content.Context
import androidx.test.core.app.ApplicationProvider
import com.vitorpamplona.quartz.nip01Core.metadata.MetadataEvent
import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter
import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent
import com.vitorpamplona.quartz.nip22Comments.CommentEvent
import org.junit.After
import org.junit.Before
import org.junit.Test
class SearchTest {
private lateinit var db: SQLiteEventStore
companion object Companion {
val profile =
MetadataEvent(
id = "490d7439e530423f2540d4f2bdb73a0a2935f3df9e1f2a6f699a140c7db311fe",
pubKey = "70a9b3c312a6b83e476739bd29d60ca700da1d5b982cbca87b5f3d27d4038d67",
createdAt = 1740669816,
tags =
arrayOf(
arrayOf("alt", "User profile for Vitor"),
arrayOf("name", "Vitor"),
),
content = "{\"name\":\"Vitor\"}",
sig = "977a6152199f17d103d8d56736ed1b7767054464cf9423d017c01c8cdd2344698f0a5e13da8dff98d01bb1f798837e3b6271e1fd1cac861bb90686f622ae6ef4",
)
val comment =
CommentEvent(
id = "fecb2ecf61a1433d417a784d10bd1e8ec19a916170a53ca8fb3a15fc666a6592",
pubKey = "f8ff11c7a7d3478355d3b4d174e5a473797a906ea4aa61aa9b6bc0652c1ea17a",
createdAt = 1747753115,
tags =
arrayOf(
arrayOf("alt", "Reply to geo:drt3n"),
arrayOf("I", "geo:drt3n"),
arrayOf("I", "geo:drt3"),
arrayOf("I", "geo:drt"),
arrayOf("I", "geo:dr"),
arrayOf("I", "geo:d"),
arrayOf("K", "geo"),
arrayOf("i", "geo:drt3n"),
arrayOf("i", "geo:drt3"),
arrayOf("i", "geo:drt"),
arrayOf("i", "geo:dr"),
arrayOf("i", "geo:d"),
arrayOf("k", "geo"),
),
content = "testing",
sig = "12070e663272f1227c639fb834eb2122fc7bb995f4c49e55ebb1dfe2135ef7347d44810bacd2e64fd26b8826fd47d2800ce6c3d3b579bb3afe39088ffd4faa60",
)
}
@Before
fun setup() {
val context = ApplicationProvider.getApplicationContext<Context>()
db = SQLiteEventStore(context, null)
}
@After
fun tearDown() {
db.close()
}
@Test
fun testTagWithSearch() {
db.insertEvent(comment)
db.insertEvent(profile)
db.assertQuery(null, Filter(search = "testing1"))
db.assertQuery(comment, Filter(search = "testing"))
db.assertQuery(comment, Filter(kinds = listOf(CommentEvent.KIND), search = "testing"))
db.assertQuery(null, Filter(kinds = listOf(TextNoteEvent.KIND), search = "testing"))
db.delete(comment.id)
db.assertQuery(null, Filter(search = "testing"))
db.assertQuery(null, Filter(kinds = listOf(CommentEvent.KIND), search = "testing"))
db.insertEvent(comment)
db.assertQuery(comment, Filter(search = "testing"))
db.assertQuery(comment, Filter(kinds = listOf(CommentEvent.KIND), search = "testing"))
}
}
@@ -0,0 +1,97 @@
/**
* Copyright (c) 2025 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
* Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.quartz.nip03Timestamp.ots
import com.vitorpamplona.quartz.nip01Core.core.Event
import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair
import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerInternal
import com.vitorpamplona.quartz.nip03Timestamp.OtsEvent
import com.vitorpamplona.quartz.nip03Timestamp.OtsResolver
import junit.framework.TestCase.assertEquals
import junit.framework.TestCase.assertNotNull
import junit.framework.TestCase.fail
import kotlinx.coroutines.runBlocking
import org.junit.Test
class OtsTest {
val resolver = OtsResolver()
val otsEvent = "{\"content\":\"AE9wZW5UaW1lc3RhbXBzAABQcm9vZgC/ieLohOiSlAEIqCiiW0FlsU9lqK5f1A+cL6CGJ1Ah4V/A1yNJY/stUE3wECJz6ng/QxU5Z6xwaMx97qkI//AQqJv8bEMrGTplGWRv5qm4DgjxIHkcQqzpL0Fjr9VBAAijDe0IsQYpOhw1SIjZIgQa6i16CPEEZck7CvAIxR0AloJzCZoAg9/jDS75DI4uLWh0dHBzOi8vYWxpY2UuYnRjLmNhbGVuZGFyLm9wZW50aW1lc3RhbXBzLm9yZ//wEOwPtjIkKI1hmtv9t1kuxZcI8QRlyTsK8Ahl0wrCSggZzgCD3+MNLvkMjiwraHR0cHM6Ly9ib2IuYnRjLmNhbGVuZGFyLm9wZW50aW1lc3RhbXBzLm9yZ//wEE1dVGa8JCuf2ek0c5ybDKII8SCBoVz8Sal45Kd1O8STWIGJTcl5JPtAZBZitqk3BE9MqAjxBGXJOwrwCHoGVgAZi9q9AIPf4w0u+QyOKShodHRwczovL2Zpbm5leS5jYWxlbmRhci5ldGVybml0eXdhbGwuY29t8BAFZFXFYg7DJJ0OzjmJ0FKWCPEEZck7CvAI0M49IcBR5bf/AIPf4w0u+QyOIyJodHRwczovL2J0Yy5jYWxlbmRhci5jYXRhbGxheHkuY29tCPEgoE3IfYTmxxo4W/x/QYp/NGX6Wu93gSQkwbpjpOhZORcI8SDWLLurVQaXHdUuwivCfTfuxYCaq+AzypSGqLDAVocrEgjwIBfgjta16y13Gp4etQOCa9YiKEcM+/9AieG/vZolr3IDCPAgMR2zFCb384CEi8tVuI2fHgLT3I9zpe7oqJTzCcEqxWEI8SAJSdgeeosr7IxdOt8r7f0ipWc8FI6GAhgep8zSRgWikAjxIGxYmtCsC79Tx4z4YsT1WuMo+ycMkwhGQsQltF597cchCPAgCLrBf9vR1Aex6yY+vSkXAvLjMKdMqM/a1g8zNPwLeJcI8SBDCbTk4CczTuiIyZeUyYRVh31BZdjaSd2nU/pBQxu+6QjwIOwqE9/WqGC6CHH0i+tr7edvYX5PstSDf08KmnMqsqCoCPEgQIdEBg358vfek3Qjfyrgl51iCU6WUWmThsGLPDTcB0QI8SAX4dp64iI8pBx+zBqAQwUN6XgZ1cEfT8+2vha/9I1vzwjxWQEAAAAB8YJxvxJJth8OnxIV6UOXveIZAcJPTHcAkWgnucpuYqYAAAAAAP3///8CRhoMAAAAAAAWABTUUZK82uAvbU3vVyaaPIOddZBicwAAAAAAAAAAImog8ARCqgwACAjwIB9TcMDLhzgeS1Uw647lNvCfWECkkUvfrrOe6nay0sGdCAjwICYfs90sbPggoMICyOHGYbmOzop2L9mlnh4xqiBLY7yPCAjxINDiVWOBHnRmGJleQdB9myvJAJbNJ9kciZlTOkgJy89mCAjwIHfxqDLdwycj1Vtyth2CaSDdLQwiey9oV6Xov4stLpWNCAjxIGurJYpKJnKp9+y7MAdC+gXgHOiAu5P3RRUFW9l5hGaCCAjxICXqs9hdY0QMP/MNeqlt6s7xaIYtEXZ1CLvou5gaZNEICAjxIHdYxVeI76NXbT2zHcv6lw+v819Ooib7KWxc1GAsiX2fCAjwICPWdi3uBXOlIdmYi+V9C7wAqLyGE4DMoHD+GtvLizqiCAjwIOF2vENtWN5okEMMS+JSf1SGTY9yYP9j0JjXLbC1s+N1CAjxINWsgCtsPxhRNbe372k8/20WDbiL9e8934hGF256DvRECAjwII3mi+Li06j10ORxg0dYMkcsyGb115Jiqq1YEV3K/u+aCAgABYiWDXPXGQEDw9Qy\",\"created_at\":1707690688,\"id\":\"759f9da5846e936fab06766a524b36ba71c03bbc69ad0944fb8ee4bb1f3dd705\",\"kind\":1040,\"pubkey\":\"82fbb08c8ef45c4d71c88368d0ae805bc62fb92f166ab04a0b7a0c83d8cbc29a\",\"sig\":\"07c7896c8cbb97b5d7483097590c9d31b73f35c1ad9e752002bb5c1776cbd852e1d32704333d6930c9bc3e40f8b899a1f2e9f91cc3bf797d86acdecba7792576\",\"tags\":[[\"e\",\"a828a25b4165b14f65a8ae5fd40f9c2fa086275021e15fc0d7234963fb2d504d\"],[\"p\",\"595ca8eaace5899cb6ab7e2542bfc972136376f2eabc09287f1857eb8f167e53\"],[\"alt\",\"NIP-03 time stamp\"]]}"
val otsEvent2 = "{\"content\":\"AE9wZW5UaW1lc3RhbXBzAABQcm9vZgC/ieLohOiSlAEIqGNPU2jhd4no+zg2ytDkuf5PIoivr8KHI8BL68aKGNbwENyCNtiEN98IzIZgEu3cl6YI//AQ6TkSRd3BTGhDHCK1KkJc+AjxIAHaizG++NNL3Vm13BJrIhT7Br6tEYpb0TVRGaadgiUMCPAgOSDREH9v1Y50UHu79LfC4Lcd9WklQJzRQpw+Unb/pyII8QRltDD58AgqrxfAVrLw7QCD3+MNLvkMji4taHR0cHM6Ly9hbGljZS5idGMuY2FsZW5kYXIub3BlbnRpbWVzdGFtcHMub3Jn//AQQMq/CLpGwY60nmddPS7OVgjxIDKxqd9nl+Mej41vP52Wd7gv7004r3n1rFGDObS8icRvCPAgH9TB/kwvXJEEw+h9Ce6fLaI3MORjtTEge0GbAefT6W4I8QRltDD58AhRcoU3gAo/swCD3+MNLvkMjiwraHR0cHM6Ly9ib2IuYnRjLmNhbGVuZGFyLm9wZW50aW1lc3RhbXBzLm9yZ//wECWtWsKo0uvSr8BYonjs3DEI8CBlsh2ng1Spl0K4oStYElGuMJsjd2uo5nXB+apo5A7ipwjxIM8oxynBwNA+QS/X7Ebtl1kyhFgfoOQioASNfCBzZ4gaCPEEZbQw+fAId6Yd5cw5gioAg9/jDS75DI4pKGh0dHBzOi8vZmlubmV5LmNhbGVuZGFyLmV0ZXJuaXR5d2FsbC5jb23wEJmPzXQbxv0AFTIyjTWjMskI8CAurbkrfrBtlinZXSDxj+m/oIkze57hGjTSxu1Xs87XYQjwIPk/LMD0zIgKoEE2dfeoYrrdHuO6dwmghTwUFajH2QzkCPEEZbQw+fAIE+Pq1/Wmdpj/AIPf4w0u+QyOIyJodHRwczovL2J0Yy5jYWxlbmRhci5jYXRhbGxheHkuY29tCPAgB2CbqkV7VpjRKIl3Ea6cBmB/EHcSN/YCgcc1E+mc07QI8CAfpkZ2Hh4Rukz3x4il3tZqQtlDlbna+I6so2t2YSEmMQjwIJOv32jbsMa2HJwpleRCKLEhgYOoHCSfpv1ZO0YNNNFsCPAgLMM7eFfCjokQfU4gdU5WpG/wBLkO9lDRF0GktL6ujt8I8SCRxJ0bC1PQ8qFmI/1jh8AS5d1/6VRJNMt1Hz41QmNr3QjwIBmgrKBF+OZ3y+XOMv2E7IZ4WwLr2u2H+ehsBfy7cPlICPEgm4ZMCSXzZVWu40d+zk2edaur6KOauo8X7V2KaFBR1VoI8SBKVVOiyq6IFqGn/15kLwk7L8upMAIZ0znjhYxYqSTQCQjwIMBD1twPZ33GxbwTiuOCeJPkoP++6R2wYpCii8UBTdgwCPAg84VkgMXwrt2xxRoeC1/6CtsFctki+w3m8Rs5/6g/IhEI8CCnzZQDhJyicX7bS7U8PMUObuC9Y4TXe+4THoXBMMXkxwjxIMZ0oAvshpcwowR3qPEDbwKZ6B4NPSU4Hz/+4PnD74gnCPAgIfLZEKqAvkMNXfakXoNq1UVqGSzL4Z86z5GzUfbvw8UI8SC8KoIeLvjd4vJ/xhNVphakPRd80YKeNkeYEuVH8k2EtAjwIPRtinLLxzt8iuw0XZtpTDzEstZOTNYVm+Bi3fEzdeIuCPFZAQAAAAE8vsasINN5DKon0KakX2HNdCB126ZLKXrw4PfvyEfqbwAAAAAA/f///wLPGw8AAAAAABYAFCvgxlh6msa4ZOtgvlc5KiZCx7IvAAAAAAAAAAAiaiDwBKygDAAICPEgB/2DJ3s6gMky/PceGZocTFRXjZUiCCAhHGYwQk/8yrUICPEgDuSd6+PJHUMuEHyLcKFxw7xfvRHRfInjkV3/Zy3BxqAICPEgZDgQ+4VXzlOIkGoO8EVxDgs2cWaeh4EEiaqa/y50gKAICPAgFI+zIuYcMF69GmPQVsXa9oy8eng7MeRZdIxArQyeX3oICPEgX5HYIuImpiSTEapgEssEW4l+W+4aRfNCG3pZf7z0hCoICPEgPkAbOSjFdtS4NT7MXgMYVQoQhI1JZtdFxUu4J3NTt7IICPEguW3qyuyGjctu5d9rM9P9ZCs/ZK4vAc+z21b9ygklgWAICPEgu4e2645xtvGhI1Zzuiv23vRhwE8uC9vj1TAgNg/C8UcICPAgwoQX8X0nY4HoQLRsJ0z8JCWQDzRh2iL2QXEb8z3gbjIICPAgzTwlPRtStsLJWhz3Q/0l8tMnrPSHVuh+zCiGk95dW2MICPAgGcBEuYZyzFNapHOfnJ9Q515QzO2VbIRhlVI0vIhd4jwICPAgidRMoM2pA+KmVJenVrLcbollsbUg9lL9bmv1C1dSxswICPAgZinGakwhbHdanTaRJeBkEUlbhfNokvj8b5KneyG+wzIICAAFiJYNc9cZAQOtwTI=\",\"created_at\":1706324334,\"id\":\"2ad074ddb7724eb13b4244b49cf2321b1057f37fdf8ce102e6329b839cf763a9\",\"kind\":1040,\"pubkey\":\"82fbb08c8ef45c4d71c88368d0ae805bc62fb92f166ab04a0b7a0c83d8cbc29a\",\"sig\":\"ad7274bb32ba9e9cfdbd52f4887e8a2fda1047c75a7185b2ab7ff254ebac14ed48a2b60737494d655e24c9400eeeec7e29293a77bfcaafaecd94b350c9a2c22b\",\"tags\":[[\"e\",\"a8634f5368e17789e8fb3836cad0e4b9fe4f2288afafc28723c04bebc68a18d6\"],[\"p\",\"c31e22c3715c1bde5608b7e0d04904f22f5fc453ba1806d21c9f2382e1e58c6c\"],[\"alt\",\"NIP-03 time stamp\"]]}"
val otsPendingEvent = "{\"id\":\"12fa15ad4b4cf9dc5940389325b69b93c5c1f59c049c701ee669b275299fdaf1\",\"pubkey\":\"dcaa6c8a2f47b6fef4a34b20e8843c59dbe7c5f07a402338c09fd147dd01d22b\",\"created_at\":1708877521,\"kind\":1040,\"tags\":[[\"e\",\"a8634f5368e17789e8fb3836cad0e4b9fe4f2288afafc28723c04bebc68a18d6\"],[\"alt\",\"Opentimestamps Attestation\"]],\"content\":\"AE9wZW5UaW1lc3RhbXBzAABQcm9vZgC/ieLohOiSlAEIqGNPU2jhd4no+zg2ytDkuf5PIoivr8KHI8BL68aKGNbwELidvzr0usf55CkpKf6OABQI//AQK3sWd2tq+7KO8YNJIARJugjxBGXbZtPwCL0H4/7GL5+SAIPf4w0u+QyOLCtodHRwczovL2JvYi5idGMuY2FsZW5kYXIub3BlbnRpbWVzdGFtcHMub3Jn//AQPDZsJgN1TnJXoUzlsgo93wjwIIfBc7LUqkCbC1BLZRZ+6LXztK50UdH5xe7fn40bupkrCPEEZdtm0/AI0CADXN5ZIncAg9/jDS75DI4uLWh0dHBzOi8vYWxpY2UuYnRjLmNhbGVuZGFyLm9wZW50aW1lc3RhbXBzLm9yZ/AQcELcSrE04cuGKlZQf2LeVwjwILUDSf9vK2GaefKTpn/LV2oUsQaA5WbqaP3C+1ZxQfRNCPEEZdtm0/AIbCtb+yRXFqUAg9/jDS75DI4pKGh0dHBzOi8vZmlubmV5LmNhbGVuZGFyLmV0ZXJuaXR5d2FsbC5jb20=\",\"sig\":\"f6854c0228c15c08aeb70bbabe9ed87bbb7289fab31b13cabac15138bb71179553e06080b83f4a813fbdaf614f63293beea3fc73fe865da6551193fa4d38de04\"}"
val otsEvent2Digest = "a8634f5368e17789e8fb3836cad0e4b9fe4f2288afafc28723c04bebc68a18d6"
@Test
fun verifyNostrEvent() {
val ots = Event.fromJson(otsEvent) as OtsEvent
println(resolver.info(ots.otsByteArray()))
assertEquals(1707688818L, ots.verify(resolver))
}
@Test
fun verifyNostrEvent2() {
val ots = Event.fromJson(otsEvent2) as OtsEvent
println(resolver.info(ots.otsByteArray()))
assertEquals(1706322179L, ots.verify(resolver))
}
@Test
fun verifyNostrPendingEvent() {
val ots = Event.fromJson(otsPendingEvent) as OtsEvent
println(resolver.info(ots.otsByteArray()))
assertEquals(null, ots.verify(resolver))
val eventId =
ots.digestEventId() ?: run {
fail("Should not be null")
return
}
val upgraded = OtsEvent.upgrade(ots.otsByteArray(), eventId, resolver)
assertNotNull(upgraded)
val signer = NostrSignerInternal(KeyPair())
val newOts = runBlocking { signer.sign(OtsEvent.build(eventId, upgraded!!)) }
println(newOts.toJson())
println(resolver.info(newOts.otsByteArray()))
assertEquals(1708879025L, newOts.verify(resolver))
}
@Test
fun createOTSEventAndVerify() {
val signer = NostrSignerInternal(KeyPair())
val ots =
runBlocking {
signer.sign(OtsEvent.build(otsEvent2Digest, OtsEvent.stamp(otsEvent2Digest, resolver)))
}
println(ots.toJson())
println(resolver.info(ots.otsByteArray()))
assertEquals(null, ots.verify(resolver))
}
}
@@ -0,0 +1,113 @@
/**
* Copyright (c) 2025 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
* Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.quartz.nip04Dm
import com.vitorpamplona.quartz.nip01Core.core.hexToByteArray
import com.vitorpamplona.quartz.nip01Core.core.toHexKey
import com.vitorpamplona.quartz.nip01Core.crypto.Nip01
import com.vitorpamplona.quartz.nip04Dm.crypto.EncryptedInfo
import com.vitorpamplona.quartz.nip04Dm.crypto.Encryption
import junit.framework.TestCase.assertEquals
import junit.framework.TestCase.assertTrue
import org.junit.Test
class EncryptionTest {
private val nip04 = Encryption()
val sk1 = "91ba716fa9e7ea2fcbad360cf4f8e0d312f73984da63d90f524ad61a6a1e7dbe".hexToByteArray()
val sk2 = "96f6fa197aa07477ab88f6981118466ae3a982faab8ad5db9d5426870c73d220".hexToByteArray()
val pk1 = Nip01.pubKeyCreate(sk1)
val pk2 = Nip01.pubKeyCreate(sk2)
val expectedShared = "7ce22696eb0e303ddaa491bdf2a56b79d249f2d861b8e012a933e01dc4beba81"
@Test
fun conversationKeyTest() {
assertEquals(
expectedShared,
nip04.computeSharedSecret(sk2, pk1).toHexKey(),
)
assertEquals(
expectedShared,
nip04.computeSharedSecret(sk1, pk2).toHexKey(),
)
}
@Test
fun encryptDecryptTest() {
val message = "testing"
val cipher = nip04.encrypt(message, sk2, pk1)
assertEquals(message, nip04.decrypt(cipher, sk2, pk1))
assertEquals(message, nip04.decrypt(cipher, sk1, pk2))
val cipher2 = nip04.encrypt(message, sk1, pk2)
assertEquals(message, nip04.decrypt(cipher2, sk2, pk1))
assertEquals(message, nip04.decrypt(cipher2, sk1, pk2))
}
@Test
fun decryptTest() {
val cipher = "zJxfaJ32rN5Dg1ODjOlEew==?iv=EV5bUjcc4OX2Km/zPp4ndQ=="
assertEquals("nanana", nip04.decrypt(cipher, nip04.computeSharedSecret(sk2, pk1)))
assertEquals("nanana", nip04.decrypt(cipher, nip04.computeSharedSecret(sk1, pk2)))
}
@Test
fun decryptLargePayloadTest() {
val ciphertext =
"6f8dMstm+udOu7yipSn33orTmwQpWbtfuY95NH+eTU1kArysWJIDkYgI2D25EAGIDJsNd45jOJ2NbVOhFiL3ZP/NWsTwXokk34iyHyA/lkjzugQ1bHXoMD1fP/Ay4hB4al1NHb8HXHKZaxPrErwdRDb8qa/I6dXb/1xxyVvNQBHHvmsM5yIFaPwnCN1DZqXf2KbTA/Ekz7Hy+7R+Sy3TXLQDFpWYqykppkXc7Fs0qSuPRyxz5+anuN0dxZa9GTwTEnBrZPbthKkNRrvZMdTGJ6WumOh9aUq8OJJWy9aOgsXvs7qjN1UqcCqQqYaVnEOhCaqWNDsVtsFrVDj+SaLIBvCiomwF4C4nIgngJ5I69tx0UNI0q+ZnvOGQZ7m1PpW2NYP7Yw43HJNdeUEQAmdCPnh/PJwzLTnIxHmQU7n7SPlMdV0SFa6H8y2HHvex697GAkyE5t8c2uO24OnqIwF1tR3blIqXzTSRl0GA6QvrSj2p4UtnWjvF7xT7RiIEyTtgU/AsihTrXyXzWWZaIBJogpgw6erlZqWjCH7sZy/WoGYEiblobOAqMYxax6vRbeuGtoYksr/myX+x9rfLrYuoDRTw4woXOLmMrrj+Mf0TbAgc3SjdkqdsPU1553rlSqIEZXuFgoWmxvVQDtekgTYyS97G81TDSK9nTJT5ilku8NVq2LgtBXGwsNIw/xekcOUzJke3kpnFPutNaexR1VF3ohIuqRKYRGcd8ADJP2lfwMcaGRiplAmFoaVS1YUhQwYFNq9rMLf7YauRGV4BJg/t9srdGxf5RoKCvRo+XM/nLxxysTR9MVaEP/3lDqjwChMxs+eWfLHE5vRWV8hUEqdrWNZV29gsx5nQpzJ4PARGZVu310pQzc6JAlc2XAhhFk6RamkYJnmCSMnb/RblzIATBi2kNrCVAlaXIon188inB62rEpZGPkRIP7PUfu27S/elLQHBHeGDsxOXsBRo1gl3te+raoBHsxo6zvRnYbwdAQa5taDE63eh+fT6kFI+xYmXNAQkU8Dp0MVhEh4JQI06Ni/AKrvYpC95TXXIphZcF+/Pv/vaGkhG2X9S3uhugwWK?iv=2vWkOQQi0WynNJz/aZ4k2g=="
val expected = "zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz"
assertEquals(expected, nip04.decrypt(ciphertext, nip04.computeSharedSecret(sk2, pk1)))
assertEquals(expected, nip04.decrypt(ciphertext, nip04.computeSharedSecret(sk1, pk2)))
}
@Test
fun isNIP04Encode() {
assertTrue(EncryptedInfo.isNIP04("Xj/oZZolaItdyQ5v7xYFpA==?iv=+a6zagBp+mr5m1aFbHQ8lA=="))
assertTrue(EncryptedInfo.isNIP04("zJxfaJ32rN5Dg1ODjOlEew==?iv=EV5bUjcc4OX2Km/zPp4ndQ=="))
assertTrue(
EncryptedInfo.isNIP04("6f8dMstm+udOu7yipSn33orTmwQpWbtfuY95NH+eTU1kArysWJIDkYgI2D25EAGIDJsNd45jOJ2NbVOhFiL3ZP/NWsTwXokk34iyHyA/lkjzugQ1bHXoMD1fP/Ay4hB4al1NHb8HXHKZaxPrErwdRDb8qa/I6dXb/1xxyVvNQBHHvmsM5yIFaPwnCN1DZqXf2KbTA/Ekz7Hy+7R+Sy3TXLQDFpWYqykppkXc7Fs0qSuPRyxz5+anuN0dxZa9GTwTEnBrZPbthKkNRrvZMdTGJ6WumOh9aUq8OJJWy9aOgsXvs7qjN1UqcCqQqYaVnEOhCaqWNDsVtsFrVDj+SaLIBvCiomwF4C4nIgngJ5I69tx0UNI0q+ZnvOGQZ7m1PpW2NYP7Yw43HJNdeUEQAmdCPnh/PJwzLTnIxHmQU7n7SPlMdV0SFa6H8y2HHvex697GAkyE5t8c2uO24OnqIwF1tR3blIqXzTSRl0GA6QvrSj2p4UtnWjvF7xT7RiIEyTtgU/AsihTrXyXzWWZaIBJogpgw6erlZqWjCH7sZy/WoGYEiblobOAqMYxax6vRbeuGtoYksr/myX+x9rfLrYuoDRTw4woXOLmMrrj+Mf0TbAgc3SjdkqdsPU1553rlSqIEZXuFgoWmxvVQDtekgTYyS97G81TDSK9nTJT5ilku8NVq2LgtBXGwsNIw/xekcOUzJke3kpnFPutNaexR1VF3ohIuqRKYRGcd8ADJP2lfwMcaGRiplAmFoaVS1YUhQwYFNq9rMLf7YauRGV4BJg/t9srdGxf5RoKCvRo+XM/nLxxysTR9MVaEP/3lDqjwChMxs+eWfLHE5vRWV8hUEqdrWNZV29gsx5nQpzJ4PARGZVu310pQzc6JAlc2XAhhFk6RamkYJnmCSMnb/RblzIATBi2kNrCVAlaXIon188inB62rEpZGPkRIP7PUfu27S/elLQHBHeGDsxOXsBRo1gl3te+raoBHsxo6zvRnYbwdAQa5taDE63eh+fT6kFI+xYmXNAQkU8Dp0MVhEh4JQI06Ni/AKrvYpC95TXXIphZcF+/Pv/vaGkhG2X9S3uhugwWK?iv=2vWkOQQi0WynNJz/aZ4k2g=="),
)
}
@Test
fun isNIP04EncodeWithBug() {
assertTrue(
EncryptedInfo.isNIP04(
"QOAYBWa88ConWs2C4kSvNqAcowCtg0ZRtAl7FyLSv9VMaJH4oCiDx0h8VLBnV97HdE4lv" +
"TW7AYC1eEw8/t1dbe0qRc3XrOt7MrPAO8yqpy1/3lFB1+10kip0+KdgT8Quvv02wTP8Dqi" +
"xpr2fliAIG2ONvDn+O5V0q9aVUN9HitgL/myTyR0T42edmxWeZoMBEOKvJyO80FekSsgVL" +
"ASafA/T5z4xs8oG88pSe9wSbSsw0xNjJeh3xLRCLuEuA9KI8hQ1Ys9nEax2UlaB/IL3o77" +
"OwBL+rrdUbNHTxYifgygRhg3BaXMsXRFNJbqYeMaRaNbvHkLVAQV2jLY4P/cKHBjEcTC/f" +
"lrCc2NCYF34rOQUY5EJVnFzM8qYVw6xNupBHTS7WFx1r60cPjG19P/+yoiTZ6bPdHTU0X2" +
"t64ovF2YWUq6/iKAclMaZDhWfrKqf82e62oIff55WQw2bw8A/jtBQVCf66EtEJ2OSFxNaZ" +
"rO+A4oLkHDCnAV+6fYzwo89gPOvORcVvSvg55yGiBFUZx9EHS6kdH1SU80/Mbxe2oI=" +
"?iv=gxz9pUFJFZHuV+D+hgKEOw==-null",
),
)
}
}
@@ -0,0 +1,57 @@
/**
* Copyright (c) 2025 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
* Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.quartz.nip04Dm
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.vitorpamplona.quartz.nip01Core.crypto.Nip01
import com.vitorpamplona.quartz.nip04Dm.crypto.Nip04
import org.junit.Assert.assertEquals
import org.junit.Test
import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
class Nip04Test {
@Test
fun encryptDecryptNIP4Test() {
val msg = "Hi"
val privateKey = Nip01.privKeyCreate()
val publicKey = Nip01.pubKeyCreate(privateKey)
val encrypted = Nip04.encrypt(msg, privateKey, publicKey)
val decrypted = Nip04.decrypt(encrypted, privateKey, publicKey)
assertEquals(msg, decrypted)
}
@Test
fun encryptSharedSecretDecryptNIP4Test() {
val msg = "Hi"
val privateKey = Nip01.privKeyCreate()
val publicKey = Nip01.pubKeyCreate(privateKey)
val encrypted = Nip04.encrypt(msg, privateKey, publicKey)
val decrypted = Nip04.decrypt(encrypted, privateKey, publicKey)
assertEquals(msg, decrypted)
}
}
@@ -0,0 +1,81 @@
/**
* Copyright (c) 2025 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
* Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.quartz.nip06KeyDerivation
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.vitorpamplona.quartz.nip01Core.core.toHexKey
import junit.framework.TestCase.assertEquals
import org.junit.Test
import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
class Bip32SeedDerivationTest {
val seedDerivation = Bip32SeedDerivation()
val masterBitcoin =
seedDerivation.generate(
Bip39Mnemonics.toSeed("gun please vital unable phone catalog explain raise erosion zoo truly exist", ""),
)
val nostrMnemonic0 =
seedDerivation.generate(
Bip39Mnemonics.toSeed("leader monkey parrot ring guide accident before fence cannon height naive bean", ""),
)
val nostrMnemonic1 =
seedDerivation.generate(
Bip39Mnemonics.toSeed("what bleak badge arrange retreat wolf trade produce cricket blur garlic valid proud rude strong choose busy staff weather area salt hollow arm fade", ""),
)
@Test
fun restoreBIP44Wallet() {
val privateKey = seedDerivation.derivePrivateKey(masterBitcoin, KeyPath("m/44'/1'/0'"))
assertEquals("50b3e7905c642309c8a8b73df5a49757a10f2bebb5804571b9db9004cce8a190", privateKey.toHexKey())
}
@Test
fun restoreBIP49Wallet() {
val privateKey = seedDerivation.derivePrivateKey(masterBitcoin, KeyPath("m/49'/1'/0'"))
assertEquals("154c02c0b66899291a19012207642ba096a2d3ebf51baf153c9495976feb1b30", privateKey.toHexKey())
}
@Test
fun restoreBIP84Wallet() {
val privateKey = seedDerivation.derivePrivateKey(masterBitcoin, KeyPath("m/84'/1'/0'"))
assertEquals("53e8c09a0e3ddcd8d68821c1e99e823966e99df91fb253e1f453a443ba543cb2", privateKey.toHexKey())
}
@Test
fun testGenerateMasterKeyForNostrMnemonics0() {
assertEquals(
"dbbcc0e112894d1430d5bc348d1bd72e8ac339952702be1fe572de80fe1b7fcb",
nostrMnemonic0.secretkeybytes.toHexKey(),
)
}
@Test
fun testGenerateMasterKeyForNostrMnemonics1() {
assertEquals(
"d58d40d5724435552fa442350b75e0ff95a19d990e908e3a516bcc88f780108f",
nostrMnemonic1.secretkeybytes.toHexKey(),
)
}
}
@@ -0,0 +1,68 @@
/**
* Copyright (c) 2025 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
* Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.quartz.nip06KeyDerivation
import androidx.test.ext.junit.runners.AndroidJUnit4
import junit.framework.TestCase.assertEquals
import org.junit.Test
import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
class Bip39KeyPathTest {
// m/44'/1237'/<account>'/0/0
private val nip6Base: KeyPath =
KeyPath("")
.derive(Hardener.hardened(44L))
.derive(Hardener.hardened(1237L))
private fun nip6Path(account: Long): KeyPath =
nip6Base
.derive(Hardener.hardened(account))
.derive(0L)
.derive(0L)
@Test
fun testKeyPath() {
// m/44'/1237'/<account>'/0/0
val path = nip6Path(0).path
assertEquals(2147483692L, path[0])
assertEquals(2147484885L, path[1])
assertEquals(2147483648L, path[2])
assertEquals(0L, path[3])
assertEquals(0L, path[4])
}
@Test
fun testKeyDecompiledPath() {
// m/44'/1237'/<account>'/0/0
val path = KeyPath.computePath("m/44'/1237'/0'/0/0")
assertEquals(2147483692L, path[0])
assertEquals(2147484885L, path[1])
assertEquals(2147483648L, path[2])
assertEquals(0L, path[3])
assertEquals(0L, path[4])
}
@Test
fun testHardening() {
assertEquals(2147483692, Hardener.hardened(44))
}
}
@@ -0,0 +1,123 @@
/**
* Copyright (c) 2025 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
* Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.quartz.nip06KeyDerivation
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import com.vitorpamplona.quartz.nip01Core.core.toHexKey
import com.vitorpamplona.quartz.utils.Hex
import com.vitorpamplona.quartz.utils.RandomInstance
import junit.framework.TestCase.assertEquals
import junit.framework.TestCase.assertTrue
import junit.framework.TestCase.fail
import org.junit.Test
import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
class Bip39MnemonicsTest {
private val tests =
jacksonObjectMapper()
.readTree(
InstrumentationRegistry
.getInstrumentation()
.context.assets
.open("bip39.vectors.json"),
)
@Test
fun toSeed() {
val mnemonics = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"
val passphrase = ""
val seed = Bip39Mnemonics.toSeed(mnemonics, passphrase)
assertEquals(
"5eb00bbddcf069084889a8ab9155568165f5c453ccb85e70811aaed6f6da5fc19a5ac40b389cd370d086206dec8aa6c43daea6690f20ad3d8d48b2d2ce9e38e4",
Hex.encode(seed),
)
}
@Test
fun referenceTests() {
tests.get("english").map {
val raw = it.get(0).asText()
val mnemonics = it.get(1).asText()
val seed = it.get(2).asText()
assertEquals(Bip39Mnemonics.toMnemonics(Hex.decode(raw)).joinToString(" "), mnemonics)
assertEquals(Hex.encode(Bip39Mnemonics.toSeed(Bip39Mnemonics.toMnemonics(Hex.decode(raw)), "TREZOR")), seed)
}
}
@Test
fun validateMnemonicsValid() {
for (i in 0..99) {
for (length in listOf(16, 20, 24, 28, 32, 36, 40)) {
val mnemonics = Bip39Mnemonics.toMnemonics(RandomInstance.bytes(length))
Bip39Mnemonics.validate(mnemonics)
}
}
}
@Test
fun testNostrMnemonics1() {
assertEquals(
"173b9c5f0d165502d08a4d122b2c9bf1e33e27806eac119713600a263c1241101dc55fb7cffb8f48a59b19a5ba65b037904f907bb8d08eb5bff8a17e85c2ee93",
Bip39Mnemonics.toSeed("leader monkey parrot ring guide accident before fence cannon height naive bean", "").toHexKey(),
)
}
@Test
fun testNostrMnemonics2() {
assertEquals(
"5e2bd11b4d371f25098ed95ded029e2b9268cf188e6b764023bafbbd8fe843244fb72ca8f66c9378085d69fcb4d4224e709ffe071acafa7b7d5eb54b2905d553",
Bip39Mnemonics.toSeed("what bleak badge arrange retreat wolf trade produce cricket blur garlic valid proud rude strong choose busy staff weather area salt hollow arm fade", "").toHexKey(),
)
}
@Test
fun testSnortMnemonics() {
assertEquals(
"b8fb0abca0a032d74b6a58def8ba51402810528d0a7bb9acec8fa1880e1003e13c4421b916cb4c8a861f2694b255010fdc91c2d1ac24c8ce51287ac65d1eb399",
Bip39Mnemonics.toSeed("clog remember sample endorse mountain key rib hurry question supreme palm future stage style swing faith erase thumb then warm truth mule vivid endless", "").toHexKey(),
)
}
@Test
fun validateMnemonicsInvalid() {
val invalidMnemonics =
listOf(
"",
// one word missing
"gravity machine north sort system female filter attitude volume fold club stay feature office ecology stable narrow",
// one extra word
"gravity machine north sort system female filter attitude volume fold club stay feature office ecology stable narrow fog fog",
// wrong word
"gravity machine north sort system female filter attitude volume fold club stay feature office ecology stable narrow fig",
)
invalidMnemonics.map {
try {
Bip39Mnemonics.validate(it)
fail()
} catch (e: Exception) {
assertTrue(true)
}
}
}
}
@@ -0,0 +1,70 @@
/**
* Copyright (c) 2025 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
* Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.quartz.nip06KeyDerivation
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.vitorpamplona.quartz.nip01Core.core.toHexKey
import junit.framework.TestCase.assertEquals
import org.junit.Ignore
import org.junit.Test
import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
class Nip06Test {
val nip06 = Nip06()
// private key (hex): 7f7ff03d123792d6ac594bfa67bf6d0c0ab55b6b1fdb6249303fe861f1ccba9a
// nsec: nsec10allq0gjx7fddtzef0ax00mdps9t2kmtrldkyjfs8l5xruwvh2dq0lhhkp
private val menemonic0 = "leader monkey parrot ring guide accident before fence cannon height naive bean"
// private key (hex): c15d739894c81a2fcfd3a2df85a0d2c0dbc47a280d092799f144d73d7ae78add
// nsec: nsec1c9wh8xy5eqdzln7n5t0ctgxjcrdug73gp5yj0x03gntn67h83twssdfhel
private val menemonic1 = "what bleak badge arrange retreat wolf trade produce cricket blur garlic valid proud rude strong choose busy staff weather area salt hollow arm fade"
// private key (hex): c15d739894c81a2fcfd3a2df85a0d2c0dbc47a280d092799f144d73d7ae78add
// nsec: nsec1c9wh8xy5eqdzln7n5t0ctgxjcrdug73gp5yj0x03gntn67h83twssdfhel
private val snortTest = "clog remember sample endorse mountain key rib hurry question supreme palm future stage style swing faith erase thumb then warm truth mule vivid endless"
@Test
fun fromSeedNip06TestVector0() {
val privateKeyHex = nip06.privateKeyFromMnemonic(menemonic0).toHexKey()
assertEquals("7f7ff03d123792d6ac594bfa67bf6d0c0ab55b6b1fdb6249303fe861f1ccba9a", privateKeyHex)
val privateKeyHex21 = nip06.privateKeyFromMnemonic(menemonic0, 21).toHexKey()
assertEquals("576390ec69951fcfbf159f2aac0965bb2e6d7a07da2334992af3225c57eaefca", privateKeyHex21)
}
@Test
fun fromSeedNip06TestVector1() {
val privateKeyHex = nip06.privateKeyFromMnemonic(menemonic1).toHexKey()
assertEquals("c15d739894c81a2fcfd3a2df85a0d2c0dbc47a280d092799f144d73d7ae78add", privateKeyHex)
val privateKeyHex21 = nip06.privateKeyFromMnemonic(menemonic1, 42).toHexKey()
assertEquals("ad993054383da74e955f8b86346365b5ffd6575992e1de3738dda9f94407052b", privateKeyHex21)
}
@Test
@Ignore("Snort is not correctly implemented")
fun fromSeedNip06FromSnort() {
val privateKeyNsec = nip06.privateKeyFromMnemonic(snortTest).toHexKey()
assertEquals("nsec1ppw9ltr2x9qwg9a2qnmgv98tfruy2ejnja7me76mwmsreu3s8u2sscj5nt", privateKeyNsec)
}
}
@@ -0,0 +1,146 @@
/**
* Copyright (c) 2025 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
* Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.quartz.nip10Notes
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.vitorpamplona.quartz.nip01Core.tags.events.ETag
import com.vitorpamplona.quartz.nip01Core.tags.events.taggedEventIds
import com.vitorpamplona.quartz.nip01Core.tags.events.taggedEvents
import com.vitorpamplona.quartz.nip01Core.tags.people.PTag
import com.vitorpamplona.quartz.nip01Core.tags.people.taggedUserIds
import com.vitorpamplona.quartz.nip01Core.tags.people.taggedUsers
import com.vitorpamplona.quartz.nip01Core.verify
import junit.framework.TestCase.assertEquals
import junit.framework.TestCase.assertTrue
import org.junit.Test
import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
class CitationTests {
val note =
TextNoteEvent(
"98b574c3527f0ffb30b7271084e3f07480733c7289f8de424d29eae82e36c758",
"46fcbe3065eaf1ae7811465924e48923363ff3f526bd6f73d7c184b16bd8ce4d",
1683596206,
arrayOf(
arrayOf("e", "27ac621d7dc4a932e1a79f984308e7d20656dd6fddb2ce9cdfcb6a67b9a7bcc3", "", "root"),
arrayOf("e", "be7245af96210a0dd048cab4ad38e52dbd6c09a53ea21a7edb6be8898e5727cc", "", "reply"),
arrayOf("p", "22aa81510ee63fe2b16cae16e0921f78e9ba9882e2868e7e63ad6d08ae9b5954"),
arrayOf("p", "22aa81510ee63fe2b16cae16e0921f78e9ba9882e2868e7e63ad6d08ae9b5954"),
arrayOf("p", "3f770d65d3a764a9c5cb503ae123e62ec7598ad035d836e2a810f3877a745b24"),
arrayOf("p", "ec4d241c334311b3a304433ee3442be29d0e88e7ec19b85edf2bba29b93565e2"),
arrayOf("p", "0fe0b18b4dbf0e0aa40fcd47209b2a49b3431fc453b460efcf45ca0bd16bd6ac"),
arrayOf("p", "8c0da4862130283ff9e67d889df264177a508974e2feb96de139804ea66d6168"),
arrayOf("p", "63fe6318dc58583cfe16810f86dd09e18bfd76aabc24a0081ce2856f330504ed"),
arrayOf("p", "4523be58d395b1b196a9b8c82b038b6895cb02b683d0c253a955068dba1facd0"),
arrayOf("p", "460c25e682fda7832b52d1f22d3d22b3176d972f60dcdc3212ed8c92ef85065c"),
),
"Astral:\n\nhttps://void.cat/d/A5Fba5B1bcxwEmeyoD9nBs.webp\n\nIris:\n\nhttps://void.cat/d/44hTcVvhRps6xYYs99QsqA.webp\n\nSnort:\n\nhttps://void.cat/d/4nJD5TRePuQChM5tzteYbU.webp\n\nAmethyst agrees with Astral which I suspect are both wrong. nostr:npub13sx6fp3pxq5rl70x0kyfmunyzaa9pzt5utltjm0p8xqyafndv95q3saapa nostr:npub1v0lxxxxutpvrelsksy8cdhgfux9l6a42hsj2qzquu2zk7vc9qnkszrqj49 nostr:npub1g53mukxnjkcmr94fhryzkqutdz2ukq4ks0gvy5af25rgmwsl4ngq43drvk nostr:npub1gcxzte5zlkncx26j68ez60fzkvtkm9e0vrwdcvsjakxf9mu9qewqlfnj5z ",
"4aa5264965018fa12a326686ad3d3bd8beae3218dcc83689b19ca1e6baeb791531943c15363aa6707c7c0c8b2d601deca1f20c32078b2872d356cdca03b04cce",
)
@Test
fun verifyEvent() {
assertTrue(note.verify())
}
@Test
fun testCitedUsers() {
val expectedCitations =
setOf(
"8c0da4862130283ff9e67d889df264177a508974e2feb96de139804ea66d6168",
"63fe6318dc58583cfe16810f86dd09e18bfd76aabc24a0081ce2856f330504ed",
"4523be58d395b1b196a9b8c82b038b6895cb02b683d0c253a955068dba1facd0",
"460c25e682fda7832b52d1f22d3d22b3176d972f60dcdc3212ed8c92ef85065c",
)
assertEquals(expectedCitations, note.citedUsers())
}
@Test
fun testTagsWithoutCitations() {
val expectedTagsWithoutCitations =
listOf(
"27ac621d7dc4a932e1a79f984308e7d20656dd6fddb2ce9cdfcb6a67b9a7bcc3",
"be7245af96210a0dd048cab4ad38e52dbd6c09a53ea21a7edb6be8898e5727cc",
)
assertEquals(expectedTagsWithoutCitations, note.tagsWithoutCitations())
}
@Test
fun testTaggedEventIds() {
val eventIds =
listOf(
"27ac621d7dc4a932e1a79f984308e7d20656dd6fddb2ce9cdfcb6a67b9a7bcc3",
"be7245af96210a0dd048cab4ad38e52dbd6c09a53ea21a7edb6be8898e5727cc",
)
assertEquals(eventIds, note.taggedEventIds())
}
@Test
fun testTaggedEvents() {
val eventIds =
listOf(
ETag("27ac621d7dc4a932e1a79f984308e7d20656dd6fddb2ce9cdfcb6a67b9a7bcc3"),
ETag("be7245af96210a0dd048cab4ad38e52dbd6c09a53ea21a7edb6be8898e5727cc"),
)
assertEquals(eventIds, note.taggedEvents())
}
@Test
fun testTaggedUsers() {
val eventIds =
listOf(
PTag("22aa81510ee63fe2b16cae16e0921f78e9ba9882e2868e7e63ad6d08ae9b5954"),
PTag("22aa81510ee63fe2b16cae16e0921f78e9ba9882e2868e7e63ad6d08ae9b5954"),
PTag("3f770d65d3a764a9c5cb503ae123e62ec7598ad035d836e2a810f3877a745b24"),
PTag("ec4d241c334311b3a304433ee3442be29d0e88e7ec19b85edf2bba29b93565e2"),
PTag("0fe0b18b4dbf0e0aa40fcd47209b2a49b3431fc453b460efcf45ca0bd16bd6ac"),
PTag("8c0da4862130283ff9e67d889df264177a508974e2feb96de139804ea66d6168"),
PTag("63fe6318dc58583cfe16810f86dd09e18bfd76aabc24a0081ce2856f330504ed"),
PTag("4523be58d395b1b196a9b8c82b038b6895cb02b683d0c253a955068dba1facd0"),
PTag("460c25e682fda7832b52d1f22d3d22b3176d972f60dcdc3212ed8c92ef85065c"),
)
assertEquals(eventIds, note.taggedUsers())
}
@Test
fun testTaggedUserIds() {
val eventIds =
listOf(
"22aa81510ee63fe2b16cae16e0921f78e9ba9882e2868e7e63ad6d08ae9b5954",
"22aa81510ee63fe2b16cae16e0921f78e9ba9882e2868e7e63ad6d08ae9b5954",
"3f770d65d3a764a9c5cb503ae123e62ec7598ad035d836e2a810f3877a745b24",
"ec4d241c334311b3a304433ee3442be29d0e88e7ec19b85edf2bba29b93565e2",
"0fe0b18b4dbf0e0aa40fcd47209b2a49b3431fc453b460efcf45ca0bd16bd6ac",
"8c0da4862130283ff9e67d889df264177a508974e2feb96de139804ea66d6168",
"63fe6318dc58583cfe16810f86dd09e18bfd76aabc24a0081ce2856f330504ed",
"4523be58d395b1b196a9b8c82b038b6895cb02b683d0c253a955068dba1facd0",
"460c25e682fda7832b52d1f22d3d22b3176d972f60dcdc3212ed8c92ef85065c",
)
assertEquals(eventIds, note.taggedUserIds())
}
}
@@ -0,0 +1,341 @@
/**
* Copyright (c) 2025 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
* Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.quartz.nip10Notes
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.RelayUrlNormalizer
import com.vitorpamplona.quartz.nip01Core.tags.people.PTag
import com.vitorpamplona.quartz.nip01Core.tags.people.taggedUsers
import com.vitorpamplona.quartz.nip10Notes.tags.MarkedETag
import junit.framework.TestCase.assertEquals
import org.junit.Test
import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
class ThreadingTests {
@Test
fun testLegacyEvent() {
val note =
TextNoteEvent(
"",
"",
0,
arrayOf(
arrayOf("p", "4ca4f5533e40da5e0508796d409e6bb35a50b26fc304345617ab017183d83ac0"),
arrayOf("p", "534780e44da7b494485e85cd4cca6af4f6caa1627472432b6f2a4ece0e9e54ec"),
arrayOf("p", "77ce56f89d1228f7ff3743ce1ad1b254857b9008564727ebd5a1f317362f6ca7"),
arrayOf("e", "89f220b63465c93542b1a78caa3a952cf4f196e91a50596493c8093c533ebc4d"),
arrayOf("e", "090c037b2e399ee74d9f134758928948dd9154413ca1a1acb37155046e03a051"),
arrayOf("e", "567b7c11f0fe582361e3cea6fcc7609a8942dfe196ee1b98d5604c93fbeea976"),
arrayOf("e", "49aff7ae6daeaaa2777931b90f9bb29f6cb01c5a3d7d88c8ba82d890f264afb4"),
arrayOf("e", "5e081ebb19153357d7c31e8a10b9ceeef29313f58dc8d701f66727fab02aef64"),
arrayOf("e", "bbd72f0ae14374aa8fb166b483cfcf99b57d7f4cf1600ccbf17c350040834631"),
arrayOf("e", "b857504288c18a15950dd05b9e8772c62ca6289d5aac373c0a8ee5b132e94e7c"),
),
"",
"",
)
val taggedUsers =
listOf(
PTag("4ca4f5533e40da5e0508796d409e6bb35a50b26fc304345617ab017183d83ac0"),
PTag("534780e44da7b494485e85cd4cca6af4f6caa1627472432b6f2a4ece0e9e54ec"),
PTag("77ce56f89d1228f7ff3743ce1ad1b254857b9008564727ebd5a1f317362f6ca7"),
)
assertEquals(taggedUsers, note.taggedUsers())
val expectedReply = MarkedETag("b857504288c18a15950dd05b9e8772c62ca6289d5aac373c0a8ee5b132e94e7c", null, MarkedETag.MARKER.REPLY)
// check replies
assertEquals(null, note.markedReply())
assertEquals(expectedReply, note.unmarkedReply())
assertEquals(expectedReply, note.reply())
val replyTos =
listOf(
"89f220b63465c93542b1a78caa3a952cf4f196e91a50596493c8093c533ebc4d",
"090c037b2e399ee74d9f134758928948dd9154413ca1a1acb37155046e03a051",
"567b7c11f0fe582361e3cea6fcc7609a8942dfe196ee1b98d5604c93fbeea976",
"49aff7ae6daeaaa2777931b90f9bb29f6cb01c5a3d7d88c8ba82d890f264afb4",
"5e081ebb19153357d7c31e8a10b9ceeef29313f58dc8d701f66727fab02aef64",
"bbd72f0ae14374aa8fb166b483cfcf99b57d7f4cf1600ccbf17c350040834631",
"b857504288c18a15950dd05b9e8772c62ca6289d5aac373c0a8ee5b132e94e7c",
)
assertEquals(emptyList<String>(), note.markedReplyTos())
assertEquals(replyTos, note.unmarkedReplyTos())
assertEquals(expectedReply.eventId, note.replyingTo())
assertEquals(expectedReply.eventId, note.replyingToAddressOrEvent())
// check root
val expectedRoot = MarkedETag("89f220b63465c93542b1a78caa3a952cf4f196e91a50596493c8093c533ebc4d", null, MarkedETag.MARKER.ROOT)
assertEquals(null, note.markedRoot())
assertEquals(expectedRoot, note.unmarkedRoot())
assertEquals(expectedRoot, note.root())
// check quotes
assertEquals(emptySet<String>(), note.citedUsers())
assertEquals(emptySet<String>(), note.findCitations())
assertEquals(replyTos, note.tagsWithoutCitations())
}
@Test
fun testMarkedEvent() {
val note =
TextNoteEvent(
"",
"",
0,
arrayOf(
arrayOf("p", "4ca4f5533e40da5e0508796d409e6bb35a50b26fc304345617ab017183d83ac0"),
arrayOf("p", "534780e44da7b494485e85cd4cca6af4f6caa1627472432b6f2a4ece0e9e54ec"),
arrayOf("e", "77ce56f89d1228f7ff3743ce1ad1b254857b9008564727ebd5a1f317362f6ca7"),
arrayOf("e", "bbd72f0ae14374aa8fb166b483cfcf99b57d7f4cf1600ccbf17c350040834631", "", "root"),
arrayOf("e", "b857504288c18a15950dd05b9e8772c62ca6289d5aac373c0a8ee5b132e94e7c", "", "reply"),
),
"",
"",
)
val taggedUsers =
listOf(
PTag("4ca4f5533e40da5e0508796d409e6bb35a50b26fc304345617ab017183d83ac0"),
PTag("534780e44da7b494485e85cd4cca6af4f6caa1627472432b6f2a4ece0e9e54ec"),
)
assertEquals(taggedUsers, note.taggedUsers())
val expectedReply = MarkedETag("b857504288c18a15950dd05b9e8772c62ca6289d5aac373c0a8ee5b132e94e7c", null, MarkedETag.MARKER.REPLY)
val expectedReplyWrong = MarkedETag("77ce56f89d1228f7ff3743ce1ad1b254857b9008564727ebd5a1f317362f6ca7", null, MarkedETag.MARKER.REPLY)
// check replies
assertEquals(expectedReply, note.markedReply())
assertEquals(expectedReplyWrong, note.unmarkedReply())
assertEquals(expectedReply, note.reply())
val markedReplies =
listOf(
"bbd72f0ae14374aa8fb166b483cfcf99b57d7f4cf1600ccbf17c350040834631",
"b857504288c18a15950dd05b9e8772c62ca6289d5aac373c0a8ee5b132e94e7c",
)
val unmarkedReplies =
listOf(
"77ce56f89d1228f7ff3743ce1ad1b254857b9008564727ebd5a1f317362f6ca7",
)
assertEquals(markedReplies, note.markedReplyTos())
assertEquals(unmarkedReplies, note.unmarkedReplyTos())
assertEquals(expectedReply.eventId, note.replyingTo())
assertEquals(expectedReply.eventId, note.replyingToAddressOrEvent())
// check root
val expectedRoot = MarkedETag("bbd72f0ae14374aa8fb166b483cfcf99b57d7f4cf1600ccbf17c350040834631", null, MarkedETag.MARKER.ROOT)
val expectedRootWrong = MarkedETag("77ce56f89d1228f7ff3743ce1ad1b254857b9008564727ebd5a1f317362f6ca7", null, MarkedETag.MARKER.ROOT)
assertEquals(expectedRoot, note.markedRoot())
assertEquals(expectedRootWrong, note.unmarkedRoot())
assertEquals(expectedRoot, note.root())
// check quotes
assertEquals(emptySet<String>(), note.citedUsers())
assertEquals(emptySet<String>(), note.findCitations())
assertEquals(markedReplies, note.tagsWithoutCitations())
}
@Test
fun testMarkedInverted() {
val note =
TextNoteEvent(
"",
"",
0,
arrayOf(
arrayOf("p", "4ca4f5533e40da5e0508796d409e6bb35a50b26fc304345617ab017183d83ac0", "wss://goiaba.com"),
arrayOf("p", "534780e44da7b494485e85cd4cca6af4f6caa1627472432b6f2a4ece0e9e54ec"),
arrayOf("p", "77ce56f89d1228f7ff3743ce1ad1b254857b9008564727ebd5a1f317362f6ca7"),
arrayOf("e", "5e081ebb19153357d7c31e8a10b9ceeef29313f58dc8d701f66727fab02aef64", "", "reply"),
arrayOf("e", "bbd72f0ae14374aa8fb166b483cfcf99b57d7f4cf1600ccbf17c350040834631", "wss://banana.com", "root", "4ca4f5533e40da5e0508796d409e6bb35a50b26fc304345617ab017183d83ac0"),
arrayOf("e", "b857504288c18a15950dd05b9e8772c62ca6289d5aac373c0a8ee5b132e94e7c"),
),
"",
"",
)
val taggedUsers =
listOf(
PTag("4ca4f5533e40da5e0508796d409e6bb35a50b26fc304345617ab017183d83ac0", RelayUrlNormalizer.normalize("wss://goiaba.com")),
PTag("534780e44da7b494485e85cd4cca6af4f6caa1627472432b6f2a4ece0e9e54ec"),
PTag("77ce56f89d1228f7ff3743ce1ad1b254857b9008564727ebd5a1f317362f6ca7"),
)
assertEquals(taggedUsers, note.taggedUsers())
val expectedReply = MarkedETag("5e081ebb19153357d7c31e8a10b9ceeef29313f58dc8d701f66727fab02aef64", null, MarkedETag.MARKER.REPLY)
val expectedReplyWrong = MarkedETag("b857504288c18a15950dd05b9e8772c62ca6289d5aac373c0a8ee5b132e94e7c", null, MarkedETag.MARKER.REPLY)
// check replies
assertEquals(expectedReply, note.markedReply())
assertEquals(expectedReplyWrong, note.unmarkedReply())
assertEquals(expectedReply, note.reply())
val markedReplies =
listOf(
"bbd72f0ae14374aa8fb166b483cfcf99b57d7f4cf1600ccbf17c350040834631",
"5e081ebb19153357d7c31e8a10b9ceeef29313f58dc8d701f66727fab02aef64",
)
val unmarkedReplies =
listOf(
"b857504288c18a15950dd05b9e8772c62ca6289d5aac373c0a8ee5b132e94e7c",
)
assertEquals(markedReplies, note.markedReplyTos())
assertEquals(unmarkedReplies, note.unmarkedReplyTos())
assertEquals(expectedReply.eventId, note.replyingTo())
assertEquals(expectedReply.eventId, note.replyingToAddressOrEvent())
// check root
val expectedRoot = MarkedETag("bbd72f0ae14374aa8fb166b483cfcf99b57d7f4cf1600ccbf17c350040834631", null, MarkedETag.MARKER.ROOT)
val expectedRootWrong = MarkedETag("b857504288c18a15950dd05b9e8772c62ca6289d5aac373c0a8ee5b132e94e7c", null, MarkedETag.MARKER.ROOT)
assertEquals(expectedRoot, note.markedRoot())
assertEquals(expectedRootWrong, note.unmarkedRoot())
assertEquals(expectedRoot, note.root())
// check quotes
assertEquals(emptySet<String>(), note.citedUsers())
assertEquals(emptySet<String>(), note.findCitations())
assertEquals(markedReplies, note.tagsWithoutCitations())
}
@Test
fun testOnlyRoot() {
val note =
TextNoteEvent(
"",
"",
0,
arrayOf(
arrayOf("p", "534780e44da7b494485e85cd4cca6af4f6caa1627472432b6f2a4ece0e9e54ec", "wss://banana.com"),
arrayOf("e", "9abbfd9b9ac5ecdab45d14b8bf8d746139ea039e931a1b376d19a239f1946590", "", "root", "534780e44da7b494485e85cd4cca6af4f6caa1627472432b6f2a4ece0e9e54ec"),
),
"",
"",
)
val taggedUsers =
listOf(
PTag("534780e44da7b494485e85cd4cca6af4f6caa1627472432b6f2a4ece0e9e54ec", RelayUrlNormalizer.normalize("wss://banana.com")),
)
assertEquals(taggedUsers, note.taggedUsers())
val expectedReply = MarkedETag("9abbfd9b9ac5ecdab45d14b8bf8d746139ea039e931a1b376d19a239f1946590", null, MarkedETag.MARKER.REPLY)
// check replies
assertEquals(null, note.markedReply())
assertEquals(null, note.unmarkedReply())
assertEquals(null, note.reply())
val replyTos =
listOf(
"9abbfd9b9ac5ecdab45d14b8bf8d746139ea039e931a1b376d19a239f1946590",
)
assertEquals(replyTos, note.markedReplyTos())
assertEquals(emptyList<String>(), note.unmarkedReplyTos())
assertEquals(expectedReply.eventId, note.replyingTo())
assertEquals(expectedReply.eventId, note.replyingToAddressOrEvent())
// check root
val expectedRoot = MarkedETag("9abbfd9b9ac5ecdab45d14b8bf8d746139ea039e931a1b376d19a239f1946590", null, MarkedETag.MARKER.ROOT, "534780e44da7b494485e85cd4cca6af4f6caa1627472432b6f2a4ece0e9e54ec")
assertEquals(expectedRoot, note.markedRoot())
assertEquals(null, note.unmarkedRoot())
assertEquals(expectedRoot, note.root())
// check quotes
assertEquals(emptySet<String>(), note.citedUsers())
assertEquals(emptySet<String>(), note.findCitations())
assertEquals(replyTos, note.tagsWithoutCitations())
}
@Test
fun testOnlyReply() {
val note =
TextNoteEvent(
"",
"",
0,
arrayOf(
arrayOf("p", "534780e44da7b494485e85cd4cca6af4f6caa1627472432b6f2a4ece0e9e54ec", "wss://banana.com"),
arrayOf("e", "9abbfd9b9ac5ecdab45d14b8bf8d746139ea039e931a1b376d19a239f1946590", "", "reply", "534780e44da7b494485e85cd4cca6af4f6caa1627472432b6f2a4ece0e9e54ec"),
),
"",
"",
)
val taggedUsers =
listOf(
PTag("534780e44da7b494485e85cd4cca6af4f6caa1627472432b6f2a4ece0e9e54ec", RelayUrlNormalizer.normalize("wss://banana.com")),
)
assertEquals(taggedUsers, note.taggedUsers())
val expectedReply = MarkedETag("9abbfd9b9ac5ecdab45d14b8bf8d746139ea039e931a1b376d19a239f1946590", null, MarkedETag.MARKER.REPLY)
// check replies
assertEquals(expectedReply, note.markedReply())
assertEquals(null, note.unmarkedReply())
assertEquals(expectedReply, note.reply())
val replyTos =
listOf(
"9abbfd9b9ac5ecdab45d14b8bf8d746139ea039e931a1b376d19a239f1946590",
)
assertEquals(replyTos, note.markedReplyTos())
assertEquals(emptyList<String>(), note.unmarkedReplyTos())
assertEquals(expectedReply.eventId, note.replyingTo())
assertEquals(expectedReply.eventId, note.replyingToAddressOrEvent())
// check root
val expectedRoot = MarkedETag("9abbfd9b9ac5ecdab45d14b8bf8d746139ea039e931a1b376d19a239f1946590", null, MarkedETag.MARKER.ROOT, "534780e44da7b494485e85cd4cca6af4f6caa1627472432b6f2a4ece0e9e54ec")
assertEquals(null, note.markedRoot())
assertEquals(null, note.unmarkedRoot())
assertEquals(null, note.root())
// check quotes
assertEquals(emptySet<String>(), note.citedUsers())
assertEquals(emptySet<String>(), note.findCitations())
assertEquals(replyTos, note.tagsWithoutCitations())
}
}
@@ -0,0 +1,68 @@
/**
* Copyright (c) 2025 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
* Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.quartz.nip13Pow
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair
import com.vitorpamplona.quartz.nip01Core.signers.EventTemplate
import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerSync
import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent
import com.vitorpamplona.quartz.nip13Pow.miner.PoWMiner
import org.junit.Assert.assertEquals
import org.junit.Test
import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
class PoWMinerTest {
val baseTemplate =
EventTemplate<TextNoteEvent>(
1683596206,
TextNoteEvent.KIND,
arrayOf(
arrayOf("e", "27ac621d7dc4a932e1a79f984308e7d20656dd6fddb2ce9cdfcb6a67b9a7bcc3", "", "root"),
arrayOf("e", "be7245af96210a0dd048cab4ad38e52dbd6c09a53ea21a7edb6be8898e5727cc", "", "reply"),
arrayOf("p", "22aa81510ee63fe2b16cae16e0921f78e9ba9882e2868e7e63ad6d08ae9b5954"),
arrayOf("p", "22aa81510ee63fe2b16cae16e0921f78e9ba9882e2868e7e63ad6d08ae9b5954"),
arrayOf("p", "3f770d65d3a764a9c5cb503ae123e62ec7598ad035d836e2a810f3877a745b24"),
arrayOf("p", "ec4d241c334311b3a304433ee3442be29d0e88e7ec19b85edf2bba29b93565e2"),
arrayOf("p", "0fe0b18b4dbf0e0aa40fcd47209b2a49b3431fc453b460efcf45ca0bd16bd6ac"),
arrayOf("p", "8c0da4862130283ff9e67d889df264177a508974e2feb96de139804ea66d6168"),
arrayOf("p", "63fe6318dc58583cfe16810f86dd09e18bfd76aabc24a0081ce2856f330504ed"),
arrayOf("p", "4523be58d395b1b196a9b8c82b038b6895cb02b683d0c253a955068dba1facd0"),
arrayOf("p", "460c25e682fda7832b52d1f22d3d22b3176d972f60dcdc3212ed8c92ef85065c"),
),
"Astral:\n\nhttps://void.cat/d/A5Fba5B1bcxwEmeyoD9nBs.webp\n\nIris:\n\nhttps://void.cat/d/44hTcVvhRps6xYYs99QsqA.webp\n\nSnort:\n\nhttps://void.cat/d/4nJD5TRePuQChM5tzteYbU.webp\n\nAmethyst agrees with Astral which I suspect are both wrong. nostr:npub13sx6fp3pxq5rl70x0kyfmunyzaa9pzt5utltjm0p8xqyafndv95q3saapa nostr:npub1v0lxxxxutpvrelsksy8cdhgfux9l6a42hsj2qzquu2zk7vc9qnkszrqj49 nostr:npub1g53mukxnjkcmr94fhryzkqutdz2ukq4ks0gvy5af25rgmwsl4ngq43drvk nostr:npub1gcxzte5zlkncx26j68ez60fzkvtkm9e0vrwdcvsjakxf9mu9qewqlfnj5z ",
)
@Test
fun mine() {
val signer = NostrSignerSync(KeyPair())
println("Starting")
val template = PoWMiner.run(baseTemplate, signer.pubKey, 25)
println("Finished")
val event = signer.sign(template)
println(event?.toJson())
assertEquals(25, event!!.pow())
}
}
@@ -0,0 +1,73 @@
/**
* Copyright (c) 2025 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
* Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.quartz.nip13Pow
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.vitorpamplona.quartz.nip01Core.core.hexToByteArray
import com.vitorpamplona.quartz.nip13Pow.miner.PoWRankEvaluator
import org.junit.Assert.assertEquals
import org.junit.Test
import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
class PoWRankEvaluatorTest {
val tests =
mapOf(
"000006d8c378af1779d2feebc7603a125d99eca0ccf1085959b307f64e5dd358" to 21,
"6bf5b4f434813c64b523d2b0e6efe18f3bd0cbbd0a5effd8ece9e00fd2531996" to 1,
"00003479309ecdb46b1c04ce129d2709378518588bed6776e60474ebde3159ae" to 18,
"01a76167d41add96be4959d9e618b7a35f26551d62c43c11e5e64094c6b53c83" to 7,
"ac4f44bae06a45ebe88cfbd3c66358750159650a26c0d79e8ccaa92457fca4f6" to 0,
"0000000000000000006cfbd3c66358750159650a26c0d79e8ccaa92457fca4f6" to 73,
"00000026c91e9fc75fdb95b367776e2594b931cebda6d5ca3622501006669c9e" to 26,
)
@Test
fun testHex() {
tests.forEach {
assertEquals(it.value, PoWRankEvaluator.calculatePowRankOf(it.key))
}
}
@Test
fun testByte() {
tests.forEach {
assertEquals(it.value, PoWRankEvaluator.calculatePowRankOf(it.key.hexToByteArray()))
}
}
val commitmentTest = "00000026c91e9fc75fdb95b367776e2594b931cebda6d5ca3622501006669c9e"
@Test
fun setPoWIfCommited25() {
assertEquals(25, PoWRankEvaluator.compute(commitmentTest, 25))
}
@Test
fun setPoWIfCommited26() {
assertEquals(26, PoWRankEvaluator.compute(commitmentTest, 26))
}
@Test
fun setPoWIfCommited27() {
assertEquals(26, PoWRankEvaluator.compute(commitmentTest, 27))
}
}
@@ -0,0 +1,75 @@
/**
* Copyright (c) 2025 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
* Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.quartz.nip17Dm
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation
import com.vitorpamplona.quartz.nip01Core.core.hexToByteArray
import com.vitorpamplona.quartz.nip17Dm.files.encryption.AESGCM
import junit.framework.TestCase.assertEquals
import org.junit.Test
import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
class AESGCMTest {
val decryptionNonce = "01e77c94bd5aba3e3cbb69594e7ba07c"
val decryptionKey = "c128ecffab90ee7810e3df08e7fb2cc39a8d40f24201f48b2b36e23b34ac50ee"
val cipher = AESGCM(decryptionKey.hexToByteArray(), decryptionNonce.toByteArray(Charsets.UTF_8))
@Test
fun encryptDecrypt() {
val encrypted = cipher.encrypt("Testing".toByteArray(Charsets.UTF_8))
val decrypted = cipher.decrypt(encrypted)
assertEquals("Testing", String(decrypted))
}
@Test
fun imageTest() {
val image =
getInstrumentation().context.assets.open("ovxxk2vz.jpg").use {
it.readAllBytes()
}
val decrypted = cipher.decrypt(image)
assertEquals(44201, decrypted.size)
}
@Test
fun videoTest2() {
val myCipher =
AESGCM(
"373d19850ebc8ed5b0fefcca5cd6f27fde9cb6ac54fd32f6b4fad9d68ebe8ee0".hexToByteArray(),
"95e67b6874784a54299b58b8990499bd".hexToByteArray(),
)
val encrypted =
getInstrumentation().context.assets.open("trouble_video").use {
it.readAllBytes()
}
val decrypted = myCipher.decrypt(encrypted)
assertEquals(1277122, decrypted.size)
}
}
@@ -0,0 +1,40 @@
/**
* Copyright (c) 2025 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
* Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.quartz.nip17Dm
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.vitorpamplona.quartz.nip17Dm.base.ChatroomKey
import kotlinx.collections.immutable.persistentSetOf
import org.junit.Assert.assertEquals
import org.junit.Test
import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
class ChatroomKeyTest {
@Test
fun testEquals() {
val k1 = ChatroomKey(persistentSetOf("Key1", "Key2"))
val k2 = ChatroomKey(persistentSetOf("Key1", "Key2"))
assertEquals(k1, k2)
assertEquals(k1.hashCode(), k2.hashCode())
}
}
@@ -0,0 +1,169 @@
/**
* Copyright (c) 2025 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
* Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.quartz.nip19Bech32
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.vitorpamplona.quartz.experimental.medical.FhirResourceEvent
import com.vitorpamplona.quartz.nip01Core.core.hexToByteArray
import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair
import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerInternal
import com.vitorpamplona.quartz.nip01Core.verify
import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent
import com.vitorpamplona.quartz.nip19Bech32.entities.NEmbed
import com.vitorpamplona.quartz.utils.Hex
import junit.framework.TestCase.assertEquals
import junit.framework.TestCase.assertNotNull
import junit.framework.TestCase.assertTrue
import kotlinx.coroutines.runBlocking
import org.junit.Assert
import org.junit.Test
import org.junit.runner.RunWith
import java.util.concurrent.CountDownLatch
import java.util.concurrent.TimeUnit
@RunWith(AndroidJUnit4::class)
class NIP19EmbedTests {
@Test
fun testEmbedKind1Event() {
val signer =
NostrSignerInternal(
KeyPair(Hex.decode("e8e7197ccc53c9ed4cf9b1c8dce085475fa1ffdd71f2c14e44fe23d0bdf77598")),
)
val textNote: TextNoteEvent =
runBlocking {
signer.sign(
TextNoteEvent.build("I like this. It could solve the ninvite problem in #1062, and it seems like it could be applied very broadly to limit the spread of events that shouldn't stand on their own or need to be private. The one question I have is how long are these embeds? If it's 50 lines of text, that breaks the human readable (or at least parseable) requirement of kind 1s. Also, encoding json in a tlv is silly, we should at least use the tlv to reduce the payload size."),
)
}
assertNotNull(textNote)
val bech32 = NEmbed.create(textNote!!)
println(bech32)
val decodedNote = (Nip19Parser.uriToRoute(bech32)?.entity as NEmbed).event
assertTrue(decodedNote.verify())
assertEquals(textNote!!.toJson(), decodedNote.toJson())
}
@Test
fun testVisionPrescriptionEmbedEvent() {
val signer =
NostrSignerInternal(
KeyPair(Hex.decode("e8e7197ccc53c9ed4cf9b1c8dce085475fa1ffdd71f2c14e44fe23d0bdf77598")),
)
val eyeglassesPrescriptionEvent =
runBlocking {
signer.sign(FhirResourceEvent.build(visionPrescriptionFhir))
}
assertNotNull(eyeglassesPrescriptionEvent)
val bech32 = NEmbed.create(eyeglassesPrescriptionEvent)
println(eyeglassesPrescriptionEvent.toJson())
println(bech32)
val decodedNote = (Nip19Parser.uriToRoute(bech32)?.entity as NEmbed).event
assertTrue(decodedNote.verify())
assertEquals(eyeglassesPrescriptionEvent.toJson(), decodedNote.toJson())
}
@Test
fun testVisionPrescriptionBundleEmbedEvent() {
val signer =
NostrSignerInternal(
KeyPair(Hex.decode("e8e7197ccc53c9ed4cf9b1c8dce085475fa1ffdd71f2c14e44fe23d0bdf77598")),
)
val countDownLatch = CountDownLatch(1)
val eyeglassesPrescriptionEvent =
runBlocking {
signer.sign(FhirResourceEvent.build(visionPrescriptionBundle))
}
Assert.assertTrue(countDownLatch.await(1, TimeUnit.SECONDS))
assertNotNull(eyeglassesPrescriptionEvent)
val bech32 = NEmbed.create(eyeglassesPrescriptionEvent)
println(eyeglassesPrescriptionEvent.toJson())
println(bech32)
val decodedNote = (Nip19Parser.uriToRoute(bech32)?.entity as NEmbed).event
assertTrue(decodedNote.verify())
assertEquals(eyeglassesPrescriptionEvent.toJson(), decodedNote.toJson())
}
@Test
fun testVisionPrescriptionBundle2EmbedEvent() {
val signer =
NostrSignerInternal(
KeyPair(decodePrivateKeyAsHexOrNull("nsec1arn3jlxv20y76n8ek8ydecy9ga06rl7aw8evznjylc3ap00hwkvqx4vvy6")!!.hexToByteArray()),
)
val eyeglassesPrescriptionEvent =
runBlocking {
signer.sign(FhirResourceEvent.build(visionPrescriptionBundle2))
}
assertNotNull(eyeglassesPrescriptionEvent)
val bech32 = NEmbed.create(eyeglassesPrescriptionEvent)
println(eyeglassesPrescriptionEvent.toJson())
println(bech32)
val decodedNote = (Nip19Parser.uriToRoute(bech32)?.entity as NEmbed).event
assertTrue(decodedNote.verify())
assertEquals(eyeglassesPrescriptionEvent.toJson(), decodedNote.toJson())
}
@Test
fun testTimsNembed() {
val uri = "nembed1r79ssq9446hkwqhl642ukmku8qg0c92pu7w3j0jyfte8tc7tvg85vmrys8x3sqgle5vjy7jpjswqhphl0kd6yf4sz0n3peyjq5rp3zkat4w6c6j3f7um0724jmfu5456xxgg2yxkn8dp23j64xsn9npcggzafyh2effyntqrqxzja8dp52kpcvc9zqxlj86e8mx05vevzxkeprjkfs4wmppxm3p96vj6yvu2mqgf5l4v99492r2qsggquxuv93uzx244652h2kkj8xseg9xkq0afpygknjtty9j4ju5v0nm9mezux9wyl6s5wr7lzce7cj397mnu0u04ha7aq3w7exelrhe3zs3l3urwa9sp36u80npllrs0hmsxqdn0fsuyav3nv0azjs5suzuurg2uymncjxez8p9xksc2j6gw992enjflgrdd7n5uq2xrpvfrd3rckw624ey0elvm6grr27tyzlf4vaswgm5vc3hdyczsl983g2j8e67r6z5zt30lat84ma4wclkwwxxrcflvdsuwd7346h7zqav4vdwe3gkt9lr87sfk4aqd2aey03tt4eyspldrqcmkx9pqe2pn63rv7grwwalr86akuldnvjm6m87wrw9sdwns8wq0rnsmj57vqwtc3g7hkwum3vl2dda78dwkycgfzw6qna3ufhpatcvq5a4hm4ehl45an8umwt0clf7rn77ctke475qglwu86hhfwhn7dkca4pkfpyc4y75rll6nvr5qc8nlhf8mk22celn5mecvyuzxd830drhdck9tcdpcafymk8wajwu2w8ha8gatggjfvq0a4jlf2sdamzj0ysqks9dk8me3q7a0qpmf6vykurkrcls4pug3u4pn4u26ezx3h8e482n07x2nsmu80dpufxqc0ttcyzhnppguxma4d8aumdawnlsyy7yzcuxl7lw5y9p4nv5h8fn6u8anpm2tsze3p6mgxy9j9uuqfxg2jvlmtjpakna5m4hln0msmw804hnun96h66fh62270yhhljnmmdl7jln07ll5vft7e870hemcld34a09n943ed6629fgtctsftma9q6tf4jfm2p0ukd2j2n2dpz53fqrkk4ctdcy2j5jar095g5jntf6u807ggkzauzt6uqkwk4tg5w7w55kskspc9663zx5dzzzfwpg3q546g2ve4kukr70n0a46eyce2crsqqq247ql5"
val decodedNote = (Nip19Parser.uriToRoute(uri)?.entity as NEmbed).event
assertTrue(decodedNote.verify())
assertEquals(timsPrescription, decodedNote.toJson())
}
val visionPrescriptionFhir = "{\"resourceType\":\"VisionPrescription\",\"status\":\"active\",\"created\":\"2014-06-15\",\"patient\":{\"reference\":\"Patient/Donald Duck\"},\"dateWritten\":\"2014-06-15\",\"prescriber\":{\"reference\":\"Practitioner/Adam Careful\"},\"lensSpecification\":[{\"eye\":\"right\",\"sphere\":-2,\"prism\":[{\"amount\":0.5,\"base\":\"down\"}],\"add\":2},{\"eye\":\"left\",\"sphere\":-1,\"cylinder\":-0.5,\"axis\":180,\"prism\":[{\"amount\":0.5,\"base\":\"up\"}],\"add\":2}]}"
val visionPrescriptionBundle = "{\"resourceType\":\"Bundle\",\"id\":\"bundle-vision-test\",\"type\":\"document\",\"entry\":[{\"resourceType\":\"Practitioner\",\"id\":\"2\",\"active\":true,\"name\":[{\"use\":\"official\",\"family\":\"Careful\",\"given\":[\"Adam\"]}],\"gender\":\"male\"},{\"resourceType\":\"Patient\",\"id\":\"1\",\"active\":true,\"name\":[{\"use\":\"official\",\"family\":\"Duck\",\"given\":[\"Donald\"]}],\"gender\":\"male\"},{\"resourceType\":\"VisionPrescription\",\"status\":\"active\",\"created\":\"2014-06-15\",\"patient\":{\"reference\":\"#1\"},\"dateWritten\":\"2014-06-15\",\"prescriber\":{\"reference\":\"#2\"},\"lensSpecification\":[{\"eye\":\"right\",\"sphere\":-2,\"prism\":[{\"amount\":0.5,\"base\":\"down\"}],\"add\":2},{\"eye\":\"left\",\"sphere\":-1,\"cylinder\":-0.5,\"axis\":180,\"prism\":[{\"amount\":0.5,\"base\":\"up\"}],\"add\":2}]}]}"
val visionPrescriptionBundle2 = "{\"resourceType\":\"Bundle\",\"id\":\"bundle-vision-test\",\"type\":\"document\",\"entry\":[{\"resourceType\":\"Practitioner\",\"id\":\"2\",\"active\":true,\"name\":[{\"use\":\"official\",\"family\":\"Smith\",\"given\":[\"Dr. Joe\"]}],\"gender\":\"male\"},{\"resourceType\":\"Patient\",\"id\":\"1\",\"active\":true,\"name\":[{\"use\":\"official\",\"family\":\"Doe\",\"given\":[\"Jane\"]}],\"gender\":\"male\"},{\"resourceType\":\"VisionPrescription\",\"status\":\"active\",\"created\":\"2014-06-15\",\"patient\":{\"reference\":\"#1\"},\"dateWritten\":\"2014-06-15\",\"lensSpecification\":[{\"eye\":\"right\",\"sphere\":-2,\"prism\":[{\"amount\":0.5,\"base\":\"down\"}],\"add\":2},{\"eye\":\"left\",\"sphere\":-1,\"cylinder\":-0.5,\"axis\":180,\"prism\":[{\"amount\":0.5,\"base\":\"up\"}],\"add\":2}]}]}"
val timsPrescription = "{\"id\":\"18d8b22e6455dfc9f4c6d6be8c2cf015e961b8d160dfe5e4b7fc1578f2c4e0be\",\"pubkey\":\"46f1826abf5b03de972192e619e25fa94d775a1c555efe53a775412dbf49889b\",\"created_at\":1739566773,\"kind\":82,\"tags\":[[\"p\",\"46f1826abf5b03de972192e619e25fa94d775a1c555efe53a775412dbf49889b\"]],\"content\":\"{\\\"resourceType\\\": \\\"VisionPrescription\\\", \\\"id\\\": \\\"eyeglass-prescription-001\\\", \\\"status\\\": \\\"active\\\", \\\"created\\\": \\\"2025-02-14T10:00:00Z\\\", \\\"patient\\\": {\\\"reference\\\": \\\"Patient/12345\\\", \\\"display\\\": \\\"John Doe\\\"}, \\\"encounter\\\": {\\\"reference\\\": \\\"Encounter/67890\\\"}, \\\"dateWritten\\\": \\\"2025-02-10T15:00:00Z\\\", \\\"prescriber\\\": {\\\"reference\\\": \\\"Practitioner/56789\\\", \\\"display\\\": \\\"Dr. Emily Smith\\\"}, \\\"lensSpecification\\\": [{\\\"product\\\": {\\\"coding\\\": [{\\\"system\\\": \\\"http://terminology.hl7.org/CodeSystem/ex-visionprescriptionproduct\\\", \\\"code\\\": \\\"lens\\\", \\\"display\\\": \\\"Eyeglasses\\\"}]}, \\\"eye\\\": \\\"right\\\", \\\"sphere\\\": -2.5, \\\"cylinder\\\": -1.0, \\\"axis\\\": 180, \\\"prism\\\": [{\\\"amount\\\": 0.5, \\\"base\\\": \\\"up\\\"}], \\\"add\\\": 2.0, \\\"duration\\\": {\\\"value\\\": 24, \\\"unit\\\": \\\"months\\\", \\\"system\\\": \\\"http://unitsofmeasure.org\\\", \\\"code\\\": \\\"mo\\\"}, \\\"note\\\": [{\\\"text\\\": \\\"Right eye prescription for near-sightedness with astigmatism.\\\"}]}, {\\\"product\\\": {\\\"coding\\\": [{\\\"system\\\": \\\"http://terminology.hl7.org/CodeSystem/ex-visionprescriptionproduct\\\", \\\"code\\\": \\\"lens\\\", \\\"display\\\": \\\"Eyeglasses\\\"}]}, \\\"eye\\\": \\\"left\\\", \\\"sphere\\\": -3.0, \\\"cylinder\\\": -0.75, \\\"axis\\\": 160, \\\"prism\\\": [{\\\"amount\\\": 0.5, \\\"base\\\": \\\"down\\\"}], \\\"add\\\": 2.0, \\\"duration\\\": {\\\"value\\\": 24, \\\"unit\\\": \\\"months\\\", \\\"system\\\": \\\"http://unitsofmeasure.org\\\", \\\"code\\\": \\\"mo\\\"}, \\\"note\\\": [{\\\"text\\\": \\\"Left eye prescription for near-sightedness with astigmatism.\\\"}]}]}\",\"sig\":\"d22d3b86aea397094de8b6cdf69decdfd886c90008aeebf95fd43a2770b37d486b313bff5bdb44b78e33bbaa3f336d74ee8b36bc5b16050374054246c72d93c2\"}"
}
@@ -0,0 +1,96 @@
/**
* Copyright (c) 2025 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
* Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.quartz.nip22Comments
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.vitorpamplona.quartz.nip01Core.core.hexToByteArray
import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair
import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerSync
import com.vitorpamplona.quartz.nip01Core.verify
import com.vitorpamplona.quartz.nip73ExternalIds.location.GeohashId
import com.vitorpamplona.quartz.nip73ExternalIds.location.geohashedScope
import junit.framework.TestCase.assertEquals
import junit.framework.TestCase.assertFalse
import junit.framework.TestCase.assertTrue
import org.junit.Test
import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
class GeolocatedComments {
private val privateKey = "f410f88bcec6cbfda04d6a273c7b1dd8bba144cd45b71e87109cfa11dd7ed561".hexToByteArray()
private val signer = NostrSignerSync(KeyPair(privateKey))
val event =
CommentEvent(
"fecb2ecf61a1433d417a784d10bd1e8ec19a916170a53ca8fb3a15fc666a6592",
"f8ff11c7a7d3478355d3b4d174e5a473797a906ea4aa61aa9b6bc0652c1ea17a",
1747753115,
arrayOf(
arrayOf("alt", "Reply to geo:drt3n"),
arrayOf("I", "geo:drt3n"),
arrayOf("I", "geo:drt3"),
arrayOf("I", "geo:drt"),
arrayOf("I", "geo:dr"),
arrayOf("I", "geo:d"),
arrayOf("K", "geo"),
arrayOf("i", "geo:drt3n"),
arrayOf("i", "geo:drt3"),
arrayOf("i", "geo:drt"),
arrayOf("i", "geo:dr"),
arrayOf("i", "geo:d"),
arrayOf("k", "geo"),
),
"testing",
"12070e663272f1227c639fb834eb2122fc7bb995f4c49e55ebb1dfe2135ef7347d44810bacd2e64fd26b8826fd47d2800ce6c3d3b579bb3afe39088ffd4faa60",
)
@Test
fun verifyEvent() {
assertTrue(event.verify())
}
@Test
fun testScopes() {
assertEquals("drt3n", event.geohashedScope())
assertTrue(event.isTaggedScope("drt3n", GeohashId::match))
assertTrue(event.isTaggedScope(GeohashId.toScope("drt3n")))
assertFalse(event.isTaggedScope("drt3n"))
}
@Test
fun testCreation() {
val event =
signer.sign(
CommentEvent.replyExternalIdentity(
"Message",
GeohashId("drt3n"),
),
)
assertEquals("drt3n", event!!.geohashedScope())
assertTrue(event.isTaggedScope(GeohashId.toScope("drt3n")))
assertFalse(event.isTaggedScope("drt3n"))
assertTrue(event.hasScopeKind(GeohashId.KIND))
assertTrue(event.hasRootScopeKind(GeohashId.KIND))
assertTrue(event.hasReplyScopeKind(GeohashId.KIND))
}
}
@@ -0,0 +1,94 @@
/**
* Copyright (c) 2025 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
* Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.quartz.nip44Encryption
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.vitorpamplona.quartz.nip01Core.core.hexToByteArray
import com.vitorpamplona.quartz.nip01Core.core.toHexKey
import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair
import com.vitorpamplona.quartz.nip01Core.crypto.Nip01
import org.junit.Assert.assertEquals
import org.junit.Test
import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
class Nip44v1Test {
private val nip44v1 = Nip44v1()
@Test
fun testSharedSecretCompatibilityWithCoracle() {
val privateKey = "f410f88bcec6cbfda04d6a273c7b1dd8bba144cd45b71e87109cfa11dd7ed561"
val publicKey = "765cd7cf91d3ad07423d114d5a39c61d52b2cdbc18ba055ddbbeec71fbe2aa2f"
val key =
nip44v1.getSharedSecret(
privateKey = privateKey.hexToByteArray(),
pubKey = publicKey.hexToByteArray(),
)
assertEquals("577c966f499dddd8e8dcc34e8f352e283cc177e53ae372794947e0b8ede7cfd8", key.toHexKey())
}
@Test
fun testSharedSecret() {
val sender = KeyPair()
val receiver = KeyPair()
val sharedSecret1 = nip44v1.getSharedSecret(sender.privKey!!, receiver.pubKey)
val sharedSecret2 = nip44v1.getSharedSecret(receiver.privKey!!, sender.pubKey)
assertEquals(sharedSecret1.toHexKey(), sharedSecret2.toHexKey())
val secretKey1 = KeyPair(privKey = sharedSecret1)
val secretKey2 = KeyPair(privKey = sharedSecret2)
assertEquals(secretKey1.pubKey.toHexKey(), secretKey2.pubKey.toHexKey())
assertEquals(secretKey1.privKey?.toHexKey(), secretKey2.privKey?.toHexKey())
}
@Test
fun encryptDecrypt() {
val msg = "Hi"
val privateKey = Nip01.privKeyCreate()
val publicKey = Nip01.pubKeyCreate(privateKey)
val encrypted = nip44v1.encrypt(msg, privateKey, publicKey)
val decrypted = nip44v1.decrypt(encrypted, privateKey, publicKey)
assertEquals(msg, decrypted)
}
@Test
fun encryptDecryptSharedSecret() {
val msg = "Hi"
val privateKey = Nip01.privKeyCreate()
val publicKey = Nip01.pubKeyCreate(privateKey)
val sharedSecret = nip44v1.getSharedSecret(privateKey, publicKey)
val encrypted = nip44v1.encrypt(msg, sharedSecret)
val decrypted = nip44v1.decrypt(encrypted, sharedSecret)
assertEquals(msg, decrypted)
}
}
@@ -0,0 +1,185 @@
/**
* Copyright (c) 2025 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
* Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.quartz.nip44Encryption
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import com.vitorpamplona.quartz.nip01Core.core.hexToByteArray
import com.vitorpamplona.quartz.nip01Core.core.toHexKey
import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair
import com.vitorpamplona.quartz.nip01Core.crypto.Nip01
import com.vitorpamplona.quartz.utils.RandomInstance
import com.vitorpamplona.quartz.utils.sha256.sha256
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
@RunWith(AndroidJUnit4::class)
class Nip44v2Test {
private val vectors: VectorFile =
jacksonObjectMapper()
.readValue(
getInstrumentation().context.assets.open("nip44.vectors.json"),
VectorFile::class.java,
)
private val nip44v2 = Nip44v2()
@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 testCompressedWith02Keys() {
val privateKeyA = "f410f88bcec6cbfda04d6a273c7b1dd8bba144cd45b71e87109cfa11dd7ed561".hexToByteArray()
val privateKeyB = "65f039136f8da8d3e87b4818746b53318d5481e24b2673f162815144223a0b5a".hexToByteArray()
val publicKeyA = Nip01.pubKeyCreate(privateKeyA)
val publicKeyB = Nip01.pubKeyCreate(privateKeyB)
assertEquals(
nip44v2.getConversationKey(privateKeyA, publicKeyB).toHexKey(),
nip44v2.getConversationKey(privateKeyB, publicKeyA).toHexKey(),
)
}
@Test
fun testCompressedWith03Keys() {
val privateKeyA = "f410f88bcec6cbfda04d6a273c7b1dd8bba144cd45b71e87109cfa11dd7ed561".hexToByteArray()
val privateKeyB = "e6159851715b4aa6190c22b899b0c792847de0a4435ac5b678f35738351c43b0".hexToByteArray()
val publicKeyA = Nip01.pubKeyCreate(privateKeyA)
val publicKeyB = Nip01.pubKeyCreate(privateKeyB)
assertEquals(
nip44v2.getConversationKey(privateKeyA, publicKeyB).toHexKey(),
nip44v2.getConversationKey(privateKeyB, publicKeyA).toHexKey(),
)
}
@Test
fun encryptDecryptTest() {
for (v in vectors.v2?.valid?.encryptDecrypt!!) {
val pub2 = KeyPair(v.sec2!!.hexToByteArray())
val conversationKey1 = nip44v2.getConversationKey(v.sec1!!.hexToByteArray(), pub2.pubKey)
assertEquals(v.conversationKey, conversationKey1.toHexKey())
val ciphertext =
nip44v2
.encryptWithNonce(
v.plaintext!!,
conversationKey1,
v.nonce!!.hexToByteArray(),
).encodePayload()
assertEquals(v.payload, ciphertext)
val pub1 = KeyPair(v.sec1.hexToByteArray())
val conversationKey2 = nip44v2.getConversationKey(v.sec2.hexToByteArray(), pub1.pubKey)
assertEquals(v.conversationKey, conversationKey2.toHexKey())
val decrypted = nip44v2.decrypt(v.payload!!, conversationKey2)
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 invalidMessageLengths() {
for (v in vectors.v2?.invalid?.encryptMsgLengths!!) {
val key = RandomInstance.bytes(32)
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)
}
}
}
private fun sha256Hex(data: ByteArray) = sha256(data).toHexKey()
}
@@ -0,0 +1,92 @@
/**
* Copyright (c) 2025 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
* Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.quartz.nip44Encryption
import com.fasterxml.jackson.annotation.JsonProperty
data class VectorFile(
val v2: V2? = V2(),
)
data class V2(
val valid: Valid? = Valid(),
val invalid: Invalid? = Invalid(),
)
data class Valid(
@field:JsonProperty("get_conversation_key") val getConversationKey: ArrayList<GetConversationKey> = arrayListOf(),
@field:JsonProperty("get_message_keys") val getMessageKeys: GetMessageKeys? = GetMessageKeys(),
@field:JsonProperty("calc_padded_len") val calcPaddedLen: ArrayList<ArrayList<Int>> = arrayListOf(),
@field:JsonProperty("encrypt_decrypt") val encryptDecrypt: ArrayList<EncryptDecrypt> = arrayListOf(),
@field:JsonProperty("encrypt_decrypt_long_msg")
val encryptDecryptLongMsg: ArrayList<EncryptDecryptLongMsg> = arrayListOf(),
)
data class Invalid(
@field:JsonProperty("encrypt_msg_lengths") val encryptMsgLengths: ArrayList<Int> = arrayListOf(),
@field:JsonProperty("get_conversation_key") val getConversationKey: ArrayList<GetConversationKey> = arrayListOf(),
@field:JsonProperty("decrypt") val decrypt: ArrayList<Decrypt> = arrayListOf(),
)
data class GetConversationKey(
val sec1: String? = null,
val pub2: String? = null,
val note: String? = null,
@field:JsonProperty("conversation_key") val conversationKey: String? = null,
)
data class GetMessageKeys(
@field:JsonProperty("conversation_key") val conversationKey: String? = null,
val keys: ArrayList<Keys> = arrayListOf(),
)
data class Keys(
@field:JsonProperty("nonce") val nonce: String? = null,
@field:JsonProperty("chacha_key") val chachaKey: String? = null,
@field:JsonProperty("chacha_nonce") val chachaNonce: String? = null,
@field:JsonProperty("hmac_key") val hmacKey: String? = null,
)
data class EncryptDecrypt(
val sec1: String? = null,
val sec2: String? = null,
@field:JsonProperty("conversation_key") val conversationKey: String? = null,
val nonce: String? = null,
val plaintext: String? = null,
val payload: String? = null,
)
data class EncryptDecryptLongMsg(
@field:JsonProperty("conversation_key") val conversationKey: String? = null,
val nonce: String? = null,
val pattern: String? = null,
val repeat: Int? = null,
@field:JsonProperty("plaintext_sha256") val plaintextSha256: String? = null,
@field:JsonProperty("payload_sha256") val payloadSha256: String? = null,
)
data class Decrypt(
@field:JsonProperty("conversation_key") val conversationKey: String? = null,
val nonce: String? = null,
val plaintext: String? = null,
val payload: String? = null,
val note: String? = null,
)
@@ -0,0 +1,262 @@
/**
* Copyright (c) 2025 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
* Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.quartz.nip46RemoteSigner
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.vitorpamplona.quartz.nip01Core.core.Event
import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair
import com.vitorpamplona.quartz.nip01Core.signers.EventTemplate
import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerInternal
import com.vitorpamplona.quartz.nip02FollowList.ReadWrite
import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent
import com.vitorpamplona.quartz.utils.TimeUtils
import junit.framework.TestCase.assertEquals
import kotlinx.coroutines.runBlocking
import org.junit.Ignore
import org.junit.Test
import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
internal class Nip46Test {
val remoteKey = NostrSignerInternal(KeyPair())
val signer = NostrSignerInternal(KeyPair())
val peer = NostrSignerInternal(KeyPair())
val dummyEvent =
EventTemplate<Event>(
createdAt = TimeUtils.now(),
kind = 1,
tags = emptyArray(),
content = "test",
)
val dummyEventSigned =
TextNoteEvent(
id = "",
pubKey = "",
createdAt = TimeUtils.now(),
tags = emptyArray(),
content = "test",
sig = "",
)
suspend fun <T : BunkerMessage> encodeDecodeEvent(req: T): T {
val eventStr = NostrConnectEvent.create(req, remoteKey.pubKey, signer).toJson()
return (Event.fromJson(eventStr) as NostrConnectEvent).decryptMessage(signer) as T
}
@Test
fun signEncoder() =
runBlocking {
val expected = BunkerRequestSign(event = dummyEvent)
val actual = encodeDecodeEvent(expected)
assertEquals(BunkerRequestSign.METHOD_NAME, actual.method)
assertEquals(expected.id, actual.id)
assertEquals(dummyEvent.createdAt, actual.event.createdAt)
}
@Test
fun connectEncoder() =
runBlocking {
val expected = BunkerRequestConnect(remoteKey = remoteKey.pubKey)
val actual = encodeDecodeEvent(expected)
assertEquals(expected.method, actual.method)
assertEquals(expected.id, actual.id)
}
@Test
fun pingEncoder() =
runBlocking {
val expected = BunkerRequestPing()
val actual = encodeDecodeEvent(expected)
assertEquals(expected.method, actual.method)
assertEquals(expected.id, actual.id)
}
@Test
fun getPubkeyEncoder() =
runBlocking {
val expected = BunkerRequestGetPublicKey()
val actual = encodeDecodeEvent(expected)
assertEquals(expected.method, actual.method)
assertEquals(expected.id, actual.id)
}
@Test
fun getRelaysEncoder() =
runBlocking {
val expected = BunkerRequestGetRelays()
val actual = encodeDecodeEvent(expected)
assertEquals(expected.method, actual.method)
assertEquals(expected.id, actual.id)
}
@Test
fun testNip04Encrypt() =
runBlocking {
val expected = BunkerRequestNip04Encrypt(pubKey = peer.pubKey, message = "Test")
val actual = encodeDecodeEvent(expected)
assertEquals(expected.method, actual.method)
assertEquals(expected.id, actual.id)
assertEquals(expected.pubKey, actual.pubKey)
assertEquals(expected.message, actual.message)
}
@Test
fun testNip44Encrypt() =
runBlocking {
val expected = BunkerRequestNip44Encrypt(pubKey = peer.pubKey, message = "Test")
val actual = encodeDecodeEvent(expected)
assertEquals(expected.method, actual.method)
assertEquals(expected.id, actual.id)
assertEquals(expected.pubKey, actual.pubKey)
assertEquals(expected.message, actual.message)
}
@Test
fun testNip04Decrypt() =
runBlocking {
val expected = BunkerRequestNip04Decrypt(pubKey = peer.pubKey, ciphertext = "Test")
val actual = encodeDecodeEvent(expected)
assertEquals(expected.method, actual.method)
assertEquals(expected.id, actual.id)
assertEquals(expected.pubKey, actual.pubKey)
assertEquals(expected.ciphertext, actual.ciphertext)
}
@Test
fun testNip44Decrypt() =
runBlocking {
val expected = BunkerRequestNip44Decrypt(pubKey = peer.pubKey, ciphertext = "Test")
val actual = encodeDecodeEvent(expected)
assertEquals(expected.method, actual.method)
assertEquals(expected.id, actual.id)
assertEquals(expected.pubKey, actual.pubKey)
assertEquals(expected.ciphertext, actual.ciphertext)
}
// Responses
@Test
fun testAckResponse() =
runBlocking {
val expected = BunkerResponseAck()
val actual = encodeDecodeEvent(expected)
assertEquals(expected.id, actual.id)
assertEquals(expected.result, actual.result)
assertEquals(expected.error, actual.error)
}
@Test
fun testPongResponse() =
runBlocking {
val expected = BunkerResponsePong()
val actual = encodeDecodeEvent(expected)
assertEquals(expected.id, actual.id)
assertEquals(expected.result, actual.result)
assertEquals(expected.error, actual.error)
}
@Test
fun testErrorResponse() =
runBlocking {
val expected = BunkerResponseError(error = "Error")
val actual = encodeDecodeEvent(expected)
assertEquals(expected.id, actual.id)
assertEquals(expected.result, actual.result)
assertEquals(expected.error, actual.error)
}
@Test
fun testEventResponse() =
runBlocking {
val expected = BunkerResponseEvent(event = dummyEventSigned)
val actual = encodeDecodeEvent(expected)
assertEquals(expected.id, actual.id)
assertEquals(expected.result, actual.result)
assertEquals(expected.error, actual.error)
assertEquals(dummyEventSigned.id, actual.event.id)
}
@Test
fun testPubkeyResponse() =
runBlocking {
val expected = BunkerResponsePublicKey(pubkey = peer.pubKey)
val actual = encodeDecodeEvent(expected)
assertEquals(expected.id, actual.id)
assertEquals(expected.result, actual.result)
assertEquals(expected.error, actual.error)
assertEquals(expected.pubkey, actual.pubkey)
}
@Test
fun testRelaysResponse() =
runBlocking {
val expected = BunkerResponseGetRelays(relays = mapOf("url" to ReadWrite(true, false)))
val actual = encodeDecodeEvent(expected)
assertEquals(expected.id, actual.id)
assertEquals(expected.result, actual.result)
assertEquals(expected.error, actual.error)
assertEquals(expected.relays["url"], actual.relays["url"])
}
@Test
@Ignore("Impossible to recreate the class since there are no hints on the json")
fun testDecryptResponse() =
runBlocking {
val expected = BunkerResponseDecrypt(plaintext = "Test")
val actual = encodeDecodeEvent(expected)
assertEquals(expected.id, actual.id)
assertEquals(expected.result, actual.result)
assertEquals(expected.error, actual.error)
assertEquals(expected.plaintext, actual.plaintext)
}
@Test
@Ignore("Impossible to recreate the class since there are no hints on the json")
fun testEncryptResponse() =
runBlocking {
val expected = BunkerResponseEncrypt(ciphertext = "Test")
val actual = encodeDecodeEvent(expected)
assertEquals(expected.id, actual.id)
assertEquals(expected.result, actual.result)
assertEquals(expected.error, actual.error)
assertEquals(expected.ciphertext, actual.ciphertext)
}
}
@@ -0,0 +1,122 @@
/**
* Copyright (c) 2025 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
* Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.quartz.nip49PrivKeyEnc
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.vitorpamplona.quartz.nip01Core.core.toHexKey
import junit.framework.TestCase.assertEquals
import junit.framework.TestCase.assertNotNull
import junit.framework.TestCase.fail
import org.junit.Test
import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
public class NIP49Test {
companion object {
val TEST_CASE = "ncryptsec1qgg9947rlpvqu76pj5ecreduf9jxhselq2nae2kghhvd5g7dgjtcxfqtd67p9m0w57lspw8gsq6yphnm8623nsl8xn9j4jdzz84zm3frztj3z7s35vpzmqf6ksu8r89qk5z2zxfmu5gv8th8wclt0h4p"
val TEST_CASE_EXPECTED = "3501454135014541350145413501453fefb02227e449e57cf4d3a3ce05378683"
val TEST_CASE_PASSWORD = "nostr"
val MAIN_TEST_CASES =
listOf<Nip49TestCase>(
Nip49TestCase(".ksjabdk.aselqwe", "14c226dbdd865d5e1645e72c7470fd0a17feb42cc87b750bab6538171b3a3f8a", 1, 0x00),
Nip49TestCase("skjdaklrnçurbç l", "f7f2f77f98890885462764afb15b68eb5f69979c8046ecb08cad7c4ae6b221ab", 2, 0x01),
Nip49TestCase("777z7z7z7z7z7z7z", "11b25a101667dd9208db93c0827c6bdad66729a5b521156a7e9d3b22b3ae8944", 3, 0x02),
Nip49TestCase(".ksjabdk.aselqwe", "14c226dbdd865d5e1645e72c7470fd0a17feb42cc87b750bab6538171b3a3f8a", 7, 0x00),
Nip49TestCase("skjdaklrnçurbç l", "f7f2f77f98890885462764afb15b68eb5f69979c8046ecb08cad7c4ae6b221ab", 8, 0x01),
Nip49TestCase("777z7z7z7z7z7z7z", "11b25a101667dd9208db93c0827c6bdad66729a5b521156a7e9d3b22b3ae8944", 9, 0x02),
Nip49TestCase("", "f7f2f77f98890885462764afb15b68eb5f69979c8046ecb08cad7c4ae6b221ab", 4, 0x00),
Nip49TestCase("", "11b25a101667dd9208db93c0827c6bdad66729a5b521156a7e9d3b22b3ae8944", 5, 0x01),
Nip49TestCase("", "f7f2f77f98890885462764afb15b68eb5f69979c8046ecb08cad7c4ae6b221ab", 1, 0x00),
Nip49TestCase("", "11b25a101667dd9208db93c0827c6bdad66729a5b521156a7e9d3b22b3ae8944", 9, 0x01),
)
}
val nip49 = Nip49()
@Test
fun decodeBech32() {
val data =
Nip49.EncryptedInfo.decodePayload(
TEST_CASE,
)!!
assertEquals(2, data.version)
assertEquals(16, data.logn)
assertEquals("52d7c3f8580e7b41953381e5bc49646b", data.salt.toHexKey())
assertEquals("c33f02a7dcaac8bdd8da23cd449783240b6ebc12edeea7bf", data.nonce.toHexKey())
assertEquals(0, data.keySecurity)
assertEquals("b8e8803440de7b3e9519c3e734cb2ac9a211ea2dc52312e5117a11a3022d813ab438719ca0b504a1193be510c3aee776", data.encryptedKey.toHexKey())
}
@Test
fun decrypt() {
val decrypted = nip49.decrypt(TEST_CASE, TEST_CASE_PASSWORD)
assertEquals(TEST_CASE_EXPECTED, decrypted)
}
@Test
fun encryptDecryptTestCase() {
val encrypted = nip49.encrypt(TEST_CASE_EXPECTED, TEST_CASE_PASSWORD, 16, 0)
val decrypted = nip49.decrypt(encrypted!!, TEST_CASE_PASSWORD)
assertEquals(TEST_CASE_EXPECTED, decrypted)
}
@Test
fun encryptDecrypt() {
MAIN_TEST_CASES.forEach {
val encrypted = nip49.encrypt(it.secretKey, it.password, it.logn, it.ksb)
assertNotNull(encrypted)
val decrypted = nip49.decrypt(encrypted!!, it.password)
assertEquals(it.secretKey, decrypted)
}
}
@Test
fun normalization() {
val samePassword1 = String(byteArrayOf(0xE2.toByte(), 0x84.toByte(), 0xAB.toByte(), 0xE2.toByte(), 0x84.toByte(), 0xA6.toByte(), 0xE1.toByte(), 0xBA.toByte(), 0x9B.toByte(), 0xCC.toByte(), 0xA3.toByte()))
val samePassword2 = String(byteArrayOf(0xC3.toByte(), 0x85.toByte(), 0xCE.toByte(), 0xA9.toByte(), 0xE1.toByte(), 0xB9.toByte(), 0xA9.toByte()))
if (samePassword1.toByteArray().contentEquals(samePassword2.toByteArray())) {
fail("Passwords should have a different byte representation")
}
val encrypted = nip49.encrypt(TEST_CASE_EXPECTED, samePassword1, 8, 0)
assertNotNull(encrypted)
val decrypted = nip49.decrypt(encrypted!!, samePassword2)
assertEquals(TEST_CASE_EXPECTED, decrypted)
}
class Nip49TestCase(
val password: String,
val secretKey: String,
val logn: Int,
val ksb: Byte,
)
}
@@ -0,0 +1,49 @@
/**
* Copyright (c) 2025 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
* Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.quartz.nip55AndroidSigner
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.vitorpamplona.quartz.nip01Core.core.hexToByteArray
import com.vitorpamplona.quartz.nip01Core.core.toHexKey
import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair
import com.vitorpamplona.quartz.nip01Core.crypto.Nip01
import org.junit.Assert.assertEquals
import org.junit.Test
import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
class SignStringTest {
@Test
fun signString() {
val random = "319cc5596fdd6cd767e5a59d976e8e059c61306af90dff1e6ee1067b3a1fdbc0".hexToByteArray()
val message = "8e58c8251bb406b6ded69e9eb14f55282a9a53bdab16fc49a3218c2ad3abc887".hexToByteArray()
val keyPair = KeyPair("a5ab474552c8f9c46c2eda5a0b68f27430ad81f96cb405e0cb4e34bf0c6494a2".hexToByteArray())
val signedMessage = Nip01.sign(message, keyPair.privKey!!, random).toHexKey()
val expectedValue = "0f9be7e01ba53d5ee6874b9180c7956269fda7a5be424634c3d17b5cfcea6da001be89183876415ba08b7dafa6cff4555e393dc228fb8769b384344e9a27b77c"
assertEquals(expectedValue, signedMessage)
val message2 = "Hello"
val signedMessage2 = signString(message2, keyPair.privKey!!, random).toHexKey()
val expectedValue2 = "7ec8194a585bfb513564113b6b7bfeaafa0254c99d24eaf92280657c2291bab908b1b7bc553c83276a0254aef5041bbe6a50e93381edc4de3d859efa1c3a5a1e"
assertEquals(expectedValue2, signedMessage2)
}
}
@@ -0,0 +1,188 @@
/**
* Copyright (c) 2025 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
* Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.quartz.nip57Zaps
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.vitorpamplona.quartz.nip01Core.core.Event
import com.vitorpamplona.quartz.nip01Core.core.toHexKey
import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair
import com.vitorpamplona.quartz.nip01Core.jackson.JsonMapper
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.RelayUrlNormalizer
import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerInternal
import com.vitorpamplona.quartz.nip57Zaps.PrivateZapEncryption.Companion.createEncryptionPrivateKey
import com.vitorpamplona.quartz.utils.Hex
import junit.framework.TestCase.assertNotNull
import junit.framework.TestCase.fail
import kotlinx.coroutines.runBlocking
import org.junit.Test
import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
class PrivateZapTests {
@Test
fun testPollZap() {
val poll =
JsonMapper.fromJson(
"""{
"content": "New poll \n\n #zappoll",
"created_at": 1682440713,
"id": "16291ba452bb0786a4bf5c278d38de73c96b58c056ed75c5ea466b0795197288",
"kind": 6969,
"pubkey": "f8ff11c7a7d3478355d3b4d174e5a473797a906ea4aa61aa9b6bc0652c1ea17a",
"sig": "ac05fa4004c3f7c42913c87b11bf9714bb61a3f0940863a6b9ff0f8105b399add72dbc09bf944c79b9a72ef009ec6905adedbd2c4c8fb3d2f57007bad8fcb279",
"tags": [
[
"poll_option",
"0",
"Test 1"
],
[
"poll_option",
"1",
"Test 2"
],
[
"value_maximum",
"null"
],
[
"value_minimum",
"null"
],
[
"consensus_threshold",
"null"
],
[
"closed_at",
"null"
]
],
"seenOn": [
"wss://relay.damus.io/"
]
}
""",
)
val loggedIn =
NostrSignerInternal(
KeyPair(
Hex.decode("e8e7197ccc53c9ed4cf9b1c8dce085475fa1ffdd71f2c14e44fe23d0bdf77598"),
),
)
val privateZapRequest =
runBlocking {
LnZapRequestEvent.create(
zappedEvent = poll,
relays = setOf(RelayUrlNormalizer.normalize("wss://relay.damus.io/")),
signer = loggedIn,
pollOption = 0,
message = "",
zapType = LnZapEvent.ZapType.PRIVATE,
toUserPubHex = null,
)
}
val recepientPK = privateZapRequest.zappedAuthor().firstOrNull()
val recepientPost = privateZapRequest.zappedPost().firstOrNull()
if (recepientPK != null && recepientPost != null) {
val privateKey =
createEncryptionPrivateKey(
loggedIn.keyPair.privKey!!.toHexKey(),
recepientPost,
privateZapRequest.createdAt,
)
val decodedPrivateZap = PrivateZapRequestBuilder().decryptAnonTag(privateZapRequest.getAnonTag(), privateKey, recepientPK)
assertNotNull(decodedPrivateZap)
} else {
fail("Should not be null")
}
}
@Test
fun testKind1PrivateZap() {
val textNote =
Event.fromJson(
"""{
"content": "Testing copied author. \n\nnostr:npub1gcxzte5zlkncx26j68ez60fzkvtkm9e0vrwdcvsjakxf9mu9qewqlfnj5z",
"created_at": 1682369982,
"id": "c757e1371d715c711ec9ef9740a3df6475d64b3d0af45ffcbfca08d273baf1c1",
"kind": 1,
"pubkey": "f8ff11c7a7d3478355d3b4d174e5a473797a906ea4aa61aa9b6bc0652c1ea17a",
"sig": "1fb5b6fd980f4c2ef058d5f4f7b166c0e5fb21eff26fe9cacd87a9aa4feb344485841ebcc26a233bf8d6ea0a66acf0db2bfdb11ad1cb04bcea4cfa3e78c3eaf1",
"tags": [
[
"p",
"460c25e682fda7832b52d1f22d3d22b3176d972f60dcdc3212ed8c92ef85065c"
]
],
"seenOn": [
"wss://relay.damus.io/"
]
}
""",
)
val loggedIn =
NostrSignerInternal(
KeyPair(Hex.decode("e8e7197ccc53c9ed4cf9b1c8dce085475fa1ffdd71f2c14e44fe23d0bdf77598")),
)
val privateZapRequest =
runBlocking {
LnZapRequestEvent.create(
zappedEvent = textNote,
relays =
setOf(
RelayUrlNormalizer.normalize("wss://relay.damus.io/"),
RelayUrlNormalizer.normalize("wss://relay.damus2.io/"),
RelayUrlNormalizer.normalize("wss://relay.damus3.io/"),
),
signer = loggedIn,
pollOption = null,
message = "test",
zapType = LnZapEvent.ZapType.PRIVATE,
toUserPubHex = null,
)
}
val recepientPK = privateZapRequest.zappedAuthor().firstOrNull()
val recepientPost = privateZapRequest.zappedPost().firstOrNull()
if (recepientPK != null && recepientPost != null) {
val privateKey =
createEncryptionPrivateKey(
loggedIn.keyPair.privKey!!.toHexKey(),
recepientPost,
privateZapRequest.createdAt,
)
val decodedPrivateZap = PrivateZapRequestBuilder().decryptAnonTag(privateZapRequest.getAnonTag(), privateKey, recepientPK)
assertNotNull(decodedPrivateZap)
} else {
fail("Should not be null")
}
}
}
@@ -0,0 +1,668 @@
/**
* Copyright (c) 2025 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
* Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.quartz.nip59GiftWraps
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.vitorpamplona.quartz.nip01Core.checkSignature
import com.vitorpamplona.quartz.nip01Core.core.Event
import com.vitorpamplona.quartz.nip01Core.core.HexKey
import com.vitorpamplona.quartz.nip01Core.core.hexToByteArray
import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair
import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerInternal
import com.vitorpamplona.quartz.nip01Core.tags.people.PTag
import com.vitorpamplona.quartz.nip01Core.tags.people.isTaggedUser
import com.vitorpamplona.quartz.nip17Dm.NIP17Factory
import com.vitorpamplona.quartz.nip17Dm.messages.ChatMessageEvent
import com.vitorpamplona.quartz.nip59Giftwrap.seals.SealedRumorEvent
import com.vitorpamplona.quartz.nip59Giftwrap.wraps.GiftWrapEvent
import com.vitorpamplona.quartz.utils.Hex
import kotlinx.coroutines.runBlocking
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNotEquals
import org.junit.Assert.assertNotNull
import org.junit.Assert.assertTrue
import org.junit.Assert.fail
import org.junit.Test
import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
class GiftWrapEventTest {
@Test()
fun testNip17Utils() =
runBlocking {
val sender = NostrSignerInternal(KeyPair())
val receiver = NostrSignerInternal(KeyPair())
val message = "Hola, que tal?"
val events =
NIP17Factory().createMessageNIP17(
ChatMessageEvent.build(
message,
listOf(PTag(receiver.pubKey, null)),
),
sender,
)
// Simulate Receiver
val eventsReceiverGets = events.wraps.filter { it.isTaggedUser(receiver.pubKey) }
eventsReceiverGets.forEach {
val event = it.unwrapThrowing(receiver)
if (event is SealedRumorEvent) {
val innerData = event.unsealThrowing(receiver)
assertEquals(message, innerData.content)
} else {
fail("Wrong Event")
}
}
// Simulate Sender
val eventsSenderGets = events.wraps.filter { it.isTaggedUser(sender.pubKey) }
eventsSenderGets.forEach {
val event = it.unwrapThrowing(sender)
if (event is SealedRumorEvent) {
val innerData = event.unsealThrowing(sender)
assertEquals(message, innerData.content)
} else {
fail("Wrong Event")
}
}
}
@Test()
fun testNip17UtilsForGroups() =
runBlocking {
val sender = NostrSignerInternal(KeyPair())
val receiver1 = NostrSignerInternal(KeyPair())
val receiver2 = NostrSignerInternal(KeyPair())
val receiver3 = NostrSignerInternal(KeyPair())
val receiver4 = NostrSignerInternal(KeyPair())
val message = "Hola, que tal?"
val receivers =
listOf(
receiver1,
receiver2,
receiver3,
receiver4,
)
val events =
NIP17Factory().createMessageNIP17(
ChatMessageEvent.build(
message,
receivers.map { PTag(it.pubKey, null) },
),
sender,
)
// Simulate Receiver
receivers.forEach { receiver ->
val eventsReceiverGets = events.wraps.filter { it.isTaggedUser(receiver.pubKey) }
eventsReceiverGets.forEach {
val event = it.unwrapThrowing(receiver)
if (event is SealedRumorEvent) {
val innerData = event.unsealThrowing(receiver)
assertEquals(message, innerData.content)
} else {
fail("Wrong Event")
}
}
}
// Simulate Sender
val eventsSenderGets = events.wraps.filter { it.isTaggedUser(sender.pubKey) }
eventsSenderGets.forEach {
val event = it.unwrapThrowing(sender)
if (event is SealedRumorEvent) {
val innerData = event.unsealThrowing(sender)
assertEquals(message, innerData.content)
} else {
fail("Wrong Event")
}
}
}
@Test()
fun testInternalsSimpleMessage() =
runBlocking {
val sender = NostrSignerInternal(KeyPair())
val receiver = NostrSignerInternal(KeyPair())
val senderMessage =
sender.sign(
ChatMessageEvent.build(
msg = "Hi There!",
to = listOf(PTag(receiver.pubKey, null)),
),
)
// MsgFor the Receiver
val encMsgFromSenderToReceiver =
SealedRumorEvent.create(
event = senderMessage,
encryptTo = receiver.pubKey,
signer = sender,
)
// Should expose sender
assertEquals(encMsgFromSenderToReceiver.pubKey, sender.pubKey)
// Should not expose receiver
assertTrue(encMsgFromSenderToReceiver.tags.isEmpty())
val giftWrapToReceiver =
GiftWrapEvent.create(
event = encMsgFromSenderToReceiver,
recipientPubKey = receiver.pubKey,
)
// Should not be signed by neither sender nor receiver
assertNotEquals(giftWrapToReceiver.pubKey, sender.pubKey)
assertNotEquals(giftWrapToReceiver.pubKey, receiver.pubKey)
// Should not include sender as recipient
assertNotEquals(giftWrapToReceiver.recipientPubKey(), sender.pubKey)
// Should be addressed to the receiver
assertEquals(giftWrapToReceiver.recipientPubKey(), receiver.pubKey)
// MsgFor the Sender
val encMsgFromSenderToSender =
SealedRumorEvent.create(
event = senderMessage,
encryptTo = sender.pubKey,
signer = sender,
)
// Should expose sender
assertEquals(encMsgFromSenderToSender.pubKey, sender.pubKey)
// Should not expose receiver
assertTrue(encMsgFromSenderToSender.tags.isEmpty())
val giftWrapToSender =
GiftWrapEvent.create(
event = encMsgFromSenderToSender,
recipientPubKey = sender.pubKey,
)
// Should not be signed by neither the sender, not the receiver
assertNotEquals(giftWrapToSender.pubKey, sender.pubKey)
assertNotEquals(giftWrapToSender.pubKey, receiver.pubKey)
// Should not be addressed to the receiver
assertNotEquals(giftWrapToSender.recipientPubKey(), receiver.pubKey)
// Should be addressed to the sender
assertEquals(giftWrapToSender.recipientPubKey(), sender.pubKey)
// Done
// -----
// Start receiving
// Receiver's side
// Makes sure it can only be decrypted by the target user
assertNotNull(giftWrapToSender)
assertNotNull(giftWrapToReceiver)
val unwrappedMsgForSenderBySender = giftWrapToSender.unwrapThrowing(sender)
assertEquals(SealedRumorEvent.KIND, unwrappedMsgForSenderBySender.kind)
assertTrue(unwrappedMsgForSenderBySender is SealedRumorEvent)
if (unwrappedMsgForSenderBySender is SealedRumorEvent) {
val unwrappedRumorToSenderBySender = unwrappedMsgForSenderBySender.unsealThrowing(sender)
assertEquals("Hi There!", unwrappedRumorToSenderBySender.content)
unwrappedMsgForSenderBySender.unsealOrNull(receiver)?.let { _ ->
fail("Should not be able to decrypt msg for the sender by the sender but decrypted with receiver")
}
}
giftWrapToReceiver.unwrapOrNull(sender)?.let { _ ->
fail("Should not be able to decrypt msg for the receiver decrypted by the sender")
}
giftWrapToReceiver.unwrapOrNull(receiver)?.let { _ ->
fail("Should not be able to decrypt msg for the sender decrypted by the receiver")
}
val unwrappedMsgForReceiverByReceiver = giftWrapToReceiver.unwrapThrowing(receiver)
assertEquals(SealedRumorEvent.KIND, unwrappedMsgForReceiverByReceiver.kind)
assertTrue(unwrappedMsgForReceiverByReceiver is SealedRumorEvent)
if (unwrappedMsgForReceiverByReceiver is SealedRumorEvent) {
val unwrappedRumorToReceiverByReceiver = unwrappedMsgForReceiverByReceiver.unsealThrowing(receiver)
assertEquals("Hi There!", unwrappedRumorToReceiverByReceiver.content)
unwrappedMsgForReceiverByReceiver.unsealOrNull(sender)?.let { _ ->
fail(
"Should not be able to decrypt msg for the receiver by the receiver but decrypted with the sender",
)
}
}
}
@Test()
fun testInternalsGroupMessage() =
runBlocking {
val sender = NostrSignerInternal(KeyPair())
val receiverA = NostrSignerInternal(KeyPair())
val receiverB = NostrSignerInternal(KeyPair())
val senderMessage =
sender.sign(
ChatMessageEvent.build(
msg = "Who is going to the party tonight?",
to = listOf(PTag(receiverA.pubKey), PTag(receiverB.pubKey)),
),
)
val msgFromSenderToReceiverA =
SealedRumorEvent.create(
event = senderMessage,
encryptTo = receiverA.pubKey,
signer = sender,
)
// Should expose sender
assertEquals(msgFromSenderToReceiverA.pubKey, sender.pubKey)
// Should not expose receiver
assertTrue(msgFromSenderToReceiverA.tags.isEmpty())
val giftWrapForReceiverA =
GiftWrapEvent.create(
event = msgFromSenderToReceiverA,
recipientPubKey = receiverA.pubKey,
)
// Should not be signed by neither sender nor receiver
assertNotEquals(giftWrapForReceiverA.pubKey, sender.pubKey)
assertNotEquals(giftWrapForReceiverA.pubKey, receiverA.pubKey)
assertNotEquals(giftWrapForReceiverA.pubKey, receiverB.pubKey)
// Should not include sender as recipient
assertNotEquals(giftWrapForReceiverA.recipientPubKey(), sender.pubKey)
// Should be addressed to the receiver
assertEquals(giftWrapForReceiverA.recipientPubKey(), receiverA.pubKey)
val msgFromSenderToReceiverB =
SealedRumorEvent.create(
event = senderMessage,
encryptTo = receiverB.pubKey,
signer = sender,
)
// Should expose sender
assertEquals(msgFromSenderToReceiverB.pubKey, sender.pubKey)
// Should not expose receiver
assertTrue(msgFromSenderToReceiverB.tags.isEmpty())
val giftWrapForReceiverB =
GiftWrapEvent.create(
event = msgFromSenderToReceiverB,
recipientPubKey = receiverB.pubKey,
)
// Should not be signed by neither sender nor receiver
assertNotEquals(giftWrapForReceiverB.pubKey, sender.pubKey)
assertNotEquals(giftWrapForReceiverB.pubKey, receiverA.pubKey)
assertNotEquals(giftWrapForReceiverB.pubKey, receiverB.pubKey)
// Should not include sender as recipient
assertNotEquals(giftWrapForReceiverB.recipientPubKey(), sender.pubKey)
// Should be addressed to the receiver
assertEquals(giftWrapForReceiverB.recipientPubKey(), receiverB.pubKey)
val msgFromSenderToSender =
SealedRumorEvent.create(
event = senderMessage,
encryptTo = sender.pubKey,
signer = sender,
)
// Should expose sender
assertEquals(msgFromSenderToSender.pubKey, sender.pubKey)
// Should not expose receiver
assertTrue(msgFromSenderToSender.tags.isEmpty())
val giftWrapToSender =
GiftWrapEvent.create(
event = msgFromSenderToSender,
recipientPubKey = sender.pubKey,
)
// Should not be signed by neither the sender, not the receiver
assertNotEquals(giftWrapToSender.pubKey, sender.pubKey)
assertNotEquals(giftWrapToSender.pubKey, receiverA.pubKey)
assertNotEquals(giftWrapToSender.pubKey, receiverB.pubKey)
// Should not be addressed to the receiver
assertNotEquals(giftWrapToSender.recipientPubKey(), receiverA.pubKey)
assertNotEquals(giftWrapToSender.recipientPubKey(), receiverB.pubKey)
// Should be addressed to the sender
assertEquals(giftWrapToSender.recipientPubKey(), sender.pubKey)
// Done
// -----
// Decrypting
// Receiver's side
// Makes sure it can only be decrypted by the target user
assertNotNull(giftWrapToSender)
assertNotNull(giftWrapForReceiverA)
assertNotNull(giftWrapForReceiverB)
val unwrappedMsgForSenderBySender = giftWrapToSender.unwrapThrowing(sender)
assertEquals(SealedRumorEvent.KIND, unwrappedMsgForSenderBySender.kind)
if (unwrappedMsgForSenderBySender is SealedRumorEvent) {
unwrappedMsgForSenderBySender.unsealOrNull(receiverA)?.let { _ ->
fail()
}
unwrappedMsgForSenderBySender.unsealOrNull(receiverB)?.let { _ ->
fail()
}
val unwrappedRumorToSenderBySender = unwrappedMsgForSenderBySender.unsealThrowing(sender)
assertEquals("Who is going to the party tonight?", unwrappedRumorToSenderBySender.content)
}
giftWrapForReceiverA.unwrapOrNull(sender)?.let { unwrappedMsgForReceiverBySenderA ->
fail("Should not be able to decode msg to the receiver A with the sender's key")
}
giftWrapForReceiverB.unwrapOrNull(sender)?.let { unwrappedMsgForReceiverBySenderB ->
fail("Should not be able to decode msg to the receiver B with the sender's key")
}
giftWrapToSender.unwrapOrNull(receiverA)?.let {
fail("Should not be able to decode msg to sender with the receiver A's key")
}
val unwrappedMsgForReceiverAByReceiverA = giftWrapForReceiverA.unwrapThrowing(receiverA)
assertEquals(SealedRumorEvent.KIND, unwrappedMsgForReceiverAByReceiverA.kind)
if (unwrappedMsgForReceiverAByReceiverA is SealedRumorEvent) {
val unwrappedRumorToReceiverAByReceiverA = unwrappedMsgForReceiverAByReceiverA.unsealThrowing(receiverA)
assertEquals("Who is going to the party tonight?", unwrappedRumorToReceiverAByReceiverA.content)
unwrappedMsgForReceiverAByReceiverA.unsealOrNull(sender)?.let { unwrappedRumorToReceiverABySender ->
fail()
}
unwrappedMsgForReceiverAByReceiverA.unsealOrNull(receiverB)?.let { unwrappedRumorToReceiverAByReceiverB ->
fail()
}
}
giftWrapForReceiverB.unwrapOrNull(receiverA)?.let {
fail("Should not be able to decode msg to sender with the receiver A's key")
}
giftWrapToSender.unwrapOrNull(receiverB)?.let { unwrappedMsgForSenderByReceiverB ->
fail("Should not be able to decode msg to sender with the receiver B's key")
}
giftWrapForReceiverA.unwrapOrNull(receiverB)?.let { unwrappedMsgForReceiverAByReceiverB ->
fail("Should not be able to decode msg to receiver A with the receiver B's key")
}
val unwrappedMsgForReceiverBByReceiverB = giftWrapForReceiverB.unwrapThrowing(receiverB)
assertEquals(SealedRumorEvent.KIND, unwrappedMsgForReceiverBByReceiverB.kind)
if (unwrappedMsgForReceiverBByReceiverB is SealedRumorEvent) {
unwrappedMsgForReceiverBByReceiverB.unsealOrNull(receiverA)?.let { unwrappedRumorToReceiverBByReceiverA ->
fail()
}
val unwrappedRumorToReceiverBByReceiverB = unwrappedMsgForReceiverBByReceiverB.unsealThrowing(receiverB)
assertEquals(
"Who is going to the party tonight?",
unwrappedRumorToReceiverBByReceiverB.content,
)
unwrappedMsgForReceiverBByReceiverB.unsealOrNull(sender)?.let { unwrappedRumorToReceiverBBySender ->
fail()
}
}
}
@Test
fun testCaseFromAmethyst1() {
val json =
"""
{
"content":"{\"ciphertext\":\"AaTN5Mt7AOeMosjHeLfai89kmvW/qJ7W2VMttAwuh6hwRGV+ylJhpDbdVRhVmkCotbDjBgS6xioLrSDcdSngFOiVMHS5dTAP0MkQv09aZlBh/NgdmyfHHd24YlHPkDuF5Yb4Vmz7kq/vmjsNZvDrTen3TG2DcEoTV9GKexdMEqyBA4LsB2DLnWfpvOi0olDkGjPGSteTaU1nCdOtN8knoEKumrxwevvbygKphorvKX/j3ojMMb0AceJM6Cr6TRIvSsQnKGEv5V8qbC/uIrQoH3N108Fd/2SY2MWuyLKRnuak9F/w82MV13elq8ngyjcktLYM5yrPg5nrxZlyJsV8D7V/g/bvhoL+UmWe0XoCR5LXzy77SfIkgA1ePKEfGp5sD2CVIzXt9zHdFwGxAKZuyB4qwrRaAFrS2xx+Bw4nnEmF6V9NhfheSCmGzTILuTePx4ubvnYw/j8Hmqd6UvM3DBNnlJ3D6po0blirfWvMe/ea+Em4CMXfq8Iq+7r4gRx8azADygKeJ+C89GTBEvS9EvgrXCVfTMVTcFc44YAZhekOqYY1BOZgfxIV4gUiJfpMMd4B9MQv/tmnewrpTsq1reSQQcEW/mXT2cnMeCZbAIJSPg8usZ30QlrH+np+YSzFKWYDP1kThcV0ElEE2Ne8KaUUFIRE5KmhBQc/qtORefCpne5s7V7J5vLjT5rinsDzzENB1XVlmY1Icx42raP5tGAL1gOK5gRHLvtcgFQR3WcDRYaNqELiYxx41j9w9lz5e00Ttla255rZkb760KSLaBFBss6wYGiYCabVgtBNpkExpCFPPEd5eAZa5rNK2QrnojYsdxEnlicF6A+zSChLy/TbzxYwyQywDfoF9F8kBakPZkAhsciQViCii2KlieRq4OgJFZGndmnS82hyPqsoJIm22vWr1iqMvSBHo/9cLj/r+lfmGVOdgM62JHckPZjOLS0QWIb9gQiT+zXZG22+eZElMYbGXVpR1dyMaQtde8ivEVVLas6kMCVKaDTHEFglaCBXjJ3RNJv73HsG1kb0rMmOj8ltbBakjHpv7M59amavuu6SReYt\",\"nonce\":\"6anNjUdNwW6MNfoKzRZcz1R09N1h8G4L\",\"v\":1}",
"created_at":1690660515,
"id":"d90739741c2f5a8c1a03aab5dc219c0b708ed6b0566044495731cd0307cf19a5",
"kind":1059,
"pubkey":"a79b7162f8ebb9c9f7aa65a48977ab7f32aa097520bc543e4d625812154ff6af",
"sig":"9b012504e779632a2a1f55562fa9a85f8ae6245cbc149b83d25b2971249053abc77f65cc068e5d025b871d743678265fede70de4eaf5af642e675a5b6210077d",
"tags":[
[
"p",
"c55f0b0cb4dd180dd4395867b28dd4c208b709144a69fb7c31a46c369a5ad1c6"
]
]
}
""".trimIndent()
val rumor: Event =
runBlocking {
val privateKey = "de6152a85a0dea3b09a08a6f8139a314d498a7b52f7e5c28858b64270abd4c70"
unwrapUnsealRumor(json, privateKey)
}
assertNotNull(rumor)
assertEquals("Hola, que tal?", rumor.content)
}
@Test
fun testCaseFromAmethyst2() {
val json =
"""
{
"content":"{\"ciphertext\":\"Zb0ZNYAcDG5y7BiCWgbxY/i7rN7TxPwr3Oaste6em4VcetuenaMu2SyH6OuCCxxmIa7kFennJD8ZCrev0086azsPNutl9I6OCoOfDQb2GoFaLoJAkE/FuW0uEoEJuN72KsKj05HEjOM6nqL2KiW0pxTCNmlGpweMwpXQdm2ItWkybNpq8+b4NJUDee2czBUd9Kr2ELbPISTYzA17z1IzPXGQw8c73NL+QX9I/QZjM/agqX2x5q11SU52xiRyVd9zHf7TMctZI4QEsqDB6xi54D1bAeZlMhVdcpQRpGDfqRz3KXFlhB3Bwdc8GLgY0aLTn6tJs4qrHP3mQkxFYk0mju0afoc0rloMEUHcBVtM18S9OrTPqfmSqFTQsjaT8g+PkmeiLBo1sXsMCS62w0abSZD9OzQtciMz70ZpcWoLjx5f8panjFClvg4tJ8czMURIHM/IFS1uKAUHBArGN8QpCw8MXQBblpyLDiEkFcSX334Zdps0OIw4z328JSdeejyRh4ks+NHDt9FcjC4iicEqfEh8OTkXuKqEAVkRyfAioNQxWQPnXDzMX0Q+BXvKzBA7NaEBDpbV36H/KnrpBBQwokV9/Byb6Seh3g6GSqRAWD3U6Nk2aBMXkD0xY8vnIqMckBeYHxn8BW7k1FdXFC9lE5xCxWZHkmksJ4f0NVaF37O6d8qOe6RK7bfUeF8/SouJEu+eEX1f4KCMboslwkdk8QA8bThGcRGn8GQBMrPKrpZwHYNyyH8jwt9pywigXJejRLDDnDp3FH/3dbZy5CfuNH6KGydf/O5xx1r316so1UPO1mL5LHJUFZVIaMaMMUsgq12gpI0lLEh5NJPpsi9e3ibkzEZGf7FlAJjJQURbQ8xacN7R+w3GWKbJNHiQbUZ2lXo6fwz33t0DrSqEW970yWPHlqxcpd27EI+qqb5IqfklQZ3RObZZBhzDvImaCPG+U7SmgLhPxnilpGjd5lw/ttiqJhPG9mYFMf1eJXSG+Q9VVkGzN7jxXYtx0q0WGjVq98ZGv5RSnF1d9+QVGCd1fiPS3rsaWdYWly8l0y2quYObJ6Mv3Wh3\",\"nonce\":\"/Q2UTTjVZthm/atcCuDjU1e4reF+ZSgZ\",\"v\":1}",
"created_at":1690660515,
"id":"087d9627d63135a5050758a69222e566c86702e930c9905f0b93ccd6bebeca3f",
"kind":1059,
"pubkey":"e59c00796ae2aa9077fc8bcd57fe8d32c0fc363f7c8b93d055c70804ffff3772",
"sig":"807cb641c314ca6910aaeefadcf87d859137520be1039eb40e39832ed59d456fdd800c5f88bba09e1b395ee90c66d5330847bdd010b63be9919bf091adbc2c2a",
"tags":[
[
"p",
"f85f315c06aaf19c2b30a96ca80d9644720655ee8d3ec43b84657a7c98f36a23"
]
]
}
""".trimIndent()
val privateKey = "409ff7654141eaa16cd2161fe5bd127aeaef71f270c67587474b78998a8e3533"
val rumor: Event =
runBlocking {
unwrapUnsealRumor(json, privateKey)
}
assertNotNull(rumor)
assertEquals("Hola, que tal?", rumor.content)
}
@Test
fun testDecryptFromCoracle() {
val json =
"""
{
"content": "{\"ciphertext\":\"fo0/Ywyfu86cXKoOOeFFlMRv+LYM6GJUL+F/J4ARv6EcAufOZP46vimlurPBLPjNgzuGemGjlTFfC3vtq84AqIsqFo3dqKunq8Vp+mmubvxIQUDzOGYvM0WE/XOiW5LEe3U3Vq399Dq07xRpXELcp4EZxGyu4Fowv2Ppb4SKpH8g+9N3z2+bwYcSxBvI6SrL+hgmVMrRlgvsUHN1d53ni9ehRseBqrLj/DqyyEiygsKm6vnEZAPKnl1MrBaVOBZmGsyjAa/G4SBVVmk78sW7xWWvo4cV+C22FluDWUOdj/bYabH4aR4scnBX3GLYqfLuzGnuQlRNsb5unXVX41+39uXzROrmNP6iYVyYxy5tfoyN7PPZ4osoKpLDUGldmXHD6RjMcAFuou4hXt2JlTPmXpj/x8qInXId5mkmU4nTGiasvsCIpJljbCujwCjbjLTcD4QrjuhMdtSsAzjT0CDv5Lmc632eKRYtDu/9B+lkqBBkp7amhzbqp8suNTnybkvbGFQQGEQnsLfNJw/GGopAuthfi8zkTgUZR/LxFR7ZKAX73G+5PQSDSjPuGH/dQEnsFo45zsh1Xro8SfUQBsPphbX2GS31Lwu5vA30O922T4UiWuU+EdNgZR0JankQ5NPgvr1uS56C3v84VwdrNWQUCwC4eYJl4Mb/OdpEy9qwsisisppq6uuzxmxd1qx3JfocnGsvB7h2g2sG+0lyZADDSobOEZEKHaBP3w+dRcJW9D95EmzPym9GO0n+33OfqFQbda7G0rzUWfPDV0gXIuZcKs/HmDqepgIZN8FG7JhRBeAv0bCbKQACre0c8tzVEn5yCYemltScdKop3pC/r6gH50jRhAlFAiIKx8R+XwuMmJRqOcH4WfkpZlfVU85/I0XJOCHWKk6BnJi/NPP9zYiZiJe+5LecqMUVjtO0YAlv138+U/3FIT/anQ4H5bjVWBZmajwf\",\"nonce\":\"Mv70S6jgrs4D1rlqV9b5DddiymGAcVVe\",\"v\":1}",
"created_at": 1690528373,
"id": "6b108e4236c3c338236ee589388ce0f91f921e1532ae52e75d1d2add6f8e691a",
"kind": 1059,
"pubkey": "627dc0248335e2bf9adac14be9494139ebbeb12c422d7df5b0e3cd72d04c209c",
"sig": "be11da487196db298e4ffb7a03e74176c37441c88b39c95b518fadce6fd02f23c58b2c435ca38c24d512713935ab719dae80bf952267630809e1f84be8e95174",
"tags": [
[
"p",
"e7764a227c12ac1ef2db79ae180392c90903b2cec1e37f5c1a4afed38117185e"
]
],
"seenOn": [
"wss://relay.damus.io/"
]
}
""".trimIndent()
val privateKey = "09e0051fdf5fdd9dd7a54713583006442cbdbf87bdcdab1a402f26e527d56771"
val rumor: Event =
runBlocking {
unwrapUnsealRumor(json, privateKey)
}
assertNotNull(rumor)
assertEquals("test", rumor.content)
}
@Test
fun testFromCoracle2() {
val json =
"""
{
"content": "{\"ciphertext\":\"Hn/dHo/I8Qk6QWWAiKyo/SfKJqQfHdV0O5tMmgqMyfHrsFoDY6IhGQP2EgCJ/6HsNQyO/8EMAmLW8w0PbDKlBKYGKGpaMwCA6B1r0rLjvu+149RJZuggRNm9rd7tNVNkNs38iqt1KYD++bohePm52q+VhAQikbX2gTONV82ROwZylAg9vjvMnYkDt45g6N97s9FRB6V7YMiUEtJnneMixa6klucpUuenQ4569tyt5vnUMD2VNhKYCc2jit2hf7k0DIhvZrVC3OdopUvxIuYYWr3r7XpuEB3HJ6Ji3ajHPzgGeFcItBR7uKZ9s6XU34F3keyZbxrv3yWHFM5NrOctAdZexSGpqWRW93M0KZUAp9HgQh3YzMLl8xt0mcrVywCgjU6Kx8IwkI0bjPU+Am8acY3cItted6hZQ4Vy1xFITdKVfPWDl3Ab59iBg9+IkY5C31wqsKPgPVVycwQE6UpaGW74gy3qZshwyoo01owvEIbVvrSJWXH7EUVvndDPvUbo+f+EVa84IEwVjPmY2oR7VsxVfqRBdmPg23OSw/9rzVybmruqaQHd3xrTTEcnG0qBc/ugCXsiuILTeScOovEnqIlKKK3KB36jMtdScdJB+b4YrzJInY1AvqU7IAgqe0vmo1LdbMtj7kjuxkXJhhQsunAbTvPigTrsOfJ08P9l7r/95kpxudgagEaW7XAjYVfLphseJT3Iy1IuQEyG5sshQ+pl/CYvkGide7ykHwm9pjSBVkD9Mdcn5X6lSnLNJEcwY43pz43r6Kq3L09qneILY3DSKyQ16Zcu1MiAMAM5r6JGvpAHqcMmixi9ORuiryjteTmY4L0vI7b3W/0RSUblXxUrb8IpeysBrFmiKJBgCoU0r/D/8tgR+Eewyp1qxKI4SfKG5GFH40zZ2oVvKyoHAR4x1oVDp/MttcnxkzAsCFL6QuJC9A/vImjsumpmYB/EChcZCOAsfqkuzH4VSjZx\",\"nonce\":\"K537d+7m5tUcXZfkr3Qk2J2G86vdBMmY\",\"v\":1}",
"created_at": 1690655012,
"id": "c4f97c6332b0a63912c44c9e1f8c7b23581dc67a8489ec1522ec205fea7133db",
"kind": 1059,
"pubkey": "8def03a22b1039256a3883d46c7ccd5562f61743100db401344284547de7ec61",
"sig": "25dcf24bdda99c04abc72274d9f7a30538a4a00a70ac4b39db4082b73823979858df93cd649c25edfb759857eac46ed70bb9ad0598f2e011d733a5a382bc4def",
"tags": [
[
"p",
"e7764a227c12ac1ef2db79ae180392c90903b2cec1e37f5c1a4afed38117185e"
]
]
}
""".trimIndent()
val privateKey = "09e0051fdf5fdd9dd7a54713583006442cbdbf87bdcdab1a402f26e527d56771"
val rumor =
runBlocking {
unwrapUnsealRumor(json, privateKey)
}
assertEquals("asdfasdfasdf", rumor.content)
assertEquals(1690659269L, rumor.createdAt)
assertEquals("827ba09d32ab81d62c60f657b350198c8aaba84372dab9ad3f4f6b8b7274b707", rumor.id)
assertEquals(14, rumor.kind)
assertEquals("subject", rumor.tags.firstOrNull()?.get(0))
assertEquals("test", rumor.tags.firstOrNull()?.get(1))
}
@Test
fun testFromCoracle3() {
val json =
"""
{
"content": "{\"ciphertext\":\"PGCodiacmCB/sClw0C6DQRpP/XIfNAKCVZdKLQpOqytgbFryhs9Z+cPldq2FajXzxs3jQsF7/EVQyWpuV8pXetvFT9tvzjg4Xcm7ZcooLUnAeAo2xZNcJontN4cGubuDqKuXy5n59yXP1fIxfnJxRTRRdCZ2edhsKeNR5NSByUi+StjV10rnfHt8AhZCpiXiZ/giTOsC4wdaeONPgMzMeljaJWLvl6n11VjmXhkx1mXIQt43CNB1hIqO3p89Mbd9p+nlLrOsR+Xs0TB4DCh4XTPbvgf7B7Z+PgOfl3GZfJy9x6TciLcF4E3Ba1zrPe4f79czCIEiJ1yrIKrzzYvv+it35DZQ8fgveFXpyHnNL29hml8PNjyOsFbCHVYLMGw88evI5PijOcpe1TtdoioX8kX5kVEQSKJXuoSjTorvbRPCgGzaa1m0J0uTpzri5VD22a/Jh2CcAnubg6w4JDdUWCogdSV3NqiJllo7ZF7WnZ3apPdRD23MEfphVBJrcLBUNlmwajnY5IvVTKTkZOP50r9dBapvMWXIo6M6zhy/5vVWJz57863pelYCRG4upaXZuNK9sMBtbiphxmFR83i8RML8KN8Q391Cd/xBN7TxJNo5p2YU25VeGZUAmHY8DYlMQDm8Br0nStAXp3T+DzTRL8FTECa8DJV+KTAPoCxqhv3B28Ehr0XAP75CsHoLU00G48cR7h3vQ0CnfKh6KXU6nnDA5OWfpMYpirACCpsnpSD0OaCQ3gkQp3zZNMS3HcOpnPK/IY7R0esbzgAkvNhkyxaIfPDdf+eRUSOA9+2Ji28MwjjY8Dw3SLdUqCOzIDjQeR/T5oNmaQJm3lZ8G0FxxC6ejD4VJX/NI/x+STeB9jWHWmHZvqKzV6JHNh6qmZb6TKSIPOHpafWFoeJFOmiiigf46sju9vRXmVEAx59HXWnvnvCBNJg877yCMulB6xyQuSdVDuotQU4tQZwCKedTHJ6GqjesM98UlJrDtdWQURwwW1qc7N8tS6PukmUVEf0jmbIWVIBmUlkcVuiSs1g1h1kjt8c4MnGTz3CSgpOd1MqxLrl9WwrTqM+YnE+yeZYUjFoewyKZIQ==\",\"nonce\":\"OdCZczJiUGR4bOGIElQ4UUH4dQmG5U/3\",\"v\":1}",
"kind": 1059,
"created_at": 1690772945,
"pubkey": "e01475e87896800b7285eb0daf263c59f811c8fc5bc8daa105d2c98b6d7c4952",
"tags": [
[
"p",
"b08d8857a92b4d6aa580ff55cc3c18c4edf313c83388c34abc118621f74f1a78"
]
],
"id": "d9fc85ece892ce45ffa737b3ddc0f8b752623181d75363b966191f8c03d2debe",
"sig": "1b20416b83f4b5b8eead11e29c185f46b5e76d1960e4505210ddd00f7a6973cc11268f52a8989e3799b774d5f3a55db95bed4d66a1b6e88ab54becec5c771c17"
}
""".trimIndent()
val privateKey = "7dd22cafc512c0bc363a259f6dcda515b13ae3351066d7976fd0bb79cbd0d700"
val rumor =
runBlocking {
unwrapUnsealRumor(json, privateKey)
}
assertEquals("8d1a56008d4e31dae2fb8bef36b3efea519eff75f57033107e2aa16702466ef2", rumor.id)
assertEquals("Howdy", rumor.content)
assertEquals(1690833960L, rumor.createdAt)
assertEquals(14, rumor.kind)
assertEquals("p", rumor.tags.firstOrNull()?.get(0))
assertEquals(
"b08d8857a92b4d6aa580ff55cc3c18c4edf313c83388c34abc118621f74f1a78",
rumor.tags.firstOrNull()?.get(1),
)
assertEquals("subject", rumor.tags.getOrNull(1)?.get(0))
assertEquals("Stuff", rumor.tags.getOrNull(1)?.get(1))
}
suspend fun unwrapUnsealRumor(
json: String,
privateKey: HexKey,
): Event {
val pkBytes = NostrSignerInternal(KeyPair(privateKey.hexToByteArray()))
val wrap = Event.fromJson(json) as GiftWrapEvent
wrap.checkSignature()
assertEquals(pkBytes.pubKey, wrap.recipientPubKey())
val event = wrap.unwrapThrowing(pkBytes)
return if (event is SealedRumorEvent) {
event.unsealThrowing(pkBytes)
} else {
println(event.toJson())
fail("Event is not a Sealed Rumor")
throw Exception("Event is not a Sealed Rumor")
}
}
@Test
fun decryptMsgFromNostrTools() {
val receiversPrivateKey =
NostrSignerInternal(
KeyPair(Hex.decode("df51ec558372612918a83446d279d683039bece79b7a721274b1d3cb612dc6af")),
)
val msg =
"""
{
"tags": [],
"content": "AUC1i3lHsEOYQZaqav8jAw/Dv25r6BpUX4r7ARaj/7JEqvtHkbtaWXEx3LvMlDJstNX1C90RIelgYTzxb4Xnql7zFmXtxGGd/gXOZzW/OCNWECTrhFTruZUcsyn2ssJMgEMBZKY3PgbAKykHlGCuWR3KI9bo+IA5sTqHlrwDGAysxBypRuAxTdtEApw1LSu2A+1UQsdHK/4HcW/fQLPguWGyPv09dftJIJkFWM8VYBQT7b5FeAEMhjlUM+lEmLMnx6qb07Ji/YMESkhzFlgGjHNVl1Q/BT4i6X+Skogl6Si3lWQzlS9oebUim1BQW+RO0IOyQLalZwjzGP+eE7Ry62ukQg7cPiqk62p7NNula17SF2Q8aVFLxr8WjbLXoWhZOWY25uFbTl7OPGGQb5TewRsjHoFeU4h05Ien3Ymf1VPqJVJCMIxU+yFZ1IMZh/vQW4BSx8VotRdNA05fz03ST88GzGxUvqEm4VW/Yp5q4UUkCDQTKmUImaSFmTser39WmvS5+dHY6ne4RwnrZR0ZYrG1bthRHycnPmaJiYsHn9Ox37EzgLR07pmNxr2+86NR3S3TLAVfTDN3XaXRee/7UfW/MXULVyuyweksIHOYBvANC0PxmGSs4UiFoCbwNi45DT2y0SwP6CxzDuM=",
"kind": 1059,
"created_at": 1694192155914,
"pubkey": "8253eb518413b57f0df329d3d4287bdef4031fd71c32ad1952d854e703dae6a7",
"id": "ae625fd43612127d63bfd1967ba32ae915100842a205fc2c3b3fc02ab3827f08",
"sig": "2807a7ab5728984144676fd34686267cbe6fe38bc2f65a3640ba9243c13e8a1ae5a9a051e8852aa0c997a3623d7fa066cf2073a233c6d7db46fb1a0d4c01e5a3"
}
""".trimIndent()
val wrap = Event.fromJson(msg) as GiftWrapEvent
wrap.checkSignature()
val event =
runBlocking {
wrap.unwrapThrowing(receiversPrivateKey)
}
assertNotNull(event)
}
}
@@ -0,0 +1,112 @@
/**
* Copyright (c) 2025 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
* Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.quartz.utils
import androidx.test.ext.junit.runners.AndroidJUnit4
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.junit.Test
import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
class HexEncodingTest {
val testHex = "48a72b485d38338627ec9d427583551f9af4f016c739b8ec0d6313540a8b12cf"
@Test
fun testHexEncodeDecodeOurs() {
assertEquals(
testHex,
Hex.encode(
Hex.decode(testHex),
),
)
}
@Test
fun testHexEncodeDecodeSecp256k1() {
assertEquals(
testHex,
fr.acinq.secp256k1.Hex.encode(
fr.acinq.secp256k1.Hex
.decode(testHex),
),
)
}
@Test
fun testRandoms() {
for (i in 0..1000) {
val bytes = RandomInstance.bytes(32)
val hex =
fr.acinq.secp256k1.Hex
.encode(bytes)
assertEquals(
fr.acinq.secp256k1.Hex
.encode(bytes),
Hex.encode(bytes),
)
assertEquals(
bytes.toList(),
Hex.decode(hex).toList(),
)
}
}
@Test
fun testIsHex() {
assertFalse("/0", Hex.isHex("/0"))
assertFalse("/.", Hex.isHex("/."))
assertFalse("::", Hex.isHex("::"))
assertFalse("!!", Hex.isHex("!!"))
assertFalse("@@", Hex.isHex("@@"))
assertFalse("GG", Hex.isHex("GG"))
assertFalse("FG", Hex.isHex("FG"))
assertFalse("`a", Hex.isHex("`a"))
assertFalse("gg", Hex.isHex("gg"))
assertFalse("fg", Hex.isHex("fg"))
}
@OptIn(ExperimentalStdlibApi::class)
@Test
fun testRandomsIsHex() {
for (i in 0..10000) {
val bytes = RandomInstance.bytes(32)
val hex = bytes.toHexString(HexFormat.Default)
assertTrue(hex, Hex.isHex(hex))
val hexUpper = bytes.toHexString(HexFormat.UpperCase)
assertTrue(hexUpper, Hex.isHex(hexUpper))
}
}
@OptIn(ExperimentalStdlibApi::class)
@Test
fun testRandomsUppercase() {
for (i in 0..1000) {
val bytes = RandomInstance.bytes(32)
val hex = bytes.toHexString(HexFormat.UpperCase)
assertEquals(
bytes.toList(),
Hex.decode(hex).toList(),
)
}
}
}