- commit
- c7bc31752bd3034d2c671beaf68d1d50acbe4d2a
- parent
- a718d767210bb588cd165bbfcff2c85b5e34eb60
- author
- Dami <dami.etoile@gmail.com>
| A | chlorine/ | 54 | +++ |
| A | chlorine/ | 117 | +++++ |
| A | demo/ | 84 | ++++ |
| A | fluorine/ | 31 | + |
| A | fluorine/ | 76 | ++++ |
| A | iodine/ | 48 | ++ |
| A | iodine/ | 95 | ++++ |
| A | test/ | 185 | +++++++++ |
| A | test/ | 84 | ++++ |
| A | test/ | 168 | ++++++++ |
| A | test/ | 648 | ++++++++++++++++++++++++++++++ |
@@ -0,0 +1,54 @@
1+# ๐ chlorine.js - remote signing2+3+chlorine.js is a miniature NIP-46 client: it talks to a signer that lives elsewhere, a bunker or a phone app, over relays. It connects with a `bunker://` URI or by showing a `nostrconnect://` URI for the signer to scan, and remembers the session so reloads reconnect without a new handshake. It composes by wearing the NIP-07 face: once connected, `window.nostr` is chlorine's signer, so xenon.js publishes through it, krypton.js steps aside, and every other nobel library just works.4+5+```html6+<script src="krypton.js"></script>7+<script src="fluorine.js"></script>8+<script src="chlorine.js"></script>9+<script src="xenon.js"></script>10+11+<form ncl-connect><input name="bunker" placeholder="bunker://..."><button>Connect</button></form>12+<form nhy-relay="wss://relay.example" nxe-publish><input name="content"><button>Post</button></form>13+```14+15+Pasting a bunker URI connects to the remote signer, and from then on the post form's events are signed by it.16+17+## Attributes18+19+| attribute | description |20+|---|---|21+| `ncl-connect` | A form that connects on submit, using its `bunker` field |22+| `ncl-nostrconnect` | On load (when not connected), gets a `nostrconnect://` URI as its `href` (links) or text, and connects when a signer answers |23+| `ncl-relay` | Space-separated relays for `ncl-nostrconnect`, from the closest ancestor. Default `nclCfg.relays` |24+| `ncl-name`, `ncl-perms` | App name (default the page title) and requested permissions in the `nostrconnect://` URI |25+| `ncl-disconnect` | Clicking it disconnects |26+| `ncl-connected` | Set on `<html>` to the user's pubkey while connected |27+28+`window.nclCfg = {relays, timeout}` sets default relays for `listen` and the request timeout (30000 ms).29+30+## Events31+32+Dispatched on `document` (or the form or link involved), or on `globalThis` outside a browser.33+34+| event | cancel to | detail |35+|---|---|---|36+| `ncl:listen` | set the `href` or text yourself (to draw a QR code) | `uri` |37+| `ncl:connected` | | `pubkey` (also fired when a stored session is restored) |38+| `ncl:auth` | skip `window.open(url)` | `url` the signer asks the user to visit. The request keeps waiting |39+| `ncl:error` | | `error` from a form or link connection |40+| `ncl:disconnected` | | |41+42+## API43+44+| function | returns |45+|---|---|46+| `chlorine.connect(bunkerUri)` | Promise of the user's pubkey. Sends `connect` with the URI's secret, then `get_public_key` |47+| `chlorine.listen({relays, name, perms, ...})` | `{uri, done}`: a `nostrconnect://` URI (extra keys become query parameters) and a Promise of the user's pubkey once a signer answers with the secret |48+| `chlorine.disconnect()` | forgets the session and restores the previous `window.nostr` |49+| `chlorine.rpc(method, params)` | Promise of the result of any NIP-46 method, such as `ping` |50+| `chlorine.signer` | the NIP-07 face `{getPublicKey, signEvent, nip44, nip04}` set as `window.nostr` |51+| `chlorine.previous` | the `window.nostr` that was there before |52+| `chlorine.session` | `{sk, pk, remote, relays, secret, user}` or `null` |53+54+Each signature, encryption and decryption is a relay round trip to the signer, so expect it to take a moment rather than be instant. The session, including the throwaway client key, is kept in `localStorage` under `ncl:session`. Requires krypton.js and fluorine.js. Tested against `nak bunker` (both flows) on tinyrelay and the dev relay.@@ -0,0 +1,117 @@
1+(()=>{2+ let g = globalThis, d = g.document, K = "ncl:session", pool = {}, wait = {}, keys = {}, n = 0, s, C3+ if (g.chlorine) return4+ let emit = (type, detail, el = d ?? g)=>el.dispatchEvent?.(new CustomEvent("ncl:" + type, {detail, cancelable:1, bubbles:1, composed:1}))5+ let store = (v)=>{6+ try { v ? localStorage[K] = JSON.stringify(v) : localStorage.removeItem(K) } catch {}7+ }8+ let ck = (pk)=>keys[s.sk + pk] ||= fluorine.key(s.sk, pk)9+ let tx = (url, m)=>{10+ let w = pool[url]11+ if (!w){12+ w = pool[url] = new WebSocket(url)13+ w.q = []14+ w.onopen = ()=>{15+ w.send(JSON.stringify(["REQ", "ncl", {kinds:[24133], "#p":[s.pk], limit:0}]))16+ w.q.splice(0).forEach((x)=>w.send(x))17+ }18+ w.onmessage = async(m)=>{19+ try {20+ let [t, , e] = JSON.parse(m.data), r21+ if (t != "EVENT" || e.kind != 24133) return22+ r = JSON.parse(await fluorine.decrypt(e.content, await ck(e.pubkey)))23+ if (e.pubkey == s.remote) wait[r.id]?.(r)24+ else if (!s.remote && r.result == s.secret) s.remote = e.pubkey, s.hook()25+ } catch {}26+ }27+ w.onclose = ()=>pool[url] == w && (delete pool[url], s?.relays.includes(url) && setTimeout(()=>s && !pool[url] && tx(url), 3e3))28+ }29+ if (m) m = JSON.stringify(m), w.readyState == 1 ? w.send(m) : w.q.push(m)30+ }31+ let close = ()=>{32+ for (let u in pool) pool[u].close(), delete pool[u]33+ }34+ let start = (x)=>{35+ close()36+ s = {sk:krypton.generate(), ...x}37+ s.pk = krypton.pub(s.sk)38+ s.relays.forEach((u)=>tx(u))39+ }40+ let rpc = async(method, params = [], id = "ncl" + ++n + Date.now())=>{41+ let e = await krypton.finish({kind:24133, created_at:Date.now() / 1e3 | 0, tags:[["p", s.remote]], content:await fluorine.encrypt(JSON.stringify({id, method, params}), await ck(s.remote))}, s.sk)42+ return new Promise((res, rej)=>{43+ let t, arm = ()=>(clearTimeout(t), t = setTimeout(()=>wait[id]({error:"timeout"}), g.nclCfg?.timeout ?? 3e4))44+ wait[id] = (r)=>{45+ if (r.result == "auth_url") return arm(emit("auth", {url:r.error}) && g.open?.(r.error))46+ clearTimeout(t)47+ delete wait[id]48+ r.error ? rej(Error(r.error)) : res(r.result)49+ }50+ arm()51+ s.relays.forEach((u)=>tx(u, ["EVENT", e]))52+ })53+ }54+ let use = ()=>{55+ if (g.nostr != signer) C.previous = g.nostr, g.nostr = signer56+ d?.documentElement.setAttribute("ncl-connected", s.user)57+ emit("connected", {pubkey:s.user})58+ return s.user59+ }60+ let ready = async()=>{61+ s.user = await rpc("get_public_key")62+ store(s)63+ return use()64+ }65+ let connect = async(uri)=>{66+ let u = new URL(uri.trim()), p = u.searchParams67+ start({remote:u.hostname, relays:p.getAll("relay"), secret:p.get("secret")})68+ try {69+ await rpc("connect", [s.remote, s.secret].filter((x)=>x))70+ return await ready()71+ } catch(error) {72+ close()73+ s = null74+ throw error75+ }76+ }77+ let listen = ({relays = g.nclCfg?.relays, ...o} = {})=>{78+ start({relays, secret:krypton.generate().slice(0, 16)})79+ let q = new URLSearchParams()80+ relays.forEach((r)=>q.append("relay", r))81+ q.set("secret", s.secret)82+ for (let k in o) q.set(k, o[k])83+ return {uri:`nostrconnect://${s.pk}?${q}`, done:new Promise((res, rej)=>s.hook = ()=>ready().then(res, rej))}84+ }85+ let disconnect = ()=>{86+ close()87+ s = null88+ store()89+ if (g.nostr == signer) g.nostr = C.previous90+ d?.documentElement.removeAttribute("ncl-connected")91+ emit("disconnected")92+ }93+ let pair = (x)=>({encrypt:(pk, text)=>rpc(x + "_encrypt", [pk, text]), decrypt:(pk, text)=>rpc(x + "_decrypt", [pk, text])})94+ let signer = {95+ getPublicKey:async()=>s.user,96+ signEvent:async({kind, content, tags = [], created_at = Date.now() / 1e3 | 0})=>JSON.parse(await rpc("sign_event", [JSON.stringify({kind, content, tags, created_at})])),97+ nip44:pair("nip44"),98+ nip04:pair("nip04")99+ }100+ C = g.chlorine = {connect, listen, disconnect, rpc, signer, get session(){ return s }}101+ try { s = JSON.parse(localStorage[K]) } catch {}102+ if (s) s.relays.forEach((u)=>tx(u)), use()103+ if (!d) return104+ let fail = (el)=>(error)=>emit("error", {error}, el)105+ d.addEventListener("submit", (evt, f = evt.target)=>{106+ if (!f.hasAttribute("ncl-connect")) return107+ evt.preventDefault()108+ connect(new FormData(f).get("bunker")).catch(fail(f))109+ })110+ d.addEventListener("click", (evt)=>evt.target.closest?.("[ncl-disconnect]") && disconnect())111+ d.addEventListener("DOMContentLoaded", (el)=>{112+ if (s || !(el = d.querySelector("[ncl-nostrconnect]"))) return113+ let a = (k)=>el.getAttribute("ncl-" + k), l = listen({relays:el.closest("[ncl-relay]")?.getAttribute("ncl-relay").split(/\s+/), name:a("name") || d.title, ...a("perms") && {perms:a("perms")}})114+ if (emit("listen", {uri:l.uri}, el)) el.tagName == "A" ? el.href = l.uri : el.textContent = l.uri115+ l.done.catch(fail(el))116+ })117+})()@@ -0,0 +1,84 @@
1+2+<html lang="en">3+<head>4+<meta charset="utf-8">5+<meta name="viewport" content="width=device-width, initial-scale=1">6+<title>nobel: iodine demo</title>7+<script src="relay.js"></script>8+<script>9+ // Recipient: ?to=npub...10+ let q = new URLSearchParams(location.search)11+</script>12+<script src="../neon/neon.js"></script>13+<script src="../krypton/krypton.js"></script>14+<script src="../fluorine/fluorine.js"></script>15+<script src="../argon/argon.js"></script>16+<script src="../hydrogen/hydrogen.js"></script>17+<script src="../xenon/xenon.js"></script>18+<script src="../radon/radon.js"></script>19+<script src="../iodine/iodine.js"></script>20+<style>21+ :root { --ink:#1d1b16; --paper:#f4efe4; --line:#d8cfbd; --gas:#6d28d9; --muted:#7a7263; }22+ * { box-sizing:border-box; }23+ body { margin:0; background:var(--paper); color:var(--ink); font:15px/1.5 ui-serif, Georgia, serif; }24+ main { max-width:640px; margin:0 auto; padding:40px 20px 80px; }25+ h1 { font:600 13px/1 ui-monospace, Menlo, monospace; letter-spacing:.12em; text-transform:uppercase; margin:0 0 4px; }26+ h1 b { color:var(--gas); }27+ .sub { color:var(--muted); margin:0 0 28px; font-style:italic; }28+ .me { font:12px ui-monospace, Menlo, monospace; color:var(--muted); margin-bottom:16px; overflow-wrap:anywhere; }29+ .me b { color:var(--ink); }30+ form { display:flex; flex-wrap:wrap; gap:8px; margin:0 0 12px; }31+ input, textarea, button { font:inherit; color:inherit; background:#fffdf8; border:1px solid var(--line); border-radius:6px; padding:8px 10px; }32+ input { flex:1 1 100%; font:12px ui-monospace, Menlo, monospace; }33+ textarea { flex:1; resize:vertical; min-height:44px; }34+ button { cursor:pointer; font:12px ui-monospace, Menlo, monospace; text-transform:uppercase; letter-spacing:.08em; }35+ button:hover { border-color:var(--gas); color:var(--gas); }36+ ol { list-style:none; padding:0; margin:28px 0 0; border-top:1px solid var(--line); }37+ li { padding:14px 0; border-bottom:1px solid var(--line); }38+ li header { display:flex; gap:8px; align-items:baseline; font:12px ui-monospace, Menlo, monospace; color:var(--muted); }39+ li header b { color:var(--ink); max-width:14ch; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }40+ li p { margin:6px 0; white-space:pre-wrap; overflow-wrap:anywhere; }41+ li a { color:var(--gas); }42+ .log { font:12px ui-monospace, Menlo, monospace; color:var(--muted); min-height:1.5em; }43+</style>44+</head>45+<body>46+<main>47+ <h1><b>iodine</b> demo</h1>48+ <p class="sub">private messages (NIP-17 gift wraps) with xenon + krypton + fluorine + neon + radon.</p>49+50+ <div class="me">you are <b id="npub">โฆ</b></div>51+52+ <form id="send" nio-send>53+ <input name="to" placeholder="recipient npub" required>54+ <textarea name="content" placeholder="a private message" required></textarea>55+ <button>Send</button>56+ </form>57+ <div class="log" id="log"></div>58+59+ <ol id="inbox" nio-inbox>60+ <template>61+ <li>62+ <header>63+ <b nrn-profile="{pubkey}" nrn-field="name">{npub}</b>64+ <span>to <span class="to">{#p}</span></span>65+ <time nne-ago="{created_at}">{ago}</time>66+ </header>67+ <p>{content}</p>68+ </li>69+ </template>70+ </ol>71+</main>72+<script>73+ send.to.value = q.get("to") ?? ""74+ let status = (evt, text)=>log.textContent = text75+ document.addEventListener("nio:sent", (evt)=>status(evt, "sent: " + evt.detail.results.map((r)=>`${r.ok ? "ok" : "failed"} ${r.relay}`).join(", ")))76+ document.addEventListener("nio:error", (evt)=>status(evt, String(evt.detail.error)))77+ document.addEventListener("nhy:event", (evt)=>{78+ let f = evt.detail.fields79+ if (f["#p"]) f["#p"] = neon.encode("npub", f["#p"]).slice(0, 16) + "โฆ"80+ })81+ ;(window.nostr ?? krypton.signer).getPublicKey().then((pk)=>npub.textContent = neon.encode("npub", pk))82+</script>83+</body>84+</html>@@ -0,0 +1,31 @@
1+# ๐งช fluorine.js - NIP-44 encryption2+3+fluorine.js is a miniature nostr-tools nip44: NIP-44 v2 encryption in about 3.5kb of ChaCha20, HMAC-SHA256 and padding over `crypto.subtle`. It derives conversation keys through krypton.js's ECDH, then encrypts and decrypts payloads that any NIP-44 client can read. Loaded after krypton.js, it gives krypton's local signer a `nip44` face like a NIP-07 extension's, and chlorine.js and iodine.js use it for private messages and encrypted content.4+5+```html6+<script src="krypton.js"></script>7+<script src="fluorine.js"></script>8+<script type="module">9+ let box = await krypton.signer.nip44.encrypt(theirPubkey, "meet at noon")10+ let text = await krypton.signer.nip44.decrypt(theirPubkey, box)11+</script>12+```13+14+The local key encrypts a message only it and the owner of `theirPubkey` can read, and decrypts it again.15+16+## API17+18+| function | returns |19+|---|---|20+| `fluorine.key(sk, pk)` | conversation key (`Uint8Array(32)`) for a hex secret key and hex public key |21+| `fluorine.encrypt(text, key, nonce?)` | base64 payload. `nonce` is a `Uint8Array(32)`, random by default |22+| `fluorine.decrypt(payload, key)` | plaintext, or throws on a bad version, length, MAC or padding |23+| `fluorine.nip44(sk)` | NIP-07 shaped `{encrypt(pk, text), decrypt(pk, payload)}` for that key, caching conversation keys |24+| `fluorine.pad(n)` | padded length for an `n` byte message |25+| `krypton.signer.nip44` | `{encrypt, decrypt}` using `krypton.key()`, added when krypton.js is loaded first |26+27+All functions return promises except `nip44` and `pad`. Plaintext must be 1 to 65535 bytes of UTF-8.28+29+## Caveats30+31+ECDH runs on krypton's BigInt arithmetic, which is not constant time, and the local key lives in `localStorage`. Prefer an extension's `nostr.nip44` for keys that matter. Tests check every official NIP-44 vector and round trips against nostr-tools.@@ -0,0 +1,76 @@
1+(()=>{2+ let g = globalThis3+ if (g.fluorine) return4+ let N = 0xfffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141n, s = crypto.subtle5+ let bytes = (h)=>Uint8Array.from(h.match(/../g) ?? [], (x)=>parseInt(x, 16))6+ let cat = (...a)=>{7+ let o = new Uint8Array(a.reduce((n, x)=>n + x.length, 0)), i = 08+ for (let x of a) o.set(x, i), i += x.length9+ return o10+ }11+ let hmac = async(k, ...a)=>new Uint8Array(await s.sign("HMAC", await s.importKey("raw", k, {name:"HMAC", hash:"SHA-256"}, false, ["sign"]), cat(...a)))12+ let fail = (m)=>{ throw Error(m) }13+ let pad = (n)=>{14+ let c = Math.max(32, 2 ** Math.ceil(Math.log2(n)) / 8)15+ return c * Math.ceil(n / c)16+ }17+ let chacha = (key, nonce, m)=>{18+ let st = new Uint32Array(16), x = new Uint32Array(16), xb = new Uint8Array(x.buffer), o = new Uint8Array(m.length)19+ st.set([0x61707865, 0x3320646e, 0x79622d32, 0x6b206574])20+ st.set(new Uint32Array(key.slice().buffer), 4)21+ st.set(new Uint32Array(nonce.slice().buffer), 13)22+ let r = (a, b, d, n)=>{23+ x[a] += x[b]24+ x[d] ^= x[a]25+ x[d] = x[d] << n | x[d] >>> 32 - n26+ }27+ let q = (a, b, c, d)=>(r(a, b, d, 16), r(c, d, b, 12), r(a, b, d, 8), r(c, d, b, 7))28+ for (let i = 0; i < m.length; i += 64, st[12]++){29+ x.set(st)30+ for (let j = 0; j < 10; j++) for (let k = 0; k < 8; k++) k < 4 ? q(k, k + 4, k + 8, k + 12) : q(k - 4, 4 + (k + 1) % 4, 8 + (k + 2) % 4, 12 + (k + 3) % 4)31+ for (let j = 0; j < 16; j++) x[j] += st[j]32+ for (let j = 0; j < 64 && i + j < m.length; j++) o[i + j] = m[i + j] ^ xb[j]33+ }34+ return o35+ }36+ let keys = async(ck, nonce)=>{37+ let t1 = await hmac(ck, nonce, [1]), t2 = await hmac(ck, t1, nonce, [2]), t3 = await hmac(ck, t2, nonce, [3]), k = cat(t1, t2, t3)38+ return [k.slice(0, 32), k.slice(32, 44), k.slice(44, 76)]39+ }40+ let key = async(sk, pk)=>{41+ let d = BigInt("0x" + sk)42+ if (!d || d >= N) fail("bad key")43+ return hmac(new TextEncoder().encode("nip44-v2"), bytes(g.krypton.shared(sk, pk)))44+ }45+ let encrypt = async(text, ck, nonce = crypto.getRandomValues(new Uint8Array(32)))=>{46+ let m = new TextEncoder().encode(text), n = m.length47+ if (!n || n > 65535) fail("bad length")48+ let p = new Uint8Array(2 + pad(n)), [ek, en, hk] = await keys(ck, nonce)49+ p.set([n >> 8, n & 255])50+ p.set(m, 2)51+ let c = chacha(ek, en, p), b = cat([2], nonce, c, await hmac(hk, nonce, c)), out = ""52+ for (let x of b) out += String.fromCharCode(x)53+ return btoa(out)54+ }55+ let decrypt = async(payload, ck)=>{56+ let l = payload.length57+ if (payload[0] == "#") fail("unsupported version")58+ if (l < 132 || l > 87472) fail("bad payload length")59+ let b = Uint8Array.from(atob(payload), (x)=>x.charCodeAt(0))60+ if (b.length < 99 || b.length > 65603) fail("bad data length")61+ if (b[0] != 2) fail("unknown version " + b[0])62+ let nonce = b.slice(1, 33), c = b.slice(33, -32), mac = b.slice(-32), [ek, en, hk] = await keys(ck, nonce), m = await hmac(hk, nonce, c), d = 063+ for (let i = 0; i < 32; i++) d |= m[i] ^ mac[i]64+ if (d) fail("invalid MAC")65+ let p = chacha(ek, en, c), n = p[0] << 8 | p[1]66+ if (!n || p.length != 2 + pad(n)) fail("invalid padding")67+ return new TextDecoder().decode(p.slice(2, 2 + n))68+ }69+ let nip44 = (sk, sks = ()=>sk, cache = {})=>{70+ let ck = (pk, k = sks())=>cache[k + pk] ||= key(k, pk)71+ return {encrypt:async(pk, text)=>encrypt(text, await ck(pk)), decrypt:async(pk, payload)=>decrypt(payload, await ck(pk))}72+ }73+ g.fluorine = {key, encrypt, decrypt, nip44, pad}74+ let signer = g.krypton?.signer75+ if (signer) signer.nip44 = nip44(null, ()=>g.krypton.key())76+})()@@ -0,0 +1,48 @@
1+# โ๏ธ iodine.js - private messages2+3+iodine.js is a miniature encrypted messenger: NIP-17 direct messages sealed and gift wrapped per NIP-59, in about 4kb. A form sends a message to someone's DM relays (their kind 10050 list), and a list shows your inbox, unwrapped and rendered like any other xenon feed. It composes with xenon.js, neon.js and radon.js, and signs and encrypts with any signer that has `nip44`: a NIP-07 extension, chlorine.js, or krypton.js with fluorine.js.4+5+```html6+<form nio-send nio-to="npub1..."><textarea name="content"></textarea><button>Send</button></form>7+<ol nio-inbox nio-with="npub1...">8+ <template><li><b nrn-profile="{pubkey}" nrn-field="name">{npub}</b> <p>{content}</p></li></template>9+</ol>10+```11+12+The form wraps each message twice, once for the recipient and once for you, and the list shows your conversation with that npub, newest first.13+14+## Attributes15+16+| attribute | description |17+|---|---|18+| `nio-send` | Form that sends its `content` field as a private message on submit |19+| `nio-to` | Recipient (`npub`, `nprofile` or hex). Falls back to a form field named `to` |20+| `nio-inbox` | Subscribes to your gift wraps (`kinds:[1059]`, `#p:[you]`) and renders the unwrapped messages with the element's template |21+| `nio-with` | Only keep messages from or to this pubkey (`npub` or hex) |22+| `nio-ignore` | iodine skips this element and its descendants |23+24+The inbox sets `nxe-filter` and `nar-ignore` on itself and hands over to xenon, so every `nxe-*` attribute, template field and `nxe:*` event works as usual, on the decrypted message. It reads from your own kind 10050 relays when you have them, else from the closest `nhy-relay` or `nhyCfg.relays`, which are also where messages go when a recipient has no DM relays.25+26+## Events27+28+| event | cancel to | detail |29+|---|---|---|30+| `nio:send` | skip sending | `rumor` (unsigned kind 14, editable), `recipients` |31+| `nio:sent` | | `rumor`, `recipients`, `results` per relay |32+| `nio:error` | | `error` |33+| `nio:message` | skip rendering | `wrap`, `rumor` (on the inbox) |34+35+## API36+37+| function | returns |38+|---|---|39+| `iodine.send(to, content, relays?)` | promise of `results`, after wrapping for `to` and yourself |40+| `iodine.wrap(rumor, pubkey)` | promise of a signed kind 1059 gift wrap for `pubkey` |41+| `iodine.unwrap(wrap)` | promise of the rumor, or `undefined` if it is not for you or does not check out |42+| `iodine.relays(pubkey)` | promise of the DM relays in `pubkey`'s kind 10050 list, `[]` after 3s |43+44+Requires xenon.js and krypton.js, fluorine.js for the gift wrap layer, and neon.js for `npub` recipients.45+46+## Caveats47+48+Extensions ask for permission to decrypt, often once per message. Messages are never cached by argon.js, so the inbox decrypts everything again on every load. Metadata privacy comes from the gift wraps: relays see only random keys, randomized times and ciphertext, though each wrap's `p` tag names its recipient and relays still see your IP address. Messages have no delivery receipts or deletion.@@ -0,0 +1,95 @@
1+(()=>{2+ let g = window, d = document, J = JSON3+ if (g.iodine) return4+ let send = (el, type, detail)=>el.dispatchEvent(new CustomEvent("nio:" + type, {detail, cancelable:1, bubbles:1, composed:1}))5+ let signer = ()=>g.nostr ?? krypton.signer6+ let hex = (s)=>{7+ if (/^[0-9a-f]{64}$/.test(s)) return s8+ let x = neon.decode(s).data9+ return x.pubkey ?? x10+ }11+ let now = ()=>Date.now() / 1e3 | 012+ let past = ()=>now() - (Math.random() * 172800 | 0)13+ let relays = (el)=>el?.closest?.("[nhy-relay]")?.getAttribute("nhy-relay").split(/\s+/) ?? g.nhyCfg?.relays ?? []14+ // Newest kind 10050 "relay" tags of pk, or [] after ~3s.15+ let lookup = (pk, el)=>new Promise((res)=>{16+ let rs = relays(el), best, n = 0, c17+ let end = ()=>res(best?.tags.filter((t)=>t[0] == "relay").map((t)=>t[1]) ?? [], c?.close())18+ c = hydrogen.req({19+ relays:rs,20+ filters:[{kinds:[10050], authors:[pk], limit:1}],21+ onevent:(e)=>e.pubkey == pk && !(best?.created_at >= e.created_at) && (best = e),22+ oneose:(u)=>rs.includes(u) && ++n == rs.length && end()23+ })24+ rs.length || end()25+ setTimeout(end, 3e3)26+ })27+ let wrap = async(rumor, to)=>{28+ let s = signer(), sk = krypton.generate()29+ let seal = await s.signEvent({pubkey:rumor.pubkey, kind:13, created_at:past(), tags:[], content:await s.nip44.encrypt(to, J.stringify(rumor))})30+ return krypton.finish({kind:1059, created_at:past(), tags:[["p", to]], content:await fluorine.nip44(sk).encrypt(to, J.stringify(seal))}, sk)31+ }32+ let unwrap = async(w)=>{33+ try {34+ let s = signer(), seal = J.parse(await s.nip44.decrypt(w.pubkey, w.content))35+ if (seal.kind != 13 || !await krypton.check(seal)) return36+ let r = J.parse(await s.nip44.decrypt(seal.pubkey, seal.content))37+ if (r.pubkey == seal.pubkey) return r38+ } catch {}39+ }40+ let dm = async(to, content, rs, el = d)=>{41+ let me = await signer().getPublicKey()42+ to = hex(to)43+ let rumor = {pubkey:me, created_at:now(), kind:14, tags:[["p", to]], content}, detail = {rumor, recipients:[...new Set([to, me])]}44+ if (!send(el, "send", detail)) return45+ rumor.id = await krypton.id(rumor)46+ let results = (await Promise.all(detail.recipients.map(async(pk)=>{47+ let w = await wrap(rumor, pk), r = rs ?? await lookup(pk, el)48+ return hydrogen.pub(w, r.length ? r : relays(el))49+ }))).flat()50+ send(el, "sent", {...detail, results})51+ return results52+ }53+ let box = async(el)=>{54+ let me = await signer().getPublicKey()55+ el.__nio = await lookup(me, el)56+ el.setAttribute("nar-ignore", "")57+ el.setAttribute("nxe-filter", J.stringify({kinds:[1059], "#p":[me]}))58+ el.dispatchEvent(new CustomEvent("nxe:process", {bubbles:1}))59+ }60+ let form = (el)=>el.addEventListener("submit", async(evt)=>{61+ evt.preventDefault()62+ let fd = new FormData(el)63+ try {64+ let rs = await dm(el.getAttribute("nio-to") || fd.get("to"), fd.get("content") ?? "", null, el)65+ if (rs?.some((r)=>r.ok)) el.reset()66+ } catch(error) {67+ send(el, "error", {error})68+ }69+ })70+ let init = (el)=>{71+ if (el.__nio !== undefined || el.closest("[nio-ignore]")) return72+ el.__nio = 073+ el.hasAttribute("nio-inbox") ? box(el).catch((error)=>send(el, "error", {error})) : form(el)74+ }75+ let process = (n)=>{76+ if (n.matches?.("[nio-inbox],[nio-send]")) init(n)77+ n.querySelectorAll?.("[nio-inbox],[nio-send]").forEach(init)78+ }79+ d.addEventListener("nhy:config", (evt)=>{80+ let c = evt.detail.cfg, el = c.elt81+ if (!el?.hasAttribute?.("nio-inbox")) return82+ if (el.__nio?.length) c.relays = el.__nio83+ c.map = async(w)=>{84+ let r = await unwrap(w), x = el.getAttribute("nio-with")85+ try { x &&= hex(x) } catch {}86+ if (r && (!x || [r.pubkey, ...r.tags.map((t)=>t[0] == "p" && t[1])].includes(x)) && send(el, "message", {wrap:w, rumor:r})) return r87+ }88+ })89+ g.iodine = {send:dm, wrap, unwrap, relays:lookup}90+ let start = ()=>{91+ new MutationObserver((recs)=>recs.forEach((r)=>r.addedNodes.forEach(process))).observe(d.documentElement, {childList:1, subtree:1})92+ process(d.body)93+ }94+ d.readyState == "loading" ? d.addEventListener("DOMContentLoaded", start) : start()95+})()@@ -0,0 +1,185 @@
1+// NIP-46 against a real `nak bunker` on the dev relay, plus a scripted signer for auth_url.2+import { assert, assertEquals, assertRejects } from "jsr:@std/assert@1";3+import { chromium } from "npm:playwright@1.58";4+import { serve } from "../server.ts";5+import "../krypton/krypton.js";6+import "../fluorine/fluorine.js";7+8+localStorage.removeItem("ncl:session");9+await import("../chlorine/chlorine.js");10+const { krypton, fluorine, chlorine } = globalThis;11+const opts = { sanitizeResources: false, sanitizeOps: false };12+13+const server = serve(0);14+const base = `http://localhost:${server.addr.port}`;15+const relay = Deno.env.get("NCL_RELAY") ?? `ws://localhost:${server.addr.port}/relay`;16+const cfg = await Deno.makeTempDir();17+const sk = krypton.generate(), user = krypton.pub(sk);18+19+function spawn(...args) {20+ const p = new Deno.Command("nak", { args: ["--config-path", cfg, ...args], stdout: "piped", stderr: "piped" }).spawn();21+ let out = "";22+ const pump = async (r) => { for await (const c of r.pipeThrough(new TextDecoderStream())) out += c; };23+ pump(p.stdout);24+ pump(p.stderr);25+ return { p, out: () => out.replace(/\x1b\[[0-9;]*m/g, "") };26+}27+const until = async (f, ms = 1e4) => {28+ for (const t = Date.now(); Date.now() - t < ms; await new Promise((r) => setTimeout(r, 50))) if (f()) return f();29+ throw Error("timed out");30+};31+const once = (type) => new Promise((r) => globalThis.addEventListener("ncl:" + type, (e) => r(e.detail), { once: true }));32+33+// Tests that need nak are skipped when it is not installed.34+const hasNak = await (async () => new Deno.Command("nak", { args: ["--version"] }).output())().then(() => true, () => false);35+const bunker = hasNak && spawn("bunker", "--sec", sk, "-s", "s3cret", relay);36+const uri = hasNak && await until(() => /bunker:\/\/\S+/.exec(bunker.out())?.[0]);37+await new Promise((r) => setTimeout(r, 500));38+39+Deno.test({ name: "chlorine connects to nak bunker via bunker://", ignore: !hasNak, ...opts }, async () => {40+ const previous = { getPublicKey: () => "prev" };41+ globalThis.nostr = previous;42+ const connected = once("connected");43+ assertEquals(await chlorine.connect(uri), user);44+ assertEquals((await connected).pubkey, user);45+ assertEquals(globalThis.nostr, chlorine.signer);46+ assertEquals(chlorine.previous, previous);47+ assertEquals(await nostr.getPublicKey(), user);48+ assertEquals(await chlorine.rpc("get_public_key"), user);49+ assertEquals(await chlorine.rpc("ping"), "pong");50+ assertEquals(JSON.parse(localStorage["ncl:session"]).user, user);51+52+ const e = await nostr.signEvent({ kind: 1, created_at: Math.floor(Date.now() / 1e3), tags: [["t", "nobel"]], content: "hi from chlorine" });53+ assertEquals(e.pubkey, user);54+ assertEquals(e.content, "hi from chlorine");55+ assert(await krypton.check(e));56+57+ const other = krypton.generate(), otherPk = krypton.pub(other);58+ const ct = await nostr.nip44.encrypt(otherPk, "secret โก text");59+ assertEquals(await fluorine.decrypt(ct, await fluorine.key(other, user)), "secret โก text");60+ assertEquals(await nostr.nip44.decrypt(otherPk, ct), "secret โก text");61+ const back = await fluorine.encrypt("reply", await fluorine.key(other, user));62+ assertEquals(await nostr.nip44.decrypt(otherPk, back), "reply");63+64+ const gone = once("disconnected");65+ chlorine.disconnect();66+ await gone;67+ assertEquals(globalThis.nostr, previous);68+ assertEquals(localStorage.getItem("ncl:session"), null);69+ assertEquals(chlorine.session, null);70+});71+72+Deno.test({ name: "chlorine rejects a wrong secret", ignore: !hasNak, ...opts }, async () => {73+ const bad = uri.replace(/secret=\w+/, "secret=nope");74+ await assertRejects(() => chlorine.connect(bad), Error, "unauthorized");75+ assertEquals(chlorine.session, null);76+});77+78+Deno.test({ name: "chlorine nostrconnect:// flow with nak bunker connect", ignore: !hasNak, ...opts }, async () => {79+ const { uri: nc, done } = chlorine.listen({ relays: [relay], name: "chlorine test", perms: "sign_event:1" });80+ assert(nc.startsWith("nostrconnect://" + chlorine.session.pk + "?relay="));81+ const c = spawn("bunker", "connect", nc);82+ assertEquals((await c.p.status).code, 0, c.out());83+ assertEquals(await done, user);84+ assertEquals(chlorine.session.remote, user);85+ const e = await nostr.signEvent({ kind: 1, content: "via nostrconnect" });86+ assert(await krypton.check(e));87+ assertEquals(e.pubkey, user);88+ chlorine.disconnect();89+});90+91+Deno.test({ name: "chlorine waits through auth_url and emits ncl:auth", ...opts }, async () => {92+ // A scripted remote signer whose user key differs from its own key.93+ const rsk = krypton.generate(), rpk = krypton.pub(rsk), usk = krypton.generate();94+ const ws = new WebSocket(relay);95+ await new Promise((r) => ws.onopen = r);96+ ws.send(JSON.stringify(["REQ", "s", { kinds: [24133], "#p": [rpk], limit: 0 }]));97+ ws.onmessage = async (m) => {98+ const [t, , e] = JSON.parse(m.data);99+ if (t != "EVENT") return;100+ const k = await fluorine.key(rsk, e.pubkey), req = JSON.parse(await fluorine.decrypt(e.content, k));101+ const reply = async (o) => ws.send(JSON.stringify(["EVENT", await krypton.finish({ kind: 24133, created_at: Math.floor(Date.now() / 1e3), tags: [["p", e.pubkey]], content: await fluorine.encrypt(JSON.stringify({ id: req.id, ...o }), k) }, rsk)]));102+ if (req.method == "connect") return reply({ result: req.params[1] == "s3" ? "ack" : "", error: req.params[1] == "s3" ? undefined : "bad secret" });103+ if (req.method == "get_public_key") return reply({ result: krypton.pub(usk) });104+ if (req.method == "sign_event") {105+ await reply({ result: "auth_url", error: "https://signer.example/approve" });106+ await new Promise((r) => setTimeout(r, 300));107+ return reply({ result: JSON.stringify(await krypton.finish(JSON.parse(req.params[0]), usk)) });108+ }109+ };110+ assertEquals(await chlorine.connect(`bunker://${rpk}?relay=${encodeURIComponent(relay)}&secret=s3`), krypton.pub(usk));111+ const auth = once("auth");112+ const e = await nostr.signEvent({ kind: 1, content: "needs approval" });113+ assertEquals((await auth).url, "https://signer.example/approve");114+ assertEquals(e.pubkey, krypton.pub(usk));115+ assert(await krypton.check(e));116+ chlorine.disconnect();117+ ws.close();118+});119+120+async function chromiumPath() {121+ if (Deno.env.get("CHROMIUM")) return Deno.env.get("CHROMIUM");122+ const cache = `${Deno.env.get("HOME")}/Library/Caches/ms-playwright`;123+ try {124+ const dirs = [...Deno.readDirSync(cache)].map((d) => d.name).filter((n) => n.startsWith("chromium_headless_shell-")).sort();125+ const dir = `${cache}/${dirs.at(-1)}`;126+ return `${dir}/${[...Deno.readDirSync(dir)][0].name}/chrome-headless-shell`;127+ } catch { return undefined; }128+}129+130+Deno.test({ name: "chlorine in the browser: form, xenon publish, reload, nostrconnect", ignore: !hasNak, ...opts }, async (t) => {131+ const browser = await chromium.launch({ executablePath: await chromiumPath(), args: ["--disable-features=LocalNetworkAccessChecks"] });132+ const page = await browser.newPage(), errors = [];133+ page.on("pageerror", (e) => errors.push(e.message));134+ const html = `<!doctype html><title>chlorine page</title>135+ <script src="/krypton/krypton.js"></script><script src="/fluorine/fluorine.js"></script>136+ <script src="/chlorine/chlorine.js"></script><script src="/hydrogen/hydrogen.js"></script><script src="/xenon/xenon.js"></script>137+ <form id="login" ncl-connect><input name="bunker"><button>Connect</button></form>138+ <p ncl-relay="${relay}"><a id="nc" ncl-nostrconnect ncl-name="page">scan</a></p>139+ <form id="post" nhy-relay="${relay}" nxe-publish><input name="content"><button>Post</button></form>140+ <button id="out" ncl-disconnect>Log out</button>`;141+ await page.route(base + "/page.html", (r) => r.fulfill({ contentType: "text/html", body: html }));142+ try {143+ await page.goto(base + "/page.html");144+ await t.step("an ncl-nostrconnect link gets a nostrconnect:// href", async () => {145+ assert((await page.getAttribute("#nc", "href")).startsWith("nostrconnect://"));146+ });147+ await t.step("the ncl-connect form connects and xenon publishes through the bunker", async () => {148+ await page.fill("#login input", `bunker://${user}?relay=${encodeURIComponent(relay)}&secret=s3cret`);149+ await page.click("#login button");150+ await page.waitForFunction((u) => document.documentElement.getAttribute("ncl-connected") == u, user);151+ const published = page.evaluate(() => new Promise((r) => document.addEventListener("nxe:published", (e) => r(e.detail.cfg), { once: true })));152+ await page.fill("#post input", "posted via bunker");153+ await page.click("#post button");154+ const cfg = await published;155+ assertEquals(cfg.event.pubkey, user);156+ assert(cfg.results.every((r) => r.ok), JSON.stringify(cfg.results));157+ });158+ await t.step("a reload restores the session without a handshake", async () => {159+ await page.reload();160+ assertEquals(await page.evaluate(() => document.documentElement.getAttribute("ncl-connected")), user);161+ assertEquals(await page.evaluate(() => document.querySelector("#nc").getAttribute("href")), null);162+ const e = await page.evaluate(() => nostr.signEvent({ kind: 1, content: "after reload" }));163+ assert(await krypton.check(e));164+ });165+ await t.step("ncl-disconnect logs out, then nostrconnect logs back in", async () => {166+ await page.click("#out");167+ assertEquals(await page.evaluate(() => [document.documentElement.hasAttribute("ncl-connected"), !!window.nostr, localStorage["ncl:session"]]), [false, false, undefined]);168+ await page.reload();169+ const nc = await page.getAttribute("#nc", "href");170+ assert(nc.includes("name=page"));171+ const c = spawn("bunker", "connect", nc);172+ assertEquals((await c.p.status).code, 0, c.out());173+ await page.waitForFunction((u) => document.documentElement.getAttribute("ncl-connected") == u, user);174+ });175+ assertEquals(errors, []);176+ } finally {177+ await browser.close();178+ }179+});180+181+Deno.test({ name: "cleanup", ...opts }, async () => {182+ if (bunker) bunker.p.kill(), await bunker.p.status;183+ await server.shutdown();184+ await Deno.remove(cfg, { recursive: true });185+});@@ -0,0 +1,84 @@
1+import { assert, assertEquals, assertRejects } from "jsr:@std/assert@1";2+import { v2 } from "npm:nostr-tools@2.17.0/nip44";3+import "../krypton/krypton.js";4+import "../fluorine/fluorine.js";5+6+const { krypton, fluorine } = globalThis;7+const V = JSON.parse(await Deno.readTextFile(new URL("./nip44.vectors.json", import.meta.url))).v2;8+const hex = (b) => [...b].map((x) => x.toString(16).padStart(2, "0")).join("");9+const bytes = (h) => Uint8Array.from(h.match(/../g), (x) => parseInt(x, 16));10+const sha = async (x) => hex(new Uint8Array(await crypto.subtle.digest("SHA-256", typeof x == "string" ? new TextEncoder().encode(x) : x)));11+12+Deno.test("conversation keys match vectors", async () => {13+ for (const v of V.valid.get_conversation_key) assertEquals(hex(await fluorine.key(v.sec1, v.pub2)), v.conversation_key);14+});15+16+Deno.test("invalid conversation keys throw", async () => {17+ for (const v of V.invalid.get_conversation_key) await assertRejects(() => fluorine.key(v.sec1, v.pub2), Error, undefined, v.note);18+});19+20+Deno.test("message key nonces encrypt like nostr-tools", async () => {21+ // message keys are internal; check they drive encryption identically to nostr-tools for each nonce22+ const ck = bytes(V.valid.get_message_keys.conversation_key);23+ for (const k of V.valid.get_message_keys.keys) {24+ const n = bytes(k.nonce);25+ assertEquals(await fluorine.encrypt("message keys", ck, n), v2.encrypt("message keys", ck, n));26+ }27+});28+29+Deno.test("padded lengths match vectors", () => {30+ for (const [n, p] of V.valid.calc_padded_len) assertEquals(fluorine.pad(n), p);31+});32+33+Deno.test("encrypt/decrypt vectors", async () => {34+ for (const v of V.valid.encrypt_decrypt) {35+ const ck = await fluorine.key(v.sec1, krypton.pub(v.sec2));36+ assertEquals(hex(ck), v.conversation_key);37+ assertEquals(hex(await fluorine.key(v.sec2, krypton.pub(v.sec1))), v.conversation_key);38+ assertEquals(await fluorine.encrypt(v.plaintext, ck, bytes(v.nonce)), v.payload);39+ assertEquals(await fluorine.decrypt(v.payload, ck), v.plaintext);40+ }41+});42+43+Deno.test("long message vectors", async () => {44+ for (const v of V.valid.encrypt_decrypt_long_msg) {45+ const text = v.pattern.repeat(v.repeat), ck = bytes(v.conversation_key);46+ assertEquals(await sha(text), v.plaintext_sha256);47+ const payload = await fluorine.encrypt(text, ck, bytes(v.nonce));48+ assertEquals(await sha(payload), v.payload_sha256);49+ assertEquals(await fluorine.decrypt(payload, ck), text);50+ }51+});52+53+Deno.test("invalid payloads are rejected", async () => {54+ for (const v of V.invalid.decrypt) await assertRejects(() => fluorine.decrypt(v.payload, bytes(v.conversation_key)), Error, undefined, v.note);55+});56+57+Deno.test("invalid message lengths are rejected", async () => {58+ const ck = crypto.getRandomValues(new Uint8Array(32));59+ for (const n of V.invalid.encrypt_msg_lengths) await assertRejects(() => fluorine.encrypt("a".repeat(n), ck));60+});61+62+Deno.test("round trips with nostr-tools", async () => {63+ for (let i = 0; i < 20; i++) {64+ const a = krypton.generate(), b = krypton.generate();65+ const ck = await fluorine.key(a, krypton.pub(b));66+ assertEquals(hex(ck), hex(v2.utils.getConversationKey(bytes(b), krypton.pub(a))));67+ const text = "hรฉllo ๐ฆ ".repeat(1 + i * 37);68+ assertEquals(v2.decrypt(await fluorine.encrypt(text, ck), ck), text);69+ assertEquals(await fluorine.decrypt(v2.encrypt(text, ck), ck), text);70+ }71+});72+73+Deno.test("nip44 signer shape", async () => {74+ const a = krypton.generate(), b = krypton.generate();75+ const A = fluorine.nip44(a), B = fluorine.nip44(b);76+ const c = await A.encrypt(krypton.pub(b), "secret");77+ assertEquals(await B.decrypt(krypton.pub(a), c), "secret");78+ assertEquals(await A.decrypt(krypton.pub(b), c), "secret");79+ assert(krypton.signer.nip44);80+ const local = krypton.key();81+ const d = await krypton.signer.nip44.encrypt(krypton.pub(b), "to b");82+ assertEquals(await B.decrypt(krypton.pub(local), d), "to b");83+ assertEquals(await krypton.signer.nip44.decrypt(krypton.pub(b), await B.encrypt(krypton.pub(local), "to local")), "to local");84+});@@ -0,0 +1,168 @@
1+// Drives demo/iodine.html in headless Chromium: NIP-17 private messages between separate browser contexts.2+import { assert, assertEquals } from "jsr:@std/assert@1";3+import { chromium } from "npm:playwright@1.58";4+import * as nip17 from "npm:nostr-tools@2.17.0/nip17";5+import * as nip59 from "npm:nostr-tools@2.17.0/nip59";6+import { generateSecretKey, getPublicKey, verifyEvent } from "npm:nostr-tools@2.17.0/pure";7+import { serve } from "../server.ts";8+9+// Use CHROMIUM if set, else the newest headless shell already in the Playwright cache.10+async function chromiumPath() {11+ if (Deno.env.get("CHROMIUM")) return Deno.env.get("CHROMIUM");12+ const cache = `${Deno.env.get("HOME")}/Library/Caches/ms-playwright`;13+ try {14+ const dirs = [...Deno.readDirSync(cache)].map((d) => d.name).filter((n) => n.startsWith("chromium_headless_shell-")).sort();15+ const dir = `${cache}/${dirs.at(-1)}`;16+ const arch = [...Deno.readDirSync(dir)][0].name;17+ return `${dir}/${arch}/chrome-headless-shell`;18+ } catch { return undefined; }19+}20+21+// Raw REQ against the relay, collecting stored events until EOSE.22+function fetchRelay(url, filter) {23+ return new Promise((res, rej) => {24+ const ws = new WebSocket(url), out = [];25+ ws.onopen = () => ws.send(JSON.stringify(["REQ", "t", filter]));26+ ws.onerror = rej;27+ ws.onmessage = (m) => {28+ const [t, , e] = JSON.parse(m.data);29+ if (t === "EVENT") out.push(e);30+ if (t === "EOSE") res(out, ws.close());31+ };32+ });33+}34+35+function publishRelay(url, event) {36+ return new Promise((res, rej) => {37+ const ws = new WebSocket(url);38+ ws.onopen = () => ws.send(JSON.stringify(["EVENT", event]));39+ ws.onerror = rej;40+ ws.onmessage = (m) => res(JSON.parse(m.data)[2], ws.close());41+ });42+}43+44+Deno.test({ name: "iodine: NIP-17 private messages", sanitizeResources: false, sanitizeOps: false }, async (t) => {45+ const server = serve(0);46+ const relay = `ws://localhost:${server.addr.port}/relay`;47+ const url = `http://localhost:${server.addr.port}/demo/iodine.html`;48+ const browser = await chromium.launch({ executablePath: await chromiumPath() });49+ const errors = [];50+ const open = async () => {51+ const ctx = await browser.newContext();52+ const page = await ctx.newPage();53+ page.on("pageerror", (e) => errors.push(e.message));54+ await page.goto(url);55+ await page.waitForFunction(() => document.querySelector("#inbox").hasAttribute("nxe-filter"));56+ const pk = await page.evaluate(() => krypton.signer.getPublicKey());57+ return { page, pk, npub: await page.textContent("#npub") };58+ };59+ const items = (page, sel = "#inbox") => page.locator(`${sel} li`);60+ try {61+ const A = await open(), B = await open(), C = await open();62+ assert(A.pk !== B.pk && B.pk !== C.pk, "separate contexts, separate keys");63+ const plain = "meet at <b>noon</b>";64+65+ await t.step("B's kind 10050 DM relays are looked up", async () => {66+ const r = await B.page.evaluate(async (rs) => hydrogen.pub(await krypton.signer.signEvent({ kind: 10050, created_at: Date.now() / 1e3 | 0, tags: [["relay", rs]], content: "" }), nhyCfg.relays), relay + "?dm");67+ assert(r[0].ok);68+ assertEquals(await A.page.evaluate((pk) => iodine.relays(pk), B.pk), [relay + "?dm"]);69+ assertEquals(await A.page.evaluate((pk) => iodine.relays(pk), C.pk), []);70+ });71+72+ await t.step("A sends to B through the form", async () => {73+ await A.page.fill("#send input", B.npub);74+ await A.page.fill("#send textarea", plain);75+ await A.page.click("#send button");76+ await A.page.waitForFunction(() => /^sent/.test(document.querySelector("#log").textContent), null, { timeout: 8000 })77+ .catch(async (e) => { throw new Error(`status: ${await A.page.textContent("#log")}`, { cause: e }); });78+ const log = await A.page.textContent("#log");79+ assert(log.includes(`ok ${relay}?dm`), "B's wrap goes to B's DM relay: " + log);80+ assert(log.includes(`ok ${relay},`) || log.endsWith(`ok ${relay}`), "A's copy falls back to the element's relays: " + log);81+ assertEquals(await A.page.inputValue("#send textarea"), "", "form resets");82+ });83+84+ await t.step("B's inbox shows the message with A as author", async () => {85+ await B.page.waitForSelector("#inbox li");86+ const li = items(B.page).first();87+ assertEquals(await li.locator("p").textContent(), plain, "content is escaped text");88+ assertEquals(await li.locator("p b").count(), 0);89+ assertEquals(await li.locator("b").first().textContent(), A.npub);90+ });91+92+ await t.step("A's own inbox shows A's copy", async () => {93+ await A.page.waitForSelector("#inbox li");94+ assertEquals(await items(A.page).first().locator("p").textContent(), plain);95+ assertEquals(await items(A.page).first().locator("b").first().textContent(), A.npub);96+ assertEquals(await items(A.page).count(), 1);97+ });98+99+ await t.step("C sees nothing, and the relay holds only random pubkeys and ciphertext", async () => {100+ await C.page.waitForTimeout(500);101+ assertEquals(await items(C.page).count(), 0);102+ const wraps = await fetchRelay(relay, { kinds: [1059] });103+ assertEquals(wraps.length, 2);104+ assertEquals(new Set(wraps.map((w) => w.tags[0][1])), new Set([A.pk, B.pk]));105+ for (const w of wraps) {106+ assert(![A.pk, B.pk, C.pk].includes(w.pubkey), "wrap signed by a random key");107+ assert(!w.content.includes("noon") && !w.content.includes(A.pk));108+ assert(verifyEvent(w));109+ assert(w.created_at <= Date.now() / 1e3 + 1 && w.created_at > Date.now() / 1e3 - 2 * 86400 - 5);110+ }111+ assertEquals((await fetchRelay(relay, { kinds: [13, 14] })).length, 0);112+ assertEquals(await C.page.evaluate((w) => iodine.unwrap(w), wraps[0]), undefined, "C cannot unwrap");113+ });114+115+ await t.step("argon caches no messages (nar-ignore)", async () => {116+ for (const p of [A.page, B.page]) {117+ assertEquals(await p.evaluate(async () => (await argon.query([{ kinds: [13, 14, 1059] }])).length), 0);118+ }119+ });120+121+ await t.step("nio-with filters conversations", async () => {122+ await C.page.evaluate((to) => iodine.send(to, "hi from C"), B.npub);123+ await B.page.waitForFunction(() => document.querySelectorAll("#inbox li").length === 2);124+ assertEquals(await items(B.page).first().locator("p").textContent(), "hi from C", "newest first");125+ await B.page.evaluate(([a, c]) => {126+ document.querySelector("main").insertAdjacentHTML("beforeend",127+ `<ol id="withA" nio-inbox nio-with="${a}"><template><li><p>{content}</p></li></template></ol>` +128+ `<ol id="withC" nio-inbox nio-with="${c}"><template><li><p>{content}</p></li></template></ol>`);129+ }, [A.npub, C.pk]);130+ await B.page.waitForSelector("#withA li");131+ await B.page.waitForSelector("#withC li");132+ await B.page.waitForTimeout(300);133+ assertEquals(await items(B.page, "#withA").allTextContents(), [plain]);134+ assertEquals(await items(B.page, "#withC").allTextContents(), ["hi from C"]);135+ // A's own copy is "to B", so it belongs to A's conversation with B136+ await A.page.evaluate((b) => document.querySelector("main").insertAdjacentHTML("beforeend",137+ `<ol id="withB" nio-inbox nio-with="${b}"><template><li><p>{content}</p></li></template></ol>`), B.pk);138+ await A.page.waitForSelector("#withB li");139+ assertEquals(await items(A.page, "#withB").allTextContents(), [plain]);140+ });141+142+ await t.step("nostr-tools gift wraps unwrap in iodine", async () => {143+ const sk = generateSecretKey(), pk = getPublicKey(sk);144+ const w = nip17.wrapEvent(sk, { publicKey: B.pk }, "hi from nostr-tools");145+ const r = await B.page.evaluate((w) => iodine.unwrap(w), w);146+ assertEquals([r.pubkey, r.kind, r.content, r.tags[0]], [pk, 14, "hi from nostr-tools", ["p", B.pk]]);147+ assert(await publishRelay(relay, w));148+ await B.page.waitForFunction(() => document.querySelector("#inbox li p").textContent === "hi from nostr-tools");149+ });150+151+ await t.step("iodine gift wraps unwrap in nostr-tools", async () => {152+ const sk = generateSecretKey(), pk = getPublicKey(sk);153+ const res = await A.page.evaluate((pk) => iodine.send(pk, "hi to nostr-tools", [nhyCfg.relays[0]]), pk);154+ assert(res.every((r) => r.ok));155+ const [w] = await fetchRelay(relay, { kinds: [1059], "#p": [pk] });156+ const r = nip59.unwrapEvent(w, sk);157+ assertEquals([r.pubkey, r.kind, r.content, r.tags], [A.pk, 14, "hi to nostr-tools", [["p", pk]]]);158+ const w2 = await A.page.evaluate(async (pk) => iodine.wrap({ pubkey: await krypton.signer.getPublicKey(), created_at: 1, kind: 14, tags: [], content: "x", id: "0".repeat(64) }, pk), pk);159+ assert(verifyEvent(w2) && w2.kind === 1059);160+ assertEquals(nip59.unwrapEvent(w2, sk).content, "x");161+ });162+163+ assertEquals(errors, []);164+ } finally {165+ await browser.close();166+ await server.shutdown();167+ }168+});@@ -0,0 +1,648 @@
1+{2+ "v2": {3+ "valid": {4+ "get_conversation_key": [5+ {6+ "sec1": "315e59ff51cb9209768cf7da80791ddcaae56ac9775eb25b6dee1234bc5d2268",7+ "pub2": "c2f9d9948dc8c7c38321e4b85c8558872eafa0641cd269db76848a6073e69133",8+ "conversation_key": "3dfef0ce2a4d80a25e7a328accf73448ef67096f65f79588e358d9a0eb9013f1"9+ },10+ {11+ "sec1": "a1e37752c9fdc1273be53f68c5f74be7c8905728e8de75800b94262f9497c86e",12+ "pub2": "03bb7947065dde12ba991ea045132581d0954f042c84e06d8c00066e23c1a800",13+ "conversation_key": "4d14f36e81b8452128da64fe6f1eae873baae2f444b02c950b90e43553f2178b"14+ },15+ {16+ "sec1": "98a5902fd67518a0c900f0fb62158f278f94a21d6f9d33d30cd3091195500311",17+ "pub2": "aae65c15f98e5e677b5050de82e3aba47a6fe49b3dab7863cf35d9478ba9f7d1",18+ "conversation_key": "9c00b769d5f54d02bf175b7284a1cbd28b6911b06cda6666b2243561ac96bad7"19+ },20+ {21+ "sec1": "86ae5ac8034eb2542ce23ec2f84375655dab7f836836bbd3c54cefe9fdc9c19f",22+ "pub2": "59f90272378089d73f1339710c02e2be6db584e9cdbe86eed3578f0c67c23585",23+ "conversation_key": "19f934aafd3324e8415299b64df42049afaa051c71c98d0aa10e1081f2e3e2ba"24+ },25+ {26+ "sec1": "2528c287fe822421bc0dc4c3615878eb98e8a8c31657616d08b29c00ce209e34",27+ "pub2": "f66ea16104c01a1c532e03f166c5370a22a5505753005a566366097150c6df60",28+ "conversation_key": "c833bbb292956c43366145326d53b955ffb5da4e4998a2d853611841903f5442"29+ },30+ {31+ "sec1": "49808637b2d21129478041813aceb6f2c9d4929cd1303cdaf4fbdbd690905ff2",32+ "pub2": "74d2aab13e97827ea21baf253ad7e39b974bb2498cc747cdb168582a11847b65",33+ "conversation_key": "4bf304d3c8c4608864c0fe03890b90279328cd24a018ffa9eb8f8ccec06b505d"34+ },35+ {36+ "sec1": "af67c382106242c5baabf856efdc0629cc1c5b4061f85b8ceaba52aa7e4b4082",37+ "pub2": "bdaf0001d63e7ec994fad736eab178ee3c2d7cfc925ae29f37d19224486db57b",38+ "conversation_key": "a3a575dd66d45e9379904047ebfb9a7873c471687d0535db00ef2daa24b391db"39+ },40+ {41+ "sec1": "0e44e2d1db3c1717b05ffa0f08d102a09c554a1cbbf678ab158b259a44e682f1",42+ "pub2": "1ffa76c5cc7a836af6914b840483726207cb750889753d7499fb8b76aa8fe0de",43+ "conversation_key": "a39970a667b7f861f100e3827f4adbf6f464e2697686fe1a81aeda817d6b8bdf"44+ },45+ {46+ "sec1": "5fc0070dbd0666dbddc21d788db04050b86ed8b456b080794c2a0c8e33287bb6",47+ "pub2": "31990752f296dd22e146c9e6f152a269d84b241cc95bb3ff8ec341628a54caf0",48+ "conversation_key": "72c21075f4b2349ce01a3e604e02a9ab9f07e35dd07eff746de348b4f3c6365e"49+ },50+ {51+ "sec1": "1b7de0d64d9b12ddbb52ef217a3a7c47c4362ce7ea837d760dad58ab313cba64",52+ "pub2": "24383541dd8083b93d144b431679d70ef4eec10c98fceef1eff08b1d81d4b065",53+ "conversation_key": "dd152a76b44e63d1afd4dfff0785fa07b3e494a9e8401aba31ff925caeb8f5b1"54+ },55+ {56+ "sec1": "df2f560e213ca5fb33b9ecde771c7c0cbd30f1cf43c2c24de54480069d9ab0af",57+ "pub2": "eeea26e552fc8b5e377acaa03e47daa2d7b0c787fac1e0774c9504d9094c430e",58+ "conversation_key": "770519e803b80f411c34aef59c3ca018608842ebf53909c48d35250bd9323af6"59+ },60+ {61+ "sec1": "cffff919fcc07b8003fdc63bc8a00c0f5dc81022c1c927c62c597352190d95b9",62+ "pub2": "eb5c3cca1a968e26684e5b0eb733aecfc844f95a09ac4e126a9e58a4e4902f92",63+ "conversation_key": "46a14ee7e80e439ec75c66f04ad824b53a632b8409a29bbb7c192e43c00bb795"64+ },65+ {66+ "sec1": "64ba5a685e443e881e9094647ddd32db14444bb21aa7986beeba3d1c4673ba0a",67+ "pub2": "50e6a4339fac1f3bf86f2401dd797af43ad45bbf58e0801a7877a3984c77c3c4",68+ "conversation_key": "968b9dbbfcede1664a4ca35a5d3379c064736e87aafbf0b5d114dff710b8a946"69+ },70+ {71+ "sec1": "dd0c31ccce4ec8083f9b75dbf23cc2878e6d1b6baa17713841a2428f69dee91a",72+ "pub2": "b483e84c1339812bed25be55cff959778dfc6edde97ccd9e3649f442472c091b",73+ "conversation_key": "09024503c7bde07eb7865505891c1ea672bf2d9e25e18dd7a7cea6c69bf44b5d"74+ },75+ {76+ "sec1": "af71313b0d95c41e968a172b33ba5ebd19d06cdf8a7a98df80ecf7af4f6f0358",77+ "pub2": "2a5c25266695b461ee2af927a6c44a3c598b8095b0557e9bd7f787067435bc7c",78+ "conversation_key": "fe5155b27c1c4b4e92a933edae23726a04802a7cc354a77ac273c85aa3c97a92"79+ },80+ {81+ "sec1": "6636e8a389f75fe068a03b3edb3ea4a785e2768e3f73f48ffb1fc5e7cb7289dc",82+ "pub2": "514eb2064224b6a5829ea21b6e8f7d3ea15ff8e70e8555010f649eb6e09aec70",83+ "conversation_key": "ff7afacd4d1a6856d37ca5b546890e46e922b508639214991cf8048ddbe9745c"84+ },85+ {86+ "sec1": "94b212f02a3cfb8ad147d52941d3f1dbe1753804458e6645af92c7b2ea791caa",87+ "pub2": "f0cac333231367a04b652a77ab4f8d658b94e86b5a8a0c472c5c7b0d4c6a40cc",88+ "conversation_key": "e292eaf873addfed0a457c6bd16c8effde33d6664265697f69f420ab16f6669b"89+ },90+ {91+ "sec1": "aa61f9734e69ae88e5d4ced5aae881c96f0d7f16cca603d3bed9eec391136da6",92+ "pub2": "4303e5360a884c360221de8606b72dd316da49a37fe51e17ada4f35f671620a6",93+ "conversation_key": "8e7d44fd4767456df1fb61f134092a52fcd6836ebab3b00766e16732683ed848"94+ },95+ {96+ "sec1": "5e914bdac54f3f8e2cba94ee898b33240019297b69e96e70c8a495943a72fc98",97+ "pub2": "5bd097924f606695c59f18ff8fd53c174adbafaaa71b3c0b4144a3e0a474b198",98+ "conversation_key": "f5a0aecf2984bf923c8cd5e7bb8be262d1a8353cb93959434b943a07cf5644bc"99+ },100+ {101+ "sec1": "8b275067add6312ddee064bcdbeb9d17e88aa1df36f430b2cea5cc0413d8278a",102+ "pub2": "65bbbfca819c90c7579f7a82b750a18c858db1afbec8f35b3c1e0e7b5588e9b8",103+ "conversation_key": "2c565e7027eb46038c2263563d7af681697107e975e9914b799d425effd248d6"104+ },105+ {106+ "sec1": "1ac848de312285f85e0f7ec208aac20142a1f453402af9b34ec2ec7a1f9c96fc",107+ "pub2": "45f7318fe96034d23ee3ddc25b77f275cc1dd329664dd51b89f89c4963868e41",108+ "conversation_key": "b56e970e5057a8fd929f8aad9248176b9af87819a708d9ddd56e41d1aec74088"109+ },110+ {111+ "sec1": "295a1cf621de401783d29d0e89036aa1c62d13d9ad307161b4ceb535ba1b40e6",112+ "pub2": "840115ddc7f1034d3b21d8e2103f6cb5ab0b63cf613f4ea6e61ae3d016715cdd",113+ "conversation_key": "b4ee9c0b9b9fef88975773394f0a6f981ca016076143a1bb575b9ff46e804753"114+ },115+ {116+ "sec1": "a28eed0fe977893856ab9667e06ace39f03abbcdb845c329a1981be438ba565d",117+ "pub2": "b0f38b950a5013eba5ab4237f9ed29204a59f3625c71b7e210fec565edfa288c",118+ "conversation_key": "9d3a802b45bc5aeeb3b303e8e18a92ddd353375710a31600d7f5fff8f3a7285b"119+ },120+ {121+ "sec1": "7ab65af72a478c05f5c651bdc4876c74b63d20d04cdbf71741e46978797cd5a4",122+ "pub2": "f1112159161b568a9cb8c9dd6430b526c4204bcc8ce07464b0845b04c041beda",123+ "conversation_key": "943884cddaca5a3fef355e9e7f08a3019b0b66aa63ec90278b0f9fdb64821e79"124+ },125+ {126+ "sec1": "95c79a7b75ba40f2229e85756884c138916f9d103fc8f18acc0877a7cceac9fe",127+ "pub2": "cad76bcbd31ca7bbda184d20cc42f725ed0bb105b13580c41330e03023f0ffb3",128+ "conversation_key": "81c0832a669eea13b4247c40be51ccfd15bb63fcd1bba5b4530ce0e2632f301b"129+ },130+ {131+ "sec1": "baf55cc2febd4d980b4b393972dfc1acf49541e336b56d33d429bce44fa12ec9",132+ "pub2": "0c31cf87fe565766089b64b39460ebbfdedd4a2bc8379be73ad3c0718c912e18",133+ "conversation_key": "37e2344da9ecdf60ae2205d81e89d34b280b0a3f111171af7e4391ded93b8ea6"134+ },135+ {136+ "sec1": "6eeec45acd2ed31693c5256026abf9f072f01c4abb61f51cf64e6956b6dc8907",137+ "pub2": "e501b34ed11f13d816748c0369b0c728e540df3755bab59ed3327339e16ff828",138+ "conversation_key": "afaa141b522ddb27bb880d768903a7f618bb8b6357728cae7fb03af639b946e6"139+ },140+ {141+ "sec1": "261a076a9702af1647fb343c55b3f9a4f1096273002287df0015ba81ce5294df",142+ "pub2": "b2777c863878893ae100fb740c8fab4bebd2bf7be78c761a75593670380a6112",143+ "conversation_key": "76f8d2853de0734e51189ced523c09427c3e46338b9522cd6f74ef5e5b475c74"144+ },145+ {146+ "sec1": "ed3ec71ca406552ea41faec53e19f44b8f90575eda4b7e96380f9cc73c26d6f3",147+ "pub2": "86425951e61f94b62e20cae24184b42e8e17afcf55bafa58645efd0172624fae",148+ "conversation_key": "f7ffc520a3a0e9e9b3c0967325c9bf12707f8e7a03f28b6cd69ae92cf33f7036"149+ },150+ {151+ "sec1": "5a788fc43378d1303ac78639c59a58cb88b08b3859df33193e63a5a3801c722e",152+ "pub2": "a8cba2f87657d229db69bee07850fd6f7a2ed070171a06d006ec3a8ac562cf70",153+ "conversation_key": "7d705a27feeedf78b5c07283362f8e361760d3e9f78adab83e3ae5ce7aeb6409"154+ },155+ {156+ "sec1": "63bffa986e382b0ac8ccc1aa93d18a7aa445116478be6f2453bad1f2d3af2344",157+ "pub2": "b895c70a83e782c1cf84af558d1038e6b211c6f84ede60408f519a293201031d",158+ "conversation_key": "3a3b8f00d4987fc6711d9be64d9c59cf9a709c6c6481c2cde404bcc7a28f174e"159+ },160+ {161+ "sec1": "e4a8bcacbf445fd3721792b939ff58e691cdcba6a8ba67ac3467b45567a03e5c",162+ "pub2": "b54053189e8c9252c6950059c783edb10675d06d20c7b342f73ec9fa6ed39c9d",163+ "conversation_key": "7b3933b4ef8189d347169c7955589fc1cfc01da5239591a08a183ff6694c44ad"164+ },165+ {166+ "sec1": "fffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364139",167+ "pub2": "0000000000000000000000000000000000000000000000000000000000000002",168+ "conversation_key": "8b6392dbf2ec6a2b2d5b1477fc2be84d63ef254b667cadd31bd3f444c44ae6ba",169+ "note": "sec1 = n-2, pub2: random, 0x02"170+ },171+ {172+ "sec1": "0000000000000000000000000000000000000000000000000000000000000002",173+ "pub2": "1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdeb",174+ "conversation_key": "be234f46f60a250bef52a5ee34c758800c4ca8e5030bf4cc1a31d37ba2104d43",175+ "note": "sec1 = 2, pub2: rand"176+ },177+ {178+ "sec1": "0000000000000000000000000000000000000000000000000000000000000001",179+ "pub2": "79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798",180+ "conversation_key": "3b4610cb7189beb9cc29eb3716ecc6102f1247e8f3101a03a1787d8908aeb54e",181+ "note": "sec1 == pub2"182+ }183+ ],184+ "get_message_keys": {185+ "conversation_key": "a1a3d60f3470a8612633924e91febf96dc5366ce130f658b1f0fc652c20b3b54",186+ "keys": [187+ {188+ "nonce": "e1e6f880560d6d149ed83dcc7e5861ee62a5ee051f7fde9975fe5d25d2a02d72",189+ "chacha_key": "f145f3bed47cb70dbeaac07f3a3fe683e822b3715edb7c4fe310829014ce7d76",190+ "chacha_nonce": "c4ad129bb01180c0933a160c",191+ "hmac_key": "027c1db445f05e2eee864a0975b0ddef5b7110583c8c192de3732571ca5838c4"192+ },193+ {194+ "nonce": "e1d6d28c46de60168b43d79dacc519698512ec35e8ccb12640fc8e9f26121101",195+ "chacha_key": "e35b88f8d4a8f1606c5082f7a64b100e5d85fcdb2e62aeafbec03fb9e860ad92",196+ "chacha_nonce": "22925e920cee4a50a478be90",197+ "hmac_key": "46a7c55d4283cb0df1d5e29540be67abfe709e3b2e14b7bf9976e6df994ded30"198+ },199+ {200+ "nonce": "cfc13bef512ac9c15951ab00030dfaf2626fdca638dedb35f2993a9eeb85d650",201+ "chacha_key": "020783eb35fdf5b80ef8c75377f4e937efb26bcbad0e61b4190e39939860c4bf",202+ "chacha_nonce": "d3594987af769a52904656ac",203+ "hmac_key": "237ec0ccb6ebd53d179fa8fd319e092acff599ef174c1fdafd499ef2b8dee745"204+ },205+ {206+ "nonce": "ea6eb84cac23c5c1607c334e8bdf66f7977a7e374052327ec28c6906cbe25967",207+ "chacha_key": "ff68db24b34fa62c78ac5ffeeaf19533afaedf651fb6a08384e46787f6ce94be",208+ "chacha_nonce": "50bb859aa2dde938cc49ec7a",209+ "hmac_key": "06ff32e1f7b29753a727d7927b25c2dd175aca47751462d37a2039023ec6b5a6"210+ },211+ {212+ "nonce": "8c2e1dd3792802f1f9f7842e0323e5d52ad7472daf360f26e15f97290173605d",213+ "chacha_key": "2f9daeda8683fdeede81adac247c63cc7671fa817a1fd47352e95d9487989d8b",214+ "chacha_nonce": "400224ba67fc2f1b76736916",215+ "hmac_key": "465c05302aeeb514e41c13ed6405297e261048cfb75a6f851ffa5b445b746e4b"216+ },217+ {218+ "nonce": "05c28bf3d834fa4af8143bf5201a856fa5fac1a3aee58f4c93a764fc2f722367",219+ "chacha_key": "1e3d45777025a035be566d80fd580def73ed6f7c043faec2c8c1c690ad31c110",220+ "chacha_nonce": "021905b1ea3afc17cb9bf96f",221+ "hmac_key": "74a6e481a89dcd130aaeb21060d7ec97ad30f0007d2cae7b1b11256cc70dfb81"222+ },223+ {224+ "nonce": "5e043fb153227866e75a06d60185851bc90273bfb93342f6632a728e18a07a17",225+ "chacha_key": "1ea72c9293841e7737c71567d8120145a58991aaa1c436ef77bf7adb83f882f1",226+ "chacha_nonce": "72f69a5a5f795465cee59da8",227+ "hmac_key": "e9daa1a1e9a266ecaa14e970a84bce3fbbf329079bbccda626582b4e66a0d4c9"228+ },229+ {230+ "nonce": "7be7338eaf06a87e274244847fe7a97f5c6a91f44adc18fcc3e411ad6f786dbf",231+ "chacha_key": "881e7968a1f0c2c80742ee03cd49ea587e13f22699730f1075ade01931582bf6",232+ "chacha_nonce": "6e69be92d61c04a276021565",233+ "hmac_key": "901afe79e74b19967c8829af23617d7d0ffbf1b57190c096855c6a03523a971b"234+ },235+ {236+ "nonce": "94571c8d590905bad7becd892832b472f2aa5212894b6ce96e5ba719c178d976",237+ "chacha_key": "f80873dd48466cb12d46364a97b8705c01b9b4230cb3ec3415a6b9551dc42eef",238+ "chacha_nonce": "3dda53569cfcb7fac1805c35",239+ "hmac_key": "e9fc264345e2839a181affebc27d2f528756e66a5f87b04bf6c5f1997047051e"240+ },241+ {242+ "nonce": "13a6ee974b1fd759135a2c2010e3cdda47081c78e771125e4f0c382f0284a8cb",243+ "chacha_key": "bc5fb403b0bed0d84cf1db872b6522072aece00363178c98ad52178d805fca85",244+ "chacha_nonce": "65064239186e50304cc0f156",245+ "hmac_key": "e872d320dde4ed3487958a8e43b48aabd3ced92bc24bb8ff1ccb57b590d9701a"246+ },247+ {248+ "nonce": "082fecdb85f358367b049b08be0e82627ae1d8edb0f27327ccb593aa2613b814",249+ "chacha_key": "1fbdb1cf6f6ea816349baf697932b36107803de98fcd805ebe9849b8ad0e6a45",250+ "chacha_nonce": "2e605e1d825a3eaeb613db9c",251+ "hmac_key": "fae910f591cf3c7eb538c598583abad33bc0a03085a96ca4ea3a08baf17c0eec"252+ },253+ {254+ "nonce": "4c19020c74932c30ec6b2d8cd0d5bb80bd0fc87da3d8b4859d2fb003810afd03",255+ "chacha_key": "1ab9905a0189e01cda82f843d226a82a03c4f5b6dbea9b22eb9bc953ba1370d4",256+ "chacha_nonce": "cbb2530ea653766e5a37a83a",257+ "hmac_key": "267f68acac01ac7b34b675e36c2cef5e7b7a6b697214add62a491bedd6efc178"258+ },259+ {260+ "nonce": "67723a3381497b149ce24814eddd10c4c41a1e37e75af161930e6b9601afd0ff",261+ "chacha_key": "9ecbd25e7e2e6c97b8c27d376dcc8c5679da96578557e4e21dba3a7ef4e4ac07",262+ "chacha_nonce": "ef649fcf335583e8d45e3c2e",263+ "hmac_key": "04dbbd812fa8226fdb45924c521a62e3d40a9e2b5806c1501efdeba75b006bf1"264+ },265+ {266+ "nonce": "42063fe80b093e8619b1610972b4c3ab9e76c14fd908e642cd4997cafb30f36c",267+ "chacha_key": "211c66531bbcc0efcdd0130f9f1ebc12a769105eb39608994bcb188fa6a73a4a",268+ "chacha_nonce": "67803605a7e5010d0f63f8c8",269+ "hmac_key": "e840e4e8921b57647369d121c5a19310648105dbdd008200ebf0d3b668704ff8"270+ },271+ {272+ "nonce": "b5ac382a4be7ac03b554fe5f3043577b47ea2cd7cfc7e9ca010b1ffbb5cf1a58",273+ "chacha_key": "b3b5f14f10074244ee42a3837a54309f33981c7232a8b16921e815e1f7d1bb77",274+ "chacha_nonce": "4e62a0073087ed808be62469",275+ "hmac_key": "c8efa10230b5ea11633816c1230ca05fa602ace80a7598916d83bae3d3d2ccd7"276+ },277+ {278+ "nonce": "e9d1eba47dd7e6c1532dc782ff63125db83042bb32841db7eeafd528f3ea7af9",279+ "chacha_key": "54241f68dc2e50e1db79e892c7c7a471856beeb8d51b7f4d16f16ab0645d2f1a",280+ "chacha_nonce": "a963ed7dc29b7b1046820a1d",281+ "hmac_key": "aba215c8634530dc21c70ddb3b3ee4291e0fa5fa79be0f85863747bde281c8b2"282+ },283+ {284+ "nonce": "a94ecf8efeee9d7068de730fad8daf96694acb70901d762de39fa8a5039c3c49",285+ "chacha_key": "c0565e9e201d2381a2368d7ffe60f555223874610d3d91fbbdf3076f7b1374dd",286+ "chacha_nonce": "329bb3024461e84b2e1c489b",287+ "hmac_key": "ac42445491f092481ce4fa33b1f2274700032db64e3a15014fbe8c28550f2fec"288+ },289+ {290+ "nonce": "533605ea214e70c25e9a22f792f4b78b9f83a18ab2103687c8a0075919eaaa53",291+ "chacha_key": "ab35a5e1e54d693ff023db8500d8d4e79ad8878c744e0eaec691e96e141d2325",292+ "chacha_nonce": "653d759042b85194d4d8c0a7",293+ "hmac_key": "b43628e37ba3c31ce80576f0a1f26d3a7c9361d29bb227433b66f49d44f167ba"294+ },295+ {296+ "nonce": "7f38df30ceea1577cb60b355b4f5567ff4130c49e84fed34d779b764a9cc184c",297+ "chacha_key": "a37d7f211b84a551a127ff40908974eb78415395d4f6f40324428e850e8c42a3",298+ "chacha_nonce": "b822e2c959df32b3cb772a7c",299+ "hmac_key": "1ba31764f01f69b5c89ded2d7c95828e8052c55f5d36f1cd535510d61ba77420"300+ },301+ {302+ "nonce": "11b37f9dbc4d0185d1c26d5f4ed98637d7c9701fffa65a65839fa4126573a4e5",303+ "chacha_key": "964f38d3a31158a5bfd28481247b18dd6e44d69f30ba2a40f6120c6d21d8a6ba",304+ "chacha_nonce": "5f72c5b87c590bcd0f93b305",305+ "hmac_key": "2fc4553e7cedc47f29690439890f9f19c1077ef3e9eaeef473d0711e04448918"306+ },307+ {308+ "nonce": "8be790aa483d4cdd843189f71f135b3ec7e31f381312c8fe9f177aab2a48eafa",309+ "chacha_key": "95c8c74d633721a131316309cf6daf0804d59eaa90ea998fc35bac3d2fbb7a94",310+ "chacha_nonce": "409a7654c0e4bf8c2c6489be",311+ "hmac_key": "21bb0b06eb2b460f8ab075f497efa9a01c9cf9146f1e3986c3bf9da5689b6dc4"312+ },313+ {314+ "nonce": "19fd2a718ea084827d6bd73f509229ddf856732108b59fc01819f611419fd140",315+ "chacha_key": "cc6714b9f5616c66143424e1413d520dae03b1a4bd202b82b0a89b0727f5cdc8",316+ "chacha_nonce": "1b7fd2534f015a8f795d8f32",317+ "hmac_key": "2bef39c4ce5c3c59b817e86351373d1554c98bc131c7e461ed19d96cfd6399a0"318+ },319+ {320+ "nonce": "3c2acd893952b2f6d07d8aea76f545ca45961a93fe5757f6a5a80811d5e0255d",321+ "chacha_key": "c8de6c878cb469278d0af894bc181deb6194053f73da5014c2b5d2c8db6f2056",322+ "chacha_nonce": "6ffe4f1971b904a1b1a81b99",323+ "hmac_key": "df1cd69dd3646fca15594284744d4211d70e7d8472e545d276421fbb79559fd4"324+ },325+ {326+ "nonce": "7dbea4cead9ac91d4137f1c0a6eebb6ba0d1fb2cc46d829fbc75f8d86aca6301",327+ "chacha_key": "c8e030f6aa680c3d0b597da9c92bb77c21c4285dd620c5889f9beba7446446b0",328+ "chacha_nonce": "a9b5a67d081d3b42e737d16f",329+ "hmac_key": "355a85f551bc3cce9a14461aa60994742c9bbb1c81a59ca102dc64e61726ab8e"330+ },331+ {332+ "nonce": "45422e676cdae5f1071d3647d7a5f1f5adafb832668a578228aa1155a491f2f3",333+ "chacha_key": "758437245f03a88e2c6a32807edfabff51a91c81ca2f389b0b46f2c97119ea90",334+ "chacha_nonce": "263830a065af33d9c6c5aa1f",335+ "hmac_key": "7c581cf3489e2de203a95106bfc0de3d4032e9d5b92b2b61fb444acd99037e17"336+ },337+ {338+ "nonce": "babc0c03fad24107ad60678751f5db2678041ff0d28671ede8d65bdf7aa407e9",339+ "chacha_key": "bd68a28bd48d9ffa3602db72c75662ac2848a0047a313d2ae2d6bc1ac153d7e9",340+ "chacha_nonce": "d0f9d2a1ace6c758f594ffdd",341+ "hmac_key": "eb435e3a642adfc9d59813051606fc21f81641afd58ea6641e2f5a9f123bb50a"342+ },343+ {344+ "nonce": "7a1b8aac37d0d20b160291fad124ab697cfca53f82e326d78fef89b4b0ea8f83",345+ "chacha_key": "9e97875b651a1d30d17d086d1e846778b7faad6fcbc12e08b3365d700f62e4fe",346+ "chacha_nonce": "ccdaad5b3b7645be430992eb",347+ "hmac_key": "6f2f55cf35174d75752f63c06cc7cbc8441759b142999ed2d5a6d09d263e1fc4"348+ },349+ {350+ "nonce": "8370e4e32d7e680a83862cab0da6136ef607014d043e64cdf5ecc0c4e20b3d9a",351+ "chacha_key": "1472bed5d19db9c546106de946e0649cd83cc9d4a66b087a65906e348dcf92e2",352+ "chacha_nonce": "ed02dece5fc3a186f123420b",353+ "hmac_key": "7b3f7739f49d30c6205a46b174f984bb6a9fc38e5ccfacef2dac04fcbd3b184e"354+ },355+ {356+ "nonce": "9f1c5e8a29cd5677513c2e3a816551d6833ee54991eb3f00d5b68096fc8f0183",357+ "chacha_key": "5e1a7544e4d4dafe55941fcbdf326f19b0ca37fc49c4d47e9eec7fb68cde4975",358+ "chacha_nonce": "7d9acb0fdc174e3c220f40de",359+ "hmac_key": "e265ab116fbbb86b2aefc089a0986a0f5b77eda50c7410404ad3b4f3f385c7a7"360+ },361+ {362+ "nonce": "c385aa1c37c2bfd5cc35fcdbdf601034d39195e1cabff664ceb2b787c15d0225",363+ "chacha_key": "06bf4e60677a13e54c4a38ab824d2ef79da22b690da2b82d0aa3e39a14ca7bdd",364+ "chacha_nonce": "26b450612ca5e905b937e147",365+ "hmac_key": "22208152be2b1f5f75e6bfcc1f87763d48bb7a74da1be3d102096f257207f8b3"366+ },367+ {368+ "nonce": "3ff73528f88a50f9d35c0ddba4560bacee5b0462d0f4cb6e91caf41847040ce4",369+ "chacha_key": "850c8a17a23aa761d279d9901015b2bbdfdff00adbf6bc5cf22bd44d24ecabc9",370+ "chacha_nonce": "4a296a1fb0048e5020d3b129",371+ "hmac_key": "b1bf49a533c4da9b1d629b7ff30882e12d37d49c19abd7b01b7807d75ee13806"372+ },373+ {374+ "nonce": "2dcf39b9d4c52f1cb9db2d516c43a7c6c3b8c401f6a4ac8f131a9e1059957036",375+ "chacha_key": "17f8057e6156ba7cc5310d01eda8c40f9aa388f9fd1712deb9511f13ecc37d27",376+ "chacha_nonce": "a8188daff807a1182200b39d",377+ "hmac_key": "47b89da97f68d389867b5d8a2d7ba55715a30e3d88a3cc11f3646bc2af5580ef"378+ }379+ ]380+ },381+ "calc_padded_len": [382+ [16, 32],383+ [32, 32],384+ [33, 64],385+ [37, 64],386+ [45, 64],387+ [49, 64],388+ [64, 64],389+ [65, 96],390+ [100, 128],391+ [111, 128],392+ [200, 224],393+ [250, 256],394+ [320, 320],395+ [383, 384],396+ [384, 384],397+ [400, 448],398+ [500, 512],399+ [512, 512],400+ [515, 640],401+ [700, 768],402+ [800, 896],403+ [900, 1024],404+ [1020, 1024],405+ [65536, 65536]406+ ],407+ "encrypt_decrypt": [408+ {409+ "sec1": "0000000000000000000000000000000000000000000000000000000000000001",410+ "sec2": "0000000000000000000000000000000000000000000000000000000000000002",411+ "conversation_key": "c41c775356fd92eadc63ff5a0dc1da211b268cbea22316767095b2871ea1412d",412+ "nonce": "0000000000000000000000000000000000000000000000000000000000000001",413+ "plaintext": "a",414+ "payload": "AgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABee0G5VSK0/9YypIObAtDKfYEAjD35uVkHyB0F4DwrcNaCXlCWZKaArsGrY6M9wnuTMxWfp1RTN9Xga8no+kF5Vsb"415+ },416+ {417+ "sec1": "0000000000000000000000000000000000000000000000000000000000000002",418+ "sec2": "0000000000000000000000000000000000000000000000000000000000000001",419+ "conversation_key": "c41c775356fd92eadc63ff5a0dc1da211b268cbea22316767095b2871ea1412d",420+ "nonce": "f00000000000000000000000000000f00000000000000000000000000000000f",421+ "plaintext": "๐๐ซ",422+ "payload": "AvAAAAAAAAAAAAAAAAAAAPAAAAAAAAAAAAAAAAAAAAAPSKSK6is9ngkX2+cSq85Th16oRTISAOfhStnixqZziKMDvB0QQzgFZdjLTPicCJaV8nDITO+QfaQ61+KbWQIOO2Yj"423+ },424+ {425+ "sec1": "5c0c523f52a5b6fad39ed2403092df8cebc36318b39383bca6c00808626fab3a",426+ "sec2": "4b22aa260e4acb7021e32f38a6cdf4b673c6a277755bfce287e370c924dc936d",427+ "conversation_key": "3e2b52a63be47d34fe0a80e34e73d436d6963bc8f39827f327057a9986c20a45",428+ "nonce": "b635236c42db20f021bb8d1cdff5ca75dd1a0cc72ea742ad750f33010b24f73b",429+ "plaintext": "่กจใใA้ทลรฉ๏ผข้รรยชฤ
รฑไธใ๐ ",430+ "payload": "ArY1I2xC2yDwIbuNHN/1ynXdGgzHLqdCrXUPMwELJPc7s7JqlCMJBAIIjfkpHReBPXeoMCyuClwgbT419jUWU1PwaNl4FEQYKCDKVJz+97Mp3K+Q2YGa77B6gpxB/lr1QgoqpDf7wDVrDmOqGoiPjWDqy8KzLueKDcm9BVP8xeTJIxs="431+ },432+ {433+ "sec1": "8f40e50a84a7462e2b8d24c28898ef1f23359fff50d8c509e6fb7ce06e142f9c",434+ "sec2": "b9b0a1e9cc20100c5faa3bbe2777303d25950616c4c6a3fa2e3e046f936ec2ba",435+ "conversation_key": "d5a2f879123145a4b291d767428870f5a8d9e5007193321795b40183d4ab8c2b",436+ "nonce": "b20989adc3ddc41cd2c435952c0d59a91315d8c5218d5040573fc3749543acaf",437+ "plaintext": "ability๐ค็ ศบศพ",438+ "payload": "ArIJia3D3cQc0sQ1lSwNWakTFdjFIY1QQFc/w3SVQ6yvbG2S0x4Yu86QGwPTy7mP3961I1XqB6SFFTzqDZZavhxoWMj7mEVGMQIsh2RLWI5EYQaQDIePSnXPlzf7CIt+voTD"439+ },440+ {441+ "sec1": "875adb475056aec0b4809bd2db9aa00cff53a649e7b59d8edcbf4e6330b0995c",442+ "sec2": "9c05781112d5b0a2a7148a222e50e0bd891d6b60c5483f03456e982185944aae",443+ "conversation_key": "3b15c977e20bfe4b8482991274635edd94f366595b1a3d2993515705ca3cedb8",444+ "nonce": "8d4442713eb9d4791175cb040d98d6fc5be8864d6ec2f89cf0895a2b2b72d1b1",445+ "plaintext": "pepper๐ัะถะฐะบ",446+ "payload": "Ao1EQnE+udR5EXXLBA2Y1vxb6IZNbsL4nPCJWisrctGxY3AduCS+jTUgAAnfvKafkmpy15+i9YMwCdccisRa8SvzW671T2JO4LFSPX31K4kYUKelSAdSPwe9NwO6LhOsnoJ+"447+ },448+ {449+ "sec1": "eba1687cab6a3101bfc68fd70f214aa4cc059e9ec1b79fdb9ad0a0a4e259829f",450+ "sec2": "dff20d262bef9dfd94666548f556393085e6ea421c8af86e9d333fa8747e94b3",451+ "conversation_key": "4f1538411098cf11c8af216836444787c462d47f97287f46cf7edb2c4915b8a5",452+ "nonce": "2180b52ae645fcf9f5080d81b1f0b5d6f2cd77ff3c986882bb549158462f3407",453+ "plaintext": "( อกยฐ อส อกยฐ)",454+ "payload": "AiGAtSrmRfz59QgNgbHwtdbyzXf/PJhogrtUkVhGLzQHv4qhKQwnFQ54OjVMgqCea/Vj0YqBSdhqNR777TJ4zIUk7R0fnizp6l1zwgzWv7+ee6u+0/89KIjY5q1wu6inyuiv"455+ },456+ {457+ "sec1": "d5633530f5bcfebceb5584cfbbf718a30df0751b729dd9a789b9f30c0587d74e",458+ "sec2": "b74e6a341fb134127272b795a08b59250e5fa45a82a2eb4095e4ce9ed5f5e214",459+ "conversation_key": "75fe686d21a035f0c7cd70da64ba307936e5ca0b20710496a6b6b5f573377bdd",460+ "nonce": "e4cd5f7ce4eea024bc71b17ad456a986a74ac426c2c62b0a15eb5c5c8f888b68",461+ "plaintext": "ู
ูููุงููุดูุฉู ุณูุจููู ุงูุณูุชูุฎูุฏูุงู
ู ุงููููุบูุฉู ููู ุงููููุธูู
ู ุงููููุงุฆูู
ูุฉู ูููููู
ููุฎูุตูู ุงูุชููุทูุจููููุงุชู ุงููุญุงุณููุจููููุฉูุ",462+ "payload": "AuTNX3zk7qAkvHGxetRWqYanSsQmwsYrChXrXFyPiItoIBsWu1CB+sStla2M4VeANASHxM78i1CfHQQH1YbBy24Tng7emYW44ol6QkFD6D8Zq7QPl+8L1c47lx8RoODEQMvNCbOk5ffUV3/AhONHBXnffrI+0025c+uRGzfqpYki4lBqm9iYU+k3Tvjczq9wU0mkVDEaM34WiQi30MfkJdRbeeYaq6kNvGPunLb3xdjjs5DL720d61Flc5ZfoZm+CBhADy9D9XiVZYLKAlkijALJur9dATYKci6OBOoc2SJS2Clai5hOVzR0yVeyHRgRfH9aLSlWW5dXcUxTo7qqRjNf8W5+J4jF4gNQp5f5d0YA4vPAzjBwSP/5bGzNDslKfcAH"463+ },464+ {465+ "sec1": "d5633530f5bcfebceb5584cfbbf718a30df0751b729dd9a789b9f30c0587d74e",466+ "sec2": "b74e6a341fb134127272b795a08b59250e5fa45a82a2eb4095e4ce9ed5f5e214",467+ "conversation_key": "75fe686d21a035f0c7cd70da64ba307936e5ca0b20710496a6b6b5f573377bdd",468+ "nonce": "38d1ca0abef9e5f564e89761a86cee04574b6825d3ef2063b10ad75899e4b023",469+ "plaintext": "ุงููู ูู ุงูู
ุฌู
ู ุนุฉ (5)",470+ "payload": "AjjRygq++eX1ZOiXYahs7gRXS2gl0+8gY7EK11iZ5LAjbOTrlfrxak5Lki42v2jMPpLSicy8eHjsWkkMtF0i925vOaKG/ZkMHh9ccQBdfTvgEGKzztedqDCAWb5TP1YwU1PsWaiiqG3+WgVvJiO4lUdMHXL7+zKKx8bgDtowzz4QAwI="471+ },472+ {473+ "sec1": "d5633530f5bcfebceb5584cfbbf718a30df0751b729dd9a789b9f30c0587d74e",474+ "sec2": "b74e6a341fb134127272b795a08b59250e5fa45a82a2eb4095e4ce9ed5f5e214",475+ "conversation_key": "75fe686d21a035f0c7cd70da64ba307936e5ca0b20710496a6b6b5f573377bdd",476+ "nonce": "4f1a31909f3483a9e69c8549a55bbc9af25fa5bbecf7bd32d9896f83ef2e12e0",477+ "plaintext": "๐๐๐๐ ็คพๆ็งๅญธ้ข่ชๅญธ็ ็ฉถๆ",478+ "payload": "Ak8aMZCfNIOp5pyFSaVbvJryX6W77Pe9MtmJb4PvLhLgh/TsxPLFSANcT67EC1t/qxjru5ZoADjKVEt2ejdx+xGvH49mcdfbc+l+L7gJtkH7GLKpE9pQNQWNHMAmj043PAXJZ++fiJObMRR2mye5VHEANzZWkZXMrXF7YjuG10S1pOU="479+ },480+ {481+ "sec1": "d5633530f5bcfebceb5584cfbbf718a30df0751b729dd9a789b9f30c0587d74e",482+ "sec2": "b74e6a341fb134127272b795a08b59250e5fa45a82a2eb4095e4ce9ed5f5e214",483+ "conversation_key": "75fe686d21a035f0c7cd70da64ba307936e5ca0b20710496a6b6b5f573377bdd",484+ "nonce": "a3e219242d85465e70adcd640b564b3feff57d2ef8745d5e7a0663b2dccceb54",485+ "plaintext": "๐ ๐ ๐ 0๏ธโฃ 1๏ธโฃ 2๏ธโฃ 3๏ธโฃ 4๏ธโฃ 5๏ธโฃ 6๏ธโฃ 7๏ธโฃ 8๏ธโฃ 9๏ธโฃ ๐ Powerููููุตูุจููููุตูุจูุฑุฑู เฅฃ เฅฃh เฅฃ เฅฃๅ",486+ "payload": "AqPiGSQthUZecK3NZAtWSz/v9X0u+HRdXnoGY7LczOtUf05aMF89q1FLwJvaFJYICZoMYgRJHFLwPiOHce7fuAc40kX0wXJvipyBJ9HzCOj7CgtnC1/cmPCHR3s5AIORmroBWglm1LiFMohv1FSPEbaBD51VXxJa4JyWpYhreSOEjn1wd0lMKC9b+osV2N2tpbs+rbpQem2tRen3sWflmCqjkG5VOVwRErCuXuPb5+hYwd8BoZbfCrsiAVLd7YT44dRtKNBx6rkabWfddKSLtreHLDysOhQUVOp/XkE7OzSkWl6sky0Hva6qJJ/V726hMlomvcLHjE41iKmW2CpcZfOedg=="487+ }488+ ],489+ "encrypt_decrypt_long_msg": [490+ {491+ "conversation_key": "8fc262099ce0d0bb9b89bac05bb9e04f9bc0090acc181fef6840ccee470371ed",492+ "nonce": "326bcb2c943cd6bb717588c9e5a7e738edf6ed14ec5f5344caa6ef56f0b9cff7",493+ "pattern": "x",494+ "repeat": 65535,495+ "plaintext_sha256": "09ab7495d3e61a76f0deb12cb0306f0696cbb17ffc12131368c7a939f12f56d3",496+ "payload_sha256": "90714492225faba06310bff2f249ebdc2a5e609d65a629f1c87f2d4ffc55330a"497+ },498+ {499+ "conversation_key": "56adbe3720339363ab9c3b8526ffce9fd77600927488bfc4b59f7a68ffe5eae0",500+ "nonce": "ad68da81833c2a8ff609c3d2c0335fd44fe5954f85bb580c6a8d467aa9fc5dd0",501+ "pattern": "!",502+ "repeat": 65535,503+ "plaintext_sha256": "6af297793b72ae092c422e552c3bb3cbc310da274bd1cf9e31023a7fe4a2d75e",504+ "payload_sha256": "8013e45a109fad3362133132b460a2d5bce235fe71c8b8f4014793fb52a49844"505+ },506+ {507+ "conversation_key": "7fc540779979e472bb8d12480b443d1e5eb1098eae546ef2390bee499bbf46be",508+ "nonce": "34905e82105c20de9a2f6cd385a0d541e6bcc10601d12481ff3a7575dc622033",509+ "pattern": "๐ฆ",510+ "repeat": 16383,511+ "plaintext_sha256": "a249558d161b77297bc0cb311dde7d77190f6571b25c7e4429cd19044634a61f",512+ "payload_sha256": "b3348422471da1f3c59d79acfe2fe103f3cd24488109e5b18734cdb5953afd15"513+ }514+ ]515+ },516+ "invalid": {517+ "encrypt_msg_lengths": [0, 65536, 100000, 10000000],518+ "get_conversation_key": [519+ {520+ "sec1": "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",521+ "pub2": "1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef",522+ "note": "sec1 higher than curve.n"523+ },524+ {525+ "sec1": "0000000000000000000000000000000000000000000000000000000000000000",526+ "pub2": "1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef",527+ "note": "sec1 is 0"528+ },529+ {530+ "sec1": "fffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364139",531+ "pub2": "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",532+ "note": "pub2 is invalid, no sqrt, all-ff"533+ },534+ {535+ "sec1": "fffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141",536+ "pub2": "1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef",537+ "note": "sec1 == curve.n"538+ },539+ {540+ "sec1": "0000000000000000000000000000000000000000000000000000000000000002",541+ "pub2": "1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef",542+ "note": "pub2 is invalid, no sqrt"543+ },544+ {545+ "sec1": "0102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f20",546+ "pub2": "0000000000000000000000000000000000000000000000000000000000000000",547+ "note": "pub2 is point of order 3 on twist"548+ },549+ {550+ "sec1": "0102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f20",551+ "pub2": "eb1f7200aecaa86682376fb1c13cd12b732221e774f553b0a0857f88fa20f86d",552+ "note": "pub2 is point of order 13 on twist"553+ },554+ {555+ "sec1": "0102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f20",556+ "pub2": "709858a4c121e4a84eb59c0ded0261093c71e8ca29efeef21a6161c447bcaf9f",557+ "note": "pub2 is point of order 3319 on twist"558+ }559+ ],560+ "decrypt": [561+ {562+ "conversation_key": "ca2527a037347b91bea0c8a30fc8d9600ffd81ec00038671e3a0f0cb0fc9f642",563+ "nonce": "daaea5ca345b268e5b62060ca72c870c48f713bc1e00ff3fc0ddb78e826f10db",564+ "plaintext": "n o b l e",565+ "payload": "#Atqupco0WyaOW2IGDKcshwxI9xO8HgD/P8Ddt46CbxDbrhdG8VmJdU0MIDf06CUvEvdnr1cp1fiMtlM/GrE92xAc1K5odTpCzUB+mjXgbaqtntBUbTToSUoT0ovrlPwzGjyp",566+ "note": "unknown encryption version"567+ },568+ {569+ "conversation_key": "36f04e558af246352dcf73b692fbd3646a2207bd8abd4b1cd26b234db84d9481",570+ "nonce": "ad408d4be8616dc84bb0bf046454a2a102edac937c35209c43cd7964c5feb781",571+ "plaintext": "โ ๏ธ",572+ "payload": "AK1AjUvoYW3IS7C/BGRUoqEC7ayTfDUgnEPNeWTF/reBZFaha6EAIRueE9D1B1RuoiuFScC0Q94yjIuxZD3JStQtE8JMNacWFs9rlYP+ZydtHhRucp+lxfdvFlaGV/sQlqZz",573+ "note": "unknown encryption version 0"574+ },575+ {576+ "conversation_key": "ca2527a037347b91bea0c8a30fc8d9600ffd81ec00038671e3a0f0cb0fc9f642",577+ "nonce": "daaea5ca345b268e5b62060ca72c870c48f713bc1e00ff3fc0ddb78e826f10db",578+ "plaintext": "n o s t r",579+ "payload": "Atัupco0WyaOW2IGDKcshwxI9xO8HgD/P8Ddt46CbxDbrhdG8VmJZE0UICD06CUvEvdnr1cp1fiMtlM/GrE92xAc1EwsVCQEgWEu2gsHUVf4JAa3TpgkmFc3TWsax0v6n/Wq",580+ "note": "invalid base64"581+ },582+ {583+ "conversation_key": "cff7bd6a3e29a450fd27f6c125d5edeb0987c475fd1e8d97591e0d4d8a89763c",584+ "nonce": "09ff97750b084012e15ecb84614ce88180d7b8ec0d468508a86b6d70c0361a25",585+ "plaintext": "ยฏ\\_(ใ)_/ยฏ",586+ "payload": "Agn/l3ULCEAS4V7LhGFM6IGA17jsDUaFCKhrbXDANholyySBfeh+EN8wNB9gaLlg4j6wdBYh+3oK+mnxWu3NKRbSvQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA",587+ "note": "invalid MAC"588+ },589+ {590+ "conversation_key": "cfcc9cf682dfb00b11357f65bdc45e29156b69db424d20b3596919074f5bf957",591+ "nonce": "65b14b0b949aaa7d52c417eb753b390e8ad6d84b23af4bec6d9bfa3e03a08af4",592+ "plaintext": "๐ฅ",593+ "payload": "AmWxSwuUmqp9UsQX63U7OQ6K1thLI69L7G2b+j4DoIr0oRWQ8avl4OLqWZiTJ10vIgKrNqjoaX+fNhE9RqmR5g0f6BtUg1ijFMz71MO1D4lQLQfW7+UHva8PGYgQ1QpHlKgR",594+ "note": "invalid MAC"595+ },596+ {597+ "conversation_key": "5254827d29177622d40a7b67cad014fe7137700c3c523903ebbe3e1b74d40214",598+ "nonce": "7ab65dbb8bbc2b8e35cafb5745314e1f050325a864d11d0475ef75b3660d91c1",599+ "plaintext": "elliptic-curve cryptography",600+ "payload": "Anq2XbuLvCuONcr7V0UxTh8FAyWoZNEdBHXvdbNmDZHB573MI7R7rrTYftpqmvUpahmBC2sngmI14/L0HjOZ7lWGJlzdh6luiOnGPc46cGxf08MRC4CIuxx3i2Lm0KqgJ7vA",601+ "note": "invalid padding"602+ },603+ {604+ "conversation_key": "fea39aca9aa8340c3a78ae1f0902aa7e726946e4efcd7783379df8096029c496",605+ "nonce": "7d4283e3b54c885d6afee881f48e62f0a3f5d7a9e1cb71ccab594a7882c39330",606+ "plaintext": "noble",607+ "payload": "An1Cg+O1TIhdav7ogfSOYvCj9dep4ctxzKtZSniCw5MwRrrPJFyAQYZh5VpjC2QYzny5LIQ9v9lhqmZR4WBYRNJ0ognHVNMwiFV1SHpvUFT8HHZN/m/QarflbvDHAtO6pY16",608+ "note": "invalid padding"609+ },610+ {611+ "conversation_key": "0c4cffb7a6f7e706ec94b2e879f1fc54ff8de38d8db87e11787694d5392d5b3f",612+ "nonce": "6f9fd72667c273acd23ca6653711a708434474dd9eb15c3edb01ce9a95743e9b",613+ "plaintext": "censorship-resistant and global social network",614+ "payload": "Am+f1yZnwnOs0jymZTcRpwhDRHTdnrFcPtsBzpqVdD6b2NZDaNm/TPkZGr75kbB6tCSoq7YRcbPiNfJXNch3Tf+o9+zZTMxwjgX/nm3yDKR2kHQMBhVleCB9uPuljl40AJ8kXRD0gjw+aYRJFUMK9gCETZAjjmrsCM+nGRZ1FfNsHr6Z",615+ "note": "invalid padding"616+ },617+ {618+ "conversation_key": "5cd2d13b9e355aeb2452afbd3786870dbeecb9d355b12cb0a3b6e9da5744cd35",619+ "nonce": "b60036976a1ada277b948fd4caa065304b96964742b89d26f26a25263a5060bd",620+ "plaintext": "0",621+ "payload": "",622+ "note": "invalid payload length: 0"623+ },624+ {625+ "conversation_key": "d61d3f09c7dfe1c0be91af7109b60a7d9d498920c90cbba1e137320fdd938853",626+ "nonce": "1a29d02c8b4527745a2ccb38bfa45655deb37bc338ab9289d756354cea1fd07c",627+ "plaintext": "1",628+ "payload": "Ag==",629+ "note": "invalid payload length: 4"630+ },631+ {632+ "conversation_key": "873bb0fc665eb950a8e7d5971965539f6ebd645c83c08cd6a85aafbad0f0bc47",633+ "nonce": "c826d3c38e765ab8cc42060116cd1464b2a6ce01d33deba5dedfb48615306d4a",634+ "plaintext": "2",635+ "payload": "AqxgToSh3H7iLYRJjoWAM+vSv/Y1mgNlm6OWWjOYUClrFF8=",636+ "note": "invalid payload length: 48"637+ },638+ {639+ "conversation_key": "9f2fef8f5401ac33f74641b568a7a30bb19409c76ffdc5eae2db6b39d2617fbe",640+ "nonce": "9ff6484642545221624eaac7b9ea27133a4cc2356682a6033aceeef043549861",641+ "plaintext": "3",642+ "payload": "Ap/2SEZCVFIhYk6qx7nqJxM6TMI1ZoKmAzrO7vBDVJhhuZXWiM20i/tIsbjT0KxkJs2MZjh1oXNYMO9ggfk7i47WQA==",643+ "note": "invalid payload length: 92"644+ }645+ ]646+ }647+ }648+}