commit 496c35cb051a4bb65a69b6aa2d5a81dedfe47a74 Author: Laan Tungir Date: Thu Jul 16 20:36:25 2026 -0400 first diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..0f67229 --- /dev/null +++ b/.gitignore @@ -0,0 +1,22 @@ +# Build artifacts +build/ +*.o +*.so +*.a +*.dylib + +# Node.js +node_modules/ +package-lock.json + +# Source maps +*.bundle.js.map + +# IDE +.vscode/ +.idea/ +*.swp +*.swo +*~ + +# OS diff --git a/README.md b/README.md new file mode 100644 index 0000000..6719c84 --- /dev/null +++ b/README.md @@ -0,0 +1,702 @@ +# Post-Quantum Nostr + +A migration strategy for bringing post-quantum security to Nostr without breaking the social graph, without requiring consensus on a single post-quantum algorithm, and without forcing existing users to abandon their identities. + +## Table of Contents + +- [The Problem](#the-problem) +- [Why Bitcoin's Hash-the-Pubkey Trick Doesn't Work for Nostr](#why-bitcoins-hash-the-pubkey-trick-doesnt-work-for-nostr) +- [The Strategy: Overview](#the-strategy-overview) +- [Component 1: Seed Phrase as Algorithm-Agnostic Root of Trust](#component-1-seed-phrase-as-algorithm-agnostic-root-of-trust) +- [Component 2: Multi-Scheme Cross-Signed Key-Link Events](#component-2-multi-scheme-cross-signed-key-link-events) +- [Component 3: OpenTimestamps for Pre-Quantum Anchoring](#component-3-opentimestamps-for-pre-quantum-anchoring) +- [Component 4: Migrating Existing Users with Raw nsec](#component-4-migrating-existing-users-with-raw-nsec) +- [Component 5: Quantum-Safe Self-Storage](#component-5-quantum-safe-self-storage) +- [Component 6: Deterministic Wallet Compartmentalization](#component-6-deterministic-wallet-compartmentalization) +- [The Complete Migration Flow](#the-complete-migration-flow) +- [What Remains Unsolved](#what-remains-unsolved) +- [Implementation Status](#implementation-status) + +--- + +## The Problem + +Nostr's cryptography is built entirely on secp256k1: + +- **Identity**: Your pubkey IS your identity — it's in every event, it's your npub, it's what filters use, it's what NIP-05 verifies, it's what the social graph is built on. +- **Authentication**: Every event is signed with a Schnorr signature over secp256k1. Signature verification requires the public key, which is published in plaintext on every event. +- **Encryption (NIP-04)**: Uses secp256k1 ECDH to derive a shared secret, then AES-256-CBC. The shared X coordinate is used directly as the AES key. The IV is published in plaintext. +- **Encryption (NIP-44)**: Uses secp256k1 ECDH to derive a shared secret, then HKDF → ChaCha20 + HMAC-SHA256. The nonce is published in plaintext inside the payload. + +**Shor's algorithm** breaks the elliptic curve discrete logarithm problem on secp256k1 in polynomial time. With a sufficiently large fault-tolerant quantum computer, an attacker who has recorded any Nostr event can: + +1. Extract the public key from the event. +2. Run Shor's algorithm to recover the private key. +3. Forge signatures, decrypt all NIP-04/NIP-44 messages, and impersonate the user. + +### NIP-04 vs NIP-44: No Quantum Advantage + +NIP-44 is a substantial classical-security upgrade over NIP-04 (authenticated encryption, key separation, per-message keys, better cipher, length-hiding padding), but it offers **zero** post-quantum security advantage. Both derive their shared secret from secp256k1 ECDH. Everything NIP-44 adds (HKDF, per-message nonces, ChaCha20, HMAC-SHA256, custom padding) sits downstream of the broken ECDH step. + +The NIP-44 spec itself acknowledges this: + +> No post-quantum security: a powerful quantum computer would be able to decrypt the messages + +The symmetric primitives in both NIPs (AES-256, ChaCha20, HMAC-SHA256, HKDF-SHA256) are adequately post-quantum (~128-bit PQ security via Grover's quadratic speedup). The weakness is exclusively the secp256k1 ECDH key agreement, which has zero post-quantum security in both NIPs. + +--- + +## Why Bitcoin's Hash-the-Pubkey Trick Doesn't Work for Nostr + +Bitcoin has limited quantum resistance because addresses are hashes of pubkeys (RIPEMD160(SHA256(pubkey))), and pubkeys are only revealed at spend time. This works because of three properties that Nostr does not have: + +| Property | Bitcoin | Nostr | +|---|---|---| +| Identifier | Address (hash of pubkey) | Pubkey directly | +| Pubkey revealed | At spend time only | On every event | +| After reveal | UTXO consumed, key discarded | Identity persists, key reused | + +In Nostr, the pubkey is load-bearing in ways it isn't in Bitcoin: + +1. **The pubkey IS the identity.** It's in every event, it's your npub, it's what filters use, it's what NIP-05 verifies, it's what the social graph is built on. +2. **The pubkey must be revealed for every event.** Schnorr signature verification requires the public key. You can't verify a signature with a hash of the pubkey. +3. **Identity is never consumed.** You publish thousands of events over years with the same pubkey. There's no "consume and rotate" moment. + +If Nostr published hash(pubkey) instead, you'd get quantum resistance for exactly **zero seconds** — the time between key generation and your first event publication, at which point the pubkey must be revealed for signature verification. + +The only ways to avoid revealing the pubkey (ZK-SNARKs, giving up public verifiability, one-time keys per event) are impractical or fundamentally change what Nostr is. + +**The real fix is post-quantum signatures and post-quantum key agreement, not hashing the pubkey.** This document describes how to get there without breaking Nostr. + +--- + +## The Strategy: Overview + +```mermaid +flowchart TD + subgraph SECURITY["Security Layers - All Quantum-Resistant"] + L1[Layer 1: Seed phrase - PQ-safe root of trust] + L2[Layer 2: Key-link event + OTS - PQ-safe identity migration] + L3[Layer 3: PQ signatures - PQ-safe future authentication] + L4[Layer 4: OTS on important events - PQ-safe historical provenance] + L5[Layer 5: OTP or PQ encryption - PQ-safe confidentiality] + end + + L1 --> L2 --> L3 + L1 --> L5 + L2 --> L4 +``` + +The strategy uses existing Nostr infrastructure (NIP-06, NIP-03) plus one new event type (a key-link NIP). No new cryptographic primitives are needed for the migration itself — PQ algorithms are used additively, and the community doesn't need to agree on which one to use. + +### Design Principles + +1. **No social graph loss** — existing follows and identity carry forward +2. **No user re-onboarding** — the seed phrase becomes the new root of trust +3. **Backward compatible** — legacy clients continue working; PQ is additive +4. **No algorithm consensus required** — link to all viable PQ schemes now, converge later +5. **Pre-quantum timestamping** — establish links now, before quantum computers exist +6. **Transparent to users** — the client handles everything at seed-phrase login + +--- + +## Component 1: Seed Phrase as Algorithm-Agnostic Root of Trust + +The BIP39 seed is a 64-byte entropy string derived from a mnemonic via PBKDF2-HMAC-SHA512 (2048 iterations). This is a symmetric KDF — no public-key cryptography involved. A quantum computer provides no advantage beyond Grover's quadratic speedup. + +**The seed is algorithm-agnostic.** It's just entropy. You can derive any key type from it: + +```mermaid +flowchart TD + SEED[BIP39 Seed - 64 bytes, quantum-safe] + SEED --> BIP32[BIP32 derivation - secp256k1 keypair] + SEED --> PQKEM[HKDF derivation - ML-KEM keypair] + SEED --> PQDSA[HKDF derivation - ML-DSA keypair] + SEED --> SLHDSA[HKDF derivation - SLH-DSA keypair] + SEED --> SYMKEY[HKDF derivation - symmetric encryption key] + + style SEED fill:#cfc + style BIP32 fill:#fcc + style PQKEM fill:#cfc + style PQDSA fill:#cfc + style SLHDSA fill:#cfc + style SYMKEY fill:#cfc +``` + +### PQ Key Derivation from BIP39 Seeds + +BIP32 is specific to secp256k1. For PQ keys, use HKDF-SHA256 (quantum-resistant) to derive key material from the BIP39 seed: + +``` +For each PQ algorithm A: + pq_seed_A = HKDF-Extract(salt="nostr-pq-v1", ikm=bip39_seed) + pq_key_material_A = HKDF-Expand(pq_seed_A, info=A.identifier, L=A.key_size) + pq_keypair_A = A.keygen(pq_key_material_A) +``` + +The identifier string (`"ml-dsa-65"`, `"slh-dsa-128s"`, `"ml-kem-768"`, etc.) is the only algorithm-specific part. New algorithms can be added later without changing the derivation scheme. + +### Seed Phrase Entropy Considerations + +| Mnemonic length | Entropy | Post-quantum security (Grover's) | Assessment | +|---|---|---|---| +| 12 words | 128 bits | ~64 bits | Borderline | +| 24 words | 256 bits | ~128 bits | Adequate | + +For users concerned about quantum attacks on the seed entropy itself, 24-word mnemonics are recommended. The bigger risk to seed phrases is classical (theft, phishing), not quantum. + +--- + +## Component 2: Multi-Scheme Cross-Signed Key-Link Events + +This is the core innovation. Instead of choosing one PQ algorithm, **cross-sign with all viable PQ schemes now.** Let consensus emerge later. + +### The Standardization Uncertainty Problem + +The PQ standardization landscape is still evolving: + +| Scheme | NIST Status | Concern | +|---|---|---| +| ML-DSA (Dilithium) | FIPS 204, finalized 2024 | Lattice-based — could have undiscovered structural weaknesses | +| SLH-DSA (SPHINCS+) | FIPS 205, finalized 2024 | Hash-based, very conservative, but huge signatures (8-50 KB) | +| FN-DSA (Falcon) | FIPS 206, still draft | Compact but complex FFT, implementation concerns | +| ML-KEM (Kyber) | FIPS 203, finalized 2024 | Lattice-based — same class as Dilithium | + +History gives reason to be cautious: SIKE was a NIST PQ finalist that was broken in 2022 using a classical algorithm that ran in an hour on a laptop. If the Nostr community had picked SIKE early, we'd be back to square one. + +### The Multi-Scheme Solution + +```mermaid +flowchart TD + SEED[BIP39 Seed] --> SECP[secp256k1 keypair] + SEED --> MLDSA[ML-DSA keypair] + SEED --> SLHDSA[SLH-DSA keypair] + SEED --> FALCON[FN-DSA keypair] + SEED --> MLKEM[ML-KEM keypair] + + SECP --> SIGN[Schnorr sign] + MLDSA --> SIGN2[ML-DSA sign] + SLHDSA --> SIGN3[SLH-DSA sign] + FALCON --> SIGN4[FN-DSA sign] + + SIGN --> EVENT[Key-Link Event] + SIGN2 --> EVENT + SIGN3 --> EVENT + SIGN4 --> EVENT + + EVENT --> OTS[OpenTimestamp via NIP-03] + OTS --> RELAY[Published to relays NOW] + + RELAY --> CLIENT1[PQ-aware client - picks ML-DSA] + RELAY --> CLIENT2[PQ-aware client - picks SLH-DSA] + RELAY --> CLIENT3[Legacy client - ignores all PQ keys] + + style EVENT fill:#ffa + style OTS fill:#cfc +``` + +### Key-Link Event Format + +A new event kind (to be assigned) containing multiple cross-signed links: + +```json +{ + "kind": , + "pubkey": "", + "content": { + "links": [ + { + "algorithm": "ml-dsa-65", + "public_key": "", + "signature": "" + }, + { + "algorithm": "slh-dsa-128s", + "public_key": "", + "signature": "" + }, + { + "algorithm": "fn-dsa-512", + "public_key": "", + "signature": "" + }, + { + "algorithm": "ml-kem-768", + "public_key": "", + "note": "KEM key for encryption; ownership asserted by secp256k1 signature over this content" + } + ], + "statement": "All keys in this event are derived from the same seed and controlled by the same entity as secp256k1 key . This link is established pre-quantum.", + "secp256k1_signature": "" + } +} +``` + +Each PQ signature scheme independently signs the same link statement. The secp256k1 Schnorr signature covers the entire content (including all PQ public keys and signatures), binding them together. + +### ML-KEM Cannot Sign + +ML-KEM (Kyber) is a KEM (Key Encapsulation Mechanism), not a signature scheme. It cannot sign the link statement. Instead, its binding is proven indirectly: the secp256k1 signature covers the ML-KEM public key in the content, and the ML-KEM key is derived from the same seed. The proof is: "the entity that controls the secp256k1 key (proven by Schnorr signature) asserts that this ML-KEM key is also theirs." + +This is weaker than a cross-signature but sufficient because: +- The secp256k1 signature is valid NOW (pre-quantum), establishing the link while we can trust secp256k1 +- The ML-KEM key is used for encryption, not authentication — a false claim of ownership would just mean someone can't decrypt messages sent to you, which is self-correcting + +### Why This Is Better Than Picking One Scheme + +| Concern | Pick-one approach | Multi-scheme approach | +|---|---|---| +| Scheme gets broken later | Migration fails, need new link | Other links still valid | +| Standardization changes | May need to re-link | Already covered | +| Consensus not reached | Can't proceed | Proceed immediately | +| Event size | Small | Larger (~20-30 KB), but one-time cost | +| Client complexity | Support one scheme | Support any subset | +| Race condition risk | Must publish before quantum | Published now, all schemes | + +Event size is a non-issue because this is a **one-time publication**, not per-event overhead. + +### Revocation: If a Scheme Is Broken Later + +If a specific PQ scheme is later found to be weak (like SIKE was): + +1. **Clients simply stop trusting the broken scheme** — no revocation event needed; clients just ignore links to algorithms on a known-broken list +2. The remaining links are still valid +3. Optionally, publish a new key-link event without the broken scheme, signed by the remaining valid PQ keys + +--- + +## Component 3: OpenTimestamps for Pre-Quantum Anchoring + +NIP-03 (OpenTimestamps Attestations for Events) already exists and is implemented in nostr_core_lib. It anchors Nostr event hashes to the Bitcoin blockchain via OpenTimestamps. + +### Why Bitcoin Timestamping Is Quantum-Resistant + +The Bitcoin proof-of-work chain is quantum-resistant — not because Bitcoin's signatures are PQ-safe (they're not for revealed pubkeys), but because **re-mining historical blocks is infeasible even with a quantum computer.** Grover's algorithm gives only a quadratic speedup on mining, which doesn't help retroactively alter past blocks. + +An attacker who breaks your secp256k1 key via Shor's can forge new events but **cannot backdate an event to before a specific Bitcoin block** — they'd need to produce a valid OTS proof anchoring it to a past block, which requires either: +- Re-mining that Bitcoin block (impossible, even with a quantum computer, due to cumulative proof-of-work) +- Finding a SHA256 preimage for the event hash (still infeasible with Grover's quadratic speedup on 256-bit hashes) + +### The Critical Refinement: OpenTimestamp the Key-Link Event + +The key-link event itself MUST be timestamped via NIP-03. This prevents a race condition attack: + +**Without OTS on the key-link event:** +- Attacker breaks secp256k1 key via Shor's +- Attacker publishes a fraudulent key-link event linking your secp256k1 key to their PQ key +- Both real and fraudulent events have valid secp256k1 signatures (key is compromised) +- Clients can't distinguish them — `created_at` is forgeable + +**With OTS on the key-link event:** +- Your real key-link event is anchored to Bitcoin block N (before quantum computers) +- Attacker's fraudulent event is published later and cannot produce an OTS proof for block N or earlier +- Clients verify: "the key-link event with the earliest valid OTS proof wins" +- **The real key-link is cryptographically distinguishable from the fraudulent one** + +### What Should Be OpenTimestamped + +Not every event needs OTS. Priority list: + +1. **Key-link events** — absolutely critical, foundation of PQ migration +2. **Identity-defining events** — NIP-05 verification, profile events (kind 0) +3. **Important content** — contracts, announcements, evidence +4. **High-value events** — financial events (NIP-60 Cashu wallets, zaps) + +Routine events (regular posts, reactions) probably don't need OTS — the impact of retroactive forgery is low. + +--- + +## Component 4: Migrating Existing Users with Raw nsec + +Most Nostr users today have a raw nsec (random private key) with no seed phrase. Asking them to abandon their identity and social graph to adopt a seed phrase is a non-starter. This component addresses how to migrate them. + +### The Challenge + +A secp256k1 private key is 32 bytes (256 bits). BIP39 encodes 11 bits per word. Encoding the old nsec requires 24 extra words. The old nsec was generated randomly — it cannot be "derived" from a seed (that's a preimage problem). It must be **encoded** (directly or encrypted). + +### Approach A: Extended Phrase (36 words) + +```mermaid +flowchart LR + subgraph CREATION["One-time migration"] + OLD[Old nsec - 32 bytes] + NEW[Generate 12-word BIP39 phrase] + NEW --> SEED[BIP39 seed] + SEED --> ENCKEY[Derive encryption key via HKDF] + ENCKEY --> ENC[Encrypt old nsec: ChaCha20] + OLD --> ENC + ENC --> CIPHER[32 bytes encrypted nsec] + CIPHER --> WORDS[Encode as 24 BIP39 words + checksum] + NEW --> PHRASE[Full phrase: 12 + 24 = 36 words] + WORDS --> PHRASE + end + + subgraph RECOVERY["Recovery with full phrase"] + FULL[36 words entered] + FULL --> FIRST[First 12 words] + FULL --> LAST[Last 24 words] + FIRST --> SEED2[BIP39 seed] + SEED2 --> ENCKEY2[Derive encryption key] + LAST --> CIPHER2[Decode to 32 bytes] + ENCKEY2 --> DEC[Decrypt] + CIPHER2 --> DEC + DEC --> OLDRECOV[Old nsec recovered] + SEED2 --> PQKEYS[PQ keys derived] + end + + subgraph FUTURE["After migration - drop old key"] + FIRST12[First 12 words only] + FIRST12 --> SEED3[BIP39 seed] + SEED3 --> PQKEYS2[PQ keys only] + end +``` + +**Design:** +1. Generate a standard 12-word BIP39 phrase (128 bits entropy) +2. Derive a 32-byte encryption key from the BIP39 seed: `enc_key = HKDF-Extract(salt="nostr-legacy-key", ikm=bip39_seed)` +3. Encrypt the old nsec: `encrypted_nsec = ChaCha20(enc_key, nonce=0, old_nsec)` → 32 bytes +4. Encode the 32-byte encrypted nsec as 24 BIP39 words (with its own checksum) +5. Full phrase: 12 words (new seed) + 24 words (encrypted old nsec) = 36 words + +**Recovery with 36 words:** First 12 → BIP39 seed → PQ keys + encryption key. Last 24 → decode → decrypt → old nsec. Both keys recovered. + +**Recovery with 12 words (after migration):** First 12 → PQ keys only. Old nsec no longer needed. + +**Why encrypt the old nsec?** The extra 24 words are meaningless without the first 12 (partial compromise protection). If someone steals only the last 24 words: useless. If someone steals only the first 12 words: they get PQ keys but not the old nsec. + +### Approach B: 12 Words + Relay Backup (Recommended) + +```mermaid +flowchart TD + subgraph SETUP["One-time migration"] + OLD[Old nsec] + NEW[Generate 12-word BIP39 phrase] + NEW --> SEED[BIP39 seed] + SEED --> NEWSECP[New secp256k1 key via NIP-06] + SEED --> PQKEYS[PQ keys via HKDF] + SEED --> ENCKEY[Symmetric encryption key via HKDF] + ENCKEY --> ENC[Encrypt old nsec] + OLD --> ENC + ENC --> EVENT30078[kind 30078: encrypted old nsec] + NEWSECP --> SIGN1[Sign event with new secp256k1 key] + SIGN1 --> EVENT30078 + EVENT30078 --> RELAY[Publish to relays] + + OLD --> KEYLINK[Key-link event: cross-signed] + NEWSECP --> KEYLINK + PQKEYS --> KEYLINK + KEYLINK --> OTS[OpenTimestamp via NIP-03] + OTS --> BTC[Anchored in Bitcoin] + KEYLINK --> RELAY + end + + subgraph RECOVER["Recovery from 12 words only"] + WORDS[12 words entered] + WORDS --> SEED2[BIP39 seed] + SEED2 --> NEWSECP2[New secp256k1 key] + SEED2 --> PQKEYS2[PQ keys] + SEED2 --> ENCKEY2[Encryption key] + NEWSECP2 --> FETCH[Fetch kind 30078 from relays] + FETCH --> CIPHER[Encrypted old nsec] + ENCKEY2 --> DEC[Decrypt] + CIPHER --> DEC + DEC --> OLDRECOV[Old nsec recovered] + end +``` + +**Design:** +1. Generate a 12-word BIP39 phrase +2. Derive from seed: new secp256k1 key (NIP-06 path), PQ keys (HKDF), symmetric encryption key (HKDF) +3. Encrypt old nsec with the symmetric key: `encrypted_nsec = ChaCha20-Poly1305(enc_key, old_nsec)` +4. Publish as kind 30078 event, signed with the **new** secp256k1 key +5. Publish key-link event (cross-signed, OpenTimestamped via NIP-03) +6. User backs up only 12 words + +**Recovery:** 12 words → derive new key + encryption key → fetch kind 30078 from relays → decrypt → old nsec. Both keys recovered from 12 words + relay access. + +**No circular dependency:** The kind 30078 event is found by the new pubkey (derived from the 12-word seed). The new secp256k1 key is the "storage identity." The old nsec is just data inside the event. + +### Approach C: Hybrid (Best of Both) + +- Primary backup: 12-word phrase (PQ keys + relay recovery of old nsec) +- Optional fallback: also write down 24 extra words (encrypted old nsec) in case relays are unavailable +- If relays available: 12 words suffice +- If relays down: 12 + 24 = 36 words recover everything + +### Comparison + +| Property | 36-word phrase | 12-word + relay backup | Hybrid | +|---|---|---|---| +| Backup size | 36 words | 12 words | 12 or 36 words | +| Relay dependency | None | Yes | Optional | +| Custom format | Yes | No | Optional | +| Self-contained | Yes | No | Yes (with 36) | +| Quantum-safe | Yes | Yes | Yes | + +--- + +## Component 5: Quantum-Safe Self-Storage + +For self-storage use cases (e.g., kind 30078 application data), NIP-04 and NIP-44 are unnecessary and quantum-vulnerable. Two quantum-safe alternatives: + +### Option A: One-Time Pad (Information-Theoretic Security) + +A one-time pad provides **information-theoretic security** — the strongest security guarantee in cryptography, stronger than any computational security (including post-quantum). It's a mathematical proof of perfect secrecy: no computer that could ever exist, in any universe governed by the laws of physics, can break it. + +**Requirements:** +- Pad must be truly random (hardware RNG recommended, e.g., TrueRNG) +- Pad must be as long as the data +- Pad must never be reused +- Pad must be stored securely (e.g., USB drive) + +**For self-storage, OTP is a natural fit** because you sidestep OTP's biggest limitation (key distribution between parties) — you're encrypting for yourself, so the pad lives on your device. + +**What a quantum attacker can and cannot do with OTP-encrypted kind 30078:** +- ❌ Read your stored data — OTP is information-theoretically secure +- ✅ Forge your signature — Shor's breaks secp256k1 (identity compromised) +- ✅ Impersonate you, delete/replace your events +- ❌ Decrypt your stored data — even with your private key, the data is protected by the pad + +This gives **clean separation**: quantum breakage of your secp256k1 key compromises your identity/authentication but NOT your stored data's confidentiality. + +**Critical warning: pad reuse is fatal.** If any pad section is used twice, XOR of the two ciphertexts equals XOR of the two plaintexts — the encryption breaks. Multi-device sync requires an atomic reservation protocol for pad sections. + +### Option B: Symmetric Key from Seed (Computational PQ Security) + +Derive a 256-bit symmetric key from the BIP39 seed (independent of secp256k1) and encrypt with ChaCha20-Poly1305 or AES-256-GCM: + +``` +storage_key = HKDF-Extract(salt="nostr-storage-v1", ikm=bip39_seed) +ciphertext = ChaCha20-Poly1305(storage_key, nonce, plaintext) +``` + +This is post-quantum because: +- HKDF-SHA256 is symmetric (~128-bit PQ security) +- ChaCha20 / AES-256 are symmetric (~128-bit PQ security) +- The secp256k1 key is never part of the encryption path + +**Tradeoff vs OTP:** Simpler (no pad management, no USB drive), but computationally secure rather than information-theoretically secure. Adequate for most use cases. + +### Comparison + +| Property | OTP | Symmetric key from seed | +|---|---|---| +| Security level | Information-theoretic (perfect secrecy) | Computational, ~128-bit PQ | +| Key material | Pad as long as all data | Single 256-bit key | +| Storage overhead | Pad on USB drive | Just the seed phrase | +| Quantum resistance | Unconditional | Conditional (Grover's is optimal) | +| Future-proofing | Permanent | Could theoretically fall to new quantum algorithms | +| Multi-device | Requires pad sync + offset coordination | Just share the seed | +| Practicality | More complex | Simpler | + +--- + +## Component 6: Deterministic Wallet Compartmentalization + +BIP32/BIP39 deterministic wallets (NIP-06) provide meaningful quantum compartmentalization through their tree structure. + +### The Core Protection: Chain Codes Are Secret + +When Shor's algorithm recovers a leaf private key from its published public key, the attacker gets only that key. To climb the tree (recover the parent private key), they need the parent chain code: + +``` +parent_private_key = child_private_key - IL mod n +where IL = HMAC-SHA512(parent_chain_code, parent_public_key || index)[:32] +``` + +The parent chain code is NOT published in normal Nostr usage. HMAC-SHA512 is a symmetric primitive — a quantum computer provides no advantage. **Without chain codes, the attacker is stuck at the leaf level.** + +### The Firewall Effect of Hardened Derivation + +The NIP-06 path `m/44'/1237'/account'/0/0` has hardened derivation at three levels. This means: +- Breaking all keys in Account 0 does NOT compromise Account 1 (hardened at `account'` level) +- The seed/master key remains secure even if all account keys are broken +- Hardened derivation requires the parent **private** key, not just the public key + +### The Critical Vulnerability: Chain Code Leakage + +If an attacker obtains a chain code at any level AND breaks one child key at that level via Shor's, they can: +1. Compute `IL` using the chain code + child public key + index +2. Recover the parent private key +3. Derive ALL children at that level +4. Continue climbing if parent chain codes are also known + +**Where chain codes leak:** published xpubs (extended public keys), wallet software that exports xpubs, backup exposure. In standard Nostr usage, chain codes are not published. + +### Summary + +| Scenario | Compromised? | +|---|---| +| One leaf key broken via Shor's | Only that key | +| Multiple leaf keys from same account broken | Only those keys | +| Leaf key broken + chain code at that level known | Parent + all siblings | +| Account key broken + chain code at account level known | Parent + all accounts | +| Seed phrase stolen (classical) | Everything | +| Seed safe, quantum attacker, no chain codes leaked | Only published keys | + +### Recommendations + +1. Use separate accounts for separate Nostr identities — hardened derivation contains the damage +2. Never publish xpubs — chain codes are the keys to the kingdom +3. Use 24-word mnemonics for adequate PQ security on the seed entropy +4. Don't reuse keys across services — each identity gets its own account index +5. Assume published keys WILL be broken — plan for containment, not prevention + +--- + +## The Complete Migration Flow + +```mermaid +flowchart TD + subgraph NOW["Pre-quantum - NOW"] + USER[User with raw nsec OR seed phrase] + USER --> MIGRATE[Client: Enable PQ migration] + MIGRATE --> SEED[Generate or use 12-word BIP39 phrase] + SEED --> DERIVE[Derive: new secp256k1 + all PQ keys + storage key] + DERIVE --> ENCRYPT[Encrypt old nsec if migrating from raw nsec] + ENCRYPT --> STORE30078[Publish kind 30078: encrypted old nsec] + DERIVE --> KEYLINK[Publish key-link event: cross-signed by all PQ schemes] + KEYLINK --> OTS[OpenTimestamp the key-link event via NIP-03] + OTS --> BTC[Anchored in Bitcoin block] + KEYLINK --> RELAY[Published to relays] + STORE30078 --> RELAY + SEED --> BACKUP[User writes down 12-word phrase] + end + + subgraph TRANSITION["Transition period - dual signing"] + DUAL[Client signs events with both secp256k1 and PQ keys] + DUAL --> LEGACY[Legacy clients verify secp256k1 signature] + DUAL --> PQAWARE[PQ-aware clients verify PQ signature] + end + + subgraph FUTURE["Post-quantum - when quantum computers arrive"] + ATTACK[Attacker breaks secp256k1 via Shors] + ATTACK --> FORGE[Can forge secp256k1 signatures] + ATTACK --> CANNOT1[Cannot forge PQ signatures] + ATTACK --> CANNOT2[Cannot backdate events - OTS proofs anchor history] + ATTACK --> CANNOT3[Cannot break OTP-encrypted storage] + + PQAWARE --> VERIFY[Verify key-link event via OTS] + VERIFY --> USEPQ[Switch to PQ signatures] + USEPQ --> REJECT[Reject forged secp256k1-only events] + end + + subgraph RETIRE["Old key retirement"] + DROP[User drops extra backup, keeps 12 words only] + DROP --> PQONLY[Client uses PQ keys only] + PQONLY --> DONE[Migration complete] + end + + NOW --> TRANSITION --> FUTURE --> RETIRE + + style NOW fill:#cfc + style TRANSITION fill:#ffa + style FUTURE fill:#fcc + style RETIRE fill:#cfc +``` + +### User Experience + +1. User clicks "Enable quantum-safe migration" in their client +2. Client generates a 12-word phrase, shows it to the user to write down +3. Client handles everything else automatically: + - Derives new secp256k1 key + all PQ keys from seed + - Encrypts old nsec and publishes as kind 30078 (if migrating from raw nsec) + - Publishes cross-signed key-link event + - OpenTimestamps the key-link event via NIP-03 +4. During transition: client signs with both old and new keys +5. After quantum migration: client uses PQ keys only, old key retired +6. User drops extra backup, keeps 12 words + +--- + +## What Remains Unsolved + +### Historical Event Authenticity (Partially Solved) + +Past secp256k1-signed events that were NOT OpenTimestamped remain forgeable after quantum break. An attacker can create fake old events that appear to be from you. + +**Mitigation:** OpenTimestamp important events now (NIP-03). Events with valid OTS proofs are cryptographically anchored to a pre-quantum timestamp and cannot be forged retroactively. Events without OTS proofs become untrustworthy after quantum break. + +**Not fully solved:** Routine events (regular posts, reactions) that aren't worth OpenTimestamping will become forgeable. The impact is low for most content, but high for events used as evidence, contracts, or historical records. + +### Relay Trust for Recovery + +Approach B (12 words + relay backup) depends on relays retaining the kind 30078 event containing the encrypted old nsec. If all relays delete it, the old nsec is lost. + +**Mitigations:** +- Publish to multiple relays +- Use relays you control or trust +- Use the hybrid approach (Approach C) with 36-word fallback +- The event is small and signed, so it's easy to preserve + +### PQ Signature Size + +PQ signatures are much larger than secp256k1 Schnorr signatures: + +| Scheme | Signature size | +|---|---| +| secp256k1 Schnorr | 64 bytes | +| ML-DSA-65 | ~3.3 KB | +| SLH-DSA-128s | ~8 KB | +| FN-DSA-512 | ~0.7 KB | + +This increases event sizes during the transition period (dual signing). After migration, events can be signed with PQ only. FN-DSA (Falcon) offers the most compact PQ signatures. + +### PQ Algorithm Risk + +Any PQ algorithm could theoretically be broken by a new classical or quantum algorithm (as SIKE was). The multi-scheme approach hedges against this — if one scheme falls, the others remain linked. But if ALL lattice-based schemes are broken simultaneously, only SLH-DSA (hash-based, very conservative) remains as a fallback. + +--- + +## Implementation Status + +This is a design document. The following building blocks already exist: + +### Existing Infrastructure + +| Component | Status | Location | +|---|---|---| +| NIP-06 (key derivation from mnemonic) | Implemented | nostr_core_lib: `nip006.c` | +| NIP-03 (OpenTimestamps) | Implemented | nostr_core_lib: `nip003.c` | +| NIP-44 (encrypted payloads) | Implemented | nostr_core_lib: `nip044.c` | +| NIP-04 (encrypted DMs, deprecated) | Implemented | nostr_core_lib: `nip004.c` | +| OTP cipher (information-theoretic encryption) | Implemented | ~/lt/otp | + +### To Be Built + +| Component | Description | +|---|---| +| Key-link NIP | New NIP defining the cross-signed key-link event format | +| PQ key derivation from BIP39 seeds | HKDF-based derivation of ML-DSA, SLH-DSA, FN-DSA, ML-KEM keys | +| PQ signature support | Integration of liboqs or similar for PQ signature verification | +| Migration tooling | Client-side tooling for migrating raw nsec users to seed phrases | +| Kind 30078 encrypted storage | Symmetric-key or OTP encryption for self-storage | +| Extended phrase encoding | 36-word phrase format (Approach A/C) | + +### Dependencies + +- [liboqs](https://github.com/open-quantum-safe/liboqs) — Open Quantum Safe library for PQ algorithms +- [OpenTimestamps](https://opentimestamps.org/) — already integrated via NIP-03 +- BIP39/BIP32 — already integrated via NIP-06 + +--- + +## References + +- [NIP-03: OpenTimestamps Attestations for Events](https://github.com/nostr-protocol/nips/blob/master/03.md) +- [NIP-06: Basic key derivation from mnemonic seed phrase](https://github.com/nostr-protocol/nips/blob/master/06.md) +- [NIP-44: Encrypted Payloads (Versioned)](https://github.com/nostr-protocol/nips/blob/master/44.md) +- [BIP39: Mnemonic code for generating deterministic keys](https://github.com/bitcoin/bips/blob/master/bip-0039.mediawiki) +- [BIP32: Hierarchical Deterministic Wallets](https://github.com/bitcoin/bips/blob/master/bip-0032.mediawiki) +- [NIST FIPS 203: ML-KEM (Kyber)](https://csrc.nist.gov/pubs/fips/203/final) +- [NIST FIPS 204: ML-DSA (Dilithium)](https://csrc.nist.gov/pubs/fips/204/final) +- [NIST FIPS 205: SLH-DSA (SPHINCS+)](https://csrc.nist.gov/pubs/fips/205/final) +- [OpenTimestamps](https://opentimestamps.org/) +- [Open Quantum Safe](https://openquantumsafe.org/) + +--- + +## License + +This document is released into the public domain. The migration strategy described herein is intended as a community resource for the Nostr ecosystem. diff --git a/Rube_Goldberg's__Self-Operating_Napkin__(cropped).gif.ots b/Rube_Goldberg's__Self-Operating_Napkin__(cropped).gif.ots new file mode 100644 index 0000000..5bb4f11 Binary files /dev/null and b/Rube_Goldberg's__Self-Operating_Napkin__(cropped).gif.ots differ diff --git a/build-pq-bundle.js b/build-pq-bundle.js new file mode 100644 index 0000000..798c84a --- /dev/null +++ b/build-pq-bundle.js @@ -0,0 +1,36 @@ +/** + * Build script for the post-quantum crypto bundle. + * Bundles pq-crypto.mjs and all its dependencies into a single + * ESM file that can be loaded directly in the browser. + * + * Usage: node build-pq-bundle.js + */ + +const esbuild = require('esbuild'); +const path = require('path'); + +async function build() { + console.log('🔧 Building PQ crypto bundle...'); + + await esbuild.build({ + entryPoints: ['www/js/pq-crypto.mjs'], + bundle: true, + format: 'esm', + target: ['es2020'], + outfile: 'www/pq-crypto.bundle.js', + sourcemap: true, + minify: false, // keep readable for demo + logLevel: 'info', + // Pure JS — no WASM files to handle + define: { + 'process.env.NODE_ENV': '"production"' + } + }); + + console.log('✅ PQ crypto bundle built: www/pq-crypto.bundle.js'); +} + +build().catch(err => { + console.error('❌ Build failed:', err); + process.exit(1); +}); diff --git a/explanation.md b/explanation.md new file mode 100644 index 0000000..00f64db --- /dev/null +++ b/explanation.md @@ -0,0 +1,273 @@ +# How the NIP-QR Event Is Constructed: Step by Step + +## The Misconception + +The seed phrase is NOT used directly as the private key for PQ signing. The seed phrase is a source of entropy that is used to **derive** separate keys for each PQ algorithm via **BIP32 HD wallet derivation paths** — the same standard used by NIP-06 for secp256k1 keys. + +Here's the exact flow: + +--- + +## Step 1: Generate a new seed phrase + +``` +random entropy (128 bits) → BIP39 mnemonic (12 words) +``` + +Example: `apple garden forest ocean desert meadow river bridge mountain sunset valley horizon` + +This is just 128 bits of random entropy encoded as 12 words. It contains no cryptographic keys yet. It's algorithm-agnostic — it's just randomness. + +## Step 2: Convert the mnemonic to a BIP39 seed + +``` +mnemonic → PBKDF2-HMAC-SHA512(mnemonic, "mnemonic", 2048 iterations) → 64-byte seed +``` + +This is the standard BIP39 derivation. The 12 words become a 64-byte (512-bit) seed. This seed is still just entropy — it's not a key for any specific algorithm. + +## Step 3: Create the BIP32 master key + +``` +64-byte BIP39 seed → BIP32 master key (HMAC-SHA512(seed, "Bitcoin seed")) +``` + +This is the root of the HD wallet tree. From here, we derive all keys — both secp256k1 and PQ — down standard BIP32 paths. + +## Step 4: Derive all keys from BIP32 paths + +All keys are derived under the NIP-06 base path `m/44'/1237'/0'/0/`, using different child indices: + +| Child index | Algorithm | Seed length | BIP32 path | How derived | +|---|---|---|---|---| +| 0 | secp256k1 (NIP-06) | 32 bytes | `m/44'/1237'/0'/0/0` | Standard BIP32 — private key used directly | +| 1 | ML-DSA-44 | 32 bytes | `m/44'/1237'/0'/0/1` | Single child — 32-byte private key is the PQ seed | +| 2 | ML-DSA-65 | 32 bytes | `m/44'/1237'/0'/0/2` | Single child — 32-byte private key is the PQ seed | +| 3+4 | SLH-DSA-128s | 48 bytes | `m/44'/1237'/0'/0/3` + `m/44'/1237'/0'/0/4` | Two children concatenated (64 bytes), first 48 used | +| 5+6 | Falcon-512 | 48 bytes | `m/44'/1237'/0'/0/5` + `m/44'/1237'/0'/0/6` | Two children concatenated (64 bytes), first 48 used | +| 7+8 | ML-KEM-768 | 64 bytes | `m/44'/1237'/0'/0/7` + `m/44'/1237'/0'/0/8` | Two children concatenated (64 bytes) | + +### Why some algorithms need two children + +BIP32 child derivation produces exactly 32 bytes (the private key) per child. Some PQ algorithms need more than 32 bytes for their `keygen()` seed: + +- **ML-DSA-44** and **ML-DSA-65**: need 32 bytes — one child is enough +- **SLH-DSA-128s**: needs 48 bytes — derive two children (64 bytes), use the first 48 +- **Falcon-512**: needs 48 bytes — derive two children (64 bytes), use the first 48 +- **ML-KEM-768**: needs 64 bytes — derive two children and concatenate (64 bytes) + +### Why BIP32 and not HKDF? + +BIP32 is the standard for HD wallets. Using BIP32 paths gives us: + +1. **Consistency with NIP-06** — PQ keys are derived the same way as secp256k1 keys, just at different child indices +2. **Compartmentalization** — hardened derivation at the account level means breaking one key doesn't compromise siblings +3. **Wallet compatibility** — HD wallet infrastructure (Amber, hardware wallets, etc.) can derive these same paths +4. **Tree structure** — the derivation path documents exactly which key is where +5. **Security** — BIP32 uses HMAC-SHA512 (the same HMAC used everywhere in HD wallets), not a custom construction + +### What is HMAC? + +HMAC (Hash-based Message Authentication Code) is the cryptographic primitive that BIP32 is built on. Every BIP32 child key derivation is: + +``` +HMAC-SHA512(parent_chain_code, data) → 32-byte private key + 32-byte chain code +``` + +This is not a separate or weaker construction — it IS BIP32. Using BIP32 paths means we're using the same HMAC-based derivation that all HD wallets use. + +## Step 5: Generate PQ keypairs from the BIP32-derived seeds + +Each PQ algorithm's `keygen()` function takes the BIP32-derived seed and deterministically produces a keypair: + +``` +child 1 (32 bytes) → ML-DSA-44 keygen() → {publicKey: 1312 bytes, secretKey: 2560 bytes} +child 2 (32 bytes) → ML-DSA-65 keygen() → {publicKey: 1952 bytes, secretKey: 4032 bytes} +child 3+4 (48 bytes) → SLH-DSA-128s keygen() → {publicKey: 32 bytes, secretKey: 64 bytes} +child 5+6 (48 bytes) → Falcon-512 keygen() → {publicKey: 897 bytes, secretKey: 1281 bytes} +child 7+8 (64 bytes) → ML-KEM-768 keygen() → {publicKey: 1184 bytes, secretKey: 2400 bytes} +``` + +The `keygen(seed)` functions in `@noble/post-quantum` use the seed as a deterministic RNG to drive key generation. This means: + +- **Same seed phrase → same BIP32 master key → same child keys → same PQ keypairs** +- The PQ key generation is deterministic and reproducible +- If a user loses their PQ keys, they can re-derive them from the seed phrase + +## Step 6: Construct the link statement + +A text statement is created that binds all the keys together: + +``` +"Identity is migrating to successor . + All PQ keys listed below are derived from the same BIP39 seed as . + This link is established pre-quantum." +``` + +This statement is what gets signed by all the keys. + +## Step 7: Sign the statement with each PQ private key + +The statement (as bytes) is signed independently by each PQ signature scheme: + +``` +statement_bytes → ML-DSA-44 sign(statement_bytes, ml_dsa44_secretKey) → signature (2420 bytes) +statement_bytes → ML-DSA-65 sign(statement_bytes, ml_dsa65_secretKey) → signature (3309 bytes) +statement_bytes → SLH-DSA-128s sign(statement_bytes, slh_dsa_secretKey) → signature (7856 bytes) +statement_bytes → Falcon-512 sign(statement_bytes, falcon_secretKey) → signature (~666 bytes) +``` + +ML-KEM-768 does NOT sign — it's a KEM (Key Encapsulation Mechanism), not a signature scheme. Its public key is included in the event, and its ownership is proven by the secp256k1 signature covering it. + +## Step 8: Sign the statement with Account #2's secp256k1 key + +The successor's secp256k1 private key (derived at `m/44'/1237'/0'/0/0`) signs the statement + all PQ public keys: + +``` +statement + pq_public_keys → Schnorr sign(secp256k1_private_key) → successor_signature +``` + +This proves that the seed-derived secp256k1 key (Account #2) endorses the PQ keys. Since both Account #2's secp256k1 key and the PQ keys come from the same BIP32 tree (same seed), this transitively proves PQ key ownership. + +## Step 9: Build the NIP-QR event content + +All the pieces are assembled into a JSON object: + +```json +{ + "statement": "Identity is migrating to successor ...", + "successor_pubkey": "", + "successor_signature": "", + "pq_keys": [ + { + "algorithm": "ml-dsa-44", + "public_key": "", + "signature": "" + }, + { + "algorithm": "ml-dsa-65", + "public_key": "", + "signature": "" + }, + { + "algorithm": "slh-dsa-128s", + "public_key": "", + "signature": "" + }, + { + "algorithm": "falcon-512", + "public_key": "", + "signature": "" + }, + { + "algorithm": "ml-kem-768", + "public_key": "", + "note": "KEM key for encryption; ownership asserted by secp256k1 signature over this content" + } + ] +} +``` + +## Step 10: Build the Nostr event and sign with Account #1 + +The JSON content is placed into a standard Nostr event: + +```json +{ + "kind": 30078, + "pubkey": "", + "created_at": 1720780000, + "tags": [ + ["d", "nip-qr-migration"], + ["successor", ""], + ["algorithm", "ml-dsa-44"], + ["algorithm", "ml-dsa-65"], + ["algorithm", "slh-dsa-128s"], + ["algorithm", "falcon-512"], + ["algorithm", "ml-kem-768"] + ], + "content": "" +} +``` + +This unsigned event is sent to the user's signer (via `window.nostr.signEvent()`), which signs it with Account #1's secp256k1 private key — the user's existing Nostr identity. The signer returns the `id` and `sig` fields. + +## Step 11: Publish the complete event + +The fully signed event is published to Nostr relays. + +--- + +## Summary: Who Signs What + +| Signer | What they sign | Signature type | Purpose | +|---|---|---|---| +| ML-DSA-44 private key | The link statement | PQ signature (lattice, Cat 2) | Proves PQ key ownership post-quantum | +| ML-DSA-65 private key | The link statement | PQ signature (lattice, Cat 3) | Higher security level PQ signature | +| SLH-DSA-128s private key | The link statement | PQ signature (hash-based, Cat 1) | Conservative fallback if lattice schemes break | +| Falcon-512 private key | The link statement | PQ signature (lattice, Cat 1) | Compact signatures, compatible with trbouma's PR | +| Account #2 secp256k1 key | Statement + all PQ public keys | Schnorr signature | Proves seed-derived key endorses PQ keys | +| Account #1 secp256k1 key | The entire Nostr event | Schnorr signature | Old identity authorizes the migration | +| ML-KEM-768 | (nothing — KEM, not signature) | N/A | Ownership proven by Account #2's signature covering its public key | + +## Summary: BIP32 Key Derivation Tree + +``` +12-word mnemonic (128 bits entropy) + | + | PBKDF2-HMAC-SHA512 (2048 iterations) + v +64-byte BIP39 seed + | + | HMAC-SHA512 (BIP32 master key derivation) + v +BIP32 master key + | + | derive m/44'/1237'/0'/0/ (NIP-06 base path) + v +Account 0, change 0 + | + +--→ child 0 → secp256k1 keypair (Account #2, NIP-06) + | | + | v + | signs the link statement (Schnorr) + | + +--→ child 1 → 32-byte seed → ML-DSA-44 keypair + | | + | v + | signs the link statement (ML-DSA-44) + | + +--→ child 2 → 32-byte seed → ML-DSA-65 keypair + | | + | v + | signs the link statement (ML-DSA-65) + | + +--→ child 3+4 → 48-byte seed → SLH-DSA-128s keypair + | | + | v + | signs the link statement (SLH-DSA-128s) + | + +--→ child 5+6 → 48-byte seed → Falcon-512 keypair + | | + | v + | signs the link statement (Falcon-512) + | + +--→ child 7+8 → 64-byte seed → ML-KEM-768 keypair + | + v + (no signature — KEM, used for encryption) +``` + +The key insight: **the seed phrase is not a key — it's the root of a BIP32 HD wallet tree.** Each PQ algorithm gets its own child key at a specific derivation path, just like secp256k1 keys are derived at `m/44'/1237'/0'/0/0`. The same seed always produces the same tree, making all keys recoverable. + +## The 5 PQ Algorithms + +| Algorithm | FIPS | Type | Security level | Key size | Sig size | Why included | +|---|---|---|---|---|---|---| +| ML-DSA-44 | 204 | Signature | Cat 2 (~AES-128) | 1312 B | 2420 B | Used by trbouma's PR #2185 — compatibility | +| ML-DSA-65 | 204 | Signature | Cat 3 (~AES-192) | 1952 B | 3309 B | NIST-recommended minimum for new deployments | +| SLH-DSA-128s | 205 | Signature | Cat 1 (hash-based) | 32 B | 7856 B | Conservative fallback — if all lattice schemes break | +| Falcon-512 | 206 (draft) | Signature | Cat 1 (~AES-128) | 897 B | ~666 B | Used by trbouma's PR — compact signatures | +| ML-KEM-768 | 203 | KEM | Cat 3 (~AES-192) | 1184 B | N/A | PQ encryption — replaces ECDH in NIP-04/NIP-44 | + +We include all 5 because the multi-scheme design means we don't have to pick one. If any single algorithm is later broken, the others remain valid. This is the key advantage over approaches that pick a single algorithm. diff --git a/laans_explanation.md b/laans_explanation.md new file mode 100644 index 0000000..1e476e3 --- /dev/null +++ b/laans_explanation.md @@ -0,0 +1,17 @@ +# Laan's explanation + +1. Create a new fresh seed phrase. This will be your post quantum (pq) nostr account. +2. With this new seed phrase, we are going to create 5 different nsec npub key pairs. Each one using a different post quantum algorythm. +3. Construct the link statement + +A text statement is created that binds all the keys together: + +``` +"Identity is migrating to successor . + All PQ keys listed below are derived from the same BIP39 seed as . + This link is established pre-quantum." +``` + +This statement is what gets signed by all the keys. +2. With your old account, create an event. +3. \ No newline at end of file diff --git a/package.json b/package.json new file mode 100644 index 0000000..7023846 --- /dev/null +++ b/package.json @@ -0,0 +1,24 @@ +{ + "name": "post_quantum_nostr", + "version": "1.0.0", + "description": "A migration strategy for bringing post-quantum security to Nostr without breaking the social graph, without requiring consensus on a single post-quantum algorithm, and without forcing existing users to abandon their identities.", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "keywords": [], + "author": "", + "license": "ISC", + "type": "commonjs", + "devDependencies": { + "esbuild": "^0.28.1" + }, + "dependencies": { + "@noble/curves": "^2.2.0", + "@noble/hashes": "^2.2.0", + "@noble/post-quantum": "^0.6.1", + "@scure/base": "^2.2.0", + "@scure/bip32": "^2.2.0", + "@scure/bip39": "^2.2.0" + } +} diff --git a/plans/opentimestamps_plan.md b/plans/opentimestamps_plan.md new file mode 100644 index 0000000..559fd6e --- /dev/null +++ b/plans/opentimestamps_plan.md @@ -0,0 +1,204 @@ +# OpenTimestamps Integration Plan + +## Why OpenTimestamps? + +The NIP-QR migration event establishes a link between a Nostr identity and post-quantum keys. This link must be provably created **before** quantum computers exist. If an attacker later breaks secp256k1 via Shor's algorithm, they could forge a fraudulent NIP-QR event with a different set of PQ keys. Without timestamping, clients cannot distinguish the real event from a forged one. + +OpenTimestamps anchors the event to a Bitcoin block, proving it existed at a specific time. Re-mining historical Bitcoin blocks is infeasible even with a quantum computer (Grover's algorithm gives only a quadratic speedup on mining, which doesn't help retroactively alter past blocks). + +## How OpenTimestamps Works + +1. **Submit a hash** to an OpenTimestamps calendar server (e.g., `https://alice.btc.calendar.opentimestamps.org`) +2. The calendar server aggregates hashes from multiple users into a Merkle tree +3. The calendar server commits the Merkle root to a Bitcoin transaction (OP_RETURN) +4. The server returns a **pending .ots file** — a proof file that contains the Merkle path but no Bitcoin confirmation yet +5. **Wait for the Bitcoin transaction to confirm** (typically 1-3 blocks, ~10-30 minutes) +6. The .ots file can then be **upgraded** to include the Bitcoin confirmation proof +7. The final .ots file can be **verified** against the Bitcoin blockchain + +## NIP-03: OpenTimestamps Attestations for Events + +NIP-03 defines `kind: 1040` — an event that contains an OTS proof for another event: + +```json +{ + "kind": 1040, + "tags": [ + ["e", "", ""], + ["k", ""] + ], + "content": "" +} +``` + +- The OTS proof MUST prove the referenced event's `id` (which is `SHA-256(serialized event)`) +- The content MUST be the full content of an `.ots` file with at least one Bitcoin attestation +- NIP-03 is marked `unrecommended` but is the only standard for OTS in Nostr + +## The Plan + +### Step 1: Submit the NIP-QR event hash to OpenTimestamps + +After the NIP-QR event is signed (step 4 of our flow), before or after publishing to relays: + +``` +POST https://alice.btc.calendar.opentimestamps.org/timestamp +Content-Type: application/x-www-form-urlencoded +Body: +``` + +The event `id` IS the SHA-256 hash (it's `SHA-256(serialized event)` in hex). We need to: +1. Convert the event `id` (hex string) to raw 32 bytes +2. POST those bytes to the calendar server +3. Receive the .ots file (binary) in the response + +### Step 2: Save the pending .ots file + +The response from the calendar server is a binary .ots file (pending proof). Store this: +- In browser memory (for immediate use) +- Offer the user a download link (so they can keep it) + +### Step 3: Wait for Bitcoin confirmation + +The .ots proof is initially "pending" — it contains the Merkle path but no Bitcoin block confirmation. The user needs to wait for the calendar server to commit to Bitcoin (typically 1-3 blocks). + +Options: +- **Option A**: Show the user a "pending" status and a link to upgrade later +- **Option B**: Poll the calendar server's upgrade endpoint periodically +- **Option C**: Use a WebSocket or long-poll to wait for confirmation + +For the demo, **Option A** is simplest: show the pending .ots file, explain it needs time to confirm, and provide instructions for upgrading later. + +### Step 4: Upgrade the .ots file (later) + +The pending .ots file can be upgraded by submitting it to the calendar server: + +``` +POST https://alice.btc.calendar.opentimestamps.org +Content-Type: application/x-www-form-urlencoded +Body: +``` + +The server returns the upgraded .ots file with Bitcoin confirmation proof. + +For the demo, this can be a manual step the user does later (or we can poll automatically). + +### Step 5: Publish the NIP-03 event (kind 1040) + +Once the .ots file is confirmed (has a Bitcoin attestation), publish a `kind: 1040` event: + +```json +{ + "kind": 1040, + "pubkey": "", + "tags": [ + ["e", "", ""], + ["k", "30078"] + ], + "content": "" +} +``` + +This event is signed by the user's secp256k1 key (via `window.nostr.signEvent()`) and published to relays. + +### Step 6: Verify the OTS proof + +The verification page (`verify.html`) can also verify the OTS proof: +1. Find the kind 1040 event referencing the NIP-QR event +2. Extract the .ots file from the content (base64-decode) +3. Verify the .ots proof against the Bitcoin blockchain +4. This proves the NIP-QR event existed at the time of the Bitcoin block + +## Implementation Details + +### API Endpoints + +| Purpose | URL | Method | +|---|---|---| +| Submit hash for timestamping | `https://alice.btc.calendar.opentimestamps.org/timestamp` | POST (raw bytes) | +| Upgrade pending .ots | `https://alice.btc.calendar.opentimestamps.org` | POST (raw .ots bytes) | +| Verify .ots proof | Client-side verification (no server needed) | — | + +Multiple calendar servers can be used for redundancy: +- `https://alice.btc.calendar.opentimestamps.org` +- `https://bob.btc.calendar.opentimestamps.org` +- `https://btc.calendar.catallaxy.com` + +### Browser Implementation + +The OpenTimestamps API uses raw binary POST requests, which can be done with `fetch()`: + +```javascript +// Submit event hash for timestamping +async function timestampEvent(eventIdHex) { + const hashBytes = hexToBytes(eventIdHex); // 32 bytes + const response = await fetch('https://alice.btc.calendar.opentimestamps.org/timestamp', { + method: 'POST', + body: hashBytes + }); + const otsFile = await response.arrayBuffer(); // binary .ots file + return new Uint8Array(otsFile); +} +``` + +### UI Changes + +Add a new step to the checklist: +1. Sign in with your Nostr identity +2. Generate a quantum-safe seed phrase +3. Derive post-quantum keys from seed +4. Sign the NIP-QR migration event +5. Publish event to relays +6. **Timestamp the event with OpenTimestamps** (new) +7. Verify the event signatures (link to verify.html) + +After publishing to relays (step 5), the page automatically: +1. Submits the event hash to the OpenTimestamps calendar +2. Receives the pending .ots file +3. Shows the .ots file (base64) and a download link +4. Explains that the proof needs Bitcoin confirmation (10-30 minutes) +5. Offers to check/upgrade the proof later + +### OTS Verification + +For verifying .ots files in the browser, we can use the `javascript-opentimestamps` library: +- npm: `javascript-opentimestamps` +- GitHub: https://github.com/opentimestamps/javascript-opentimestamps + +This library can verify .ots files against a Bitcoin blockchain API (e.g., Blockstream's esplora API at `https://blockstream.info/api`). + +## Updated Checklist + +1. Sign in with your Nostr identity +2. Generate a quantum-safe seed phrase +3. Derive post-quantum keys from seed +4. Sign the NIP-QR migration event +5. Publish event to relays +6. Timestamp the event with OpenTimestamps +7. Verify the event signatures (link to verify.html) + +## Updated Flow + +``` +Sign in → Generate seed → Derive PQ keys → Sign event → Publish to relays + → Submit hash to OpenTimestamps → Receive pending .ots + → (wait for Bitcoin confirmation) + → Upgrade .ots → Publish kind 1040 event → Done +``` + +## What to Build + +1. **`timestampEvent(eventIdHex)`** function — submits hash to OTS calendar, returns .ots file +2. **`upgradeOts(otsBytes)`** function — upgrades pending .ots to confirmed +3. **UI for step 6** — shows OTS status, pending .ots file, download link, upgrade button +4. **Publish kind 1040 event** — after .ots is confirmed, sign and publish the NIP-03 event +5. **OTS verification in verify.html** — verify .ots proof against Bitcoin blockchain +6. **Install `javascript-opentimestamps`** — for client-side .ots verification + +## Considerations + +- **CORS**: The OpenTimestamps calendar servers support CORS, so browser requests should work +- **Time**: Bitcoin confirmation takes 10-30 minutes — the user may need to come back later +- **Multiple calendars**: For redundancy, submit to multiple calendar servers +- **NIP-03 unrecommended status**: We use it anyway because it's the only standard, and the concept is sound +- **The event id IS the hash**: Nostr event ids are SHA-256 hashes, so we timestamp the id directly diff --git a/research/amber_integration_analysis.md b/research/amber_integration_analysis.md new file mode 100644 index 0000000..b634495 --- /dev/null +++ b/research/amber_integration_analysis.md @@ -0,0 +1,186 @@ +# Amber Integration Analysis for PQ Nostr Web App + +## Amber Capabilities (from source code analysis) + +[Amber](https://github.com/greenart7c3/Amber) is an Android Nostr event signer that keeps the nsec segregated in a dedicated app. Based on source code inspection: + +### Seed Phrase Support ✅ +- **NIP-06 key derivation**: Uses `com.vitorpamplona.quartz.nip06KeyDerivation.Bip39Mnemonics` and `Nip06` for deriving secp256k1 keys from mnemonics +- **Stores seed words**: `DataStoreAccess.SEED_WORDS` persists the mnemonic in Android encrypted storage ([`DataStoreAccess.kt`](../Amber/app/src/main/java/com/greenart7c3/nostrsigner/DataStoreAccess.kt:41)) +- **Generates new seeds**: Login screen generates random entropy → `Bip39Mnemonics.toMnemonics(entropy)` ([`LoginScreen.kt`](../Amber/app/src/main/java/com/greenart7c3/nostrsigner/ui/LoginScreen.kt:428)) +- **Mnemonic login**: UI supports 12 or 24 word entry with account index selection ([`MnemonicLoginInput.kt`](../Amber/app/src/main/java/com/greenart7c3/nostrsigner/ui/components/MnemonicLoginInput.kt:48)) +- **Seed backup**: Account export includes encrypted seed words ([`AccountExportService.kt`](../Amber/app/src/main/java/com/greenart7c3/nostrsigner/service/AccountExportService.kt:193)) + +### Signing Operations ✅ +Amber supports these signer types ([`SignerType.kt`](../Amber/app/src/main/java/com/greenart7c3/nostrsigner/models/SignerType.kt:5)): +- `SIGN_EVENT` — sign any Nostr event +- `GET_PUBLIC_KEY` — return npub +- `NIP04_ENCRYPT` / `NIP04_DECRYPT` +- `NIP44_ENCRYPT` / `NIP44_DECRYPT` +- `NIP44_V3_ENCRYPT` / `NIP44_V3_DECRYPT` (new v3 cipher with kind+scope context) +- `DECRYPT_ZAP_EVENT` + +### NIP-46 Remote Signing ✅ +- Amber acts as a NIP-46 signing device — "your smartphone act as a NIP-46 signing device without any need for servers or additional hardware" +- Web apps can connect to Amber via NIP-46 (nostrconnect:// URI) +- Supports permission management per-app + +### Multiple Accounts ✅ +- Supports multiple accounts, each with its own nsec/seed words + +--- + +## How Amber Fits the PQ Web App Vision + +### The Key Insight + +Amber already stores the **seed phrase** (BIP39 mnemonic) and uses it to derive secp256k1 keys via NIP-06. The seed phrase is algorithm-agnostic — it's just 128/256 bits of entropy. Our plan's **Component 1** (seed phrase as root of trust) means PQ keys can be derived from the *same seed* using different derivation paths. + +### Three Architecture Options + +#### Option A: Web App + Amber Signing (Minimal Amber Changes) + +``` +┌─────────────────┐ NIP-46 ┌──────────────────┐ +│ Web App │ ◄──────────────► │ Amber │ +│ (browser) │ │ (Android) │ +│ │ │ │ +│ 1. User enters │ getPublicKey │ Returns npub │ +│ seed phrase │ ──────────────► │ │ +│ (client-side │ │ │ +│ only, WASM) │ signEvent │ Signs key-link │ +│ │ (key-link) │ event with │ +│ 2. Derives PQ │ ──────────────► │ secp256k1 │ +│ keys from │ │ │ +│ seed (WASM) │ │ │ +│ │ │ │ +│ 3. PQ-signs │ │ │ +│ key-link │ │ │ +│ client-side │ │ │ +│ │ │ │ +│ 4. Assembles & │ │ │ +│ publishes │ │ │ +│ key-link │ │ │ +│ event │ │ │ +└─────────────────┘ └──────────────────┘ +``` + +**Flow**: +1. User connects to Amber via NIP-46 for secp256k1 signing +2. User enters their seed phrase in the web app (client-side only, WASM, never sent to server) +3. Web app derives PQ keys (ML-DSA, ML-KEM, SLH-DSA) from seed using liboqs-WASM +4. Web app requests Amber to sign the key-link event content with secp256k1 +5. Web app PQ-signs the key-link event content client-side +6. Web app assembles the complete key-link event (secp256k1 sig + PQ sigs) and publishes to relays +7. Web app requests OpenTimestamps attestation via NIP-03 + +**Pros**: No Amber modifications needed. Full PQ key derivation in browser. +**Cons**: User must enter seed phrase in browser (even though client-side only). Trust model requires user to verify the web app isn't exfiltrating the seed. + +#### Option B: Extend Amber for PQ Key Derivation (Best Security) + +``` +┌─────────────────┐ NIP-46 ┌──────────────────┐ +│ Web App │ ◄──────────────► │ Amber │ +│ (browser) │ │ (Android) │ +│ │ │ │ +│ 1. Connect to │ new method: │ Derives PQ keys │ +│ Amber via │ pq_derive_keys │ from stored seed │ +│ NIP-46 │ ──────────────► │ (liboqs native) │ +│ │ │ │ +│ 2. Requests PQ │ Returns PQ │ Returns PQ │ +│ pubkey list │ ◄────────────── │ pubkeys only │ +│ │ │ (privkeys stay │ +│ 3. Requests │ new method: │ in Amber) │ +│ key-link │ pq_sign │ │ +│ signing │ ──────────────► │ PQ-signs event │ +│ │ │ with PQ privkeys │ +│ 4. Requests │ signEvent │ │ +│ secp256k1 │ (existing) │ secp256k1 signs │ +│ signing │ ──────────────► │ │ +│ │ │ │ +│ 5. Assembles & │ │ │ +│ publishes │ │ │ +└─────────────────┘ └──────────────────┘ +``` + +**Flow**: +1. User connects to Amber via NIP-46 +2. Web app calls new NIP-46 method `pq_derive_keys` — Amber derives PQ keys from its stored seed phrase using liboqs (native Android) +3. Amber returns only the PQ *public* keys (private keys never leave Amber) +4. Web app constructs the key-link event content with all pubkeys +5. Web app calls `pq_sign` — Amber PQ-signs the event content with each PQ private key +6. Web app calls `signEvent` — Amber secp256k1-signs the event +7. Web app assembles and publishes the complete key-link event + +**Pros**: Seed phrase never leaves Amber. PQ private keys never leave Amber. Best security model — consistent with Amber's philosophy of "private keys should be exposed to as few systems as possible." +**Cons**: Requires Amber modifications (new NIP-46 methods, liboqs integration for Android). + +#### Option C: Hybrid (Pragmatic) + +``` +┌─────────────────┐ NIP-46 ┌──────────────────┐ +│ Web App │ ◄──────────────► │ Amber │ +│ (browser) │ │ (Android) │ +│ │ │ │ +│ User chooses: │ │ │ +│ │ │ │ +│ Path 1 (Amber │ signEvent │ Signs with │ +│ has seed): │ ──────────────► │ secp256k1 │ +│ Request secp │ │ │ +│ sig from Amber │ │ │ +│ Enter seed in │ │ │ +│ web app for PQ │ │ │ +│ key derivation │ │ │ +│ (client-side) │ │ │ +│ │ │ │ +│ Path 2 (raw │ signEvent │ Signs with │ +│ nsec, no seed │ ──────────────► │ secp256k1 │ +│ in Amber): │ │ │ +│ Generate new │ │ │ +│ seed in web app │ │ │ +│ (client-side), │ │ │ +│ derive PQ keys, │ │ │ +│ request secp │ │ │ +│ sig from Amber │ │ │ +└─────────────────┘ └──────────────────┘ +``` + +**Pros**: Works for both existing seed-phrase users and raw-nsec users. No Amber changes required for initial launch. Can upgrade to Option B later. +**Cons**: Seed phrase still enters browser for PQ derivation. Two code paths. + +--- + +## Recommendation: Option B (Extend Amber) + +This aligns with: +1. **Amber's security philosophy**: "Private keys should be exposed to as few systems as possible" +2. **Our plan's Component 1**: Seed phrase as algorithm-agnostic root of trust — Amber already stores it +3. **Our plan's Component 2**: Cross-signed key-link events — Amber can sign with both secp256k1 and PQ keys +4. **Community discussion** (Issue #1971): trbouma and mikedilger both advocate for HSM/enclave-based PQ key storage + +### Required Amber Changes + +1. **Add liboqs dependency**: Native library for PQ algorithms (ML-DSA, ML-KEM, SLH-DSA) +2. **PQ key derivation from seed**: New function that takes the stored BIP39 seed and derives PQ keys using a deterministic scheme (e.g., `HKDF(seed, "nostr-pq-ml-dsa-44")` → PQ private key) +3. **New NIP-46 methods**: + - `pq_get_public_keys` — return array of PQ public keys with algorithm identifiers + - `pq_sign` — sign a message with a specified PQ algorithm's private key +4. **Permission management**: New permission types for PQ signing operations +5. **UI updates**: Show PQ key information, approval screens for PQ signing requests + +### Required Web App Components + +1. **NIP-46 client**: Connect to Amber, send signing requests +2. **Key-link event builder**: Construct the event content with all pubkeys, request signatures, assemble final event +3. **OpenTimestamps client**: Request OTS attestation for the key-link event (NIP-03) +4. **Relay publisher**: Publish the key-link event to relays +5. **Verification UI**: Show the user what was created, verify the key-link event is valid +6. **No server-side secrets**: The web app is a static site — no backend that could intercept keys + +### For Users Without Amber (or Without Seed Phrase in Amber) + +The web app should also support: +- **NIP-07 browser extension signing** (nos2x, etc.) for secp256k1 signatures +- **Client-side seed phrase entry** (WASM, never transmitted) for users who want to derive PQ keys in-browser +- **New seed generation** for users who don't have a seed phrase yet (Component 4: migrating existing users with raw nsec) diff --git a/research/amber_integration_plan.md b/research/amber_integration_plan.md new file mode 100644 index 0000000..d462760 --- /dev/null +++ b/research/amber_integration_plan.md @@ -0,0 +1,813 @@ +# Amber Integration Plan: End-User Experience + +## Overview + +This document describes the complete end-to-end user experience for migrating a Nostr identity to post-quantum security using the PQ web app and Amber signer. There are **two paths** depending on how the user's account was created in Amber: + +- **Path A (Simple)**: User logged into Amber with a seed phrase (mnemonic). Amber already has the seed. PQ keys are derived from that same seed. No second account needed. +- **Path B (Migration)**: User logged into Amber with a raw nsec. No seed phrase exists. A new seed must be generated and linked to the old identity via a two-account migration event. + +The web app automatically detects which path applies by asking Amber for the account's seed words (or checking if they're empty). + +--- + +## Critical Fact: Amber Does Not Tie Raw nsec to Seed Phrases + +When a user enters a raw nsec in Amber, the seed words field is set to `null` ([`AccountStateViewModel.kt`](../Amber/app/src/main/java/com/greenart7c3/nostrsigner/ui/AccountStateViewModel.kt:144)): + +```kotlin +LocalPreferences.updatePrefsForLogin( + Amber.instance, account, + keyPair.pubKey.toHexKey(), + keyPair.privKey!!.toHexKey(), + null // ← seedWords is null for raw nsec login +) +``` + +| Login method | nsec stored | Seed words stored | Relationship | +|---|---|---|---| +| Raw nsec (`nsec1...`) | ✅ | `null` (empty) | None — nsec is random, no seed | +| Mnemonic (12/24 words) | ✅ (derived from seed via NIP-06) | ✅ | nsec IS derived from seed | + +A raw nsec is 32 random bytes. You cannot derive a seed phrase from it (preimage problem — impossible). You cannot derive the existing nsec from a new seed (different key). The only bridge between them is a **signed migration event**. + +--- + +## Prerequisites + +- **Amber** installed on Android phone (from F-Droid, GitHub Releases, or ZapStore) +- **Existing Nostr account** already in Amber +- **Desktop or mobile browser** to access the PQ web app + +--- + +## Phase 1: Connect to the PQ Web App (Both Paths) + +### Screen 1: Landing Page + +``` +┌─────────────────────────────────────────────────────┐ +│ │ +│ 🔒 Post-Quantum Nostr Migration │ +│ │ +│ Make your Nostr identity quantum-resistant. │ +│ Your followers stay. Your npub doesn't change. │ +│ │ +│ ┌─────────────────────────────────────┐ │ +│ │ Connect with Amber │ │ +│ └─────────────────────────────────────┘ │ +│ │ +│ ┌─────────────────────────────────────┐ │ +│ │ Connect with browser extension │ │ +│ └─────────────────────────────────────┘ │ +│ │ +│ How it works | FAQ | Source code │ +│ │ +└─────────────────────────────────────────────────────┘ +``` + +**User action**: Taps "Connect with Amber" + +**Behind the scenes**: +- Web app generates a random NIP-46 connection keypair +- Web app displays a `nostrconnect://` URI or QR code +- Web app opens a WebSocket to a relay, listening for Amber's response + +### Screen 2: Connect to Amber + +``` +┌─────────────────────────────────────────────────────┐ +│ │ +│ Connect Amber │ +│ │ +│ Scan this QR code with Amber, or open this │ +│ link on your phone: │ +│ │ +│ ┌───────────────────┐ │ +│ │ ▓▓▓▓▓▓▓▓▓▓▓▓▓ │ │ +│ │ ▓▓▓▓▓▓▓▓▓▓▓▓▓ │ │ +│ │ ▓▓▓▓▓▓▓▓▓▓▓▓▓ │ │ +│ └───────────────────┘ │ +│ │ +│ nostrconnect://... │ +│ │ +│ Waiting for Amber... │ +│ ●●○○○ │ +│ │ +└─────────────────────────────────────────────────────┘ +``` + +**User action**: Opens the link on their phone (or scans QR code) + +### On Amber (Phone): Connection Request + +``` +┌─────────────────────────────────────────────────────┐ +│ │ +│ pq-nostr.app wants to connect │ +│ │ +│ This app is requesting to: │ +│ • Read your public key │ +│ • Sign events │ +│ • Encrypt and decrypt messages │ +│ │ +│ Account: npub1abc... (Account #1) │ +│ │ +│ ┌─────────────┐ ┌─────────────┐ │ +│ │ Approve │ │ Reject │ │ +│ └─────────────┘ └─────────────┘ │ +│ │ +└─────────────────────────────────────────────────────┘ +``` + +**User action**: Taps "Approve" + +**Behind the scenes**: +- Amber sends a NIP-46 `connect` response to the web app via the relay +- Web app now has a NIP-46 session with Amber +- Web app requests `get_public_key` → receives the user's npub + +### Screen 3: Account Detected — Path Selection + +The web app asks Amber if the account has seed words. This determines which path to show. + +**Behind the scenes**: +- Web app requests seed words from Amber (via a new NIP-46 method, or by asking the user to check Amber's backup screen) +- If seed words exist → **Path A (Simple)** +- If seed words are empty → **Path B (Migration)** + +--- + +## Path A: Simple Path (User Has Seed Phrase in Amber) + +*This path applies to users who logged into Amber with a 12/24-word mnemonic. Amber already stores their seed phrase. PQ keys are derived from that same seed. No second account needed.* + +### Screen A1: Seed Phrase Retrieval + +``` +┌─────────────────────────────────────────────────────┐ +│ │ +│ ✅ Connected to Amber │ +│ │ +│ Your identity: npub1abc...xyz │ +│ Account name: Alice │ +│ Seed phrase: ✅ Found in Amber │ +│ │ +│ Great! Your account already has a seed phrase. │ +│ We can derive quantum-resistant keys from it │ +│ without creating a new account. │ +│ │ +│ To proceed, we need your seed phrase to │ +│ derive the post-quantum keys in your browser. │ +│ The seed phrase will NOT be sent to any server. │ +│ │ +│ Option 1: Export from Amber │ +│ Open Amber → Account → Backup → Show Seed Words │ +│ Then type or paste them below. │ +│ │ +│ ┌─────────────────────────────────────────┐ │ +│ │ Enter your 12/24 seed words here... │ │ +│ │ │ │ +│ └─────────────────────────────────────────┘ │ +│ │ +│ ┌─────────────────────────────────────┐ │ +│ │ Continue │ │ +│ └─────────────────────────────────────┘ │ +│ │ +└─────────────────────────────────────────────────────┘ +``` + +**User action**: Opens Amber's backup screen, copies seed words, pastes into web app + +**Behind the scenes**: +- Seed phrase is handled entirely client-side (WASM), never transmitted +- Web app validates the mnemonic (BIP39 checksum) +- Web app verifies the derived secp256k1 pubkey matches the connected Amber account + +### Screen A2: Generating Quantum Keys + +``` +┌─────────────────────────────────────────────────────┐ +│ │ +│ Generating Quantum Keys... │ +│ │ +│ Deriving post-quantum keys from your seed... │ +│ │ +│ ✅ ML-DSA-65 (Dilithium) signature key │ +│ Public key: 1952 bytes │ +│ │ +│ ✅ SLH-DSA-128s (SPHINCS+) signature key │ +│ Public key: 32 bytes │ +│ │ +│ ✅ ML-KEM-768 (Kyber) encryption key │ +│ Public key: 1184 bytes │ +│ │ +│ All keys derived from your seed phrase. │ +│ Same seed → same keys, every time. │ +│ │ +│ ┌─────────────────────────────────────┐ │ +│ │ Continue │ │ +│ └─────────────────────────────────────┘ │ +│ │ +└─────────────────────────────────────────────────────┘ +``` + +**Behind the scenes**: +- Web app uses liboqs-wasm to generate PQ keypairs deterministically from the BIP39 seed +- PQ private keys exist only in browser memory (WASM), never sent anywhere + +### Screen A3: Signing the NIP-QR Event + +``` +┌─────────────────────────────────────────────────────┐ +│ │ +│ Signing the Migration Event │ +│ │ +│ PQ signatures (done in browser): │ +│ ✅ ML-DSA-65 signature │ +│ ✅ SLH-DSA-128s signature │ +│ │ +│ Amber signature (need your approval): │ +│ ⏳ Signing the NIP-QR event... │ +│ │ +│ Please approve the signing request in Amber. │ +│ │ +└─────────────────────────────────────────────────────┘ +``` + +**Behind the scenes**: +- Web app constructs the link statement: + ``` + "Identity is linked to the following PQ keys, + all derived from the same BIP39 seed. This link is + established pre-quantum." + ``` +- Web app PQ-signs the statement with each PQ private key (client-side WASM) +- Web app sends a `signEvent` request to Amber to sign the NIP-QR event + +### On Amber (Phone): Sign NIP-QR Event + +``` +┌─────────────────────────────────────────────────────┐ +│ │ +│ pq-nostr.app wants to sign │ +│ │ +│ Account: npub1abc...xyz │ +│ │ +│ Event kind: │ +│ │ +│ This is your quantum migration event. │ +│ It links your identity to quantum-resistant │ +│ keys derived from your seed phrase. │ +│ │ +│ ┌─────────────┐ ┌─────────────┐ │ +│ │ Approve │ │ Reject │ │ +│ └─────────────┘ └─────────────┘ │ +│ │ +└─────────────────────────────────────────────────────┘ +``` + +**User action**: Taps "Approve" + +**Behind the scenes**: +- Amber signs the NIP-QR event with the secp256k1 private key +- Since the secp256k1 key and PQ keys are all derived from the same seed, this single signature links them all +- No second account needed — the existing account's signature is sufficient + +### Screen A4: Publishing → Go to Phase 5 + +The web app publishes the event and requests OpenTimestamps (see Phase 5 below). + +--- + +## Path B: Migration Path (User Has Raw nsec, No Seed Phrase) + +*This path applies to users who logged into Amber with a raw nsec. No seed phrase exists. A new seed must be generated and linked to the old identity via a two-account migration.* + +### Screen B1: Migration Required + +``` +┌─────────────────────────────────────────────────────┐ +│ │ +│ ✅ Connected to Amber │ +│ │ +│ Your identity: npub1abc...xyz │ +│ Account name: Alice │ +│ Seed phrase: ❌ Not found │ +│ │ +│ Your account was created with a raw private key │ +│ (nsec), not a seed phrase. To make your identity │ +│ quantum-resistant, we need to: │ +│ │ +│ 1. Generate a new seed phrase (your quantum-safe │ +│ backup) │ +│ 2. Add it to Amber as a second account │ +│ 3. Link your old identity to the new account │ +│ and its quantum keys │ +│ │ +│ Your npub won't change. Your followers stay. │ +│ │ +│ ┌─────────────────────────────────────┐ │ +│ │ Start Migration │ │ +│ └─────────────────────────────────────┘ │ +│ │ +└─────────────────────────────────────────────────────┘ +``` + +**User action**: Taps "Start Migration" + +### Screen B2: Generate Seed Phrase + +``` +┌─────────────────────────────────────────────────────┐ +│ │ +│ Step 1 of 4: Your Quantum-Safe Backup │ +│ │ +│ We're generating a new seed phrase. This will │ +│ become your quantum-safe root of trust. │ +│ Write it down on paper. Never store it │ +│ digitally. Never share it with anyone. │ +│ │ +│ ┌─────────────────────────────────────────┐ │ +│ │ │ │ +│ │ 1. apple 7. river │ │ +│ │ 2. garden 8. bridge │ │ +│ │ 3. forest 9. mountain │ │ +│ │ 4. ocean 10. sunset │ │ +│ │ 5. desert 11. valley │ │ +│ │ 6. meadow 12. horizon │ │ +│ │ │ │ +│ └─────────────────────────────────────────┘ │ +│ │ +│ 📋 Copy 🖨️ Print │ +│ │ +│ ☐ I have written down my seed phrase │ +│ │ +│ ┌─────────────────────────────────────┐ │ +│ │ Continue │ │ +│ └─────────────────────────────────────┘ │ +│ │ +└─────────────────────────────────────────────────────┘ +``` + +**Behind the scenes**: +- Web app generates 128 bits of random entropy (crypto.getRandomValues) +- Web app converts entropy to 12-word BIP39 mnemonic (client-side, WASM) +- Seed phrase exists only in browser memory — never sent to any server +- Web app derives the BIP39 seed: `PBKDF2-HMAC-SHA512(mnemonic, "mnemonic", 2048)` +- Web app derives Account #2's secp256k1 private key via NIP-06: `m/44'/1237'/0'/0/0` + +**User action**: Writes down seed phrase, checks the box, taps "Continue" + +### Screen B3: Verify Seed Phrase + +``` +┌─────────────────────────────────────────────────────┐ +│ │ +│ Step 1 of 4: Verify Your Backup │ +│ │ +│ Type word #3, #7, and #11 to confirm you │ +│ wrote it down correctly. │ +│ │ +│ Word #3: [ forest ] │ +│ Word #7: [ river ] │ +│ Word #11: [ valley ] │ +│ │ +│ ┌─────────────────────────────────────┐ │ +│ │ Verify │ │ +│ └─────────────────────────────────────┘ │ +│ │ +└─────────────────────────────────────────────────────┘ +``` + +**User action**: Types the three words, taps "Verify" + +### Screen B4: Import Seed into Amber + +``` +┌─────────────────────────────────────────────────────┐ +│ │ +│ Step 2 of 4: Add to Amber │ +│ │ +│ You need to add this seed phrase to Amber as │ +│ a new account. This creates Account #2 — │ +│ your quantum-safe signing identity. │ +│ │ +│ On your phone, in Amber: │ +│ │ +│ 1. Tap the account switcher (top left) │ +│ 2. Tap "+ Add Account" │ +│ 3. Select "Recovery Phrase" │ +│ 4. Enter your 12 words │ +│ 5. Tap "Login" │ +│ │ +│ ┌─────────────────────────────────────────┐ │ +│ │ │ │ +│ │ Account #2 npub: │ │ +│ │ npub1def...uvw │ │ +│ │ │ │ +│ └─────────────────────────────────────────┘ │ +│ │ +│ ☐ I've added this account to Amber │ +│ │ +│ ┌─────────────────────────────────────┐ │ +│ │ Continue │ │ +│ └─────────────────────────────────────┘ │ +│ │ +└─────────────────────────────────────────────────────┘ +``` + +**Behind the scenes**: +- Web app displays Account #2's npub (derived from the seed via NIP-06) +- User needs to manually import the seed into Amber as a new account + +### On Amber (Phone): Add Account + +``` +┌─────────────────────────────────────────────────────┐ +│ │ +│ Add Account │ +│ │ +│ ┌─────────────┐ ┌─────────────┐ │ +│ │ Private Key │ │ Recovery │ │ +│ │ │ │ Phrase │ │ +│ └─────────────┘ └─────────────┘ │ +│ │ +└─────────────────────────────────────────────────────┘ +``` + +**User action**: Taps "Recovery Phrase", enters 12 words, taps "Login" + +**Behind the scenes**: +- Amber derives secp256k1 key from mnemonic via NIP-06 +- Amber stores the nsec AND seed words in encrypted storage +- Amber switches to Account #2 + +### Screen B5: Connect Account #2 to Web App + +``` +┌─────────────────────────────────────────────────────┐ +│ │ +│ Step 2 of 4: Connect Account #2 │ +│ │ +│ We need Amber to sign with Account #2 as well. │ +│ │ +│ In Amber, switch to Account #2 (npub1def...uvw), │ +│ then approve the connection request. │ +│ │ +│ ┌───────────────────┐ │ +│ │ ▓▓▓▓▓▓▓▓▓▓▓▓▓ │ │ +│ │ ▓▓▓▓▓▓▓▓▓▓▓▓▓ │ │ +│ │ ▓▓▓▓▓▓▓▓▓▓▓▓▓ │ │ +│ └───────────────────┘ │ +│ │ +│ nostrconnect://... │ +│ │ +│ Waiting for Amber (Account #2)... │ +│ ●●○○○ │ +│ │ +└─────────────────────────────────────────────────────┘ +``` + +**User action**: Switches to Account #2 in Amber, approves connection + +**Behind the scenes**: +- Web app opens a second NIP-46 session for Account #2 +- Web app now has signing access to both Account #1 and Account #2 via Amber + +### Screen B6: Generating Quantum Keys + +``` +┌─────────────────────────────────────────────────────┐ +│ │ +│ Step 3 of 4: Generating Quantum Keys │ +│ │ +│ Deriving post-quantum keys from your seed... │ +│ │ +│ ✅ ML-DSA-65 (Dilithium) signature key │ +│ Public key: 1952 bytes │ +│ │ +│ ✅ SLH-DSA-128s (SPHINCS+) signature key │ +│ Public key: 32 bytes │ +│ │ +│ ✅ ML-KEM-768 (Kyber) encryption key │ +│ Public key: 1184 bytes │ +│ │ +│ All keys derived from your seed phrase. │ +│ Same seed → same keys, every time. │ +│ │ +│ ┌─────────────────────────────────────┐ │ +│ │ Continue │ │ +│ └─────────────────────────────────────┘ │ +│ │ +└─────────────────────────────────────────────────────┘ +``` + +**Behind the scenes**: +- Web app uses liboqs-wasm to generate PQ keypairs deterministically from the BIP39 seed +- PQ private keys exist only in browser memory (WASM), never sent anywhere + +### Screen B7: Signing — Account #2 Signs the PQ Key Statement + +``` +┌─────────────────────────────────────────────────────┐ +│ │ +│ Step 3 of 4: Signing │ +│ │ +│ PQ signatures (done in browser): │ +│ ✅ ML-DSA-65 signature │ +│ ✅ SLH-DSA-128s signature │ +│ │ +│ Amber signatures (need your approval): │ +│ ⏳ Account #2 signs the PQ key statement... │ +│ │ +│ Please approve the signing request in Amber. │ +│ (Make sure Account #2 is active in Amber) │ +│ │ +└─────────────────────────────────────────────────────┘ +``` + +**Behind the scenes**: +- Web app constructs the link statement: + ``` + "Identity is migrating to successor . + All PQ keys listed below are derived from the same + BIP39 seed as . This link is established + pre-quantum." + ``` +- Web app PQ-signs the statement with ML-DSA-65 and SLH-DSA-128s private keys (client-side WASM) +- Web app sends a `signEvent` request to Amber (Account #2) to sign the statement + +### On Amber (Phone): Sign with Account #2 + +``` +┌─────────────────────────────────────────────────────┐ +│ │ +│ pq-nostr.app wants to sign │ +│ │ +│ Account: npub1def...uvw (Account #2) │ +│ │ +│ Content: │ +│ "Identity npub1abc... is migrating to │ +│ successor npub1def... All PQ keys..." │ +│ │ +│ ┌─────────────┐ ┌─────────────┐ │ +│ │ Approve │ │ Reject │ │ +│ └─────────────┘ └─────────────┘ │ +│ │ +└─────────────────────────────────────────────────────┘ +``` + +**User action**: Taps "Approve" + +### Screen B8: Signing — Account #1 Signs the Migration Event + +``` +┌─────────────────────────────────────────────────────┐ +│ │ +│ Step 3 of 4: Signing │ +│ │ +│ ✅ ML-DSA-65 signature (browser) │ +│ ✅ SLH-DSA-128s signature (browser) │ +│ ✅ Account #2 signature (Amber) │ +│ │ +│ ⏳ Account #1 signs the migration event... │ +│ │ +│ Please switch to Account #1 in Amber and │ +│ approve the signing request. │ +│ │ +└─────────────────────────────────────────────────────┘ +``` + +**User action**: Switches to Account #1 in Amber + +### On Amber (Phone): Sign with Account #1 + +``` +┌─────────────────────────────────────────────────────┐ +│ │ +│ pq-nostr.app wants to sign │ +│ │ +│ Account: npub1abc...xyz (Account #1) │ +│ │ +│ Event kind: │ +│ │ +│ This is your quantum migration event. │ +│ It links your current identity to │ +│ quantum-resistant keys. │ +│ │ +│ ┌─────────────┐ ┌─────────────┐ │ +│ │ Approve │ │ Reject │ │ +│ └─────────────┘ └─────────────┘ │ +│ │ +└─────────────────────────────────────────────────────┘ +``` + +**User action**: Taps "Approve" + +**Behind the scenes**: +- Amber signs the NIP-QR event with Account #1's secp256k1 private key (the old nsec) +- This is the critical signature — the old identity authorizing the migration +- The event is now fully signed: Account #1's sig + Account #2's sig + PQ sigs + +--- + +## Phase 5: Publish and Timestamp (Both Paths) + +### Screen 9: Publishing + +``` +┌─────────────────────────────────────────────────────┐ +│ │ +│ Step 4 of 4: Publishing │ +│ │ +│ Publishing migration event to relays... │ +│ │ +│ ✅ wss://relay.damus.io │ +│ ✅ wss://nos.lol │ +│ ✅ wss://relay.primal.net │ +│ │ +│ Requesting OpenTimestamps attestation... │ +│ ✅ Anchored to Bitcoin block 890,123 │ +│ │ +│ ┌─────────────────────────────────────┐ │ +│ │ View Migration Event │ │ +│ └─────────────────────────────────────┘ │ +│ │ +└─────────────────────────────────────────────────────┘ +``` + +**Behind the scenes**: +- Web app assembles the complete NIP-QR event with all signatures +- Web app publishes the event to multiple relays via WebSocket +- Web app submits the event hash to OpenTimestamps (NIP-03) +- OpenTimestamps anchors the event to a Bitcoin block, proving it existed at this time +- This prevents a future quantum attacker from backdating a fraudulent migration event + +### Screen 10: Success + +``` +┌─────────────────────────────────────────────────────┐ +│ │ +│ 🎉 Migration Complete! │ +│ │ +│ Your Nostr identity is now quantum-resistant. │ +│ │ +│ What happened: │ +│ • Your identity (npub1abc...xyz) is now linked │ +│ to post-quantum keys │ +│ • The link is timestamped on the Bitcoin │ +│ blockchain (cannot be forged retroactively) │ +│ • Your followers, NIP-05, and social graph │ +│ are unchanged │ +│ │ +│ What to do now: │ +│ • Keep your seed phrase safe — it's your │ +│ quantum-safe backup │ +│ • No other action needed — PQ-aware clients │ +│ will recognize your new keys automatically │ +│ │ +│ Event ID: │ +│ OTS proof: │ +│ │ +│ ┌─────────────────────────────────────┐ │ +│ │ Done │ │ +│ └─────────────────────────────────────┘ │ +│ │ +└─────────────────────────────────────────────────────┘ +``` + +**User action**: Downloads the .ots file (optional backup), taps "Done" + +--- + +## Comparison: Path A vs Path B + +| Aspect | Path A (Has Seed) | Path B (Raw nsec) | +|---|---|---| +| Amber accounts needed | 1 (existing) | 2 (existing + new) | +| Seed phrase generation | Not needed (already have one) | Generated by web app | +| Amber import step | Not needed | Manual import of seed as Account #2 | +| NIP-46 sessions | 1 | 2 (one per account) | +| Amber signing requests | 1 (sign NIP-QR event) | 2 (Account #2 signs statement, Account #1 signs event) | +| NIP-QR event structure | Single-key link (secp256k1 + PQ keys from same seed) | Two-key migration (old nsec → new seed-derived key → PQ keys) | +| Time required | ~2 minutes | ~5 minutes | +| User complexity | Low | Medium | + +### NIP-QR Event Structure Differences + +**Path A (Simple)** — single identity, seed-derived: +```json +{ + "kind": , + "pubkey": "", + "content": { + "statement": "Identity is linked to the following PQ keys, all derived from the same BIP39 seed. This link is established pre-quantum.", + "pq_keys": [ + { "algorithm": "ml-dsa-65", "public_key": "...", "signature": "..." }, + { "algorithm": "slh-dsa-128s", "public_key": "...", "signature": "..." }, + { "algorithm": "ml-kem-768", "public_key": "..." } + ] + }, + "sig": "" +} +``` + +**Path B (Migration)** — two identities, old + new: +```json +{ + "kind": , + "pubkey": "", + "tags": [ + ["successor", ""] + ], + "content": { + "statement": "Identity is migrating to successor . All PQ keys listed below are derived from the same BIP39 seed as . This link is established pre-quantum.", + "successor_pubkey": "", + "successor_signature": "", + "pq_keys": [ + { "algorithm": "ml-dsa-65", "public_key": "...", "signature": "..." }, + { "algorithm": "slh-dsa-128s", "public_key": "...", "signature": "..." }, + { "algorithm": "ml-kem-768", "public_key": "..." } + ] + }, + "sig": "" +} +``` + +The key difference: Path B includes a `successor` tag and `successor_signature` in the content, because the old identity (Account #1) is different from the seed-derived identity (Account #2). Path A doesn't need this because the identity IS the seed-derived key. + +--- + +## Post-Migration: What Changes for the User + +### Immediately (nothing changes) +- Their npub is the same +- Their followers, follows, NIP-05, profile — all unchanged +- Existing clients work exactly as before +- They can still sign events with their existing nsec + +### When PQ-aware clients adopt the NIP +- PQ-aware clients find the NIP-QR event for their npub +- Path A: clients see secp256k1 key + PQ keys, all from the same seed +- Path B: clients see Account #1 → Account #2 (successor) → PQ keys +- Clients can use their PQ encryption key (ML-KEM-768) for PQ-secure messaging +- Clients can verify PQ signatures on future events + +### If a quantum computer breaks secp256k1 +- An attacker can forge secp256k1 signatures +- But they CANNOT: + - Forge PQ signatures (ML-DSA, SLH-DSA remain secure) + - Backdate a fraudulent NIP-QR event (OTS proof anchors the real one) + - Decrypt PQ-encrypted messages (ML-KEM remains secure) +- The user's identity is preserved through the OTS-anchored migration event +- For Path B: the user continues signing with Account #2 and PQ keys +- For Path A: the user continues signing with PQ keys (secp256k1 is broken but PQ keys are safe) + +--- + +## Error Handling and Edge Cases + +### User loses seed phrase +- **Path A**: PQ keys cannot be re-derived. The NIP-QR event is already published and timestamped — the link still exists. User should re-run migration with a new seed phrase (publishes an updated NIP-QR event). +- **Path B**: Account #2's nsec is still in Amber (can be exported via NIP-49 encrypted backup). But PQ keys cannot be re-derived without the seed. Same recovery: publish a new NIP-QR event with a new seed. + +### User loses phone (Amber) +- If seed phrase was written down: import into new Amber installation, re-derive PQ keys +- If seed phrase was lost too: PQ keys are unrecoverable, but the NIP-QR event still proves the link existed. User publishes a new NIP-QR event with a new seed. + +### User doesn't have Amber +- Fall back to NIP-07 browser extension (nos2x, etc.) for secp256k1 signing +- Seed phrase is generated and handled entirely in the browser +- Less secure (seed in browser) but still functional +- Recommend installing Amber for better security + +### User already has a seed phrase in Amber (Path A) but wants to use a different seed +- They can choose to generate a new seed (switching to Path B flow) +- This might be desirable if they want a dedicated PQ-only seed phrase + +--- + +## Technical Summary + +| Component | Where it runs | What it does | +|---|---|---| +| Seed generation | Browser (WASM) | BIP39 mnemonic generation (Path B only) | +| Seed retrieval | Amber → user → browser | Manual export from Amber's backup screen | +| NIP-06 key derivation | Browser (WASM) | secp256k1 key from seed (verify it matches Amber account) | +| PQ key derivation | Browser (WASM, liboqs) | ML-DSA, SLH-DSA, ML-KEM from seed | +| PQ signing | Browser (WASM, liboqs) | Sign link statement with PQ keys | +| secp256k1 signing | Amber (Android) | Sign NIP-QR event (and successor statement for Path B) | +| Event assembly | Browser | Combine all signatures into NIP-QR event | +| Relay publishing | Browser (WebSocket) | Publish event to Nostr relays | +| OpenTimestamps | Browser → OTS API | Anchor event to Bitcoin blockchain | +| Seed phrase storage | Amber (Android encrypted storage) | BIP39 mnemonic stored encrypted | +| nsec storage | Amber (Android encrypted storage) | Account private key(s) stored encrypted | + +### No server-side secrets +The web app is a **static site** — HTML, JS, and WASM files served from a CDN or GitHub Pages. There is no backend that handles keys. The only server interactions are: +- Nostr relays (publishing the event) +- OpenTimestamps API (requesting timestamp proof) +- NIP-46 relay (communicating with Amber) + +All cryptographic operations happen either in the browser (WASM) or in Amber (Android). No private key ever touches a server. diff --git a/research/existing_pq_proposals.md b/research/existing_pq_proposals.md new file mode 100644 index 0000000..5ceb783 --- /dev/null +++ b/research/existing_pq_proposals.md @@ -0,0 +1,196 @@ +# Existing Post-Quantum Proposals in the Nostr Ecosystem + +A survey of pull requests and issues in `nostr-protocol/nips` related to post-quantum cryptography, key migration, and key rotation — and how they compare to our plan in [`README.md`](../README.md). + +Research date: 2026-07-12 + +--- + +## Summary + +There is **no merged NIP** for post-quantum security. The only merged reference is an acknowledgment in [NIP-44](https://github.com/nostr-protocol/nips/blob/master/44.md) that it has "No post-quantum security." However, there are **several open proposals and active community discussions** that overlap significantly with our plan. The community is clearly thinking about this but has not converged on a solution. + +--- + +## Directly Post-Quantum Proposals + +### 1. PR #2185: "Add NIP for PQ" (OPEN) +- **Author**: trbouma +- **Created**: 2026-01-09 | **Updated**: 2026-03-04 +- **URL**: https://github.com/nostr-protocol/nips/pull/2185 +- **Status**: Open, 7 comments, 1 commit, +113 lines + +**What it proposes**: +- Extends NIP-01 to allow events signed with **ML-DSA-44** (FIPS 204) instead of secp256k1 Schnorr +- Relaxes the `pubkey` and `sig` field length constraints in the NIP-01 event format +- PQ pubkey: 2624 hex chars (1312 bytes); PQ sig: 4840 hex chars (2420 bytes) +- Detection: if `pubkey` length > 64 hex chars, use ML-DSA-44 verification +- Includes a Python proof-of-concept using `monstr` + `oqs` (Open Quantum Safe) +- Author also has a working PQ relay: https://github.com/trbouma/pqrelay + +**What it does NOT address**: +- No migration strategy for existing users +- No key linking between old secp256k1 identity and new PQ identity +- No encryption (NIP-44/NIP-04) PQ upgrade +- No OpenTimestamps anchoring +- No seed phrase / root of trust +- Picks a single algorithm (ML-DSA-44), no multi-scheme hedging + +**Community comments**: +- vitorpamplona: "We will need to address NIP-44 encryptions with these new keys" +- kehiy: "Are PQ algos developed enough so we can choose one easily? Maybe we need to wait more" +- trbouma: "The immediate thing is that it gives nostr a post-quantum narrative to fight the FUD" + +**Comparison to our plan**: This is the simplest possible approach — just swap the signature algorithm. Our plan is significantly more comprehensive: it addresses migration, key linking, encryption, timestamping, and algorithm uncertainty. PR #2185 could be seen as a subset of our Component 3 (PQ signatures) but without the migration scaffolding. + +--- + +### 2. PR #391: "NIP-101 Algorithm Transition for Signatures and Encryption" (CLOSED) +- **Author**: eznix86 +- **Created**: 2023-03-24 | **Closed**: 2025-05-22 (not merged) +- **URL**: https://github.com/nostr-protocol/nips/pull/391 + +**What it proposes**: +- A generic algorithm transition event (`kind: 101`) containing: + - `old_pubkey`, `new_pubkey`, `old_alg`, `new_alg` + - `old_signature` (signed with old key/algorithm) + - `new_signature` (signed with new key/algorithm) +- Cross-signed transition: both old and new keys sign the transition statement +- Metadata update via `kind: 0` with `["alg", "", ""]` tag +- Clients support both old and new during transition period + +**What it does NOT address**: +- No OpenTimestamps / pre-quantum anchoring +- No encryption-specific PQ migration (mentions NIP-04 but doesn't detail PQ KEM) +- No seed phrase root of trust +- No multi-scheme hedging +- No storage encryption + +**Comparison to our plan**: This is conceptually similar to our **Component 2 (Multi-Scheme Cross-Signed Key-Link Events)**. The cross-signing concept is the same — old key signs new key and vice versa. However, our plan improves on this significantly: +- We use **OpenTimestamps** to anchor the link pre-quantum (prevents backdated fraudulent links) +- We support **multiple PQ schemes simultaneously** (no algorithm consensus needed) +- We derive keys from a **seed phrase** rather than generating new random keys +- We include **ML-KEM** for encryption, not just signatures + +--- + +### 3. Issue #1971: "NIP-44: post-quantum security" (OPEN — most active discussion) +- **Author**: paulmillr +- **Created**: 2025-07-10 | **Updated**: 2026-04-13 +- **URL**: https://github.com/nostr-protocol/nips/issues/1971 +- **26 comments** — the most active PQ discussion in the Nostr community + +**Key discussion points**: + +1. **paulmillr's framing**: The main question is not *which* algorithm, but *how* new algorithms interop with old infrastructure. Suggests hybrid ECDH + KEM approach (like Apple iMessage, Signal, WhatsApp, OpenSSH). + +2. **The "whole architecture" problem**: paulmillr identifies the core dilemma — if secp256k1 is broken, everything breaks (signing, identity), so fixing only NIP-44 encryption is insufficient. An attacker who breaks nsec can derive any subkeys. + +3. **mikedilger**: The real threat is **collect-now-decrypt-later** for encrypted data on public relays. Favors SPHINCS+ (SLH-DSA) as most conservative. Advocates **decoupling encryption from identity** (PR #1647). + +4. **trbouma**: NIP-44 is already quantum-safe *if* you use a separate key not derived from nsec. The key management is the hard part. Suggests HSM/enclave for PQ keys. + +5. **vitorpamplona**: If nsec is broken, PQ shared keys derived from it are also broken. "Looks like we will have 2 Nostr's going" — doesn't think PQ signing can be backward compatible. + +6. **paulmillr's proposal**: Add a `pqsig` field to NIP-01 events. Old key signs new PQ key and vice versa. Need key rotation mechanism. + +7. **zfatherz**: Built a working PoC — version byte `0xF1`, ML-KEM-768 + X25519 hybrid KEM, XChaCha20-Poly1305 AEAD. ~15-22ms encrypt, ~1145B overhead per packet. Identified O(N) scaling problem for group chats. Later pivoted to O(1) sender-key approach. + - Repo: https://github.com/zfatherz/nip44-0xF1-pqc + +8. **alex-s168**: SPHINCS+ has small public keys (64 bytes) but 30KB signatures. Mentions Sphinx-in-the-Head for group signatures (but multi-MB signatures). + +9. **paulmillr's final critique** (2026-04-13): Points out that in zfatherz's design, npub is still derived from nsec, so breaking nsec breaks everything including derived subkeys. + +**Comparison to our plan**: This discussion validates several core elements of our plan: +- The **collect-now-decrypt-later** threat is exactly what our OTS anchoring (Component 3) and PQ encryption (Component 5) address +- The **"whole architecture" problem** paulmillr identifies is what our key-link events (Component 2) solve — we don't just fix encryption, we migrate identity +- The **algorithm uncertainty** concern is addressed by our multi-scheme approach +- The **key derivation from nsec** weakness paulmillr identifies is why our plan uses a **seed phrase** (Component 1) as the root of trust, not nsec-derived keys +- zfatherz's PoC validates that hybrid ML-KEM + ECDH is practical for 1:1 messaging today + +--- + +## Key Migration / Rotation Proposals (Not PQ-Specific but Relevant) + +### 4. PR #1647: "nip4e: Decoupling encryption from identity" (OPEN) +- **Author**: fiatjaf +- **Created**: 2024-12-14 +- **URL**: https://github.com/nostr-protocol/nips/pull/1647 + +**What it proposes**: +- Per-device encryption keys separate from identity key +- `kind: 10044` — announce encryption public key +- `kind: 4454` — announce a new device's local key +- `kind: 4455` — share encryption secret to a new device (NIP-44 encrypted) +- Solves: FROST/MuSig2 bunkers where identity key isn't available for encryption, offline encryption, per-device performance + +**Comparison to our plan**: This is **not PQ-specific** but is foundational infrastructure. Our plan's Component 5 (Quantum-Safe Self-Storage) and the encryption migration could build on this pattern. The concept of separating encryption keys from identity keys is shared. mikedilger explicitly referenced this PR in the #1971 discussion as a prerequisite for PQ encryption. + +--- + +### 5. Issue #2237: "Key Commitment and Theft-Proof Rotation" (CLOSED) +- **Author**: leonacostaok +- **Created**: 2026-02-24 | **Closed**: 2026-02-26 +- **URL**: https://github.com/nostr-protocol/nips/issues/2237 + +**What it proposes**: +- Argon2id-based hash commitment scheme for key rotation +- `kind: 13375` — publish commitment (argon2id of a secret passphrase with pubkey as salt) +- `kind: 13376` — rotation event from new key revealing the secret +- Clients verify: argon2id(revealed_secret, old_pubkey) == commitment +- Aims to prove theft vs. voluntary migration + +**Comparison to our plan**: Different approach to the rotation problem. Uses a memory-hard KDF commitment rather than cross-signing. Does not address PQ specifically. Our plan's cross-signed key-link events are more cryptographically rigorous (actual signatures, not passphrase commitments) and include OTS anchoring to prevent backdating. + +--- + +### 6. Other Key Migration PRs (OPEN, not reviewed in detail) +- **PR #2114**: NIP D8 Key Rotation +- **PR #2137**: Key migration +- **PR #2139**: Simpler key migration +- **PR #1452**: Key Migration and Revocation +- **PR #829**: NIP-41: simple account migration +- **PR #2361**: Decoupling identity and encryption keys in NIP-17 +- **PR #637**: NIP-37: general methods for dealing with lost keys + +These are all related to key migration/rotation but are not PQ-specific. Our plan's key-link event (Component 2) is in the same category but specifically designed for PQ migration with OTS anchoring and multi-scheme support. + +--- + +## Gap Analysis: What Our Plan Uniquely Addresses + +| Feature | PR #2185 | PR #391 | Issue #1971 | PR #1647 | **Our Plan** | +|---|---|---|---|---|---| +| PQ signatures | ✅ (ML-DSA-44 only) | ✅ (generic) | Discussed | ❌ | ✅ (multi-scheme) | +| PQ encryption (KEM) | ❌ | Mentioned | ✅ (hybrid KEM) | ❌ | ✅ (ML-KEM + OTP) | +| Key linking / migration | ❌ | ✅ (cross-signed) | Discussed | ❌ | ✅ (cross-signed + OTS) | +| Pre-quantum timestamping | ❌ | ❌ | ❌ | ❌ | ✅ (NIP-03 OTS) | +| Seed phrase root of trust | ❌ | ❌ | ❌ | ❌ | ✅ (BIP39/NIP-06) | +| Multi-scheme hedging | ❌ | ❌ | ❌ (discussed) | ❌ | ✅ | +| Existing nsec migration | ❌ | ❌ | ❌ | ❌ | ✅ | +| Storage encryption | ❌ | ❌ | Discussed | Partial | ✅ (OTP / symmetric) | +| HD wallet compartmentalization | ❌ | ❌ | ❌ | ❌ | ✅ | +| Decouple encryption from identity | ❌ | ❌ | Discussed | ✅ | ✅ (via seed derivation) | + +### Key differentiators of our plan: +1. **OpenTimestamps anchoring** — no other proposal addresses the backdating attack on key-link events +2. **Multi-scheme hedging** — no other proposal links to multiple PQ algorithms simultaneously +3. **Seed phrase as root of trust** — no other proposal uses BIP39 as the algorithm-agnostic foundation +4. **Existing nsec migration** — no other proposal addresses how raw nsec users migrate +5. **HD wallet compartmentalization** — no other proposal leverages BIP32 chain codes for quantum containment + +--- + +## Recommendations + +1. **Engage with PR #2185**: trbouma's PR is the most active PQ proposal. Our plan is a superset — we should comment with our more comprehensive approach, particularly the migration strategy and OTS anchoring that their PR lacks. + +2. **Engage with Issue #1971**: This is the main community discussion. paulmillr's final critique (npub derived from nsec → everything breaks) is directly addressed by our seed-phrase approach. We should contribute our key-link + OTS design. + +3. **Reference PR #1647**: fiatjaf's encryption/identity decoupling is complementary. Our plan could adopt the `kind: 10044` pattern for announcing PQ encryption keys. + +4. **Reference PR #391**: eznix86's cross-signed transition event is conceptually similar to our key-link event. We should acknowledge this prior art and explain how our approach extends it (OTS, multi-scheme, seed-based). + +5. **Watch zfatherz's work**: The `0xF1` hybrid KEM PoC is the most advanced PQ encryption implementation. Our plan's encryption component should be compatible with this approach. + +6. **Our plan fills a unique niche**: No existing proposal combines migration strategy + OTS anchoring + multi-scheme hedging + seed phrase root of trust. This is a strong differentiator for a NIP submission. diff --git a/research/nip_qr_two_account_flow.md b/research/nip_qr_two_account_flow.md new file mode 100644 index 0000000..19ede59 --- /dev/null +++ b/research/nip_qr_two_account_flow.md @@ -0,0 +1,249 @@ +# NIP-QR: Two-Account Flow Architecture + +## The Flow (as proposed) + +``` +User has Account #1 (existing nsec, their current Nostr identity) + │ + ▼ + ┌──────────────────────────────────────────┐ + │ PQ Web App (browser) │ + │ │ + │ Step 1: User creates Account #2 in │ + │ Amber (generates new seed) │ + │ │ + │ Step 2: Web app retrieves seed phrase │ + │ from Amber (Account #2) │ + │ │ + │ Step 3: Web app derives PQ keys from │ + │ seed (client-side WASM, liboqs) │ + │ - ML-DSA-65 keypair │ + │ - SLH-DSA-128s keypair │ + │ - ML-KEM-768 keypair │ + │ │ + │ Step 4: Web app builds NIP-QR event │ + │ and requests signatures: │ + │ - Amber Account #1 signs with │ + │ old nsec (secp256k1) │ + │ - Amber Account #2 signs with │ + │ seed-derived secp256k1 │ + │ - Web app PQ-signs with each │ + │ PQ private key (client-side) │ + │ │ + │ Step 5: Web app publishes NIP-QR event │ + │ to relays + requests OTS │ + └──────────────────────────────────────────┘ +``` + +## Why Two Accounts? + +The two-account design solves a fundamental problem: **how do you cryptographically link an existing nsec-based identity to PQ keys, when the existing nsec has no relationship to any seed phrase?** + +| Account | Key source | Role in NIP-QR | +|---|---|---| +| **Account #1** (existing) | Raw nsec (random, no seed) | The identity the social graph knows about. Signs to say "I am migrating to Account #2 and its PQ keys" | +| **Account #2** (new) | Seed phrase (BIP39 → NIP-06) | The bridge. Its secp256k1 key is derived from the same seed as the PQ keys. Signs to say "These PQ keys come from the same seed as me" | + +The cryptographic chain of trust is: + +``` +Account #1 (old identity, social graph knows this npub) + │ + │ signs: "I am the same entity as Account #2" + │ (secp256k1 Schnorr signature, valid pre-quantum) + ▼ +Account #2 (seed-derived secp256k1 key) + │ + │ same seed phrase ──────► PQ keys (ML-DSA, SLH-DSA, ML-KEM) + │ │ + │ signs: "These PQ keys │ sign: "We are derived from + │ come from my seed" │ the same seed as + │ (secp256k1 Schnorr) │ Account #2" + │ │ (PQ signatures) + ▼ ▼ + NIP-QR event (published under Account #1's pubkey) +``` + +## The NIP-QR Event Structure + +```json +{ + "kind": , + "pubkey": "", + "created_at": , + "tags": [ + ["successor", ""], + ["algorithm", "ml-dsa-65"], + ["algorithm", "slh-dsa-128s"], + ["algorithm", "ml-kem-768"] + ], + "content": "", + "sig": "" +} +``` + +### Content field (JSON string): + +```json +{ + "statement": "Identity is migrating to successor . All PQ keys listed below are derived from the same BIP39 seed as . This link is established pre-quantum.", + "successor_pubkey": "", + "successor_signature": "", + "pq_keys": [ + { + "algorithm": "ml-dsa-65", + "public_key": "", + "signature": "" + }, + { + "algorithm": "slh-dsa-128s", + "public_key": "", + "signature": "" + }, + { + "algorithm": "ml-kem-768", + "public_key": "", + "note": "KEM key for encryption; ownership asserted by successor_signature covering this key" + } + ] +} +``` + +### Signature layers: + +1. **Account #1 Schnorr signature** (the event `sig` field): Signs the event ID (standard NIP-01 signing). This is the old identity saying "I authorize this migration." + +2. **Account #2 Schnorr signature** (`successor_signature` in content): Signs the statement + all PQ public keys. This proves the seed-derived key endorses the PQ keys. Since both Account #2's secp256k1 key and the PQ keys come from the same seed, this transitively proves PQ key ownership. + +3. **PQ signatures** (in each `pq_keys` entry): Each PQ signature scheme signs the statement. These are the post-quantum signatures that remain valid after secp256k1 is broken. + +4. **ML-KEM**: Cannot sign (it's a KEM, not a signature scheme). Its ownership is proven by Account #2's signature covering its public key. + +## What the Web App Does vs. What Amber Does + +| Step | Who | What | +|---|---|---| +| Create Account #2 | Amber | Generates new BIP39 seed, derives secp256k1 key via NIP-06, stores both encrypted | +| Retrieve seed phrase | Amber → Web app | User exports seed from Amber's backup screen (or future NIP-46 method) | +| Derive PQ keys | Web app (WASM) | Uses liboqs-wasm to derive ML-DSA, SLH-DSA, ML-KEM keypairs from seed via HKDF | +| PQ sign statement | Web app (WASM) | Signs the link statement with each PQ private key | +| Sign with Account #1 | Amber | NIP-46 `signEvent` — old nsec signs the NIP-QR event | +| Sign with Account #2 | Amber | NIP-46 `signEvent` — seed-derived secp256k1 signs the statement | +| Assemble event | Web app | Combines all signatures into the NIP-QR event JSON | +| Publish to relays | Web app | Sends the event to Nostr relays | +| OpenTimestamps | Web app | Requests OTS attestation via NIP-03 for the event | + +### PQ Key Derivation from Seed + +The web app derives PQ keys from the BIP39 seed using a deterministic scheme: + +``` +seed = PBKDF2-HMAC-SHA512(mnemonic, passphrase, 2048 iterations) // standard BIP39 + +// Derive each PQ key from the seed using HKDF with algorithm-specific labels +ml_dsa_privkey = HKDF-SHA512(seed, "nostr-pq-ml-dsa-65", length=32) → expand to full key +slh_dsa_privkey = HKDF-SHA512(seed, "nostr-pq-slh-dsa-128s", length=32) → expand to full key +ml_kem_privkey = HKDF-SHA512(seed, "nostr-pq-ml-kem-768", length=32) → expand to full key +``` + +Note: The exact PQ key derivation from a seed needs careful specification. liboqs key generation is typically random, not deterministic from a seed. We need to use the seed as a deterministic RNG seed (e.g., `DRBG(seed || algorithm_label)`) to make PQ key generation reproducible. This is a detail to nail down in the NIP. + +## Seed Phrase Retrieval: Three Options + +### Option 1: Manual Export (No Amber changes) +- User goes to Amber's Account #2 backup screen +- Amber displays the seed words ([`SeedWordsPage.kt`](../Amber/app/src/main/java/com/greenart7c3/nostrsigner/ui/components/SeedWordsPage.kt:38)) +- User copies/types seed words into the web app +- Web app derives PQ keys client-side + +**Pros**: No Amber code changes. Available today. +**Cons**: User manually handles seed phrase (friction, potential for error). + +### Option 2: New NIP-46 Method (Amber changes) +- Add `get_seed_words` method to Amber's NIP-46 handler +- Web app requests seed words via NIP-46 +- Amber shows approval screen: "Web app X wants to read your seed phrase" +- On approval, seed words are sent to the web app via NIP-46 (encrypted with NIP-44) + +**Pros**: Smooth UX, no manual copy. +**Cons**: Seed phrase transmitted over NIP-46 (even though encrypted). Requires Amber modification. Security concern — a malicious web app could steal the seed. + +### Option 3: Web App Generates Seed (No Amber changes for retrieval) +- Web app generates a new BIP39 seed phrase client-side (WASM) +- Web app derives both secp256k1 (Account #2) and PQ keys from the seed +- Web app displays the seed phrase for the user to write down +- User manually imports the seed phrase into Amber as Account #2 (via Amber's mnemonic login screen) +- Web app already has the seed, so no retrieval needed + +**Pros**: Web app has the seed from the start. No retrieval needed. User imports to Amber for future signing. +**Cons**: User must manually import seed into Amber (but this is a one-time step). Seed exists in browser memory temporarily. + +**Recommendation**: Start with **Option 3** (web app generates seed, user imports to Amber). It requires zero Amber changes and gives the web app immediate access to the seed for PQ key derivation. Later, Option 2 can be added for a smoother UX. + +## Complete User Journey + +### First Visit (Migration) +1. User opens the PQ web app in their browser +2. Web app connects to Amber via NIP-46 (or NIP-07 browser extension) +3. Web app requests Account #1's public key → gets existing npub +4. Web app generates a new 12-word BIP39 seed phrase (client-side WASM) +5. Web app displays the seed phrase: **"Write this down. This is your quantum-safe backup."** +6. Web app derives Account #2's secp256k1 key from seed (NIP-06: `m/44'/1237'/0'/0/0`) +7. Web app derives PQ keys from seed (liboqs-wasm) +8. Web app constructs the NIP-QR event content +9. Web app PQ-signs the statement with each PQ private key (client-side) +10. Web app requests Amber to sign the statement with Account #2's secp256k1 key + - But wait — Account #2 doesn't exist in Amber yet! The user needs to import the seed first. + - **Pause**: Web app instructs user: "Open Amber, create new account with this seed phrase" + - User imports seed into Amber as Account #2 + - Web app now requests Amber (Account #2) to sign the statement +11. Web app requests Amber (Account #1) to sign the NIP-QR event +12. Web app assembles the complete event with all signatures +13. Web app publishes the event to relays +14. Web app requests OpenTimestamps attestation (NIP-03) +15. Web app shows success: "Your identity is now quantum-resistant. Save your seed phrase." + +### Future Login (After Migration) +1. User opens any PQ-aware Nostr client +2. Client finds the NIP-QR event for their npub (Account #1) +3. Client sees: Account #1 → Account #2 → PQ keys +4. User signs in with Amber (Account #2, seed-derived) +5. Client can now verify PQ signatures and use PQ encryption + +### Post-Quantum Scenario (After secp256k1 is broken) +1. Attacker breaks Account #1's secp256k1 key via Shor's algorithm +2. Attacker can forge Account #1 signatures, but: + - Cannot forge PQ signatures (ML-DSA, SLH-DSA still secure) + - Cannot backdate a fraudulent NIP-QR event (OTS proof anchors the real one) + - Cannot forge Account #2's signature (also secp256k1, also broken — BUT the OTS-anchored NIP-QR event already established the link pre-quantum) +3. Clients verify: "The NIP-QR event with the earliest valid OTS proof wins" +4. The real NIP-QR event (anchored pre-quantum) is trusted over any fraudulent event +5. PQ signatures on the NIP-QR event remain valid and prove the PQ keys are the user's + +## What Needs to Be Built + +### Web App (new project) +- [ ] Static HTML/JS/WASM site (no backend with secrets) +- [ ] BIP39 seed generation (WASM or JS) +- [ ] NIP-06 key derivation from seed (secp256k1) +- [ ] liboqs-wasm integration for PQ key generation and signing +- [ ] PQ key derivation from seed (deterministic via HKDF/DRBG) +- [ ] NIP-46 client (connect to Amber) +- [ ] NIP-07 client (fallback for browser extension signers) +- [ ] NIP-QR event builder and assembler +- [ ] Relay publisher (WebSocket) +- [ ] NIP-03 OpenTimestamps client +- [ ] UI: seed display, progress, verification + +### Amber (no changes required for Option 3) +- Already supports: multiple accounts, seed phrase login, NIP-46 signing +- Future enhancement (Option 2): `get_seed_words` NIP-46 method + +### NIP-QR Specification (new NIP document) +- [ ] Event kind number +- [ ] Content schema +- [ ] Signature scheme (who signs what) +- [ ] PQ key derivation from seed (exact HKDF labels, DRBG seeding) +- [ ] OTS requirement +- [ ] Client verification algorithm +- [ ] Revocation/update mechanism diff --git a/research/pq_js_packages_survey.md b/research/pq_js_packages_survey.md new file mode 100644 index 0000000..ec839ac --- /dev/null +++ b/research/pq_js_packages_survey.md @@ -0,0 +1,281 @@ +# Post-Quantum Cryptography: JavaScript Package Survey + +## Executive Summary + +**We do NOT need to write our own implementations.** There are mature, well-maintained JavaScript packages for all NIST-standardized post-quantum algorithms. The standout recommendation is **[`@noble/post-quantum`](https://www.npmjs.com/package/@noble/post-quantum)** — a pure JavaScript implementation of all FIPS 203/204/205 algorithms, from the highly respected noble cryptography family (335 GitHub stars, MIT licensed, self-audited, actively maintained). + +For our PQ Nostr web app, `@noble/post-quantum` covers everything we need: ML-DSA (signatures), SLH-DSA (signatures), ML-KEM (encryption key agreement), and hybrid KEMs (X-Wing). No WASM required, works in browsers natively. + +--- + +## NIST-Standardized PQ Algorithms (FIPS) + +| Algorithm | FIPS Standard | Type | Use Case | Status | +|---|---|---|---|---| +| ML-KEM (Kyber) | FIPS 203 | KEM (key encapsulation) | Encryption key agreement | Finalized 2024 | +| ML-DSA (Dilithium) | FIPS 204 | Digital signature | Authentication | Finalized 2024 | +| SLH-DSA (SPHINCS+) | FIPS 205 | Digital signature | Authentication (conservative) | Finalized 2024 | +| FN-DSA (Falcon) | FIPS 206 | Digital signature | Authentication (compact) | Still draft | + +### Key and Signature Sizes + +| Variant | Public key | Secret key | Signature / Ciphertext | +|---|---:|---:|---:| +| ML-KEM-512 | 800 B | 1632 B | 768 B | +| ML-KEM-768 | 1184 B | 2400 B | 1088 B | +| ML-KEM-1024 | 1568 B | 3168 B | 1568 B | +| ML-DSA-44 | 1312 B | 2560 B | 2420 B | +| ML-DSA-65 | 1952 B | 4032 B | 3309 B | +| ML-DSA-87 | 2592 B | 4896 B | 4627 B | +| Falcon-512 | 897 B | 1281 B | ~666 B | +| Falcon-1024 | 1793 B | 2305 B | ~1280 B | +| SLH-DSA-128f | 32 B | 64 B | 17088 B | +| SLH-DSA-128s | 32 B | 64 B | 7856 B | +| SLH-DSA-192f | 48 B | 96 B | 35664 B | +| SLH-DSA-192s | 48 B | 96 B | 16224 B | +| SLH-DSA-256f | 64 B | 128 B | 49856 B | +| SLH-DSA-256s | 64 B | 128 B | 29792 B | + +--- + +## Recommended Package: `@noble/post-quantum` + +**npm**: https://www.npmjs.com/package/@noble/post-quantum +**GitHub**: https://github.com/paulmillr/noble-post-quantum +**Version**: 0.6.1 | **License**: MIT | **Stars**: 335 + +### Why This Is the Best Choice + +1. **Covers all algorithms we need**: ML-KEM, ML-DSA, SLH-DSA, Falcon, and hybrid KEMs (X-Wing, KitchenSink) — all in one package +2. **Pure JavaScript** — no WASM, no native bindings, works in any browser and Node.js +3. **From the noble family** — Paul Miller's noble libraries (noble-hashes, noble-curves, noble-ciphers, noble-secp256k1) are the most trusted JS crypto libraries, used widely across the ecosystem +4. **Tree-shakeable** — only the algorithms you import are bundled (16KB gzipped for everything) +5. **Self-audited** at v0.6.1 (April 2026) +6. **Deterministic key generation from seed** — `keygen(seed)` accepts an optional seed, which is exactly what we need for deriving PQ keys from a BIP39 seed phrase +7. **Actively maintained** — last updated June 2026 +8. **No external dependencies** beyond noble-hashes and noble-curves (same author, same trust model) + +### API Examples + +#### ML-DSA-65 (Dilithium) — for PQ signatures +```js +import { ml_dsa65 } from '@noble/post-quantum/ml-dsa.js'; + +// Deterministic keygen from seed (for our seed-phrase derivation) +const seed = new Uint8Array(32); // derived from BIP39 seed via HKDF +const keys = ml_dsa65.keygen(seed); + +// Sign +const msg = new TextEncoder().encode('link statement'); +const sig = ml_dsa65.sign(msg, keys.secretKey); + +// Verify +const isValid = ml_dsa65.verify(sig, msg, keys.publicKey); +``` + +#### SLH-DSA-128s (SPHINCS+) — conservative hash-based signatures +```js +import { slh_dsa_sha2_128s } from '@noble/post-quantum/slh-dsa.js'; + +const keys = slh_dsa_sha2_128s.keygen(seed); +const sig = slh_dsa_sha2_128s.sign(msg, keys.secretKey); +const isValid = slh_dsa_sha2_128s.verify(sig, msg, keys.publicKey); +``` + +#### ML-KEM-768 (Kyber) — for PQ encryption key agreement +```js +import { ml_kem768 } from '@noble/post-quantum/ml-kem.js'; + +// Alice generates keys +const aliceKeys = ml_kem768.keygen(seed); + +// Bob encapsulates a shared secret for Alice +const { cipherText, sharedSecret: bobShared } = ml_kem768.encapsulate(aliceKeys.publicKey); + +// Alice decapsulates +const aliceShared = ml_kem768.decapsulate(cipherText, aliceKeys.secretKey); +// aliceShared === bobShared +``` + +#### X-Wing (Hybrid KEM — ML-KEM-768 + X25519) +```js +import { XWing } from '@noble/post-quantum/hybrid.js'; + +const keys = XWing.keygen(); +const { cipherText, sharedSecret } = XWing.encapsulate(keys.publicKey); +const decapSecret = XWing.decapsulate(cipherText, keys.secretKey); +``` + +### Speed Benchmarks (Apple M4, operations/sec) + +| Primitive | Keygen | Signing | Verification | Shared secret | +|---|---:|---:|---:|---:| +| ML-KEM-768 | 4661 | — | — | 4089 | +| ML-DSA-65 | 669 | 271 | 565 | — | +| Falcon-512 | 14 | 749 | 2160 | — | +| SLH-DSA-SHA2-192f | 235 | 8 | 159 | — | +| SLH-DSA-SHA2-128s | ~5 | <1 | ~1000 | — | +| x/ed25519 (pre-quantum) | 12648 | 6157 | 1255 | 1981 | + +**Key takeaways for our use case**: +- ML-DSA-65 signing: ~3.7ms per signature — fast enough for a one-time key-link event +- ML-KEM-768 keygen + encapsulate: ~0.4ms — instant +- SLH-DSA is slow (117ms-2900ms per signature depending on variant) but it's a one-time cost for the key-link event +- There's an experimental WASM branch with 80% faster ML-KEM, 30% faster ML-DSA, 15x faster SLH-DSA-SHAKE + +### Security Notes +- "The library has not been independently audited yet" (self-audited at v0.6.1) +- No constant-time guarantees (side-channel attacks possible) — acceptable for our use case since PQ private keys are used in the browser, not on a server +- Uses `crypto.getRandomValues` for randomness (browser CSPRNG) + +--- + +## Alternative Packages + +### `@oqs/liboqs-js` — WASM bindings to liboqs +- **npm**: https://www.npmjs.com/package/@oqs/liboqs-js +- **GitHub**: https://github.com/open-quantum-safe/liboqs-js +- **Version**: 0.15.1 | **License**: MIT (liboqs is OQS license) +- **Stars**: 11 (new repo, created Feb 2026) + +**Pros**: +- Wraps liboqs (the reference implementation from Open Quantum Safe project) +- Supports the widest range of algorithms: ML-KEM, ML-DSA, Falcon, Classic McEliece, FrodoKEM, NTRU, HQC, CROSS, Mayo, UOV, SNOVA +- WASM performance may be better for some algorithms + +**Cons**: +- Very new (created Feb 2026, only 11 stars) +- WASM adds complexity (loading .wasm files, larger bundle) +- Less established than noble +- License is "NOASSERTION" (liboqs has its own license, not standard MIT) + +**When to use**: If we need algorithms not in noble (e.g., Classic McEliece, FrodoKEM), or if WASM performance is critical. + +### `mlkem` (crystals-kyber-js) +- **npm**: https://www.npmjs.com/package/mlkem +- **GitHub**: https://github.com/dajiaji/crystals-kyber-js +- **Version**: 2.7.0 | **License**: MIT | **Stars**: 62 + +**Pros**: +- Focused, well-maintained ML-KEM implementation +- Pure TypeScript, no dependencies +- Used by `@hpke/hybridkem-x-wing` for HPKE integration +- "The fastest pure TypeScript ML-KEM implementation" + +**Cons**: +- Only ML-KEM (no signature algorithms) +- Would need a separate package for ML-DSA/SLH-DSA + +**When to use**: If we only need ML-KEM for encryption and want a focused, tested library. + +### `@hpke/hybridkem-x-wing` +- **npm**: https://www.npmjs.com/package/@hpke/hybridkem-x-wing +- **Version**: 0.7.0 | **License**: MIT + +HPKE (Hybrid Public Key Encryption) module for X-Wing (ML-KEM-768 + X25519 hybrid KEM). Part of the hpke-js family. Useful if we want to use the HPKE framework for PQ encryption, which is the IETF standard for hybrid key exchange. + +### `dilithium-crystals-js` +- **npm**: https://www.npmjs.com/package/dilithium-crystals-js +- **Version**: 1.1.3 + +Dilithium implementation for Node.js and browsers. Older, less maintained than noble. + +### `@stenvault/pqc-wasm` +- **npm**: https://www.npmjs.com/package/@stenvault/pqc-wasm +- **Version**: 0.2.2 + +ML-KEM-768 + ML-DSA-65 WASM wrapper (RustCrypto compiled to WebAssembly). New, untested. + +### `pqclean` +- **npm**: https://www.npmjs.com/package/pqclean +- **Version**: 0.8.1 + +Node.js bindings for PQClean (all PQ implementations). Node-only, not browser-compatible. + +--- + +## What We Need for the PQ Nostr Web App + +| Need | Algorithm | Package | Status | +|---|---|---|---| +| PQ signature (primary) | ML-DSA-65 | `@noble/post-quantum` | ✅ Ready | +| PQ signature (conservative) | SLH-DSA-128s | `@noble/post-quantum` | ✅ Ready | +| PQ encryption key agreement | ML-KEM-768 | `@noble/post-quantum` | ✅ Ready | +| Hybrid KEM (PQ + classical) | X-Wing (ML-KEM-768 + X25519) | `@noble/post-quantum` | ✅ Ready | +| Deterministic keygen from seed | All above | `@noble/post-quantum` | ✅ `keygen(seed)` supported | +| Browser compatibility | All above | `@noble/post-quantum` | ✅ Pure JS, no WASM needed | +| BIP39 seed phrase | Mnemonic → seed | `@scure/bip39` or similar | ✅ Separate package needed | + +### Recommended Package Set for the Web App + +```json +{ + "dependencies": { + "@noble/post-quantum": "^0.6.1", + "@noble/curves": "^2.2.0", + "@noble/hashes": "^2.2.0", + "@scure/bip39": "^1.5.0", + "@scure/bip32": "^1.6.0", + "nostr-tools": "^2.10.0" + } +} +``` + +- `@noble/post-quantum` — PQ algorithms (ML-DSA, SLH-DSA, ML-KEM, X-Wing) +- `@noble/curves` — secp256k1 for NIP-01 signing and NIP-06 key derivation +- `@noble/hashes` — SHA-256, HKDF, etc. +- `@scure/bip39` — BIP39 mnemonic generation and validation +- `@scure/bip32` — BIP32 HD wallet derivation (for NIP-06) +- `nostr-tools` — Nostr event creation, relay communication, NIP-46 + +All of these are from the same trust ecosystem (noble / scure family by Paul Miller and Alex Obukhov). + +--- + +## PQ Key Derivation from BIP39 Seed + +One critical detail: `@noble/post-quantum`'s `keygen(seed)` functions accept a seed parameter for deterministic key generation. The seed length varies by algorithm: + +| Algorithm | Seed length | +|---|---| +| ML-KEM-768 | 64 bytes | +| ML-DSA-65 | 32 bytes | +| SLH-DSA-128s | 32 bytes (or 64 for some variants) | +| Falcon-512 | 48 bytes | + +Our derivation scheme from BIP39 seed: + +```js +import { hkdf } from '@noble/hashes/hkdf'; +import { sha512 } from '@noble/hashes/sha512'; + +// BIP39 seed (64 bytes from PBKDF2-HMAC-SHA512) +const bip39Seed = ...; // 64 bytes + +// Derive PQ key seeds using HKDF with algorithm-specific labels +const mlDsaSeed = hkdf(sha512, bip39Seed, undefined, 'nostr-pq-ml-dsa-65', 32); +const slhDsaSeed = hkdf(sha512, bip39Seed, undefined, 'nostr-pq-slh-dsa-128s', 32); +const mlKemSeed = hkdf(sha512, bip39Seed, undefined, 'nostr-pq-ml-kem-768', 64); + +// Generate PQ keypairs deterministically +const mlDsaKeys = ml_dsa65.keygen(mlDsaSeed); +const slhDsaKeys = slh_dsa_sha2_128s.keygen(slhDsaSeed); +const mlKemKeys = ml_kem768.keygen(mlKemSeed); +``` + +This means: **same seed phrase → same PQ keys, every time**. The user can recover their PQ keys by entering their seed phrase in any implementation of this scheme. + +--- + +## Summary: No Custom Implementations Needed + +| Question | Answer | +|---|---| +| Do we need to write PQ crypto implementations? | **No** — `@noble/post-quantum` covers all algorithms | +| Do we need WASM? | **No** — pure JS is fast enough for our one-time key-link event | +| Are the packages maintained? | **Yes** — noble-post-quantum updated June 2026, 335 stars | +| Are they audited? | Self-audited at v0.6.1. No independent audit yet. | +| Do they support deterministic keygen from seed? | **Yes** — `keygen(seed)` for all algorithms | +| Do they work in browsers? | **Yes** — pure JS, uses `crypto.getRandomValues` | +| What's the bundle size? | 16KB gzipped for all algorithms (tree-shakeable) | +| What do we need to write ourselves? | The NIP-QR event builder, key derivation scheme (HKDF labels), relay publishing, OTS client, UI | diff --git a/upload.sh b/upload.sh new file mode 100755 index 0000000..0a4349a --- /dev/null +++ b/upload.sh @@ -0,0 +1,43 @@ +#!/bin/bash +# +# Upload the post-quantum Nostr web app to the server. +# +# Usage: ./upload.sh +# +# Uploads the contents of www/ to ubuntu@laantungir.net:html/post-quantum/ +# The site will be accessible at https://laantungir.net/post-quantum +# + +set -e + +SERVER="ubuntu@laantungir.net" +REMOTE_DIR="html/post-quantum" +LOCAL_DIR="$(dirname "$0")/www" + +echo "🔒 Post-Quantum Nostr — Upload" +echo "==============================" +echo "Server: $SERVER" +echo "Remote: $REMOTE_DIR" +echo "Local: $LOCAL_DIR" +echo "" + +# Ensure the bundle is up to date +echo "📦 Building PQ crypto bundle..." +node "$(dirname "$0")/build-pq-bundle.js" +echo "" + +# Create the remote directory if it doesn't exist +echo "📁 Ensuring remote directory exists..." +ssh "$SERVER" "mkdir -p $REMOTE_DIR" +echo "" + +# Upload files using rsync +echo "📤 Uploading files..." +rsync -avz --delete \ + --exclude='*.map' \ + "$LOCAL_DIR/" \ + "$SERVER:$REMOTE_DIR/" +echo "" + +echo "✅ Upload complete!" +echo "🌐 Site available at: https://laantungir.net/post-quantum" diff --git a/why_what_how.md b/why_what_how.md new file mode 100644 index 0000000..fd22758 --- /dev/null +++ b/why_what_how.md @@ -0,0 +1,193 @@ +# Post-Quantum Nostr: Why, What, How + +## Why: The Problem + +### Quantum computers will break Nostr's cryptography + +Nostr's entire security model rests on secp256k1 elliptic curve cryptography: + +- **Identity** — your pubkey IS your identity. It's in every event, it's your npub, it's what NIP-05 verifies, it's what the social graph is built on. +- **Authentication** — every event is signed with a Schnorr signature over secp256k1. Signature verification requires the public key, which is published in plaintext on every event. +- **Encryption (NIP-04/NIP-44)** — both use secp256k1 ECDH to derive a shared secret, then symmetric encryption (AES-256-CBC or ChaCha20+HMAC). + +**Shor's algorithm** breaks the elliptic curve discrete logarithm problem in polynomial time. With a sufficiently large fault-tolerant quantum computer, an attacker who has recorded any Nostr event can: + +1. Extract the public key from the event +2. Run Shor's algorithm to recover the private key +3. Forge signatures, decrypt all NIP-04/NIP-44 messages, and impersonate the user + +This is not a theoretical concern. NIST has already standardized post-quantum algorithms (FIPS 203, 204, 205) and published guidance prohibiting classical cryptography after 2035. Apple, Signal, WhatsApp, and OpenSSH have already deployed post-quantum encryption. The migration is happening now. + +### Why Bitcoin's hash-the-pubkey trick doesn't work for Nostr + +Bitcoin has limited quantum resistance because addresses are hashes of pubkeys, and pubkeys are only revealed at spend time. This doesn't work for Nostr because: + +| Property | Bitcoin | Nostr | +|---|---|---| +| Identifier | Address (hash of pubkey) | Pubkey directly | +| Pubkey revealed | At spend time only | On every event | +| After reveal | UTXO consumed, key discarded | Identity persists, key reused | + +In Nostr, the pubkey is load-bearing: it IS the identity, it must be revealed for signature verification, and it's reused across thousands of events over years. Hashing the pubkey would give zero seconds of quantum resistance — the pubkey must be revealed the moment you publish your first event. + +### The collect-now-decrypt-later threat + +Even if quantum computers don't exist yet, adversaries can record encrypted Nostr messages today and decrypt them later when a quantum computer becomes available. Every NIP-04 and NIP-44 message stored on public relays is vulnerable to this attack. The symmetric ciphers (ChaCha20, AES-256) are quantum-safe, but the ECDH key agreement that produces the shared secret is not. + +### The standardization uncertainty problem + +NIST finalized ML-KEM (Kyber), ML-DSA (Dilithium), and SLH-DSA (SPHINCS+) in 2024, but the history of post-quantum cryptography includes broken schemes — SIKE was a NIST finalist broken in 2022 by a classical algorithm that ran in an hour on a laptop. If the Nostr community picks one algorithm and it's later broken, we'd be back to square one. Picking a single algorithm is risky. + +### The migration problem + +Most Nostr users today have a raw nsec (random private key) with no seed phrase. Their identity, social graph, NIP-05 verification, and follower relationships are all tied to that key. Any post-quantum migration that requires users to abandon their identity and start over is a non-starter. The migration must preserve identity, social graph, and user experience. + +--- + +## What: Our Solution + +A migration strategy that brings post-quantum security to Nostr: + +- **Without breaking the social graph** — existing follows and identity carry forward +- **Without requiring consensus on a single PQ algorithm** — link to all viable schemes now, converge later +- **Without forcing existing users to abandon their identities** — the seed phrase becomes the new root of trust +- **With pre-quantum timestamping** — establish links now, before quantum computers exist, anchored to the Bitcoin blockchain + +### The six components + +1. **Seed phrase as algorithm-agnostic root of trust** — A BIP39 mnemonic is pure entropy (128/256 bits). It's not tied to any cryptographic algorithm. PQ keys are derived from it deterministically using HKDF with algorithm-specific labels. Same seed always produces the same PQ keys. The seed is quantum-safe (symmetric KDF, Grover's quadratic speedup is negligible). + +2. **Multi-scheme cross-signed key-link events** — A new Nostr event (NIP-QR) that links a user's secp256k1 identity to multiple PQ keys simultaneously: ML-DSA-65 (lattice-based signatures), SLH-DSA-128s (hash-based signatures), and ML-KEM-768 (lattice-based KEM for encryption). Each PQ scheme independently signs the link statement. If one scheme is later broken, the others remain valid. + +3. **OpenTimestamps for pre-quantum anchoring** — The key-link event is timestamped via NIP-03 (OpenTimestamps), anchoring it to a Bitcoin block. This prevents a future quantum attacker from backdating a fraudulent key-link event, because they cannot produce a valid OTS proof for a past Bitcoin block (re-mining historical blocks is infeasible even with a quantum computer). + +4. **Migrating existing users with raw nsec** — Users who logged in with a raw nsec (no seed phrase) generate a new seed phrase. A two-account migration event links their old identity (Account #1, raw nsec) to a seed-derived successor (Account #2) and its PQ keys. The old identity signs to authorize the migration; the new identity signs to endorse the PQ keys. + +5. **Quantum-safe self-storage** — Sensitive data stored on relays (kind 30078) can be encrypted with either a one-time pad (information-theoretic security, stronger than any computational assumption) or a symmetric key derived from the seed (128-bit PQ security via Grover's). This separates data confidentiality from identity authentication — breaking secp256k1 compromises identity but not stored data. + +6. **Deterministic wallet compartmentalization** — BIP32/BIP39 HD wallets (NIP-06) provide quantum compartmentalization through their tree structure. When Shor's algorithm recovers a leaf private key, the attacker cannot climb the tree without the parent chain code (a secret symmetric value). Hardened derivation at the account level means breaking one account doesn't compromise others. + +### What this is NOT + +- It is not a new signature scheme for every event — PQ signatures are used additively for the one-time key-link event, not for routine posts +- It is not a consensus requirement — the community doesn't need to agree on one PQ algorithm; multiple schemes are linked simultaneously +- It is not a hard fork — legacy clients continue working; PQ is additive +- It is not a trust-the-server solution — all cryptography happens client-side in the browser + +--- + +## How: How It Works + +### The cryptographic chain of trust + +``` +Account #1 (old identity, social graph knows this npub) + | + | signs: "I am migrating to Account #2 and its PQ keys" + | (secp256k1 Schnorr signature, valid pre-quantum) + v +Account #2 (seed-derived secp256k1 key) + | + | same seed phrase ---------> PQ keys (ML-DSA, SLH-DSA, ML-KEM) + | | + | signs: "These PQ keys come | sign: "We are derived from + | from my seed" | the same seed as Account #2" + | (secp256k1 Schnorr) | (PQ signatures) + v v + NIP-QR event (published under Account #1's pubkey) + | + | OpenTimestamps anchor to Bitcoin block + | (proves the event existed at this time, pre-quantum) + v + Published to Nostr relays +``` + +### The NIP-QR event + +A kind 30078 event (NIP-78 application-specific data) containing: + +```json +{ + "kind": 30078, + "pubkey": "", + "tags": [ + ["d", "nip-qr-migration"], + ["successor", ""], + ["algorithm", "ml-dsa-65"], + ["algorithm", "slh-dsa-128s"], + ["algorithm", "ml-kem-768"] + ], + "content": "", + "sig": "" +} +``` + +The content JSON contains: +- A link statement binding all keys together +- Each PQ public key (base64-encoded) +- Each PQ signature over the statement (ML-DSA and SLH-DSA can sign; ML-KEM cannot — it's a KEM, not a signature scheme) +- The successor's secp256k1 signature over the statement (proves the seed-derived key endorses the PQ keys) + +### PQ key derivation from seed + +Post-quantum keys are deterministically derived from the BIP39 seed using HKDF: + +``` +seed = PBKDF2-HMAC-SHA512(mnemonic, "mnemonic", 2048) // standard BIP39 + +ml_dsa_seed = HKDF-SHA512(seed, "nostr-pq-ml-dsa-65", 32) -> ML-DSA-65 keypair +slh_dsa_seed = HKDF-SHA512(seed, "nostr-pq-slh-dsa-128s", 48) -> SLH-DSA-128s keypair +ml_kem_seed = HKDF-SHA512(seed, "nostr-pq-ml-kem-768", 64) -> ML-KEM-768 keypair +``` + +Same seed phrase always produces the same PQ keys. Users can recover their PQ keys by entering their seed phrase in any implementation of this scheme. + +### The web app + +A static web page (no backend with secrets) that: + +1. **Signs in** — uses nostr-login-lite (NIP-46 bunker, seed phrase, or browser extension) to authenticate with the user's existing Nostr identity +2. **Generates a seed phrase** — 12-word BIP39 mnemonic, generated client-side, never sent to any server +3. **Derives PQ keys** — uses `@noble/post-quantum` (pure JavaScript, no WASM) to generate ML-DSA-65, SLH-DSA-128s, and ML-KEM-768 keypairs from the seed +4. **Signs the NIP-QR event** — PQ signatures are created in the browser; the secp256k1 signature is requested from the user's signer (via `window.nostr.signEvent()`) +5. **Publishes to relays** — sends the fully signed event to Nostr relays via direct WebSocket +6. **Shows the result** — displays the complete event JSON and event ID + +All cryptographic operations happen either in the browser (JavaScript/WASM) or in the user's signer (Amber, browser extension, or bunker). No private key ever touches a server. + +### The algorithms + +| Algorithm | FIPS | Type | Key size | Signature size | Why we use it | +|---|---|---|---|---|---| +| ML-DSA-65 | 204 | Signature | 1952 bytes | 3309 bytes | Primary PQ signature (lattice-based, NIST-standardized) | +| SLH-DSA-128s | 205 | Signature | 32 bytes | 7856 bytes | Conservative fallback (hash-based, very conservative assumptions) | +| ML-KEM-768 | 203 | KEM | 1184 bytes | 1088 bytes (ciphertext) | PQ encryption key agreement (lattice-based, used by Signal, Apple) | + +We use multiple schemes simultaneously because: +- If ML-DSA is broken (lattice-based), SLH-DSA (hash-based) remains valid +- If all lattice schemes are broken, only the hash-based fallback remains +- The community doesn't need to agree on one algorithm — clients can verify whichever subset they support + +### Post-quantum scenario (when secp256k1 is broken) + +1. Attacker breaks Account #1's secp256k1 key via Shor's algorithm +2. Attacker can forge Account #1 signatures, but: + - **Cannot forge PQ signatures** — ML-DSA and SLH-DSA remain secure + - **Cannot backdate a fraudulent NIP-QR event** — the real event has an OTS proof anchored to a Bitcoin block that predates quantum computers + - **Cannot decrypt PQ-encrypted messages** — ML-KEM remains secure +3. Clients verify: "the NIP-QR event with the earliest valid OTS proof wins" +4. The real NIP-QR event (anchored pre-quantum) is cryptographically distinguishable from any fraudulent event +5. The user's identity is preserved through the OTS-anchored migration event +6. The user continues signing with Account #2 and PQ keys + +### The demo + +A working demo is deployed at **https://laantungir.net/post-quantum** that implements the full flow: sign in, generate seed, derive PQ keys, sign the NIP-QR event, and publish to relays. It uses: + +- `@noble/post-quantum` for PQ cryptography (ML-DSA-65, SLH-DSA-128s, ML-KEM-768) +- `@scure/bip39` for seed phrase generation +- `@scure/bip32` for NIP-06 key derivation +- `nostr-login-lite` for authentication +- Direct WebSocket for relay publishing + +The source code is in [`www/index.html`](www/index.html) and [`www/js/pq-crypto.mjs`](www/js/pq-crypto.mjs). To deploy updates, run `./upload.sh`. diff --git a/www/css/GenShin.ttf b/www/css/GenShin.ttf new file mode 100644 index 0000000..0c6a5c7 Binary files /dev/null and b/www/css/GenShin.ttf differ diff --git a/www/css/NotoMono-Regular.ttf b/www/css/NotoMono-Regular.ttf new file mode 100644 index 0000000..3560a3a Binary files /dev/null and b/www/css/NotoMono-Regular.ttf differ diff --git a/www/css/client.css b/www/css/client.css new file mode 100644 index 0000000..e650ed6 --- /dev/null +++ b/www/css/client.css @@ -0,0 +1,1689 @@ +/* ************************************************************************* */ +/* CLIENT CSS */ +/* ************************************************************************* */ + +/* CSS SETTINGS SHARED ACROSS ALL CLIENT PAGES. */ + +/* + * ========================================================================= + * CSS PLACEMENT RULES (FOR AI AGENTS AND DEVELOPERS) + * ========================================================================= + * + * This project uses a two-tier CSS architecture: + * + * 1. client.css — Global shared styles (this file) + * 2. + + + +
+
+
+
Post-Quantum Nostr
+
+
+ +
+
+ +
+
+ + +
+
Migration Steps
+
+ + 1. Sign in with your Nostr identity +
+
+ + 2. Generate a quantum-safe seed phrase +
+
+ + 3. Derive post-quantum keys from seed +
+
+ + 4. Sign the NIP-QR migration event +
+
+ + 5. Publish event to relays +
+
+ + 6. Timestamp the event with OpenTimestamps +
+ +
+ + + + +
+
Post-Quantum Nostr
+
+ Make your Nostr identity quantum-resistant +
+
+ Sign in with your Nostr signer to begin. Your npub won't change, your followers stay, + and your social graph is preserved. We'll generate a quantum-safe seed phrase and link + it to your current identity. +
+ +
+ +
+ + +
+
Post-Quantum Migration
+
+ Connected as: +
+
+
+ Current Bitcoin block height: +
Loading...
+
+
+ This tool will: +
    +
  1. Generate a new seed phrase (your quantum-safe backup)
  2. +
  3. Derive post-quantum keys from that seed via BIP32 paths (ML-DSA-44, ML-DSA-65, SLH-DSA-128s, Falcon-512, ML-KEM-768)
  4. +
  5. Sign a NIP-QR migration event linking your identity to the PQ keys
  6. +
  7. Publish the event to relays
  8. +
+
+
+ Important: Your seed phrase is generated and handled entirely in your browser. + It is never sent to any server. Write it down on paper — it's your quantum-safe backup. +
+ +
+ + +
+
Your Quantum-Safe Seed Phrase
+
+ We've generated a new 12-word seed phrase. This is your quantum-safe root of trust. + Write it down on paper. Never store it digitally. Never share it with anyone. +
+
+
+ + +
+ + +
+ + +
+
Deriving Post-Quantum Keys
+
+ Deriving post-quantum keys from your seed phrase via BIP32 paths. + All keys are generated deterministically — the same seed will always produce the same keys. +
+
+
+
+
+
+
+
+
ML-DSA-44 (Dilithium)
+
FIPS 204 · Cat 2 · 1312-byte pubkey · m/44'/1237'/0'/0/1
+
+
+
+
+
+
+
ML-DSA-65 (Dilithium)
+
FIPS 204 · Cat 3 · 1952-byte pubkey · m/44'/1237'/0'/0/2
+
+
+
+
+
+
+
SLH-DSA-128s (SPHINCS+)
+
FIPS 205 · Cat 1 · 32-byte pubkey · m/44'/1237'/0'/0/3+4
+
+
+
+
+
+
+
Falcon-512
+
FIPS 206 (draft) · Cat 1 · 897-byte pubkey · m/44'/1237'/0'/0/5+6
+
+
+
+
+
+
+
ML-KEM-768 (Kyber)
+
FIPS 203 · Cat 3 · 1184-byte pubkey · m/44'/1237'/0'/0/7+8
+
+
+
+
+
+ +
+ + +
+
Sign Migration Event
+
+ We'll now create the NIP-QR migration event. Each PQ key signs the attestation statement, + and your signer signs the Nostr event with your secp256k1 key. +
+
+
+
+
+
+
PQ signature: ML-DSA-44
+
Done in browser (pure JS)
+
+
+
+
+
+
PQ signature: ML-DSA-65
+
Done in browser (pure JS)
+
+
+
+
+
+
PQ signature: SLH-DSA-128s
+
Done in browser (pure JS)
+
+
+
+
+
+
PQ signature: Falcon-512
+
Done in browser (pure JS)
+
+
+
+
+
+
secp256k1 signature (NIP-01)
+
Requires approval from your signer
+
+
+
+ +
+
Event preview:
+
+
+
+ + +
+
Review & Publish
+
+ Your NIP-QR migration event has been signed. Review the event below, then publish to relays when ready. +
+
+
Full event JSON:
+
+ +
+
Relays to publish to:
+ +
+ +
+ +
+ +
+
+ +
+
+ + +
+
OpenTimestamps Attestation
+
+ The NIP-QR event is being timestamped on the Bitcoin blockchain via OpenTimestamps. + This proves the event existed at this point in time, preventing a future quantum attacker + from backdating a fraudulent event. Bitcoin confirmation typically takes 10-30 minutes. +
+
+ +
+
+ + +
Polling log:
+
+ +
+
+
+
+
Submit hash to OpenTimestamps
+
Creates a complete pending detached .ots proof
+
+
+
+
+
+
Publish pending NIP-03 event (kind 1040)
+
Publishes the pending .ots proof immediately
+
+
+
+
+
+
Wait for Bitcoin confirmation
+
Polling every 60 seconds...
+
+
+
+
+
+
Publish confirmed NIP-03 event (kind 1040)
+
Publishes the upgraded proof with a Bitcoin attestation
+
+
+
+
+
OTS proof file (base64):
+
+
+ +
+
+
+ +
+
+ +
+
+ +
+
+
+
+
+ + + + + + + + + diff --git a/www/js/pq-crypto.mjs b/www/js/pq-crypto.mjs new file mode 100644 index 0000000..8b3ff43 --- /dev/null +++ b/www/js/pq-crypto.mjs @@ -0,0 +1,715 @@ +/** + * Post-Quantum Crypto Module for Nostr + * + * Provides: + * - BIP39 seed phrase generation + * - NIP-06 key derivation (secp256k1 from seed via BIP32) + * - PQ key derivation from BIP32 HD wallet paths (5 algorithms) + * - PQ signing (ML-DSA-44, ML-DSA-65, SLH-DSA-128s, Falcon-512) + * - NIP-QR event construction + * + * Uses @noble/post-quantum (pure JS, no WASM needed) + * + * BIP32 Derivation Paths (all under m/44'/1237'/0'/0/): + * 0 — secp256k1 (NIP-06 standard, Account #2) + * 1 — ML-DSA-44 (32-byte seed) + * 2 — ML-DSA-65 (32-byte seed) + * 3+4 — SLH-DSA-128s (48-byte seed, two 32-byte children concatenated) + * 5+6 — Falcon-512 (48-byte seed, two 32-byte children concatenated) + * 7+8 — ML-KEM-768 (64-byte seed, two 32-byte children concatenated) + */ + +import { generateMnemonic, mnemonicToSeedSync, validateMnemonic } from '@scure/bip39'; +import { wordlist } from '@scure/bip39/wordlists/english.js'; +import { HDKey } from '@scure/bip32'; +import { bech32 } from '@scure/base'; +import { schnorr } from '@noble/curves/secp256k1.js'; +import { sha256 } from '@noble/hashes/sha2.js'; +import { ml_dsa44, ml_dsa65 } from '@noble/post-quantum/ml-dsa.js'; +import { slh_dsa_sha2_128s } from '@noble/post-quantum/slh-dsa.js'; +import { falcon512 } from '@noble/post-quantum/falcon.js'; +import { ml_kem768 } from '@noble/post-quantum/ml-kem.js'; + +// ============================================================================ +// BIP32 DERIVATION PATHS +// ============================================================================ + +/** + * BIP32 derivation paths for all keys. + * + * Base path: m/44'/1237'/0'/0/ (NIP-06 account 0, change 0) + * Child indices: + * 0 — secp256k1 (NIP-06 standard) + * 1 — ML-DSA-44 + * 2 — ML-DSA-65 + * 3, 4 — SLH-DSA-128s (two children, concatenated for 48-byte seed) + * 5, 6 — Falcon-512 (two children, concatenated for 48-byte seed) + * 7, 8 — ML-KEM-768 (two children, concatenated for 64-byte seed) + */ +const PQ_DERIVATION_PATHS = { + secp256k1: [0], // 32 bytes (standard NIP-06) + mlDsa44: [1], // 32 bytes + mlDsa65: [2], // 32 bytes + slhDsa: [3, 4], // 64 bytes concatenated, take first 48 + falcon512: [5, 6], // 64 bytes concatenated, take first 48 + mlKem: [7, 8], // 64 bytes concatenated +}; + +// Seed lengths required by each algorithm's keygen() +const PQ_SEED_LENGTHS = { + mlDsa44: 32, + mlDsa65: 32, + slhDsa: 48, + falcon512: 48, + mlKem: 64, +}; + +// ============================================================================ +// BIP39 SEED PHRASE +// ============================================================================ + +/** + * Generate a new 12-word BIP39 mnemonic. + * @returns {string} 12-word seed phrase + */ +export function generateSeedPhrase() { + return generateMnemonic(wordlist, 128); // 128 bits = 12 words +} + +/** + * Convert a mnemonic to a 64-byte BIP39 seed (PBKDF2-HMAC-SHA512). + * @param {string} mnemonic - 12/24 word seed phrase + * @param {string} [passphrase=''] - optional BIP39 passphrase + * @returns {Uint8Array} 64-byte seed + */ +export function mnemonicToSeed(mnemonic, passphrase = '') { + if (!validateMnemonic(mnemonic, wordlist)) { + throw new Error('Invalid mnemonic'); + } + return mnemonicToSeedSync(mnemonic, passphrase); +} + +/** + * Validate a BIP39 mnemonic. + * @param {string} mnemonic + * @returns {boolean} + */ +export function isValidMnemonic(mnemonic) { + return validateMnemonic(mnemonic, wordlist); +} + +// ============================================================================ +// BIP32 KEY DERIVATION +// ============================================================================ + +/** + * Derive a BIP32 child private key at a given path. + * + * @param {Uint8Array} bip39Seed - 64-byte BIP39 seed + * @param {number[]} childIndices - child indices under m/44'/1237'/0'/0/ + * @returns {Uint8Array} 32-byte private key + */ +function deriveBIP32Child(bip39Seed, childIndices) { + const hdKey = HDKey.fromMasterSeed(bip39Seed); + const path = `m/44'/1237'/0'/0/${childIndices.join('/')}`; + const child = hdKey.derive(path); + if (!child.privateKey) { + throw new Error(`Failed to derive private key at path ${path}`); + } + return child.privateKey; +} + +/** + * Derive a seed of the required length from BIP32 child keys. + * + * For 32-byte seeds: derive one child, use its 32-byte private key. + * For 48-byte seeds: derive two children, concatenate (64 bytes), take first 48. + * For 64-byte seeds: derive two children, concatenate (64 bytes). + * + * @param {Uint8Array} bip39Seed - 64-byte BIP39 seed + * @param {number[]} childIndices - child indices to derive + * @param {number} requiredLength - required seed length + * @returns {Uint8Array} seed bytes + */ +function derivePQSeedFromBIP32(bip39Seed, childIndices, requiredLength) { + if (childIndices.length === 1) { + // Single child — 32 bytes + const seed = deriveBIP32Child(bip39Seed, childIndices); + if (seed.length !== requiredLength) { + throw new Error(`Seed length mismatch: got ${seed.length}, expected ${requiredLength}`); + } + return seed; + } else { + // Multiple children — concatenate and truncate + let combined = new Uint8Array(0); + for (const idx of childIndices) { + const child = deriveBIP32Child(bip39Seed, [idx]); + const newCombined = new Uint8Array(combined.length + child.length); + newCombined.set(combined); + newCombined.set(child, combined.length); + combined = newCombined; + } + if (combined.length < requiredLength) { + throw new Error(`Combined seed too short: got ${combined.length}, expected ${requiredLength}`); + } + return combined.slice(0, requiredLength); + } +} + +// ============================================================================ +// NIP-06 KEY DERIVATION (secp256k1 from seed) +// ============================================================================ + +/** + * Derive a secp256k1 keypair from a BIP39 seed using NIP-06. + * Path: m/44'/1237'/0'/0/0 + * + * @param {Uint8Array} seed - 64-byte BIP39 seed + * @param {number} [accountIndex=0] - account index + * @returns {{privateKey: Uint8Array, publicKey: Uint8Array}} secp256k1 keypair + */ +export function deriveSecp256k1FromSeed(seed, accountIndex = 0) { + const hdKey = HDKey.fromMasterSeed(seed); + const path = `m/44'/1237'/${accountIndex}'/0/0`; + const child = hdKey.derive(path); + if (!child.privateKey) { + throw new Error('Failed to derive private key'); + } + return { + privateKey: child.privateKey, + publicKey: child.publicKey + }; +} + +// ============================================================================ +// PQ KEY DERIVATION FROM BIP32 PATHS +// ============================================================================ + +/** + * Derive all PQ keypairs from a BIP39 seed using BIP32 derivation paths. + * + * Paths (under m/44'/1237'/0'/0/): + * 1 — ML-DSA-44 + * 2 — ML-DSA-65 + * 3+4 — SLH-DSA-128s + * 5+6 — Falcon-512 + * 7+8 — ML-KEM-768 + * + * @param {Uint8Array} bip39Seed - 64-byte BIP39 seed + * @returns {{ + * mlDsa44: {publicKey: Uint8Array, secretKey: Uint8Array}, + * mlDsa65: {publicKey: Uint8Array, secretKey: Uint8Array}, + * slhDsa: {publicKey: Uint8Array, secretKey: Uint8Array}, + * falcon512: {publicKey: Uint8Array, secretKey: Uint8Array}, + * mlKem: {publicKey: Uint8Array, secretKey: Uint8Array} + * }} + */ +export function derivePQKeysFromSeed(bip39Seed) { + // ML-DSA-44 (32-byte seed, path child 1) + const mlDsa44Seed = derivePQSeedFromBIP32(bip39Seed, PQ_DERIVATION_PATHS.mlDsa44, PQ_SEED_LENGTHS.mlDsa44); + const mlDsa44Keys = ml_dsa44.keygen(mlDsa44Seed); + + // ML-DSA-65 (32-byte seed, path child 2) + const mlDsa65Seed = derivePQSeedFromBIP32(bip39Seed, PQ_DERIVATION_PATHS.mlDsa65, PQ_SEED_LENGTHS.mlDsa65); + const mlDsa65Keys = ml_dsa65.keygen(mlDsa65Seed); + + // SLH-DSA-128s (48-byte seed, paths children 3+4 concatenated) + const slhDsaSeed = derivePQSeedFromBIP32(bip39Seed, PQ_DERIVATION_PATHS.slhDsa, PQ_SEED_LENGTHS.slhDsa); + const slhDsaKeys = slh_dsa_sha2_128s.keygen(slhDsaSeed); + + // Falcon-512 (48-byte seed, paths children 5+6 concatenated) + const falconSeed = derivePQSeedFromBIP32(bip39Seed, PQ_DERIVATION_PATHS.falcon512, PQ_SEED_LENGTHS.falcon512); + const falconKeys = falcon512.keygen(falconSeed); + + // ML-KEM-768 (64-byte seed, paths children 7+8 concatenated) + const mlKemSeed = derivePQSeedFromBIP32(bip39Seed, PQ_DERIVATION_PATHS.mlKem, PQ_SEED_LENGTHS.mlKem); + const mlKemKeys = ml_kem768.keygen(mlKemSeed); + + return { + mlDsa44: mlDsa44Keys, + mlDsa65: mlDsa65Keys, + slhDsa: slhDsaKeys, + falcon512: falconKeys, + mlKem: mlKemKeys + }; +} + +// ============================================================================ +// PQ SIGNING +// ============================================================================ + +/** + * Sign a message with ML-DSA-44. + */ +export function signWithMLDSA44(message, secretKey) { + return ml_dsa44.sign(message, secretKey); +} + +/** + * Verify an ML-DSA-44 signature. + */ +export function verifyMLDSA44(signature, message, publicKey) { + return ml_dsa44.verify(signature, message, publicKey); +} + +/** + * Sign a message with ML-DSA-65. + */ +export function signWithMLDSA65(message, secretKey) { + return ml_dsa65.sign(message, secretKey); +} + +/** + * Verify an ML-DSA-65 signature. + */ +export function verifyMLDSA65(signature, message, publicKey) { + return ml_dsa65.verify(signature, message, publicKey); +} + +/** + * Sign a message with SLH-DSA-128s. + */ +export function signWithSLHDSA(message, secretKey) { + return slh_dsa_sha2_128s.sign(message, secretKey); +} + +/** + * Verify an SLH-DSA-128s signature. + */ +export function verifySLHDSA(signature, message, publicKey) { + return slh_dsa_sha2_128s.verify(signature, message, publicKey); +} + +/** + * Sign a message with Falcon-512. + */ +export function signWithFalcon(message, secretKey) { + return falcon512.sign(message, secretKey); +} + +/** + * Verify a Falcon-512 signature. + */ +export function verifyFalcon(signature, message, publicKey) { + return falcon512.verify(signature, message, publicKey); +} + +// ============================================================================ +// UTILITIES +// ============================================================================ + +/** + * Convert Uint8Array to base64 string. + */ +export function bytesToBase64(bytes) { + let binary = ''; + for (let i = 0; i < bytes.length; i++) { + binary += String.fromCharCode(bytes[i]); + } + return btoa(binary); +} + +/** + * Convert base64 string to Uint8Array. + */ +export function base64ToBytes(base64) { + const binary = atob(base64); + const bytes = new Uint8Array(binary.length); + for (let i = 0; i < binary.length; i++) { + bytes[i] = binary.charCodeAt(i); + } + return bytes; +} + +/** + * Convert Uint8Array to hex string. + */ +export function bytesToHex(bytes) { + return Array.from(bytes) + .map(b => b.toString(16).padStart(2, '0')) + .join(''); +} + +/** + * Convert hex string to Uint8Array. + */ +export function hexToBytes(hex) { + const bytes = new Uint8Array(hex.length / 2); + for (let i = 0; i < hex.length; i += 2) { + bytes[i / 2] = parseInt(hex.substr(i, 2), 16); + } + return bytes; +} + +/** + * Convert a hex pubkey to npub (bech32 encoding). + * @param {string} hexPubkey - 32-byte hex pubkey + * @returns {string} npub1... + */ +export function hexToNpub(hexPubkey) { + const bytes = hexToBytes(hexPubkey); + return bech32.encode('npub', bech32.toWords(bytes)); +} + +/** + * Verify a Nostr event's secp256k1 Schnorr signature (NIP-01). + * @param {object} event - Nostr event with id, pubkey, created_at, kind, tags, content, sig + * @returns {boolean} true if signature is valid + */ +export function verifyNostrEvent(event) { + // Build the event hash (NIP-01 serialization) + const serialized = JSON.stringify([ + 0, + event.pubkey, + event.created_at, + event.kind, + event.tags, + event.content + ]); + const hash = sha256(new TextEncoder().encode(serialized)); + const sig = hexToBytes(event.sig); + const pubkey = hexToBytes(event.pubkey); + return schnorr.verify(sig, hash, pubkey); +} + +// ============================================================================ +// NIP-QR EVENT CONSTRUCTION +// ============================================================================ + +/** + * Build the NIP-QR event content and tags. + * + * The content is a human-readable attestation statement (plain text). + * The PQ public keys and signatures go in the event tags. + * Each PQ key signs the content text (the attestation statement). + * + * Tag format: ['algorithm', '', '', ''] + * For ML-KEM (KEM, can't sign): ['algorithm', 'ml-kem-768', ''] + * + * @param {string} hexPubkey - The user's Nostr hex pubkey (Account #1) + * @param {number} blockHeight - Current Bitcoin block height for pre-quantum anchoring + * @param {object} pqKeys - PQ keypairs from derivePQKeysFromSeed() + * @returns {{content: string, tags: Array, statementBytes: Uint8Array}} + */ +export function buildNIPQRContent(hexPubkey, blockHeight, pqKeys) { + const npub = hexToNpub(hexPubkey); + + // Human-readable attestation statement (this is the event's content field) + // Single line with spaces (no newlines) for clean JSON display + const content = `NOSTR identity: [hex] ${hexPubkey} [npub] ${npub} is signaling migration to successor post quantum pubkeys listed in the tags of this event. This link is established pre-quantum at blockheight ${blockHeight}.`; + + const statementBytes = new TextEncoder().encode(content); + + // Sign the content text with each PQ signature scheme + const mlDsa44Sig = signWithMLDSA44(statementBytes, pqKeys.mlDsa44.secretKey); + const mlDsa65Sig = signWithMLDSA65(statementBytes, pqKeys.mlDsa65.secretKey); + const slhDsaSig = signWithSLHDSA(statementBytes, pqKeys.slhDsa.secretKey); + const falconSig = signWithFalcon(statementBytes, pqKeys.falcon512.secretKey); + + // Build tags: each PQ key's pubkey and signature in a tag + const tags = [ + ['d', 'nip-qr-migration'], + ['block_height', String(blockHeight)], + ['algorithm', 'ml-dsa-44', bytesToBase64(pqKeys.mlDsa44.publicKey), bytesToBase64(mlDsa44Sig)], + ['algorithm', 'ml-dsa-65', bytesToBase64(pqKeys.mlDsa65.publicKey), bytesToBase64(mlDsa65Sig)], + ['algorithm', 'slh-dsa-128s', bytesToBase64(pqKeys.slhDsa.publicKey), bytesToBase64(slhDsaSig)], + ['algorithm', 'falcon-512', bytesToBase64(pqKeys.falcon512.publicKey), bytesToBase64(falconSig)], + ['algorithm', 'ml-kem-768', bytesToBase64(pqKeys.mlKem.publicKey)] + ]; + + return { content, tags, statementBytes }; +} + +/** + * Verify a NIP-QR event's PQ signatures. + * Each PQ signature is verified against the event's content text. + * + * @param {string} contentText - The event's content field (human-readable statement) + * @param {Array} tags - The event's tags array + * @returns {{valid: boolean, results: Array}} verification results + */ +export function verifyNIPQRContent(contentText, tags) { + const results = []; + const msg = new TextEncoder().encode(contentText); + + for (const tag of tags) { + if (tag[0] !== 'algorithm') continue; + + const algorithm = tag[1]; + const pubKeyBase64 = tag[2]; + const sigBase64 = tag[3]; // undefined for ML-KEM + + if (algorithm === 'ml-kem-768' || !sigBase64) { + // KEM can't sign — skip verification + results.push({ algorithm, valid: true, note: 'KEM (no signature to verify)' }); + continue; + } + + const pubKey = base64ToBytes(pubKeyBase64); + const sig = base64ToBytes(sigBase64); + + let valid = false; + if (algorithm === 'ml-dsa-44') { + valid = verifyMLDSA44(sig, msg, pubKey); + } else if (algorithm === 'ml-dsa-65') { + valid = verifyMLDSA65(sig, msg, pubKey); + } else if (algorithm === 'slh-dsa-128s') { + valid = verifySLHDSA(sig, msg, pubKey); + } else if (algorithm === 'falcon-512') { + valid = verifyFalcon(sig, msg, pubKey); + } + + results.push({ algorithm, valid }); + } + + return { + valid: results.every(r => r.valid), + results + }; +} + +// ============================================================================ +// KEY SIZE INFO (for display) +// ============================================================================ + +export const PQ_KEY_INFO = { + 'ml-dsa-44': { + name: 'ML-DSA-44 (Dilithium)', + publicKeySize: 1312, + signatureSize: 2420, + fips: 'FIPS 204', + type: 'signature', + securityLevel: 'Category 2 (~AES-128)', + derivationPath: "m/44'/1237'/0'/0/1" + }, + 'ml-dsa-65': { + name: 'ML-DSA-65 (Dilithium)', + publicKeySize: 1952, + signatureSize: 3309, + fips: 'FIPS 204', + type: 'signature', + securityLevel: 'Category 3 (~AES-192)', + derivationPath: "m/44'/1237'/0'/0/2" + }, + 'slh-dsa-128s': { + name: 'SLH-DSA-128s (SPHINCS+)', + publicKeySize: 32, + signatureSize: 7856, + fips: 'FIPS 205', + type: 'signature', + securityLevel: 'Category 1 (~AES-128, hash-based)', + derivationPath: "m/44'/1237'/0'/0/3+4" + }, + 'falcon-512': { + name: 'Falcon-512', + publicKeySize: 897, + signatureSize: 666, + fips: 'FIPS 206 (draft)', + type: 'signature', + securityLevel: 'Category 1 (~AES-128, lattice-based)', + derivationPath: "m/44'/1237'/0'/0/5+6" + }, + 'ml-kem-768': { + name: 'ML-KEM-768 (Kyber)', + publicKeySize: 1184, + ciphertextSize: 1088, + fips: 'FIPS 203', + type: 'kem', + securityLevel: 'Category 3 (~AES-192)', + derivationPath: "m/44'/1237'/0'/0/7+8" + } +}; + +// ============================================================================ +// OPENTIMESTAMPS (NIP-03) +// ============================================================================ + +const OTS_CALENDAR_SERVERS = [ + 'https://alice.btc.calendar.opentimestamps.org', + 'https://bob.btc.calendar.opentimestamps.org', +]; + +// Detached .ots file prefix: +// magic header (31 bytes) + version 1 varuint + SHA-256 operation tag (0x08). +const OTS_DETACHED_PREFIX = hexToBytes( + '004f70656e54696d657374616d7073000050726f6f6600bf89e2e884e89294' + + '01' + + '08' +); + +function concatBytes(...arrays) { + const length = arrays.reduce((sum, bytes) => sum + bytes.length, 0); + const result = new Uint8Array(length); + let offset = 0; + for (const bytes of arrays) { + result.set(bytes, offset); + offset += bytes.length; + } + return result; +} + +/** + * Check whether bytes begin with the detached .ots magic header. + * @param {Uint8Array} otsBytes + * @returns {boolean} + */ +export function isDetachedOtsFile(otsBytes) { + if (!(otsBytes instanceof Uint8Array) || otsBytes.length < OTS_DETACHED_PREFIX.length) return false; + for (let i = 0; i < OTS_DETACHED_PREFIX.length; i++) { + if (otsBytes[i] !== OTS_DETACHED_PREFIX[i]) return false; + } + return true; +} + +/** + * Submit a hash to OpenTimestamps for timestamping. + * Returns a pending .ots file (binary). + * + * The API endpoint is POST {server}/digest with the raw 32-byte hash as the body. + * + * @param {string} eventIdHex - The Nostr event id (hex string, 32 bytes) + * @returns {Promise} pending .ots file bytes + */ +export async function timestampEvent(eventIdHex) { + const hashBytes = hexToBytes(eventIdHex); + + // Try each calendar server until one succeeds + for (const server of OTS_CALENDAR_SERVERS) { + try { + const response = await fetch(`${server}/digest`, { + method: 'POST', + body: hashBytes, + headers: { + 'Accept': 'application/vnd.opentimestamps.v1', + 'Content-Type': 'application/x-www-form-urlencoded' + } + }); + + if (!response.ok) { + console.warn(`[ots] Calendar ${server} returned ${response.status}`); + continue; + } + + // Calendar response is a serialized timestamp fragment, not a full + // detached .ots file. Wrap it with the OTS header, SHA-256 op, and + // the original 32-byte event digest. + const fragmentBuffer = await response.arrayBuffer(); + const fragmentBytes = new Uint8Array(fragmentBuffer); + const otsBytes = concatBytes(OTS_DETACHED_PREFIX, hashBytes, fragmentBytes); + console.log(`[ots] Received calendar fragment (${fragmentBytes.length} bytes); built detached .ots file (${otsBytes.length} bytes) from ${server}`); + return otsBytes; + } catch (e) { + console.warn(`[ots] Calendar ${server} failed:`, e.message); + } + } + + throw new Error('All OpenTimestamps calendar servers failed'); +} + +/** + * Upgrade a detached .ots file using the same-origin standards-based helper. + * + * @param {Uint8Array} otsBytes - The pending or partially upgraded .ots bytes + * @returns {Promise<{proof: Uint8Array, changed: boolean, confirmed: boolean, detail: string}>} + */ +export async function upgradeOts(otsBytes) { + const upgradeUrl = (typeof window !== 'undefined' && window.location) + ? `${window.location.origin}/ots-upgrade` + : 'https://laantungir.net/ots-upgrade'; + const response = await fetch(upgradeUrl, { + method: 'POST', + body: otsBytes, + headers: { + 'Content-Type': 'application/octet-stream', + 'Accept': 'application/json' + } + }); + if (!response.ok) { + throw new Error(`OTS upgrade helper returned HTTP ${response.status}`); + } + const result = await response.json(); + if (!result.proof) { + throw new Error(result.error || 'OTS upgrade helper returned no proof'); + } + return { + proof: base64ToBytes(result.proof), + changed: Boolean(result.changed), + confirmed: Boolean(result.confirmed), + detail: String(result.stderr || result.stdout || '').trim() + }; +} + +/** + * Check if an .ots file contains a confirmed Bitcoin attestation. + * A pending .ots file contains only "pending" attestations. + * A confirmed .ots file contains at least one Bitcoin attestation. + * + * @param {Uint8Array} otsBytes - The .ots file bytes + * @returns {boolean} true if the .ots file has a Bitcoin attestation + */ +export function isOtsConfirmed(otsBytes) { + // Exact 8-byte tag for BitcoinBlockHeaderAttestation. + const bitcoinTag = hexToBytes('0588960d73d71901'); + outer: for (let i = 0; i <= otsBytes.length - bitcoinTag.length; i++) { + for (let j = 0; j < bitcoinTag.length; j++) { + if (otsBytes[i + j] !== bitcoinTag[j]) continue outer; + } + return true; + } + return false; +} + +/** + * Store OTS workflow data in localStorage. + * Existing metadata is preserved unless explicitly overwritten. + * + * @param {string} eventId - The NIP-QR event id + * @param {Uint8Array} otsBytes - Current detached .ots bytes + * @param {object} metadata - Workflow metadata to merge + */ +export function savePendingOts(eventId, otsBytes, metadata = {}) { + let existing = {}; + try { + existing = JSON.parse(localStorage.getItem('pq-pending-ots') || '{}'); + } catch (e) { + existing = {}; + } + const data = { + ...existing, + ...metadata, + eventId, + ots: bytesToBase64(otsBytes), + timestamp: existing.timestamp || Date.now(), + updatedAt: Date.now() + }; + localStorage.setItem('pq-pending-ots', JSON.stringify(data)); +} + +/** + * Load OTS workflow data from localStorage. + * @returns {{eventId: string, ots: Uint8Array, timestamp: number, pendingPublished: boolean, confirmedPublished: boolean}|null} + */ +export function loadPendingOts() { + const data = localStorage.getItem('pq-pending-ots'); + if (!data) return null; + try { + const parsed = JSON.parse(data); + return { + ...parsed, + eventId: parsed.eventId, + ots: base64ToBytes(parsed.ots), + timestamp: parsed.timestamp, + pendingPublished: Boolean(parsed.pendingPublished), + confirmedPublished: Boolean(parsed.confirmedPublished) + }; + } catch (e) { + return null; + } +} + +/** + * Clear pending OTS data from localStorage. + */ +export function clearPendingOts() { + localStorage.removeItem('pq-pending-ots'); +} diff --git a/www/pq-crypto.bundle.js b/www/pq-crypto.bundle.js new file mode 100644 index 0000000..463d2ad --- /dev/null +++ b/www/pq-crypto.bundle.js @@ -0,0 +1,9959 @@ +var __defProp = Object.defineProperty; +var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value; +var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value); + +// node_modules/@noble/hashes/utils.js +function isBytes(a) { + return a instanceof Uint8Array || ArrayBuffer.isView(a) && a.constructor.name === "Uint8Array" && "BYTES_PER_ELEMENT" in a && a.BYTES_PER_ELEMENT === 1; +} +function anumber(n, title = "") { + if (typeof n !== "number") { + const prefix = title && `"${title}" `; + throw new TypeError(`${prefix}expected number, got ${typeof n}`); + } + if (!Number.isSafeInteger(n) || n < 0) { + const prefix = title && `"${title}" `; + throw new RangeError(`${prefix}expected integer >= 0, got ${n}`); + } +} +function abytes(value, length, title = "") { + const bytes = isBytes(value); + const len = value?.length; + const needsLen = length !== void 0; + if (!bytes || needsLen && len !== length) { + const prefix = title && `"${title}" `; + const ofLen = needsLen ? ` of length ${length}` : ""; + const got = bytes ? `length=${len}` : `type=${typeof value}`; + const message = prefix + "expected Uint8Array" + ofLen + ", got " + got; + if (!bytes) + throw new TypeError(message); + throw new RangeError(message); + } + return value; +} +function ahash(h) { + if (typeof h !== "function" || typeof h.create !== "function") + throw new TypeError("Hash must wrapped by utils.createHasher"); + anumber(h.outputLen); + anumber(h.blockLen); + if (h.outputLen < 1) + throw new Error('"outputLen" must be >= 1'); + if (h.blockLen < 1) + throw new Error('"blockLen" must be >= 1'); +} +function aexists(instance, checkFinished = true) { + if (instance.destroyed) + throw new Error("Hash instance has been destroyed"); + if (checkFinished && instance.finished) + throw new Error("Hash#digest() has already been called"); +} +function aoutput(out, instance) { + abytes(out, void 0, "digestInto() output"); + const min = instance.outputLen; + if (out.length < min) { + throw new RangeError('"digestInto() output" expected to be of length >=' + min); + } +} +function u8(arr) { + return new Uint8Array(arr.buffer, arr.byteOffset, arr.byteLength); +} +function u32(arr) { + return new Uint32Array(arr.buffer, arr.byteOffset, Math.floor(arr.byteLength / 4)); +} +function clean(...arrays) { + for (let i = 0; i < arrays.length; i++) { + arrays[i].fill(0); + } +} +function createView(arr) { + return new DataView(arr.buffer, arr.byteOffset, arr.byteLength); +} +function rotr(word, shift) { + return word << 32 - shift | word >>> shift; +} +function rotl(word, shift) { + return word << shift | word >>> 32 - shift >>> 0; +} +var isLE = /* @__PURE__ */ (() => new Uint8Array(new Uint32Array([287454020]).buffer)[0] === 68)(); +function byteSwap(word) { + return word << 24 & 4278190080 | word << 8 & 16711680 | word >>> 8 & 65280 | word >>> 24 & 255; +} +function byteSwap32(arr) { + for (let i = 0; i < arr.length; i++) { + arr[i] = byteSwap(arr[i]); + } + return arr; +} +var swap32IfBE = isLE ? (u) => u : byteSwap32; +var hasHexBuiltin = /* @__PURE__ */ (() => ( + // @ts-ignore + typeof Uint8Array.from([]).toHex === "function" && typeof Uint8Array.fromHex === "function" +))(); +var hexes = /* @__PURE__ */ Array.from({ length: 256 }, (_, i) => i.toString(16).padStart(2, "0")); +function bytesToHex(bytes) { + abytes(bytes); + if (hasHexBuiltin) + return bytes.toHex(); + let hex = ""; + for (let i = 0; i < bytes.length; i++) { + hex += hexes[bytes[i]]; + } + return hex; +} +var asciis = { _0: 48, _9: 57, A: 65, F: 70, a: 97, f: 102 }; +function asciiToBase16(ch) { + if (ch >= asciis._0 && ch <= asciis._9) + return ch - asciis._0; + if (ch >= asciis.A && ch <= asciis.F) + return ch - (asciis.A - 10); + if (ch >= asciis.a && ch <= asciis.f) + return ch - (asciis.a - 10); + return; +} +function hexToBytes(hex) { + if (typeof hex !== "string") + throw new TypeError("hex string expected, got " + typeof hex); + if (hasHexBuiltin) { + try { + return Uint8Array.fromHex(hex); + } catch (error) { + if (error instanceof SyntaxError) + throw new RangeError(error.message); + throw error; + } + } + const hl = hex.length; + const al = hl / 2; + if (hl % 2) + throw new RangeError("hex string expected, got unpadded hex of length " + hl); + const array = new Uint8Array(al); + for (let ai = 0, hi = 0; ai < al; ai++, hi += 2) { + const n1 = asciiToBase16(hex.charCodeAt(hi)); + const n2 = asciiToBase16(hex.charCodeAt(hi + 1)); + if (n1 === void 0 || n2 === void 0) { + const char = hex[hi] + hex[hi + 1]; + throw new RangeError('hex string expected, got non-hex character "' + char + '" at index ' + hi); + } + array[ai] = n1 * 16 + n2; + } + return array; +} +function utf8ToBytes(str) { + if (typeof str !== "string") + throw new TypeError("string expected"); + return new Uint8Array(new TextEncoder().encode(str)); +} +function kdfInputToBytes(data, errorTitle = "") { + if (typeof data === "string") + return utf8ToBytes(data); + return abytes(data, void 0, errorTitle); +} +function concatBytes(...arrays) { + let sum = 0; + for (let i = 0; i < arrays.length; i++) { + const a = arrays[i]; + abytes(a); + sum += a.length; + } + const res = new Uint8Array(sum); + for (let i = 0, pad2 = 0; i < arrays.length; i++) { + const a = arrays[i]; + res.set(a, pad2); + pad2 += a.length; + } + return res; +} +function checkOpts(defaults, opts2) { + if (opts2 !== void 0 && {}.toString.call(opts2) !== "[object Object]") + throw new TypeError("options must be object or undefined"); + const merged = Object.assign(defaults, opts2); + return merged; +} +function createHasher(hashCons, info = {}) { + const hashC = (msg, opts2) => hashCons(opts2).update(msg).digest(); + const tmp = hashCons(void 0); + hashC.outputLen = tmp.outputLen; + hashC.blockLen = tmp.blockLen; + hashC.canXOF = tmp.canXOF; + hashC.create = (opts2) => hashCons(opts2); + Object.assign(hashC, info); + return Object.freeze(hashC); +} +function randomBytes(bytesLength = 32) { + anumber(bytesLength, "bytesLength"); + const cr = typeof globalThis === "object" ? globalThis.crypto : null; + if (typeof cr?.getRandomValues !== "function") + throw new Error("crypto.getRandomValues must be defined"); + if (bytesLength > 65536) + throw new RangeError(`"bytesLength" expected <= 65536, got ${bytesLength}`); + return cr.getRandomValues(new Uint8Array(bytesLength)); +} +var oidNist = (suffix) => ({ + // Current NIST hashAlgs suffixes used here fit in one DER subidentifier octet. + // Larger suffix values would need base-128 OID encoding and a different length byte. + oid: Uint8Array.from([6, 9, 96, 134, 72, 1, 101, 3, 4, 2, suffix]) +}); + +// node_modules/@noble/hashes/hmac.js +var _HMAC = class { + constructor(hash, key) { + __publicField(this, "oHash"); + __publicField(this, "iHash"); + __publicField(this, "blockLen"); + __publicField(this, "outputLen"); + __publicField(this, "canXOF", false); + __publicField(this, "finished", false); + __publicField(this, "destroyed", false); + ahash(hash); + abytes(key, void 0, "key"); + this.iHash = hash.create(); + if (typeof this.iHash.update !== "function") + throw new Error("Expected instance of class which extends utils.Hash"); + this.blockLen = this.iHash.blockLen; + this.outputLen = this.iHash.outputLen; + const blockLen = this.blockLen; + const pad2 = new Uint8Array(blockLen); + pad2.set(key.length > blockLen ? hash.create().update(key).digest() : key); + for (let i = 0; i < pad2.length; i++) + pad2[i] ^= 54; + this.iHash.update(pad2); + this.oHash = hash.create(); + for (let i = 0; i < pad2.length; i++) + pad2[i] ^= 54 ^ 92; + this.oHash.update(pad2); + clean(pad2); + } + update(buf) { + aexists(this); + this.iHash.update(buf); + return this; + } + digestInto(out) { + aexists(this); + aoutput(out, this); + this.finished = true; + const buf = out.subarray(0, this.outputLen); + this.iHash.digestInto(buf); + this.oHash.update(buf); + this.oHash.digestInto(buf); + this.destroy(); + } + digest() { + const out = new Uint8Array(this.oHash.outputLen); + this.digestInto(out); + return out; + } + _cloneInto(to) { + to || (to = Object.create(Object.getPrototypeOf(this), {})); + const { oHash, iHash, finished, destroyed, blockLen, outputLen } = this; + to = to; + to.finished = finished; + to.destroyed = destroyed; + to.blockLen = blockLen; + to.outputLen = outputLen; + to.oHash = oHash._cloneInto(to.oHash); + to.iHash = iHash._cloneInto(to.iHash); + return to; + } + clone() { + return this._cloneInto(); + } + destroy() { + this.destroyed = true; + this.oHash.destroy(); + this.iHash.destroy(); + } +}; +var hmac = /* @__PURE__ */ (() => { + const hmac_ = ((hash, key, message) => new _HMAC(hash, key).update(message).digest()); + hmac_.create = (hash, key) => new _HMAC(hash, key); + return hmac_; +})(); + +// node_modules/@noble/hashes/pbkdf2.js +function pbkdf2Init(hash, _password, _salt, _opts) { + ahash(hash); + const opts2 = checkOpts({ dkLen: 32, asyncTick: 10 }, _opts); + const { c, dkLen, asyncTick } = opts2; + anumber(c, "c"); + anumber(dkLen, "dkLen"); + anumber(asyncTick, "asyncTick"); + if (c < 1) + throw new Error("iterations (c) must be >= 1"); + if (dkLen < 1) + throw new Error('"dkLen" must be >= 1'); + if (dkLen > (2 ** 32 - 1) * hash.outputLen) + throw new Error("derived key too long"); + const password = kdfInputToBytes(_password, "password"); + const salt = kdfInputToBytes(_salt, "salt"); + const DK = new Uint8Array(dkLen); + const PRF = hmac.create(hash, password); + const PRFSalt = PRF._cloneInto().update(salt); + return { c, dkLen, asyncTick, DK, PRF, PRFSalt }; +} +function pbkdf2Output(PRF, PRFSalt, DK, prfW, u) { + PRF.destroy(); + PRFSalt.destroy(); + if (prfW) + prfW.destroy(); + clean(u); + return DK; +} +function pbkdf2(hash, password, salt, opts2) { + const { c, dkLen, DK, PRF, PRFSalt } = pbkdf2Init(hash, password, salt, opts2); + let prfW; + const arr = new Uint8Array(4); + const view = createView(arr); + const u = new Uint8Array(PRF.outputLen); + for (let ti = 1, pos = 0; pos < dkLen; ti++, pos += PRF.outputLen) { + const Ti = DK.subarray(pos, pos + PRF.outputLen); + view.setInt32(0, ti, false); + (prfW = PRFSalt._cloneInto(prfW)).update(arr).digestInto(u); + Ti.set(u.subarray(0, Ti.length)); + for (let ui = 1; ui < c; ui++) { + PRF._cloneInto(prfW).update(u).digestInto(u); + for (let i = 0; i < Ti.length; i++) + Ti[i] ^= u[i]; + } + } + return pbkdf2Output(PRF, PRFSalt, DK, prfW, u); +} + +// node_modules/@noble/hashes/_md.js +function Chi(a, b, c) { + return a & b ^ ~a & c; +} +function Maj(a, b, c) { + return a & b ^ a & c ^ b & c; +} +var HashMD = class { + constructor(blockLen, outputLen, padOffset, isLE3) { + __publicField(this, "blockLen"); + __publicField(this, "outputLen"); + __publicField(this, "canXOF", false); + __publicField(this, "padOffset"); + __publicField(this, "isLE"); + // For partial updates less than block size + __publicField(this, "buffer"); + __publicField(this, "view"); + __publicField(this, "finished", false); + __publicField(this, "length", 0); + __publicField(this, "pos", 0); + __publicField(this, "destroyed", false); + this.blockLen = blockLen; + this.outputLen = outputLen; + this.padOffset = padOffset; + this.isLE = isLE3; + this.buffer = new Uint8Array(blockLen); + this.view = createView(this.buffer); + } + update(data) { + aexists(this); + abytes(data); + const { view, buffer, blockLen } = this; + const len = data.length; + for (let pos = 0; pos < len; ) { + const take = Math.min(blockLen - this.pos, len - pos); + if (take === blockLen) { + const dataView = createView(data); + for (; blockLen <= len - pos; pos += blockLen) + this.process(dataView, pos); + continue; + } + buffer.set(data.subarray(pos, pos + take), this.pos); + this.pos += take; + pos += take; + if (this.pos === blockLen) { + this.process(view, 0); + this.pos = 0; + } + } + this.length += data.length; + this.roundClean(); + return this; + } + digestInto(out) { + aexists(this); + aoutput(out, this); + this.finished = true; + const { buffer, view, blockLen, isLE: isLE3 } = this; + let { pos } = this; + buffer[pos++] = 128; + clean(this.buffer.subarray(pos)); + if (this.padOffset > blockLen - pos) { + this.process(view, 0); + pos = 0; + } + for (let i = pos; i < blockLen; i++) + buffer[i] = 0; + view.setBigUint64(blockLen - 8, BigInt(this.length * 8), isLE3); + this.process(view, 0); + const oview = createView(out); + const len = this.outputLen; + if (len % 4) + throw new Error("_sha2: outputLen must be aligned to 32bit"); + const outLen = len / 4; + const state = this.get(); + if (outLen > state.length) + throw new Error("_sha2: outputLen bigger than state"); + for (let i = 0; i < outLen; i++) + oview.setUint32(4 * i, state[i], isLE3); + } + digest() { + const { buffer, outputLen } = this; + this.digestInto(buffer); + const res = buffer.slice(0, outputLen); + this.destroy(); + return res; + } + _cloneInto(to) { + to || (to = new this.constructor()); + to.set(...this.get()); + const { blockLen, buffer, length, finished, destroyed, pos } = this; + to.destroyed = destroyed; + to.finished = finished; + to.length = length; + to.pos = pos; + if (length % blockLen) + to.buffer.set(buffer); + return to; + } + clone() { + return this._cloneInto(); + } +}; +var SHA256_IV = /* @__PURE__ */ Uint32Array.from([ + 1779033703, + 3144134277, + 1013904242, + 2773480762, + 1359893119, + 2600822924, + 528734635, + 1541459225 +]); +var SHA512_IV = /* @__PURE__ */ Uint32Array.from([ + 1779033703, + 4089235720, + 3144134277, + 2227873595, + 1013904242, + 4271175723, + 2773480762, + 1595750129, + 1359893119, + 2917565137, + 2600822924, + 725511199, + 528734635, + 4215389547, + 1541459225, + 327033209 +]); + +// node_modules/@noble/hashes/_u64.js +var U32_MASK64 = /* @__PURE__ */ BigInt(2 ** 32 - 1); +var _32n = /* @__PURE__ */ BigInt(32); +function fromBig(n, le = false) { + if (le) + return { h: Number(n & U32_MASK64), l: Number(n >> _32n & U32_MASK64) }; + return { h: Number(n >> _32n & U32_MASK64) | 0, l: Number(n & U32_MASK64) | 0 }; +} +function split(lst, le = false) { + const len = lst.length; + let Ah = new Uint32Array(len); + let Al = new Uint32Array(len); + for (let i = 0; i < len; i++) { + const { h, l } = fromBig(lst[i], le); + [Ah[i], Al[i]] = [h, l]; + } + return [Ah, Al]; +} +var shrSH = (h, _l, s) => h >>> s; +var shrSL = (h, l, s) => h << 32 - s | l >>> s; +var rotrSH = (h, l, s) => h >>> s | l << 32 - s; +var rotrSL = (h, l, s) => h << 32 - s | l >>> s; +var rotrBH = (h, l, s) => h << 64 - s | l >>> s - 32; +var rotrBL = (h, l, s) => h >>> s - 32 | l << 64 - s; +var rotlSH = (h, l, s) => h << s | l >>> 32 - s; +var rotlSL = (h, l, s) => l << s | h >>> 32 - s; +var rotlBH = (h, l, s) => l << s - 32 | h >>> 64 - s; +var rotlBL = (h, l, s) => h << s - 32 | l >>> 64 - s; +function add(Ah, Al, Bh, Bl) { + const l = (Al >>> 0) + (Bl >>> 0); + return { h: Ah + Bh + (l / 2 ** 32 | 0) | 0, l: l | 0 }; +} +var add3L = (Al, Bl, Cl) => (Al >>> 0) + (Bl >>> 0) + (Cl >>> 0); +var add3H = (low, Ah, Bh, Ch) => Ah + Bh + Ch + (low / 2 ** 32 | 0) | 0; +var add4L = (Al, Bl, Cl, Dl) => (Al >>> 0) + (Bl >>> 0) + (Cl >>> 0) + (Dl >>> 0); +var add4H = (low, Ah, Bh, Ch, Dh) => Ah + Bh + Ch + Dh + (low / 2 ** 32 | 0) | 0; +var add5L = (Al, Bl, Cl, Dl, El) => (Al >>> 0) + (Bl >>> 0) + (Cl >>> 0) + (Dl >>> 0) + (El >>> 0); +var add5H = (low, Ah, Bh, Ch, Dh, Eh) => Ah + Bh + Ch + Dh + Eh + (low / 2 ** 32 | 0) | 0; + +// node_modules/@noble/hashes/sha2.js +var SHA256_K = /* @__PURE__ */ Uint32Array.from([ + 1116352408, + 1899447441, + 3049323471, + 3921009573, + 961987163, + 1508970993, + 2453635748, + 2870763221, + 3624381080, + 310598401, + 607225278, + 1426881987, + 1925078388, + 2162078206, + 2614888103, + 3248222580, + 3835390401, + 4022224774, + 264347078, + 604807628, + 770255983, + 1249150122, + 1555081692, + 1996064986, + 2554220882, + 2821834349, + 2952996808, + 3210313671, + 3336571891, + 3584528711, + 113926993, + 338241895, + 666307205, + 773529912, + 1294757372, + 1396182291, + 1695183700, + 1986661051, + 2177026350, + 2456956037, + 2730485921, + 2820302411, + 3259730800, + 3345764771, + 3516065817, + 3600352804, + 4094571909, + 275423344, + 430227734, + 506948616, + 659060556, + 883997877, + 958139571, + 1322822218, + 1537002063, + 1747873779, + 1955562222, + 2024104815, + 2227730452, + 2361852424, + 2428436474, + 2756734187, + 3204031479, + 3329325298 +]); +var SHA256_W = /* @__PURE__ */ new Uint32Array(64); +var SHA2_32B = class extends HashMD { + constructor(outputLen) { + super(64, outputLen, 8, false); + } + get() { + const { A, B, C, D: D2, E, F: F3, G, H } = this; + return [A, B, C, D2, E, F3, G, H]; + } + // prettier-ignore + set(A, B, C, D2, E, F3, G, H) { + this.A = A | 0; + this.B = B | 0; + this.C = C | 0; + this.D = D2 | 0; + this.E = E | 0; + this.F = F3 | 0; + this.G = G | 0; + this.H = H | 0; + } + process(view, offset) { + for (let i = 0; i < 16; i++, offset += 4) + SHA256_W[i] = view.getUint32(offset, false); + for (let i = 16; i < 64; i++) { + const W15 = SHA256_W[i - 15]; + const W2 = SHA256_W[i - 2]; + const s0 = rotr(W15, 7) ^ rotr(W15, 18) ^ W15 >>> 3; + const s1 = rotr(W2, 17) ^ rotr(W2, 19) ^ W2 >>> 10; + SHA256_W[i] = s1 + SHA256_W[i - 7] + s0 + SHA256_W[i - 16] | 0; + } + let { A, B, C, D: D2, E, F: F3, G, H } = this; + for (let i = 0; i < 64; i++) { + const sigma1 = rotr(E, 6) ^ rotr(E, 11) ^ rotr(E, 25); + const T1 = H + sigma1 + Chi(E, F3, G) + SHA256_K[i] + SHA256_W[i] | 0; + const sigma0 = rotr(A, 2) ^ rotr(A, 13) ^ rotr(A, 22); + const T2 = sigma0 + Maj(A, B, C) | 0; + H = G; + G = F3; + F3 = E; + E = D2 + T1 | 0; + D2 = C; + C = B; + B = A; + A = T1 + T2 | 0; + } + A = A + this.A | 0; + B = B + this.B | 0; + C = C + this.C | 0; + D2 = D2 + this.D | 0; + E = E + this.E | 0; + F3 = F3 + this.F | 0; + G = G + this.G | 0; + H = H + this.H | 0; + this.set(A, B, C, D2, E, F3, G, H); + } + roundClean() { + clean(SHA256_W); + } + destroy() { + this.destroyed = true; + this.set(0, 0, 0, 0, 0, 0, 0, 0); + clean(this.buffer); + } +}; +var _SHA256 = class extends SHA2_32B { + constructor() { + super(32); + // We cannot use array here since array allows indexing by variable + // which means optimizer/compiler cannot use registers. + __publicField(this, "A", SHA256_IV[0] | 0); + __publicField(this, "B", SHA256_IV[1] | 0); + __publicField(this, "C", SHA256_IV[2] | 0); + __publicField(this, "D", SHA256_IV[3] | 0); + __publicField(this, "E", SHA256_IV[4] | 0); + __publicField(this, "F", SHA256_IV[5] | 0); + __publicField(this, "G", SHA256_IV[6] | 0); + __publicField(this, "H", SHA256_IV[7] | 0); + } +}; +var K512 = /* @__PURE__ */ (() => split([ + "0x428a2f98d728ae22", + "0x7137449123ef65cd", + "0xb5c0fbcfec4d3b2f", + "0xe9b5dba58189dbbc", + "0x3956c25bf348b538", + "0x59f111f1b605d019", + "0x923f82a4af194f9b", + "0xab1c5ed5da6d8118", + "0xd807aa98a3030242", + "0x12835b0145706fbe", + "0x243185be4ee4b28c", + "0x550c7dc3d5ffb4e2", + "0x72be5d74f27b896f", + "0x80deb1fe3b1696b1", + "0x9bdc06a725c71235", + "0xc19bf174cf692694", + "0xe49b69c19ef14ad2", + "0xefbe4786384f25e3", + "0x0fc19dc68b8cd5b5", + "0x240ca1cc77ac9c65", + "0x2de92c6f592b0275", + "0x4a7484aa6ea6e483", + "0x5cb0a9dcbd41fbd4", + "0x76f988da831153b5", + "0x983e5152ee66dfab", + "0xa831c66d2db43210", + "0xb00327c898fb213f", + "0xbf597fc7beef0ee4", + "0xc6e00bf33da88fc2", + "0xd5a79147930aa725", + "0x06ca6351e003826f", + "0x142929670a0e6e70", + "0x27b70a8546d22ffc", + "0x2e1b21385c26c926", + "0x4d2c6dfc5ac42aed", + "0x53380d139d95b3df", + "0x650a73548baf63de", + "0x766a0abb3c77b2a8", + "0x81c2c92e47edaee6", + "0x92722c851482353b", + "0xa2bfe8a14cf10364", + "0xa81a664bbc423001", + "0xc24b8b70d0f89791", + "0xc76c51a30654be30", + "0xd192e819d6ef5218", + "0xd69906245565a910", + "0xf40e35855771202a", + "0x106aa07032bbd1b8", + "0x19a4c116b8d2d0c8", + "0x1e376c085141ab53", + "0x2748774cdf8eeb99", + "0x34b0bcb5e19b48a8", + "0x391c0cb3c5c95a63", + "0x4ed8aa4ae3418acb", + "0x5b9cca4f7763e373", + "0x682e6ff3d6b2b8a3", + "0x748f82ee5defb2fc", + "0x78a5636f43172f60", + "0x84c87814a1f0ab72", + "0x8cc702081a6439ec", + "0x90befffa23631e28", + "0xa4506cebde82bde9", + "0xbef9a3f7b2c67915", + "0xc67178f2e372532b", + "0xca273eceea26619c", + "0xd186b8c721c0c207", + "0xeada7dd6cde0eb1e", + "0xf57d4f7fee6ed178", + "0x06f067aa72176fba", + "0x0a637dc5a2c898a6", + "0x113f9804bef90dae", + "0x1b710b35131c471b", + "0x28db77f523047d84", + "0x32caab7b40c72493", + "0x3c9ebe0a15c9bebc", + "0x431d67c49c100d4c", + "0x4cc5d4becb3e42b6", + "0x597f299cfc657e2a", + "0x5fcb6fab3ad6faec", + "0x6c44198c4a475817" +].map((n) => BigInt(n))))(); +var SHA512_Kh = /* @__PURE__ */ (() => K512[0])(); +var SHA512_Kl = /* @__PURE__ */ (() => K512[1])(); +var SHA512_W_H = /* @__PURE__ */ new Uint32Array(80); +var SHA512_W_L = /* @__PURE__ */ new Uint32Array(80); +var SHA2_64B = class extends HashMD { + constructor(outputLen) { + super(128, outputLen, 16, false); + } + // prettier-ignore + get() { + const { Ah, Al, Bh, Bl, Ch, Cl, Dh, Dl, Eh, El, Fh, Fl, Gh, Gl, Hh, Hl } = this; + return [Ah, Al, Bh, Bl, Ch, Cl, Dh, Dl, Eh, El, Fh, Fl, Gh, Gl, Hh, Hl]; + } + // prettier-ignore + set(Ah, Al, Bh, Bl, Ch, Cl, Dh, Dl, Eh, El, Fh, Fl, Gh, Gl, Hh, Hl) { + this.Ah = Ah | 0; + this.Al = Al | 0; + this.Bh = Bh | 0; + this.Bl = Bl | 0; + this.Ch = Ch | 0; + this.Cl = Cl | 0; + this.Dh = Dh | 0; + this.Dl = Dl | 0; + this.Eh = Eh | 0; + this.El = El | 0; + this.Fh = Fh | 0; + this.Fl = Fl | 0; + this.Gh = Gh | 0; + this.Gl = Gl | 0; + this.Hh = Hh | 0; + this.Hl = Hl | 0; + } + process(view, offset) { + for (let i = 0; i < 16; i++, offset += 4) { + SHA512_W_H[i] = view.getUint32(offset); + SHA512_W_L[i] = view.getUint32(offset += 4); + } + for (let i = 16; i < 80; i++) { + const W15h = SHA512_W_H[i - 15] | 0; + const W15l = SHA512_W_L[i - 15] | 0; + const s0h = rotrSH(W15h, W15l, 1) ^ rotrSH(W15h, W15l, 8) ^ shrSH(W15h, W15l, 7); + const s0l = rotrSL(W15h, W15l, 1) ^ rotrSL(W15h, W15l, 8) ^ shrSL(W15h, W15l, 7); + const W2h = SHA512_W_H[i - 2] | 0; + const W2l = SHA512_W_L[i - 2] | 0; + const s1h = rotrSH(W2h, W2l, 19) ^ rotrBH(W2h, W2l, 61) ^ shrSH(W2h, W2l, 6); + const s1l = rotrSL(W2h, W2l, 19) ^ rotrBL(W2h, W2l, 61) ^ shrSL(W2h, W2l, 6); + const SUMl = add4L(s0l, s1l, SHA512_W_L[i - 7], SHA512_W_L[i - 16]); + const SUMh = add4H(SUMl, s0h, s1h, SHA512_W_H[i - 7], SHA512_W_H[i - 16]); + SHA512_W_H[i] = SUMh | 0; + SHA512_W_L[i] = SUMl | 0; + } + let { Ah, Al, Bh, Bl, Ch, Cl, Dh, Dl, Eh, El, Fh, Fl, Gh, Gl, Hh, Hl } = this; + for (let i = 0; i < 80; i++) { + const sigma1h = rotrSH(Eh, El, 14) ^ rotrSH(Eh, El, 18) ^ rotrBH(Eh, El, 41); + const sigma1l = rotrSL(Eh, El, 14) ^ rotrSL(Eh, El, 18) ^ rotrBL(Eh, El, 41); + const CHIh = Eh & Fh ^ ~Eh & Gh; + const CHIl = El & Fl ^ ~El & Gl; + const T1ll = add5L(Hl, sigma1l, CHIl, SHA512_Kl[i], SHA512_W_L[i]); + const T1h = add5H(T1ll, Hh, sigma1h, CHIh, SHA512_Kh[i], SHA512_W_H[i]); + const T1l = T1ll | 0; + const sigma0h = rotrSH(Ah, Al, 28) ^ rotrBH(Ah, Al, 34) ^ rotrBH(Ah, Al, 39); + const sigma0l = rotrSL(Ah, Al, 28) ^ rotrBL(Ah, Al, 34) ^ rotrBL(Ah, Al, 39); + const MAJh = Ah & Bh ^ Ah & Ch ^ Bh & Ch; + const MAJl = Al & Bl ^ Al & Cl ^ Bl & Cl; + Hh = Gh | 0; + Hl = Gl | 0; + Gh = Fh | 0; + Gl = Fl | 0; + Fh = Eh | 0; + Fl = El | 0; + ({ h: Eh, l: El } = add(Dh | 0, Dl | 0, T1h | 0, T1l | 0)); + Dh = Ch | 0; + Dl = Cl | 0; + Ch = Bh | 0; + Cl = Bl | 0; + Bh = Ah | 0; + Bl = Al | 0; + const All = add3L(T1l, sigma0l, MAJl); + Ah = add3H(All, T1h, sigma0h, MAJh); + Al = All | 0; + } + ({ h: Ah, l: Al } = add(this.Ah | 0, this.Al | 0, Ah | 0, Al | 0)); + ({ h: Bh, l: Bl } = add(this.Bh | 0, this.Bl | 0, Bh | 0, Bl | 0)); + ({ h: Ch, l: Cl } = add(this.Ch | 0, this.Cl | 0, Ch | 0, Cl | 0)); + ({ h: Dh, l: Dl } = add(this.Dh | 0, this.Dl | 0, Dh | 0, Dl | 0)); + ({ h: Eh, l: El } = add(this.Eh | 0, this.El | 0, Eh | 0, El | 0)); + ({ h: Fh, l: Fl } = add(this.Fh | 0, this.Fl | 0, Fh | 0, Fl | 0)); + ({ h: Gh, l: Gl } = add(this.Gh | 0, this.Gl | 0, Gh | 0, Gl | 0)); + ({ h: Hh, l: Hl } = add(this.Hh | 0, this.Hl | 0, Hh | 0, Hl | 0)); + this.set(Ah, Al, Bh, Bl, Ch, Cl, Dh, Dl, Eh, El, Fh, Fl, Gh, Gl, Hh, Hl); + } + roundClean() { + clean(SHA512_W_H, SHA512_W_L); + } + destroy() { + this.destroyed = true; + clean(this.buffer); + this.set(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0); + } +}; +var _SHA512 = class extends SHA2_64B { + constructor() { + super(64); + __publicField(this, "Ah", SHA512_IV[0] | 0); + __publicField(this, "Al", SHA512_IV[1] | 0); + __publicField(this, "Bh", SHA512_IV[2] | 0); + __publicField(this, "Bl", SHA512_IV[3] | 0); + __publicField(this, "Ch", SHA512_IV[4] | 0); + __publicField(this, "Cl", SHA512_IV[5] | 0); + __publicField(this, "Dh", SHA512_IV[6] | 0); + __publicField(this, "Dl", SHA512_IV[7] | 0); + __publicField(this, "Eh", SHA512_IV[8] | 0); + __publicField(this, "El", SHA512_IV[9] | 0); + __publicField(this, "Fh", SHA512_IV[10] | 0); + __publicField(this, "Fl", SHA512_IV[11] | 0); + __publicField(this, "Gh", SHA512_IV[12] | 0); + __publicField(this, "Gl", SHA512_IV[13] | 0); + __publicField(this, "Hh", SHA512_IV[14] | 0); + __publicField(this, "Hl", SHA512_IV[15] | 0); + } +}; +var sha256 = /* @__PURE__ */ createHasher( + () => new _SHA256(), + /* @__PURE__ */ oidNist(1) +); +var sha512 = /* @__PURE__ */ createHasher( + () => new _SHA512(), + /* @__PURE__ */ oidNist(3) +); + +// node_modules/@scure/base/index.js +function isBytes2(a) { + return a instanceof Uint8Array || ArrayBuffer.isView(a) && a.constructor.name === "Uint8Array" && "BYTES_PER_ELEMENT" in a && a.BYTES_PER_ELEMENT === 1; +} +function isArrayOf(isString, arr) { + if (!Array.isArray(arr)) + return false; + if (arr.length === 0) + return true; + if (isString) { + return arr.every((item) => typeof item === "string"); + } else { + return arr.every((item) => Number.isSafeInteger(item)); + } +} +function afn(input) { + if (typeof input !== "function") + throw new TypeError("function expected"); + return true; +} +function astr(label, input) { + if (typeof input !== "string") + throw new TypeError(`${label}: string expected`); + return true; +} +function anumber2(n) { + if (typeof n !== "number") + throw new TypeError(`number expected, got ${typeof n}`); + if (!Number.isSafeInteger(n)) + throw new RangeError(`invalid integer: ${n}`); +} +function aArr(input) { + if (!Array.isArray(input)) + throw new TypeError("array expected"); +} +function astrArr(label, input) { + if (!isArrayOf(true, input)) + throw new TypeError(`${label}: array of strings expected`); +} +function anumArr(label, input) { + if (!isArrayOf(false, input)) + throw new TypeError(`${label}: array of numbers expected`); +} +// @__NO_SIDE_EFFECTS__ +function chain(...args) { + const id2 = (a) => a; + const wrap = (a, b) => (c) => a(b(c)); + const encode = args.map((x) => x.encode).reduceRight(wrap, id2); + const decode = args.map((x) => x.decode).reduce(wrap, id2); + return { encode, decode }; +} +// @__NO_SIDE_EFFECTS__ +function alphabet(letters) { + const lettersA = typeof letters === "string" ? letters.split("") : letters; + const len = lettersA.length; + astrArr("alphabet", lettersA); + const indexes = new Map(lettersA.map((l, i) => [l, i])); + return { + encode: (digits) => { + aArr(digits); + return digits.map((i) => { + if (!Number.isSafeInteger(i) || i < 0 || i >= len) + throw new Error(`alphabet.encode: digit index outside alphabet "${i}". Allowed: ${letters}`); + return lettersA[i]; + }); + }, + decode: (input) => { + aArr(input); + return input.map((letter) => { + astr("alphabet.decode", letter); + const i = indexes.get(letter); + if (i === void 0) + throw new Error(`Unknown letter: "${letter}". Allowed: ${letters}`); + return i; + }); + } + }; +} +// @__NO_SIDE_EFFECTS__ +function join(separator = "") { + astr("join", separator); + return { + encode: (from) => { + astrArr("join.decode", from); + return from.join(separator); + }, + decode: (to) => { + astr("join.decode", to); + return to.split(separator); + } + }; +} +// @__NO_SIDE_EFFECTS__ +function padding(bits, chr = "=") { + anumber2(bits); + astr("padding", chr); + return { + encode(data) { + astrArr("padding.encode", data); + while (data.length * bits % 8) + data.push(chr); + return data; + }, + decode(input) { + astrArr("padding.decode", input); + let end = input.length; + if (end * bits % 8) + throw new Error("padding: invalid, string should have whole number of bytes"); + for (; end > 0 && input[end - 1] === chr; end--) { + const last = end - 1; + const byte = last * bits; + if (byte % 8 === 0) + throw new Error("padding: invalid, string has too much padding"); + } + return input.slice(0, end); + } + }; +} +function convertRadix(data, from, to) { + if (from < 2) + throw new RangeError(`convertRadix: invalid from=${from}, base cannot be less than 2`); + if (to < 2) + throw new RangeError(`convertRadix: invalid to=${to}, base cannot be less than 2`); + aArr(data); + if (!data.length) + return []; + let pos = 0; + const res = []; + const digits = Array.from(data, (d) => { + anumber2(d); + if (d < 0 || d >= from) + throw new Error(`invalid integer: ${d}`); + return d; + }); + const dlen = digits.length; + while (true) { + let carry = 0; + let done = true; + for (let i = pos; i < dlen; i++) { + const digit = digits[i]; + const fromCarry = from * carry; + const digitBase = fromCarry + digit; + if (!Number.isSafeInteger(digitBase) || fromCarry / from !== carry || digitBase - digit !== fromCarry) { + throw new Error("convertRadix: carry overflow"); + } + const div = digitBase / to; + carry = digitBase % to; + const rounded = Math.floor(div); + digits[i] = rounded; + if (!Number.isSafeInteger(rounded) || rounded * to + carry !== digitBase) + throw new Error("convertRadix: carry overflow"); + if (!done) + continue; + else if (!rounded) + pos = i; + else + done = false; + } + res.push(carry); + if (done) + break; + } + for (let i = 0; i < data.length - 1 && data[i] === 0; i++) + res.push(0); + return res.reverse(); +} +var gcd = (a, b) => b === 0 ? a : gcd(b, a % b); +var radix2carry = /* @__NO_SIDE_EFFECTS__ */ (from, to) => from + (to - gcd(from, to)); +var powers = /* @__PURE__ */ (() => { + let res = []; + for (let i = 0; i < 40; i++) + res.push(2 ** i); + return res; +})(); +function convertRadix2(data, from, to, padding2) { + aArr(data); + if (from <= 0 || from > 32) + throw new RangeError(`convertRadix2: wrong from=${from}`); + if (to <= 0 || to > 32) + throw new RangeError(`convertRadix2: wrong to=${to}`); + if (/* @__PURE__ */ radix2carry(from, to) > 32) { + throw new Error(`convertRadix2: carry overflow from=${from} to=${to} carryBits=${/* @__PURE__ */ radix2carry(from, to)}`); + } + let carry = 0; + let pos = 0; + const max = powers[from]; + const mask = powers[to] - 1; + const res = []; + for (const n of data) { + anumber2(n); + if (n >= max) + throw new Error(`convertRadix2: invalid data word=${n} from=${from}`); + carry = carry << from | n; + if (pos + from > 32) + throw new Error(`convertRadix2: carry overflow pos=${pos} from=${from}`); + pos += from; + for (; pos >= to; pos -= to) + res.push((carry >> pos - to & mask) >>> 0); + const pow = powers[pos]; + if (pow === void 0) + throw new Error("invalid carry"); + carry &= pow - 1; + } + carry = carry << to - pos & mask; + if (!padding2 && pos >= from) + throw new Error("Excess padding"); + if (!padding2 && carry > 0) + throw new Error(`Non-zero padding: ${carry}`); + if (padding2 && pos > 0) + res.push(carry >>> 0); + return res; +} +// @__NO_SIDE_EFFECTS__ +function radix(num2) { + anumber2(num2); + const _256 = 2 ** 8; + return { + encode: (bytes) => { + if (!isBytes2(bytes)) + throw new TypeError("radix.encode input should be Uint8Array"); + return convertRadix(Array.from(bytes), _256, num2); + }, + decode: (digits) => { + anumArr("radix.decode", digits); + return Uint8Array.from(convertRadix(digits, num2, _256)); + } + }; +} +// @__NO_SIDE_EFFECTS__ +function radix2(bits, revPadding = false) { + anumber2(bits); + if (bits <= 0 || bits > 32) + throw new RangeError("radix2: bits should be in (0..32]"); + if (/* @__PURE__ */ radix2carry(8, bits) > 32 || /* @__PURE__ */ radix2carry(bits, 8) > 32) + throw new RangeError("radix2: carry overflow"); + return { + encode: (bytes) => { + if (!isBytes2(bytes)) + throw new TypeError("radix2.encode input should be Uint8Array"); + return convertRadix2(Array.from(bytes), 8, bits, !revPadding); + }, + decode: (digits) => { + anumArr("radix2.decode", digits); + return Uint8Array.from(convertRadix2(digits, bits, 8, revPadding)); + } + }; +} +function unsafeWrapper(fn) { + afn(fn); + return function(...args) { + try { + return fn.apply(null, args); + } catch (e) { + } + }; +} +function checksum(len, fn) { + anumber2(len); + if (len <= 0) + throw new RangeError(`checksum length must be positive: ${len}`); + afn(fn); + const _fn = fn; + return { + encode(data) { + if (!isBytes2(data)) + throw new TypeError("checksum.encode: input should be Uint8Array"); + const sum = _fn(data).slice(0, len); + const res = new Uint8Array(data.length + len); + res.set(data); + res.set(sum, data.length); + return res; + }, + decode(data) { + if (!isBytes2(data)) + throw new TypeError("checksum.decode: input should be Uint8Array"); + const payload = data.slice(0, -len); + const oldChecksum = data.slice(-len); + const newChecksum = _fn(payload).slice(0, len); + for (let i = 0; i < len; i++) + if (newChecksum[i] !== oldChecksum[i]) + throw new Error("Invalid checksum"); + return payload; + } + }; +} +var utils = /* @__PURE__ */ Object.freeze({ + alphabet, + chain, + checksum, + convertRadix, + convertRadix2, + radix, + radix2, + join, + padding +}); +var genBase58 = /* @__NO_SIDE_EFFECTS__ */ (abc) => /* @__PURE__ */ chain(/* @__PURE__ */ radix(58), /* @__PURE__ */ alphabet(abc), /* @__PURE__ */ join("")); +var base58 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ genBase58("123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz")); +var createBase58check = (sha2562) => { + afn(sha2562); + const _sha256 = sha2562; + return /* @__PURE__ */ chain(checksum(4, (data) => _sha256(_sha256(data))), base58); +}; +var BECH_ALPHABET = /* @__PURE__ */ chain(/* @__PURE__ */ alphabet("qpzry9x8gf2tvdw0s3jn54khce6mua7l"), /* @__PURE__ */ join("")); +var POLYMOD_GENERATORS = [996825010, 642813549, 513874426, 1027748829, 705979059]; +function bech32Polymod(pre) { + const b = pre >> 25; + let chk = (pre & 33554431) << 5; + for (let i = 0; i < POLYMOD_GENERATORS.length; i++) { + if ((b >> i & 1) === 1) + chk ^= POLYMOD_GENERATORS[i]; + } + return chk; +} +function bechChecksum(prefix, words, encodingConst = 1) { + const len = prefix.length; + let chk = 1; + for (let i = 0; i < len; i++) { + const c = prefix.charCodeAt(i); + if (c < 33 || c > 126) + throw new Error(`Invalid prefix (${prefix})`); + chk = bech32Polymod(chk) ^ c >> 5; + } + chk = bech32Polymod(chk); + for (let i = 0; i < len; i++) + chk = bech32Polymod(chk) ^ prefix.charCodeAt(i) & 31; + for (let v of words) + chk = bech32Polymod(chk) ^ v; + for (let i = 0; i < 6; i++) + chk = bech32Polymod(chk); + chk ^= encodingConst; + return BECH_ALPHABET.encode(convertRadix2([chk % powers[30]], 30, 5, false)); +} +// @__NO_SIDE_EFFECTS__ +function genBech32(encoding) { + const ENCODING_CONST = encoding === "bech32" ? 1 : 734539939; + const _words = /* @__PURE__ */ radix2(5); + const fromWords = _words.decode; + const toWords = _words.encode; + const fromWordsUnsafe = unsafeWrapper(fromWords); + function encode(prefix, words, limit = 90) { + astr("bech32.encode prefix", prefix); + if (isBytes2(words)) + words = Array.from(words); + anumArr("bech32.encode", words); + const plen = prefix.length; + if (plen === 0) + throw new TypeError(`Invalid prefix length ${plen}`); + const actualLength = plen + 7 + words.length; + if (limit !== false && actualLength > limit) + throw new TypeError(`Length ${actualLength} exceeds limit ${limit}`); + const lowered = prefix.toLowerCase(); + const sum = bechChecksum(lowered, words, ENCODING_CONST); + return `${lowered}1${BECH_ALPHABET.encode(words)}${sum}`; + } + function decode(str, limit = 90) { + astr("bech32.decode input", str); + const slen = str.length; + if (slen < 8 || limit !== false && slen > limit) + throw new TypeError(`invalid string length: ${slen} (${str}). Expected (8..${limit})`); + const lowered = str.toLowerCase(); + if (str !== lowered && str !== str.toUpperCase()) + throw new Error(`String must be lowercase or uppercase`); + const sepIndex = lowered.lastIndexOf("1"); + if (sepIndex === 0 || sepIndex === -1) + throw new Error(`Letter "1" must be present between prefix and data only`); + const prefix = lowered.slice(0, sepIndex); + const data = lowered.slice(sepIndex + 1); + if (data.length < 6) + throw new Error("Data must be at least 6 characters long"); + const words = BECH_ALPHABET.decode(data).slice(0, -6); + const sum = bechChecksum(prefix, words, ENCODING_CONST); + if (!data.endsWith(sum)) + throw new Error(`Invalid checksum in ${str}: expected "${sum}"`); + return { prefix, words }; + } + const decodeUnsafe = unsafeWrapper(decode); + function decodeToBytes(str) { + const { prefix, words } = decode(str, false); + return { + prefix, + words, + bytes: fromWords(words) + }; + } + function encodeFromBytes(prefix, bytes) { + return encode(prefix, toWords(bytes)); + } + return { + encode, + decode, + encodeFromBytes, + decodeToBytes, + decodeUnsafe, + fromWords, + fromWordsUnsafe, + toWords + }; +} +var bech32 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ genBech32("bech32")); + +// node_modules/@scure/bip39/index.js +var isJapanese = (wordlist2) => wordlist2[0] === "\u3042\u3044\u3053\u304F\u3057\u3093"; +function nfkd(str) { + if (typeof str !== "string") + throw new TypeError("invalid mnemonic type: " + typeof str); + return str.normalize("NFKD"); +} +function normalize(str) { + const norm = nfkd(str); + const words = norm.split(" "); + if (![12, 15, 18, 21, 24].includes(words.length)) + throw new Error("Invalid mnemonic"); + return { nfkd: norm, words }; +} +function aentropy(ent) { + abytes(ent); + if (![16, 20, 24, 28, 32].includes(ent.length)) + throw new RangeError("invalid entropy length"); +} +function generateMnemonic(wordlist2, strength = 128) { + anumber(strength); + if (strength % 32 !== 0 || strength > 256) + throw new RangeError("Invalid entropy"); + return entropyToMnemonic(randomBytes(strength / 8), wordlist2); +} +var calcChecksum = (entropy) => { + const bitsLeft = 8 - entropy.length / 4; + return new Uint8Array([sha256(entropy)[0] >> bitsLeft << bitsLeft]); +}; +function getCoder(wordlist2) { + if (!Array.isArray(wordlist2) || wordlist2.length !== 2048 || typeof wordlist2[0] !== "string") + throw new TypeError("Wordlist: expected array of 2048 strings"); + wordlist2.forEach((i) => { + if (typeof i !== "string") + throw new TypeError("wordlist: non-string element: " + i); + }); + return utils.chain(utils.checksum(1, calcChecksum), utils.radix2(11, true), utils.alphabet(wordlist2)); +} +function mnemonicToEntropy(mnemonic, wordlist2) { + const { words } = normalize(mnemonic); + const entropy = getCoder(wordlist2).decode(words); + aentropy(entropy); + return entropy; +} +function entropyToMnemonic(entropy, wordlist2) { + aentropy(entropy); + const words = getCoder(wordlist2).encode(entropy); + return words.join(isJapanese(wordlist2) ? "\u3000" : " "); +} +function validateMnemonic(mnemonic, wordlist2) { + try { + mnemonicToEntropy(mnemonic, wordlist2); + } catch (e) { + return false; + } + return true; +} +var psalt = (passphrase) => nfkd("mnemonic" + passphrase); +function mnemonicToSeedSync(mnemonic, passphrase = "") { + return pbkdf2(sha512, normalize(mnemonic).nfkd, psalt(passphrase), { + c: 2048, + dkLen: 64 + }); +} + +// node_modules/@scure/bip39/wordlists/english.js +var wordlist = /* @__PURE__ */ Object.freeze(`abandon +ability +able +about +above +absent +absorb +abstract +absurd +abuse +access +accident +account +accuse +achieve +acid +acoustic +acquire +across +act +action +actor +actress +actual +adapt +add +addict +address +adjust +admit +adult +advance +advice +aerobic +affair +afford +afraid +again +age +agent +agree +ahead +aim +air +airport +aisle +alarm +album +alcohol +alert +alien +all +alley +allow +almost +alone +alpha +already +also +alter +always +amateur +amazing +among +amount +amused +analyst +anchor +ancient +anger +angle +angry +animal +ankle +announce +annual +another +answer +antenna +antique +anxiety +any +apart +apology +appear +apple +approve +april +arch +arctic +area +arena +argue +arm +armed +armor +army +around +arrange +arrest +arrive +arrow +art +artefact +artist +artwork +ask +aspect +assault +asset +assist +assume +asthma +athlete +atom +attack +attend +attitude +attract +auction +audit +august +aunt +author +auto +autumn +average +avocado +avoid +awake +aware +away +awesome +awful +awkward +axis +baby +bachelor +bacon +badge +bag +balance +balcony +ball +bamboo +banana +banner +bar +barely +bargain +barrel +base +basic +basket +battle +beach +bean +beauty +because +become +beef +before +begin +behave +behind +believe +below +belt +bench +benefit +best +betray +better +between +beyond +bicycle +bid +bike +bind +biology +bird +birth +bitter +black +blade +blame +blanket +blast +bleak +bless +blind +blood +blossom +blouse +blue +blur +blush +board +boat +body +boil +bomb +bone +bonus +book +boost +border +boring +borrow +boss +bottom +bounce +box +boy +bracket +brain +brand +brass +brave +bread +breeze +brick +bridge +brief +bright +bring +brisk +broccoli +broken +bronze +broom +brother +brown +brush +bubble +buddy +budget +buffalo +build +bulb +bulk +bullet +bundle +bunker +burden +burger +burst +bus +business +busy +butter +buyer +buzz +cabbage +cabin +cable +cactus +cage +cake +call +calm +camera +camp +can +canal +cancel +candy +cannon +canoe +canvas +canyon +capable +capital +captain +car +carbon +card +cargo +carpet +carry +cart +case +cash +casino +castle +casual +cat +catalog +catch +category +cattle +caught +cause +caution +cave +ceiling +celery +cement +census +century +cereal +certain +chair +chalk +champion +change +chaos +chapter +charge +chase +chat +cheap +check +cheese +chef +cherry +chest +chicken +chief +child +chimney +choice +choose +chronic +chuckle +chunk +churn +cigar +cinnamon +circle +citizen +city +civil +claim +clap +clarify +claw +clay +clean +clerk +clever +click +client +cliff +climb +clinic +clip +clock +clog +close +cloth +cloud +clown +club +clump +cluster +clutch +coach +coast +coconut +code +coffee +coil +coin +collect +color +column +combine +come +comfort +comic +common +company +concert +conduct +confirm +congress +connect +consider +control +convince +cook +cool +copper +copy +coral +core +corn +correct +cost +cotton +couch +country +couple +course +cousin +cover +coyote +crack +cradle +craft +cram +crane +crash +crater +crawl +crazy +cream +credit +creek +crew +cricket +crime +crisp +critic +crop +cross +crouch +crowd +crucial +cruel +cruise +crumble +crunch +crush +cry +crystal +cube +culture +cup +cupboard +curious +current +curtain +curve +cushion +custom +cute +cycle +dad +damage +damp +dance +danger +daring +dash +daughter +dawn +day +deal +debate +debris +decade +december +decide +decline +decorate +decrease +deer +defense +define +defy +degree +delay +deliver +demand +demise +denial +dentist +deny +depart +depend +deposit +depth +deputy +derive +describe +desert +design +desk +despair +destroy +detail +detect +develop +device +devote +diagram +dial +diamond +diary +dice +diesel +diet +differ +digital +dignity +dilemma +dinner +dinosaur +direct +dirt +disagree +discover +disease +dish +dismiss +disorder +display +distance +divert +divide +divorce +dizzy +doctor +document +dog +doll +dolphin +domain +donate +donkey +donor +door +dose +double +dove +draft +dragon +drama +drastic +draw +dream +dress +drift +drill +drink +drip +drive +drop +drum +dry +duck +dumb +dune +during +dust +dutch +duty +dwarf +dynamic +eager +eagle +early +earn +earth +easily +east +easy +echo +ecology +economy +edge +edit +educate +effort +egg +eight +either +elbow +elder +electric +elegant +element +elephant +elevator +elite +else +embark +embody +embrace +emerge +emotion +employ +empower +empty +enable +enact +end +endless +endorse +enemy +energy +enforce +engage +engine +enhance +enjoy +enlist +enough +enrich +enroll +ensure +enter +entire +entry +envelope +episode +equal +equip +era +erase +erode +erosion +error +erupt +escape +essay +essence +estate +eternal +ethics +evidence +evil +evoke +evolve +exact +example +excess +exchange +excite +exclude +excuse +execute +exercise +exhaust +exhibit +exile +exist +exit +exotic +expand +expect +expire +explain +expose +express +extend +extra +eye +eyebrow +fabric +face +faculty +fade +faint +faith +fall +false +fame +family +famous +fan +fancy +fantasy +farm +fashion +fat +fatal +father +fatigue +fault +favorite +feature +february +federal +fee +feed +feel +female +fence +festival +fetch +fever +few +fiber +fiction +field +figure +file +film +filter +final +find +fine +finger +finish +fire +firm +first +fiscal +fish +fit +fitness +fix +flag +flame +flash +flat +flavor +flee +flight +flip +float +flock +floor +flower +fluid +flush +fly +foam +focus +fog +foil +fold +follow +food +foot +force +forest +forget +fork +fortune +forum +forward +fossil +foster +found +fox +fragile +frame +frequent +fresh +friend +fringe +frog +front +frost +frown +frozen +fruit +fuel +fun +funny +furnace +fury +future +gadget +gain +galaxy +gallery +game +gap +garage +garbage +garden +garlic +garment +gas +gasp +gate +gather +gauge +gaze +general +genius +genre +gentle +genuine +gesture +ghost +giant +gift +giggle +ginger +giraffe +girl +give +glad +glance +glare +glass +glide +glimpse +globe +gloom +glory +glove +glow +glue +goat +goddess +gold +good +goose +gorilla +gospel +gossip +govern +gown +grab +grace +grain +grant +grape +grass +gravity +great +green +grid +grief +grit +grocery +group +grow +grunt +guard +guess +guide +guilt +guitar +gun +gym +habit +hair +half +hammer +hamster +hand +happy +harbor +hard +harsh +harvest +hat +have +hawk +hazard +head +health +heart +heavy +hedgehog +height +hello +helmet +help +hen +hero +hidden +high +hill +hint +hip +hire +history +hobby +hockey +hold +hole +holiday +hollow +home +honey +hood +hope +horn +horror +horse +hospital +host +hotel +hour +hover +hub +huge +human +humble +humor +hundred +hungry +hunt +hurdle +hurry +hurt +husband +hybrid +ice +icon +idea +identify +idle +ignore +ill +illegal +illness +image +imitate +immense +immune +impact +impose +improve +impulse +inch +include +income +increase +index +indicate +indoor +industry +infant +inflict +inform +inhale +inherit +initial +inject +injury +inmate +inner +innocent +input +inquiry +insane +insect +inside +inspire +install +intact +interest +into +invest +invite +involve +iron +island +isolate +issue +item +ivory +jacket +jaguar +jar +jazz +jealous +jeans +jelly +jewel +job +join +joke +journey +joy +judge +juice +jump +jungle +junior +junk +just +kangaroo +keen +keep +ketchup +key +kick +kid +kidney +kind +kingdom +kiss +kit +kitchen +kite +kitten +kiwi +knee +knife +knock +know +lab +label +labor +ladder +lady +lake +lamp +language +laptop +large +later +latin +laugh +laundry +lava +law +lawn +lawsuit +layer +lazy +leader +leaf +learn +leave +lecture +left +leg +legal +legend +leisure +lemon +lend +length +lens +leopard +lesson +letter +level +liar +liberty +library +license +life +lift +light +like +limb +limit +link +lion +liquid +list +little +live +lizard +load +loan +lobster +local +lock +logic +lonely +long +loop +lottery +loud +lounge +love +loyal +lucky +luggage +lumber +lunar +lunch +luxury +lyrics +machine +mad +magic +magnet +maid +mail +main +major +make +mammal +man +manage +mandate +mango +mansion +manual +maple +marble +march +margin +marine +market +marriage +mask +mass +master +match +material +math +matrix +matter +maximum +maze +meadow +mean +measure +meat +mechanic +medal +media +melody +melt +member +memory +mention +menu +mercy +merge +merit +merry +mesh +message +metal +method +middle +midnight +milk +million +mimic +mind +minimum +minor +minute +miracle +mirror +misery +miss +mistake +mix +mixed +mixture +mobile +model +modify +mom +moment +monitor +monkey +monster +month +moon +moral +more +morning +mosquito +mother +motion +motor +mountain +mouse +move +movie +much +muffin +mule +multiply +muscle +museum +mushroom +music +must +mutual +myself +mystery +myth +naive +name +napkin +narrow +nasty +nation +nature +near +neck +need +negative +neglect +neither +nephew +nerve +nest +net +network +neutral +never +news +next +nice +night +noble +noise +nominee +noodle +normal +north +nose +notable +note +nothing +notice +novel +now +nuclear +number +nurse +nut +oak +obey +object +oblige +obscure +observe +obtain +obvious +occur +ocean +october +odor +off +offer +office +often +oil +okay +old +olive +olympic +omit +once +one +onion +online +only +open +opera +opinion +oppose +option +orange +orbit +orchard +order +ordinary +organ +orient +original +orphan +ostrich +other +outdoor +outer +output +outside +oval +oven +over +own +owner +oxygen +oyster +ozone +pact +paddle +page +pair +palace +palm +panda +panel +panic +panther +paper +parade +parent +park +parrot +party +pass +patch +path +patient +patrol +pattern +pause +pave +payment +peace +peanut +pear +peasant +pelican +pen +penalty +pencil +people +pepper +perfect +permit +person +pet +phone +photo +phrase +physical +piano +picnic +picture +piece +pig +pigeon +pill +pilot +pink +pioneer +pipe +pistol +pitch +pizza +place +planet +plastic +plate +play +please +pledge +pluck +plug +plunge +poem +poet +point +polar +pole +police +pond +pony +pool +popular +portion +position +possible +post +potato +pottery +poverty +powder +power +practice +praise +predict +prefer +prepare +present +pretty +prevent +price +pride +primary +print +priority +prison +private +prize +problem +process +produce +profit +program +project +promote +proof +property +prosper +protect +proud +provide +public +pudding +pull +pulp +pulse +pumpkin +punch +pupil +puppy +purchase +purity +purpose +purse +push +put +puzzle +pyramid +quality +quantum +quarter +question +quick +quit +quiz +quote +rabbit +raccoon +race +rack +radar +radio +rail +rain +raise +rally +ramp +ranch +random +range +rapid +rare +rate +rather +raven +raw +razor +ready +real +reason +rebel +rebuild +recall +receive +recipe +record +recycle +reduce +reflect +reform +refuse +region +regret +regular +reject +relax +release +relief +rely +remain +remember +remind +remove +render +renew +rent +reopen +repair +repeat +replace +report +require +rescue +resemble +resist +resource +response +result +retire +retreat +return +reunion +reveal +review +reward +rhythm +rib +ribbon +rice +rich +ride +ridge +rifle +right +rigid +ring +riot +ripple +risk +ritual +rival +river +road +roast +robot +robust +rocket +romance +roof +rookie +room +rose +rotate +rough +round +route +royal +rubber +rude +rug +rule +run +runway +rural +sad +saddle +sadness +safe +sail +salad +salmon +salon +salt +salute +same +sample +sand +satisfy +satoshi +sauce +sausage +save +say +scale +scan +scare +scatter +scene +scheme +school +science +scissors +scorpion +scout +scrap +screen +script +scrub +sea +search +season +seat +second +secret +section +security +seed +seek +segment +select +sell +seminar +senior +sense +sentence +series +service +session +settle +setup +seven +shadow +shaft +shallow +share +shed +shell +sheriff +shield +shift +shine +ship +shiver +shock +shoe +shoot +shop +short +shoulder +shove +shrimp +shrug +shuffle +shy +sibling +sick +side +siege +sight +sign +silent +silk +silly +silver +similar +simple +since +sing +siren +sister +situate +six +size +skate +sketch +ski +skill +skin +skirt +skull +slab +slam +sleep +slender +slice +slide +slight +slim +slogan +slot +slow +slush +small +smart +smile +smoke +smooth +snack +snake +snap +sniff +snow +soap +soccer +social +sock +soda +soft +solar +soldier +solid +solution +solve +someone +song +soon +sorry +sort +soul +sound +soup +source +south +space +spare +spatial +spawn +speak +special +speed +spell +spend +sphere +spice +spider +spike +spin +spirit +split +spoil +sponsor +spoon +sport +spot +spray +spread +spring +spy +square +squeeze +squirrel +stable +stadium +staff +stage +stairs +stamp +stand +start +state +stay +steak +steel +stem +step +stereo +stick +still +sting +stock +stomach +stone +stool +story +stove +strategy +street +strike +strong +struggle +student +stuff +stumble +style +subject +submit +subway +success +such +sudden +suffer +sugar +suggest +suit +summer +sun +sunny +sunset +super +supply +supreme +sure +surface +surge +surprise +surround +survey +suspect +sustain +swallow +swamp +swap +swarm +swear +sweet +swift +swim +swing +switch +sword +symbol +symptom +syrup +system +table +tackle +tag +tail +talent +talk +tank +tape +target +task +taste +tattoo +taxi +teach +team +tell +ten +tenant +tennis +tent +term +test +text +thank +that +theme +then +theory +there +they +thing +this +thought +three +thrive +throw +thumb +thunder +ticket +tide +tiger +tilt +timber +time +tiny +tip +tired +tissue +title +toast +tobacco +today +toddler +toe +together +toilet +token +tomato +tomorrow +tone +tongue +tonight +tool +tooth +top +topic +topple +torch +tornado +tortoise +toss +total +tourist +toward +tower +town +toy +track +trade +traffic +tragic +train +transfer +trap +trash +travel +tray +treat +tree +trend +trial +tribe +trick +trigger +trim +trip +trophy +trouble +truck +true +truly +trumpet +trust +truth +try +tube +tuition +tumble +tuna +tunnel +turkey +turn +turtle +twelve +twenty +twice +twin +twist +two +type +typical +ugly +umbrella +unable +unaware +uncle +uncover +under +undo +unfair +unfold +unhappy +uniform +unique +unit +universe +unknown +unlock +until +unusual +unveil +update +upgrade +uphold +upon +upper +upset +urban +urge +usage +use +used +useful +useless +usual +utility +vacant +vacuum +vague +valid +valley +valve +van +vanish +vapor +various +vast +vault +vehicle +velvet +vendor +venture +venue +verb +verify +version +very +vessel +veteran +viable +vibrant +vicious +victory +video +view +village +vintage +violin +virtual +virus +visa +visit +visual +vital +vivid +vocal +voice +void +volcano +volume +vote +voyage +wage +wagon +wait +walk +wall +walnut +want +warfare +warm +warrior +wash +wasp +waste +water +wave +way +wealth +weapon +wear +weasel +weather +web +wedding +weekend +weird +welcome +west +wet +whale +what +wheat +wheel +when +where +whip +whisper +wide +width +wife +wild +will +win +window +wine +wing +wink +winner +winter +wire +wisdom +wise +wish +witness +wolf +woman +wonder +wood +wool +word +work +world +worry +worth +wrap +wreck +wrestle +wrist +write +wrong +yard +year +yellow +you +young +youth +zebra +zero +zone +zoo`.split("\n")); + +// node_modules/@noble/curves/utils.js +var abytes2 = (value, length, title) => abytes(value, length, title); +var anumber3 = anumber; +var bytesToHex2 = bytesToHex; +var concatBytes2 = (...arrays) => concatBytes(...arrays); +var hexToBytes2 = (hex) => hexToBytes(hex); +var isBytes3 = isBytes; +var randomBytes2 = (bytesLength) => randomBytes(bytesLength); +var _0n = /* @__PURE__ */ BigInt(0); +var _1n = /* @__PURE__ */ BigInt(1); +function abool(value, title = "") { + if (typeof value !== "boolean") { + const prefix = title && `"${title}" `; + throw new TypeError(prefix + "expected boolean, got type=" + typeof value); + } + return value; +} +function abignumber(n) { + if (typeof n === "bigint") { + if (!isPosBig(n)) + throw new RangeError("positive bigint expected, got " + n); + } else + anumber3(n); + return n; +} +function asafenumber(value, title = "") { + if (typeof value !== "number") { + const prefix = title && `"${title}" `; + throw new TypeError(prefix + "expected number, got type=" + typeof value); + } + if (!Number.isSafeInteger(value)) { + const prefix = title && `"${title}" `; + throw new RangeError(prefix + "expected safe integer, got " + value); + } +} +function numberToHexUnpadded(num2) { + const hex = abignumber(num2).toString(16); + return hex.length & 1 ? "0" + hex : hex; +} +function hexToNumber(hex) { + if (typeof hex !== "string") + throw new TypeError("hex string expected, got " + typeof hex); + return hex === "" ? _0n : BigInt("0x" + hex); +} +function bytesToNumberBE(bytes) { + return hexToNumber(bytesToHex(bytes)); +} +function bytesToNumberLE(bytes) { + return hexToNumber(bytesToHex(copyBytes(abytes(bytes)).reverse())); +} +function numberToBytesBE(n, len) { + anumber(len); + if (len === 0) + throw new RangeError("zero length"); + n = abignumber(n); + const hex = n.toString(16); + if (hex.length > len * 2) + throw new RangeError("number too large"); + return hexToBytes(hex.padStart(len * 2, "0")); +} +function numberToBytesLE(n, len) { + return numberToBytesBE(n, len).reverse(); +} +function copyBytes(bytes) { + return Uint8Array.from(abytes2(bytes)); +} +function asciiToBytes(ascii) { + if (typeof ascii !== "string") + throw new TypeError("ascii string expected, got " + typeof ascii); + return Uint8Array.from(ascii, (c, i) => { + const charCode = c.charCodeAt(0); + if (c.length !== 1 || charCode > 127) { + throw new RangeError(`string contains non-ASCII character "${ascii[i]}" with code ${charCode} at position ${i}`); + } + return charCode; + }); +} +var isPosBig = (n) => typeof n === "bigint" && _0n <= n; +function inRange(n, min, max) { + return isPosBig(n) && isPosBig(min) && isPosBig(max) && min <= n && n < max; +} +function aInRange(title, n, min, max) { + if (!inRange(n, min, max)) + throw new RangeError("expected valid " + title + ": " + min + " <= n < " + max + ", got " + n); +} +function bitLen(n) { + if (n < _0n) + throw new Error("expected non-negative bigint, got " + n); + let len; + for (len = 0; n > _0n; n >>= _1n, len += 1) + ; + return len; +} +var bitMask = (n) => (_1n << BigInt(n)) - _1n; +function createHmacDrbg(hashLen, qByteLen, hmacFn) { + anumber(hashLen, "hashLen"); + anumber(qByteLen, "qByteLen"); + if (typeof hmacFn !== "function") + throw new TypeError("hmacFn must be a function"); + const u8n = (len) => new Uint8Array(len); + const NULL = Uint8Array.of(); + const byte0 = Uint8Array.of(0); + const byte1 = Uint8Array.of(1); + const _maxDrbgIters = 1e3; + let v = u8n(hashLen); + let k = u8n(hashLen); + let i = 0; + const reset = () => { + v.fill(1); + k.fill(0); + i = 0; + }; + const h = (...msgs) => hmacFn(k, concatBytes2(v, ...msgs)); + const reseed = (seed = NULL) => { + k = h(byte0, seed); + v = h(); + if (seed.length === 0) + return; + k = h(byte1, seed); + v = h(); + }; + const gen2 = () => { + if (i++ >= _maxDrbgIters) + throw new Error("drbg: tried max amount of iterations"); + let len = 0; + const out = []; + while (len < qByteLen) { + v = h(); + const sl = v.slice(); + out.push(sl); + len += v.length; + } + return concatBytes2(...out); + }; + const genUntil = (seed, pred) => { + reset(); + reseed(seed); + let res = void 0; + while ((res = pred(gen2())) === void 0) + reseed(); + reset(); + return res; + }; + return genUntil; +} +function validateObject(object, fields = {}, optFields = {}) { + if (Object.prototype.toString.call(object) !== "[object Object]") + throw new TypeError("expected valid options object"); + function checkField(fieldName, expectedType, isOpt) { + if (!isOpt && expectedType !== "function" && !Object.hasOwn(object, fieldName)) + throw new TypeError(`param "${fieldName}" is invalid: expected own property`); + const val = object[fieldName]; + if (isOpt && val === void 0) + return; + const current = typeof val; + if (current !== expectedType || val === null) + throw new TypeError(`param "${fieldName}" is invalid: expected ${expectedType}, got ${current}`); + } + const iter = (f, isOpt) => Object.entries(f).forEach(([k, v]) => checkField(k, v, isOpt)); + iter(fields, false); + iter(optFields, true); +} + +// node_modules/@noble/curves/abstract/modular.js +var _0n2 = /* @__PURE__ */ BigInt(0); +var _1n2 = /* @__PURE__ */ BigInt(1); +var _2n = /* @__PURE__ */ BigInt(2); +var _3n = /* @__PURE__ */ BigInt(3); +var _4n = /* @__PURE__ */ BigInt(4); +var _5n = /* @__PURE__ */ BigInt(5); +var _7n = /* @__PURE__ */ BigInt(7); +var _8n = /* @__PURE__ */ BigInt(8); +var _9n = /* @__PURE__ */ BigInt(9); +var _16n = /* @__PURE__ */ BigInt(16); +function mod(a, b) { + if (b <= _0n2) + throw new Error("mod: expected positive modulus, got " + b); + const result = a % b; + return result >= _0n2 ? result : b + result; +} +function pow2(x, power, modulo) { + if (power < _0n2) + throw new Error("pow2: expected non-negative exponent, got " + power); + let res = x; + while (power-- > _0n2) { + res *= res; + res %= modulo; + } + return res; +} +function invert(number, modulo) { + if (number === _0n2) + throw new Error("invert: expected non-zero number"); + if (modulo <= _0n2) + throw new Error("invert: expected positive modulus, got " + modulo); + let a = mod(number, modulo); + let b = modulo; + let x = _0n2, y = _1n2, u = _1n2, v = _0n2; + while (a !== _0n2) { + const q = b / a; + const r = b - a * q; + const m = x - u * q; + const n = y - v * q; + b = a, a = r, x = u, y = v, u = m, v = n; + } + const gcd2 = b; + if (gcd2 !== _1n2) + throw new Error("invert: does not exist"); + return mod(x, modulo); +} +function assertIsSquare(Fp, root, n) { + const F3 = Fp; + if (!F3.eql(F3.sqr(root), n)) + throw new Error("Cannot find square root"); +} +function sqrt3mod4(Fp, n) { + const F3 = Fp; + const p1div4 = (F3.ORDER + _1n2) / _4n; + const root = F3.pow(n, p1div4); + assertIsSquare(F3, root, n); + return root; +} +function sqrt5mod8(Fp, n) { + const F3 = Fp; + const p5div8 = (F3.ORDER - _5n) / _8n; + const n2 = F3.mul(n, _2n); + const v = F3.pow(n2, p5div8); + const nv = F3.mul(n, v); + const i = F3.mul(F3.mul(nv, _2n), v); + const root = F3.mul(nv, F3.sub(i, F3.ONE)); + assertIsSquare(F3, root, n); + return root; +} +function sqrt9mod16(P) { + const Fp_ = Field(P); + const tn = tonelliShanks(P); + const c1 = tn(Fp_, Fp_.neg(Fp_.ONE)); + const c2 = tn(Fp_, c1); + const c3 = tn(Fp_, Fp_.neg(c1)); + const c4 = (P + _7n) / _16n; + return ((Fp, n) => { + const F3 = Fp; + let tv1 = F3.pow(n, c4); + let tv2 = F3.mul(tv1, c1); + const tv3 = F3.mul(tv1, c2); + const tv4 = F3.mul(tv1, c3); + const e1 = F3.eql(F3.sqr(tv2), n); + const e2 = F3.eql(F3.sqr(tv3), n); + tv1 = F3.cmov(tv1, tv2, e1); + tv2 = F3.cmov(tv4, tv3, e2); + const e3 = F3.eql(F3.sqr(tv2), n); + const root = F3.cmov(tv1, tv2, e3); + assertIsSquare(F3, root, n); + return root; + }); +} +function tonelliShanks(P) { + if (P < _3n) + throw new Error("sqrt is not defined for small field"); + let Q4 = P - _1n2; + let S = 0; + while (Q4 % _2n === _0n2) { + Q4 /= _2n; + S++; + } + let Z = _2n; + const _Fp = Field(P); + while (FpLegendre(_Fp, Z) === 1) { + if (Z++ > 1e3) + throw new Error("Cannot find square root: probably non-prime P"); + } + if (S === 1) + return sqrt3mod4; + let cc = _Fp.pow(Z, Q4); + const Q1div2 = (Q4 + _1n2) / _2n; + return function tonelliSlow(Fp, n) { + const F3 = Fp; + if (F3.is0(n)) + return n; + if (FpLegendre(F3, n) !== 1) + throw new Error("Cannot find square root"); + let M = S; + let c = F3.mul(F3.ONE, cc); + let t = F3.pow(n, Q4); + let R = F3.pow(n, Q1div2); + while (!F3.eql(t, F3.ONE)) { + if (F3.is0(t)) + return F3.ZERO; + let i = 1; + let t_tmp = F3.sqr(t); + while (!F3.eql(t_tmp, F3.ONE)) { + i++; + t_tmp = F3.sqr(t_tmp); + if (i === M) + throw new Error("Cannot find square root"); + } + const exponent = _1n2 << BigInt(M - i - 1); + const b = F3.pow(c, exponent); + M = i; + c = F3.sqr(b); + t = F3.mul(t, c); + R = F3.mul(R, b); + } + return R; + }; +} +function FpSqrt(P) { + if (P % _4n === _3n) + return sqrt3mod4; + if (P % _8n === _5n) + return sqrt5mod8; + if (P % _16n === _9n) + return sqrt9mod16(P); + return tonelliShanks(P); +} +var FIELD_FIELDS = [ + "create", + "isValid", + "is0", + "neg", + "inv", + "sqrt", + "sqr", + "eql", + "add", + "sub", + "mul", + "pow", + "div", + "addN", + "subN", + "mulN", + "sqrN" +]; +function validateField(field) { + const initial = { + ORDER: "bigint", + BYTES: "number", + BITS: "number" + }; + const opts2 = FIELD_FIELDS.reduce((map, val) => { + map[val] = "function"; + return map; + }, initial); + validateObject(field, opts2); + asafenumber(field.BYTES, "BYTES"); + asafenumber(field.BITS, "BITS"); + if (field.BYTES < 1 || field.BITS < 1) + throw new Error("invalid field: expected BYTES/BITS > 0"); + if (field.ORDER <= _1n2) + throw new Error("invalid field: expected ORDER > 1, got " + field.ORDER); + return field; +} +function FpPow(Fp, num2, power) { + const F3 = Fp; + if (power < _0n2) + throw new Error("invalid exponent, negatives unsupported"); + if (power === _0n2) + return F3.ONE; + if (power === _1n2) + return num2; + let p = F3.ONE; + let d = num2; + while (power > _0n2) { + if (power & _1n2) + p = F3.mul(p, d); + d = F3.sqr(d); + power >>= _1n2; + } + return p; +} +function FpInvertBatch(Fp, nums, passZero = false) { + const F3 = Fp; + const inverted = new Array(nums.length).fill(passZero ? F3.ZERO : void 0); + const multipliedAcc = nums.reduce((acc, num2, i) => { + if (F3.is0(num2)) + return acc; + inverted[i] = acc; + return F3.mul(acc, num2); + }, F3.ONE); + const invertedAcc = F3.inv(multipliedAcc); + nums.reduceRight((acc, num2, i) => { + if (F3.is0(num2)) + return acc; + inverted[i] = F3.mul(acc, inverted[i]); + return F3.mul(acc, num2); + }, invertedAcc); + return inverted; +} +function FpLegendre(Fp, n) { + const F3 = Fp; + const p1mod2 = (F3.ORDER - _1n2) / _2n; + const powered = F3.pow(n, p1mod2); + const yes = F3.eql(powered, F3.ONE); + const zero = F3.eql(powered, F3.ZERO); + const no = F3.eql(powered, F3.neg(F3.ONE)); + if (!yes && !zero && !no) + throw new Error("invalid Legendre symbol result"); + return yes ? 1 : zero ? 0 : -1; +} +function nLength(n, nBitLength) { + if (nBitLength !== void 0) + anumber3(nBitLength); + if (n <= _0n2) + throw new Error("invalid n length: expected positive n, got " + n); + if (nBitLength !== void 0 && nBitLength < 1) + throw new Error("invalid n length: expected positive bit length, got " + nBitLength); + const bits = bitLen(n); + if (nBitLength !== void 0 && nBitLength < bits) + throw new Error(`invalid n length: expected bit length (${bits}) >= n.length (${nBitLength})`); + const _nBitLength = nBitLength !== void 0 ? nBitLength : bits; + const nByteLength = Math.ceil(_nBitLength / 8); + return { nBitLength: _nBitLength, nByteLength }; +} +var FIELD_SQRT = /* @__PURE__ */ new WeakMap(); +var _Field = class { + constructor(ORDER, opts2 = {}) { + __publicField(this, "ORDER"); + __publicField(this, "BITS"); + __publicField(this, "BYTES"); + __publicField(this, "isLE"); + __publicField(this, "ZERO", _0n2); + __publicField(this, "ONE", _1n2); + __publicField(this, "_lengths"); + __publicField(this, "_mod"); + if (ORDER <= _1n2) + throw new Error("invalid field: expected ORDER > 1, got " + ORDER); + let _nbitLength = void 0; + this.isLE = false; + if (opts2 != null && typeof opts2 === "object") { + if (typeof opts2.BITS === "number") + _nbitLength = opts2.BITS; + if (typeof opts2.sqrt === "function") + Object.defineProperty(this, "sqrt", { value: opts2.sqrt, enumerable: true }); + if (typeof opts2.isLE === "boolean") + this.isLE = opts2.isLE; + if (opts2.allowedLengths) + this._lengths = Object.freeze(opts2.allowedLengths.slice()); + if (typeof opts2.modFromBytes === "boolean") + this._mod = opts2.modFromBytes; + } + const { nBitLength, nByteLength } = nLength(ORDER, _nbitLength); + if (nByteLength > 2048) + throw new Error("invalid field: expected ORDER of <= 2048 bytes"); + this.ORDER = ORDER; + this.BITS = nBitLength; + this.BYTES = nByteLength; + Object.freeze(this); + } + create(num2) { + return mod(num2, this.ORDER); + } + isValid(num2) { + if (typeof num2 !== "bigint") + throw new TypeError("invalid field element: expected bigint, got " + typeof num2); + return _0n2 <= num2 && num2 < this.ORDER; + } + is0(num2) { + return num2 === _0n2; + } + // is valid and invertible + isValidNot0(num2) { + return !this.is0(num2) && this.isValid(num2); + } + isOdd(num2) { + return (num2 & _1n2) === _1n2; + } + neg(num2) { + return mod(-num2, this.ORDER); + } + eql(lhs, rhs) { + return lhs === rhs; + } + sqr(num2) { + return mod(num2 * num2, this.ORDER); + } + add(lhs, rhs) { + return mod(lhs + rhs, this.ORDER); + } + sub(lhs, rhs) { + return mod(lhs - rhs, this.ORDER); + } + mul(lhs, rhs) { + return mod(lhs * rhs, this.ORDER); + } + pow(num2, power) { + return FpPow(this, num2, power); + } + div(lhs, rhs) { + return mod(lhs * invert(rhs, this.ORDER), this.ORDER); + } + // Same as above, but doesn't normalize + sqrN(num2) { + return num2 * num2; + } + addN(lhs, rhs) { + return lhs + rhs; + } + subN(lhs, rhs) { + return lhs - rhs; + } + mulN(lhs, rhs) { + return lhs * rhs; + } + inv(num2) { + return invert(num2, this.ORDER); + } + sqrt(num2) { + let sqrt = FIELD_SQRT.get(this); + if (!sqrt) + FIELD_SQRT.set(this, sqrt = FpSqrt(this.ORDER)); + return sqrt(this, num2); + } + toBytes(num2) { + return this.isLE ? numberToBytesLE(num2, this.BYTES) : numberToBytesBE(num2, this.BYTES); + } + fromBytes(bytes, skipValidation = false) { + abytes2(bytes); + const { _lengths: allowedLengths, BYTES, isLE: isLE3, ORDER, _mod: modFromBytes } = this; + if (allowedLengths) { + if (bytes.length < 1 || !allowedLengths.includes(bytes.length) || bytes.length > BYTES) { + throw new Error("Field.fromBytes: expected " + allowedLengths + " bytes, got " + bytes.length); + } + const padded = new Uint8Array(BYTES); + padded.set(bytes, isLE3 ? 0 : padded.length - bytes.length); + bytes = padded; + } + if (bytes.length !== BYTES) + throw new Error("Field.fromBytes: expected " + BYTES + " bytes, got " + bytes.length); + let scalar = isLE3 ? bytesToNumberLE(bytes) : bytesToNumberBE(bytes); + if (modFromBytes) + scalar = mod(scalar, ORDER); + if (!skipValidation) { + if (!this.isValid(scalar)) + throw new Error("invalid field element: outside of range 0..ORDER"); + } + return scalar; + } + // TODO: we don't need it here, move out to separate fn + invertBatch(lst) { + return FpInvertBatch(this, lst); + } + // We can't move this out because Fp6, Fp12 implement it + // and it's unclear what to return in there. + cmov(a, b, condition) { + abool(condition, "condition"); + return condition ? b : a; + } +}; +Object.freeze(_Field.prototype); +function Field(ORDER, opts2 = {}) { + return new _Field(ORDER, opts2); +} +function getFieldBytesLength(fieldOrder) { + if (typeof fieldOrder !== "bigint") + throw new Error("field order must be bigint"); + if (fieldOrder <= _1n2) + throw new Error("field order must be greater than 1"); + const bitLength = bitLen(fieldOrder - _1n2); + return Math.ceil(bitLength / 8); +} +function getMinHashLength(fieldOrder) { + const length = getFieldBytesLength(fieldOrder); + return length + Math.ceil(length / 2); +} +function mapHashToField(key, fieldOrder, isLE3 = false) { + abytes2(key); + const len = key.length; + const fieldLen = getFieldBytesLength(fieldOrder); + const minLen = Math.max(getMinHashLength(fieldOrder), 16); + if (len < minLen || len > 1024) + throw new Error("expected " + minLen + "-1024 bytes of input, got " + len); + const num2 = isLE3 ? bytesToNumberLE(key) : bytesToNumberBE(key); + const reduced = mod(num2, fieldOrder - _1n2) + _1n2; + return isLE3 ? numberToBytesLE(reduced, fieldLen) : numberToBytesBE(reduced, fieldLen); +} + +// node_modules/@noble/curves/abstract/curve.js +var _0n3 = /* @__PURE__ */ BigInt(0); +var _1n3 = /* @__PURE__ */ BigInt(1); +function negateCt(condition, item) { + const neg = item.negate(); + return condition ? neg : item; +} +function normalizeZ(c, points) { + const invertedZs = FpInvertBatch(c.Fp, points.map((p) => p.Z)); + return points.map((p, i) => c.fromAffine(p.toAffine(invertedZs[i]))); +} +function validateW(W, bits) { + if (!Number.isSafeInteger(W) || W <= 0 || W > bits) + throw new Error("invalid window size, expected [1.." + bits + "], got W=" + W); +} +function calcWOpts(W, scalarBits) { + validateW(W, scalarBits); + const windows = Math.ceil(scalarBits / W) + 1; + const windowSize = 2 ** (W - 1); + const maxNumber = 2 ** W; + const mask = bitMask(W); + const shiftBy = BigInt(W); + return { windows, windowSize, mask, maxNumber, shiftBy }; +} +function calcOffsets(n, window2, wOpts) { + const { windowSize, mask, maxNumber, shiftBy } = wOpts; + let wbits = Number(n & mask); + let nextN = n >> shiftBy; + if (wbits > windowSize) { + wbits -= maxNumber; + nextN += _1n3; + } + const offsetStart = window2 * windowSize; + const offset = offsetStart + Math.abs(wbits) - 1; + const isZero = wbits === 0; + const isNeg = wbits < 0; + const isNegF = window2 % 2 !== 0; + const offsetF = offsetStart; + return { nextN, offset, isZero, isNeg, isNegF, offsetF }; +} +var pointPrecomputes = /* @__PURE__ */ new WeakMap(); +var pointWindowSizes = /* @__PURE__ */ new WeakMap(); +function getW(P) { + return pointWindowSizes.get(P) || 1; +} +function assert0(n) { + if (n !== _0n3) + throw new Error("invalid wNAF"); +} +var wNAF = class { + // Parametrized with a given Point class (not individual point) + constructor(Point2, bits) { + __publicField(this, "BASE"); + __publicField(this, "ZERO"); + __publicField(this, "Fn"); + __publicField(this, "bits"); + this.BASE = Point2.BASE; + this.ZERO = Point2.ZERO; + this.Fn = Point2.Fn; + this.bits = bits; + } + // non-const time multiplication ladder + _unsafeLadder(elm, n, p = this.ZERO) { + let d = elm; + while (n > _0n3) { + if (n & _1n3) + p = p.add(d); + d = d.double(); + n >>= _1n3; + } + return p; + } + /** + * Creates a wNAF precomputation window. Used for caching. + * Default window size is set by `utils.precompute()` and is equal to 8. + * Number of precomputed points depends on the curve size: + * 2^(𝑊−1) * (Math.ceil(𝑛 / 𝑊) + 1), where: + * - 𝑊 is the window size + * - 𝑛 is the bitlength of the curve order. + * For a 256-bit curve and window size 8, the number of precomputed points is 128 * 33 = 4224. + * @param point - Point instance + * @param W - window size + * @returns precomputed point tables flattened to a single array + */ + precomputeWindow(point, W) { + const { windows, windowSize } = calcWOpts(W, this.bits); + const points = []; + let p = point; + let base = p; + for (let window2 = 0; window2 < windows; window2++) { + base = p; + points.push(base); + for (let i = 1; i < windowSize; i++) { + base = base.add(p); + points.push(base); + } + p = base.double(); + } + return points; + } + /** + * Implements ec multiplication using precomputed tables and w-ary non-adjacent form. + * More compact implementation: + * https://github.com/paulmillr/noble-secp256k1/blob/47cb1669b6e506ad66b35fe7d76132ae97465da2/index.ts#L502-L541 + * @returns real and fake (for const-time) points + */ + wNAF(W, precomputes, n) { + if (!this.Fn.isValid(n)) + throw new Error("invalid scalar"); + let p = this.ZERO; + let f = this.BASE; + const wo = calcWOpts(W, this.bits); + for (let window2 = 0; window2 < wo.windows; window2++) { + const { nextN, offset, isZero, isNeg, isNegF, offsetF } = calcOffsets(n, window2, wo); + n = nextN; + if (isZero) { + f = f.add(negateCt(isNegF, precomputes[offsetF])); + } else { + p = p.add(negateCt(isNeg, precomputes[offset])); + } + } + assert0(n); + return { p, f }; + } + /** + * Implements unsafe EC multiplication using precomputed tables + * and w-ary non-adjacent form. + * @param acc - accumulator point to add result of multiplication + * @returns point + */ + wNAFUnsafe(W, precomputes, n, acc = this.ZERO) { + const wo = calcWOpts(W, this.bits); + for (let window2 = 0; window2 < wo.windows; window2++) { + if (n === _0n3) + break; + const { nextN, offset, isZero, isNeg } = calcOffsets(n, window2, wo); + n = nextN; + if (isZero) { + continue; + } else { + const item = precomputes[offset]; + acc = acc.add(isNeg ? item.negate() : item); + } + } + assert0(n); + return acc; + } + getPrecomputes(W, point, transform) { + let comp = pointPrecomputes.get(point); + if (!comp) { + comp = this.precomputeWindow(point, W); + if (W !== 1) { + if (typeof transform === "function") + comp = transform(comp); + pointPrecomputes.set(point, comp); + } + } + return comp; + } + cached(point, scalar, transform) { + const W = getW(point); + return this.wNAF(W, this.getPrecomputes(W, point, transform), scalar); + } + unsafe(point, scalar, transform, prev) { + const W = getW(point); + if (W === 1) + return this._unsafeLadder(point, scalar, prev); + return this.wNAFUnsafe(W, this.getPrecomputes(W, point, transform), scalar, prev); + } + // We calculate precomputes for elliptic curve point multiplication + // using windowed method. This specifies window size and + // stores precomputed values. Usually only base point would be precomputed. + createCache(P, W) { + validateW(W, this.bits); + pointWindowSizes.set(P, W); + pointPrecomputes.delete(P); + } + hasCache(elm) { + return getW(elm) !== 1; + } +}; +function mulEndoUnsafe(Point2, point, k1, k2) { + let acc = point; + let p1 = Point2.ZERO; + let p2 = Point2.ZERO; + while (k1 > _0n3 || k2 > _0n3) { + if (k1 & _1n3) + p1 = p1.add(acc); + if (k2 & _1n3) + p2 = p2.add(acc); + acc = acc.double(); + k1 >>= _1n3; + k2 >>= _1n3; + } + return { p1, p2 }; +} +function createField(order, field, isLE3) { + if (field) { + if (field.ORDER !== order) + throw new Error("Field.ORDER must match order: Fp == p, Fn == n"); + validateField(field); + return field; + } else { + return Field(order, { isLE: isLE3 }); + } +} +function createCurveFields(type, CURVE, curveOpts = {}, FpFnLE) { + if (FpFnLE === void 0) + FpFnLE = type === "edwards"; + if (!CURVE || typeof CURVE !== "object") + throw new Error(`expected valid ${type} CURVE object`); + for (const p of ["p", "n", "h"]) { + const val = CURVE[p]; + if (!(typeof val === "bigint" && val > _0n3)) + throw new Error(`CURVE.${p} must be positive bigint`); + } + const Fp = createField(CURVE.p, curveOpts.Fp, FpFnLE); + const Fn2 = createField(CURVE.n, curveOpts.Fn, FpFnLE); + const _b = type === "weierstrass" ? "b" : "d"; + const params = ["Gx", "Gy", "a", _b]; + for (const p of params) { + if (!Fp.isValid(CURVE[p])) + throw new Error(`CURVE.${p} must be valid field element of CURVE.Fp`); + } + CURVE = Object.freeze(Object.assign({}, CURVE)); + return { CURVE, Fp, Fn: Fn2 }; +} +function createKeygen(randomSecretKey, getPublicKey) { + return function keygen(seed) { + const secretKey = randomSecretKey(seed); + return { secretKey, publicKey: getPublicKey(secretKey) }; + }; +} + +// node_modules/@noble/curves/abstract/fft.js +function checkU32(n) { + if (!Number.isSafeInteger(n) || n < 0 || n > 4294967295) + throw new Error("wrong u32 integer:" + n); + return n; +} +function isPowerOfTwo(x) { + checkU32(x); + return (x & x - 1) === 0 && x !== 0; +} +function reverseBits(n, bits) { + checkU32(n); + if (!Number.isSafeInteger(bits) || bits < 0 || bits > 32) + throw new Error(`expected integer 0 <= bits <= 32, got ${bits}`); + let reversed = 0; + for (let i = 0; i < bits; i++, n >>>= 1) + reversed = reversed << 1 | n & 1; + return reversed >>> 0; +} +function log2(n) { + checkU32(n); + return 31 - Math.clz32(n); +} +function bitReversalInplace(values) { + const n = values.length; + if (!isPowerOfTwo(n)) + throw new Error("expected positive power-of-two length, got " + n); + const bits = log2(n); + for (let i = 0; i < n; i++) { + const j = reverseBits(i, bits); + if (i < j) { + const tmp = values[i]; + values[i] = values[j]; + values[j] = tmp; + } + } + return values; +} +var FFTCore = (F3, coreOpts) => { + const { N: N3, roots, dit, invertButterflies = false, skipStages = 0, brp = true } = coreOpts; + const bits = log2(N3); + if (!isPowerOfTwo(N3)) + throw new Error("FFT: Polynomial size should be power of two"); + if (roots.length !== N3) + throw new Error(`FFT: wrong roots length: expected ${N3}, got ${roots.length}`); + const isDit = dit !== invertButterflies; + isDit; + return (values) => { + if (values.length !== N3) + throw new Error("FFT: wrong Polynomial length"); + if (dit && brp) + bitReversalInplace(values); + for (let i = 0, g = 1; i < bits - skipStages; i++) { + const s = dit ? i + 1 + skipStages : bits - i; + const m = 1 << s; + const m2 = m >> 1; + const stride = N3 >> s; + for (let k = 0; k < N3; k += m) { + for (let j = 0, grp = g++; j < m2; j++) { + const rootPos = invertButterflies ? dit ? N3 - grp : grp : j * stride; + const i0 = k + j; + const i1 = k + j + m2; + const omega = roots[rootPos]; + const b = values[i1]; + const a = values[i0]; + if (isDit) { + const t = F3.mul(b, omega); + values[i0] = F3.add(a, t); + values[i1] = F3.sub(a, t); + } else if (invertButterflies) { + values[i0] = F3.add(b, a); + values[i1] = F3.mul(F3.sub(b, a), omega); + } else { + values[i0] = F3.add(a, b); + values[i1] = F3.mul(F3.sub(a, b), omega); + } + } + } + } + if (!dit && brp) + bitReversalInplace(values); + return values; + }; +}; + +// node_modules/@noble/curves/abstract/weierstrass.js +var divNearest = (num2, den) => (num2 + (num2 >= 0 ? den : -den) / _2n2) / den; +function _splitEndoScalar(k, basis, n) { + aInRange("scalar", k, _0n4, n); + const [[a1, b1], [a2, b2]] = basis; + const c1 = divNearest(b2 * k, n); + const c2 = divNearest(-b1 * k, n); + let k1 = k - c1 * a1 - c2 * a2; + let k2 = -c1 * b1 - c2 * b2; + const k1neg = k1 < _0n4; + const k2neg = k2 < _0n4; + if (k1neg) + k1 = -k1; + if (k2neg) + k2 = -k2; + const MAX_NUM = bitMask(Math.ceil(bitLen(n) / 2)) + _1n4; + if (k1 < _0n4 || k1 >= MAX_NUM || k2 < _0n4 || k2 >= MAX_NUM) { + throw new Error("splitScalar (endomorphism): failed for k"); + } + return { k1neg, k1, k2neg, k2 }; +} +function validateSigFormat(format) { + if (!["compact", "recovered", "der"].includes(format)) + throw new Error('Signature format must be "compact", "recovered", or "der"'); + return format; +} +function validateSigOpts(opts2, def) { + validateObject(opts2); + const optsn = {}; + for (let optName of Object.keys(def)) { + optsn[optName] = opts2[optName] === void 0 ? def[optName] : opts2[optName]; + } + abool(optsn.lowS, "lowS"); + abool(optsn.prehash, "prehash"); + if (optsn.format !== void 0) + validateSigFormat(optsn.format); + return optsn; +} +var DERErr = class extends Error { + constructor(m = "") { + super(m); + } +}; +var DER = { + // asn.1 DER encoding utils + Err: DERErr, + // Basic building block is TLV (Tag-Length-Value) + _tlv: { + encode: (tag, data) => { + const { Err: E } = DER; + asafenumber(tag, "tag"); + if (tag < 0 || tag > 255) + throw new E("tlv.encode: wrong tag"); + if (typeof data !== "string") + throw new TypeError('"data" expected string, got type=' + typeof data); + if (data.length & 1) + throw new E("tlv.encode: unpadded data"); + const dataLen = data.length / 2; + const len = numberToHexUnpadded(dataLen); + if (len.length / 2 & 128) + throw new E("tlv.encode: long form length too big"); + const lenLen = dataLen > 127 ? numberToHexUnpadded(len.length / 2 | 128) : ""; + const t = numberToHexUnpadded(tag); + return t + lenLen + len + data; + }, + // v - value, l - left bytes (unparsed) + decode(tag, data) { + const { Err: E } = DER; + data = abytes2(data, void 0, "DER data"); + let pos = 0; + if (tag < 0 || tag > 255) + throw new E("tlv.encode: wrong tag"); + if (data.length < 2 || data[pos++] !== tag) + throw new E("tlv.decode: wrong tlv"); + const first = data[pos++]; + const isLong = !!(first & 128); + let length = 0; + if (!isLong) + length = first; + else { + const lenLen = first & 127; + if (!lenLen) + throw new E("tlv.decode(long): indefinite length not supported"); + if (lenLen > 4) + throw new E("tlv.decode(long): byte length is too big"); + const lengthBytes = data.subarray(pos, pos + lenLen); + if (lengthBytes.length !== lenLen) + throw new E("tlv.decode: length bytes not complete"); + if (lengthBytes[0] === 0) + throw new E("tlv.decode(long): zero leftmost byte"); + for (const b of lengthBytes) + length = length << 8 | b; + pos += lenLen; + if (length < 128) + throw new E("tlv.decode(long): not minimal encoding"); + } + const v = data.subarray(pos, pos + length); + if (v.length !== length) + throw new E("tlv.decode: wrong value length"); + return { v, l: data.subarray(pos + length) }; + } + }, + // https://crypto.stackexchange.com/a/57734 Leftmost bit of first byte is 'negative' flag, + // since we always use positive integers here. It must always be empty: + // - add zero byte if exists + // - if next byte doesn't have a flag, leading zero is not allowed (minimal encoding) + _int: { + encode(num2) { + const { Err: E } = DER; + abignumber(num2); + if (num2 < _0n4) + throw new E("integer: negative integers are not allowed"); + let hex = numberToHexUnpadded(num2); + if (Number.parseInt(hex[0], 16) & 8) + hex = "00" + hex; + if (hex.length & 1) + throw new E("unexpected DER parsing assertion: unpadded hex"); + return hex; + }, + decode(data) { + const { Err: E } = DER; + if (data.length < 1) + throw new E("invalid signature integer: empty"); + if (data[0] & 128) + throw new E("invalid signature integer: negative"); + if (data.length > 1 && data[0] === 0 && !(data[1] & 128)) + throw new E("invalid signature integer: unnecessary leading zero"); + return bytesToNumberBE(data); + } + }, + toSig(bytes) { + const { Err: E, _int: int, _tlv: tlv } = DER; + const data = abytes2(bytes, void 0, "signature"); + const { v: seqBytes, l: seqLeftBytes } = tlv.decode(48, data); + if (seqLeftBytes.length) + throw new E("invalid signature: left bytes after parsing"); + const { v: rBytes, l: rLeftBytes } = tlv.decode(2, seqBytes); + const { v: sBytes, l: sLeftBytes } = tlv.decode(2, rLeftBytes); + if (sLeftBytes.length) + throw new E("invalid signature: left bytes after parsing"); + return { r: int.decode(rBytes), s: int.decode(sBytes) }; + }, + hexFromSig(sig) { + const { _tlv: tlv, _int: int } = DER; + const rs = tlv.encode(2, int.encode(sig.r)); + const ss = tlv.encode(2, int.encode(sig.s)); + const seq = rs + ss; + return tlv.encode(48, seq); + } +}; +Object.freeze(DER._tlv); +Object.freeze(DER._int); +Object.freeze(DER); +var _0n4 = /* @__PURE__ */ BigInt(0); +var _1n4 = /* @__PURE__ */ BigInt(1); +var _2n2 = /* @__PURE__ */ BigInt(2); +var _3n2 = /* @__PURE__ */ BigInt(3); +var _4n2 = /* @__PURE__ */ BigInt(4); +function weierstrass(params, extraOpts = {}) { + const validated = createCurveFields("weierstrass", params, extraOpts); + const Fp = validated.Fp; + const Fn2 = validated.Fn; + let CURVE = validated.CURVE; + const { h: cofactor, n: CURVE_ORDER } = CURVE; + validateObject(extraOpts, {}, { + allowInfinityPoint: "boolean", + clearCofactor: "function", + isTorsionFree: "function", + fromBytes: "function", + toBytes: "function", + endo: "object" + }); + const { endo, allowInfinityPoint } = extraOpts; + if (endo) { + if (!Fp.is0(CURVE.a) || typeof endo.beta !== "bigint" || !Array.isArray(endo.basises)) { + throw new Error('invalid endo: expected "beta": bigint and "basises": array'); + } + } + const lengths = getWLengths(Fp, Fn2); + function assertCompressionIsSupported() { + if (!Fp.isOdd) + throw new Error("compression is not supported: Field does not have .isOdd()"); + } + function pointToBytes2(_c, point, isCompressed) { + if (allowInfinityPoint && point.is0()) + return Uint8Array.of(0); + const { x, y } = point.toAffine(); + const bx = Fp.toBytes(x); + abool(isCompressed, "isCompressed"); + if (isCompressed) { + assertCompressionIsSupported(); + const hasEvenY = !Fp.isOdd(y); + return concatBytes2(pprefix(hasEvenY), bx); + } else { + return concatBytes2(Uint8Array.of(4), bx, Fp.toBytes(y)); + } + } + function pointFromBytes(bytes) { + abytes2(bytes, void 0, "Point"); + const { publicKey: comp, publicKeyUncompressed: uncomp } = lengths; + const length = bytes.length; + const head = bytes[0]; + const tail = bytes.subarray(1); + if (allowInfinityPoint && length === 1 && head === 0) + return { x: Fp.ZERO, y: Fp.ZERO }; + if (length === comp && (head === 2 || head === 3)) { + const x = Fp.fromBytes(tail); + if (!Fp.isValid(x)) + throw new Error("bad point: is not on curve, wrong x"); + const y2 = weierstrassEquation(x); + let y; + try { + y = Fp.sqrt(y2); + } catch (sqrtError) { + const err = sqrtError instanceof Error ? ": " + sqrtError.message : ""; + throw new Error("bad point: is not on curve, sqrt error" + err); + } + assertCompressionIsSupported(); + const evenY = Fp.isOdd(y); + const evenH = (head & 1) === 1; + if (evenH !== evenY) + y = Fp.neg(y); + return { x, y }; + } else if (length === uncomp && head === 4) { + const L = Fp.BYTES; + const x = Fp.fromBytes(tail.subarray(0, L)); + const y = Fp.fromBytes(tail.subarray(L, L * 2)); + if (!isValidXY(x, y)) + throw new Error("bad point: is not on curve"); + return { x, y }; + } else { + throw new Error(`bad point: got length ${length}, expected compressed=${comp} or uncompressed=${uncomp}`); + } + } + const encodePoint = extraOpts.toBytes === void 0 ? pointToBytes2 : extraOpts.toBytes; + const decodePoint = extraOpts.fromBytes === void 0 ? pointFromBytes : extraOpts.fromBytes; + function weierstrassEquation(x) { + const x2 = Fp.sqr(x); + const x3 = Fp.mul(x2, x); + return Fp.add(Fp.add(x3, Fp.mul(x, CURVE.a)), CURVE.b); + } + function isValidXY(x, y) { + const left = Fp.sqr(y); + const right = weierstrassEquation(x); + return Fp.eql(left, right); + } + if (!isValidXY(CURVE.Gx, CURVE.Gy)) + throw new Error("bad curve params: generator point"); + const _4a3 = Fp.mul(Fp.pow(CURVE.a, _3n2), _4n2); + const _27b2 = Fp.mul(Fp.sqr(CURVE.b), BigInt(27)); + if (Fp.is0(Fp.add(_4a3, _27b2))) + throw new Error("bad curve params: a or b"); + function acoord(title, n, banZero = false) { + if (!Fp.isValid(n) || banZero && Fp.is0(n)) + throw new Error(`bad point coordinate ${title}`); + return n; + } + function aprjpoint(other) { + if (!(other instanceof Point2)) + throw new Error("Weierstrass Point expected"); + } + function splitEndoScalarN(k) { + if (!endo || !endo.basises) + throw new Error("no endo"); + return _splitEndoScalar(k, endo.basises, Fn2.ORDER); + } + function finishEndo(endoBeta, k1p, k2p, k1neg, k2neg) { + k2p = new Point2(Fp.mul(k2p.X, endoBeta), k2p.Y, k2p.Z); + k1p = negateCt(k1neg, k1p); + k2p = negateCt(k2neg, k2p); + return k1p.add(k2p); + } + const _Point = class _Point { + /** Does NOT validate if the point is valid. Use `.assertValidity()`. */ + constructor(X, Y, Z) { + __publicField(this, "X"); + __publicField(this, "Y"); + __publicField(this, "Z"); + this.X = acoord("x", X); + this.Y = acoord("y", Y, true); + this.Z = acoord("z", Z); + Object.freeze(this); + } + static CURVE() { + return CURVE; + } + /** Does NOT validate if the point is valid. Use `.assertValidity()`. */ + static fromAffine(p) { + const { x, y } = p || {}; + if (!p || !Fp.isValid(x) || !Fp.isValid(y)) + throw new Error("invalid affine point"); + if (p instanceof _Point) + throw new Error("projective point not allowed"); + if (Fp.is0(x) && Fp.is0(y)) + return _Point.ZERO; + return new _Point(x, y, Fp.ONE); + } + static fromBytes(bytes) { + const P = _Point.fromAffine(decodePoint(abytes2(bytes, void 0, "point"))); + P.assertValidity(); + return P; + } + static fromHex(hex) { + return _Point.fromBytes(hexToBytes2(hex)); + } + get x() { + return this.toAffine().x; + } + get y() { + return this.toAffine().y; + } + /** + * + * @param windowSize + * @param isLazy - true will defer table computation until the first multiplication + * @returns + */ + precompute(windowSize = 8, isLazy = true) { + wnaf.createCache(this, windowSize); + if (!isLazy) + this.multiply(_3n2); + return this; + } + // TODO: return `this` + /** A point on curve is valid if it conforms to equation. */ + assertValidity() { + const p = this; + if (p.is0()) { + if (extraOpts.allowInfinityPoint && Fp.is0(p.X) && Fp.eql(p.Y, Fp.ONE) && Fp.is0(p.Z)) + return; + throw new Error("bad point: ZERO"); + } + const { x, y } = p.toAffine(); + if (!Fp.isValid(x) || !Fp.isValid(y)) + throw new Error("bad point: x or y not field elements"); + if (!isValidXY(x, y)) + throw new Error("bad point: equation left != right"); + if (!p.isTorsionFree()) + throw new Error("bad point: not in prime-order subgroup"); + } + hasEvenY() { + const { y } = this.toAffine(); + if (!Fp.isOdd) + throw new Error("Field doesn't support isOdd"); + return !Fp.isOdd(y); + } + /** Compare one point to another. */ + equals(other) { + aprjpoint(other); + const { X: X1, Y: Y1, Z: Z1 } = this; + const { X: X2, Y: Y2, Z: Z2 } = other; + const U1 = Fp.eql(Fp.mul(X1, Z2), Fp.mul(X2, Z1)); + const U2 = Fp.eql(Fp.mul(Y1, Z2), Fp.mul(Y2, Z1)); + return U1 && U2; + } + /** Flips point to one corresponding to (x, -y) in Affine coordinates. */ + negate() { + return new _Point(this.X, Fp.neg(this.Y), this.Z); + } + // Renes-Costello-Batina exception-free doubling formula. + // There is 30% faster Jacobian formula, but it is not complete. + // https://eprint.iacr.org/2015/1060, algorithm 3 + // Cost: 8M + 3S + 3*a + 2*b3 + 15add. + double() { + const { a, b } = CURVE; + const b3 = Fp.mul(b, _3n2); + const { X: X1, Y: Y1, Z: Z1 } = this; + let X3 = Fp.ZERO, Y3 = Fp.ZERO, Z3 = Fp.ZERO; + let t0 = Fp.mul(X1, X1); + let t1 = Fp.mul(Y1, Y1); + let t2 = Fp.mul(Z1, Z1); + let t3 = Fp.mul(X1, Y1); + t3 = Fp.add(t3, t3); + Z3 = Fp.mul(X1, Z1); + Z3 = Fp.add(Z3, Z3); + X3 = Fp.mul(a, Z3); + Y3 = Fp.mul(b3, t2); + Y3 = Fp.add(X3, Y3); + X3 = Fp.sub(t1, Y3); + Y3 = Fp.add(t1, Y3); + Y3 = Fp.mul(X3, Y3); + X3 = Fp.mul(t3, X3); + Z3 = Fp.mul(b3, Z3); + t2 = Fp.mul(a, t2); + t3 = Fp.sub(t0, t2); + t3 = Fp.mul(a, t3); + t3 = Fp.add(t3, Z3); + Z3 = Fp.add(t0, t0); + t0 = Fp.add(Z3, t0); + t0 = Fp.add(t0, t2); + t0 = Fp.mul(t0, t3); + Y3 = Fp.add(Y3, t0); + t2 = Fp.mul(Y1, Z1); + t2 = Fp.add(t2, t2); + t0 = Fp.mul(t2, t3); + X3 = Fp.sub(X3, t0); + Z3 = Fp.mul(t2, t1); + Z3 = Fp.add(Z3, Z3); + Z3 = Fp.add(Z3, Z3); + return new _Point(X3, Y3, Z3); + } + // Renes-Costello-Batina exception-free addition formula. + // There is 30% faster Jacobian formula, but it is not complete. + // https://eprint.iacr.org/2015/1060, algorithm 1 + // Cost: 12M + 0S + 3*a + 3*b3 + 23add. + add(other) { + aprjpoint(other); + const { X: X1, Y: Y1, Z: Z1 } = this; + const { X: X2, Y: Y2, Z: Z2 } = other; + let X3 = Fp.ZERO, Y3 = Fp.ZERO, Z3 = Fp.ZERO; + const a = CURVE.a; + const b3 = Fp.mul(CURVE.b, _3n2); + let t0 = Fp.mul(X1, X2); + let t1 = Fp.mul(Y1, Y2); + let t2 = Fp.mul(Z1, Z2); + let t3 = Fp.add(X1, Y1); + let t4 = Fp.add(X2, Y2); + t3 = Fp.mul(t3, t4); + t4 = Fp.add(t0, t1); + t3 = Fp.sub(t3, t4); + t4 = Fp.add(X1, Z1); + let t5 = Fp.add(X2, Z2); + t4 = Fp.mul(t4, t5); + t5 = Fp.add(t0, t2); + t4 = Fp.sub(t4, t5); + t5 = Fp.add(Y1, Z1); + X3 = Fp.add(Y2, Z2); + t5 = Fp.mul(t5, X3); + X3 = Fp.add(t1, t2); + t5 = Fp.sub(t5, X3); + Z3 = Fp.mul(a, t4); + X3 = Fp.mul(b3, t2); + Z3 = Fp.add(X3, Z3); + X3 = Fp.sub(t1, Z3); + Z3 = Fp.add(t1, Z3); + Y3 = Fp.mul(X3, Z3); + t1 = Fp.add(t0, t0); + t1 = Fp.add(t1, t0); + t2 = Fp.mul(a, t2); + t4 = Fp.mul(b3, t4); + t1 = Fp.add(t1, t2); + t2 = Fp.sub(t0, t2); + t2 = Fp.mul(a, t2); + t4 = Fp.add(t4, t2); + t0 = Fp.mul(t1, t4); + Y3 = Fp.add(Y3, t0); + t0 = Fp.mul(t5, t4); + X3 = Fp.mul(t3, X3); + X3 = Fp.sub(X3, t0); + t0 = Fp.mul(t3, t1); + Z3 = Fp.mul(t5, Z3); + Z3 = Fp.add(Z3, t0); + return new _Point(X3, Y3, Z3); + } + subtract(other) { + aprjpoint(other); + return this.add(other.negate()); + } + is0() { + return this.equals(_Point.ZERO); + } + /** + * Constant time multiplication. + * Uses wNAF method. Windowed method may be 10% faster, + * but takes 2x longer to generate and consumes 2x memory. + * Uses precomputes when available. + * Uses endomorphism for Koblitz curves. + * @param scalar - by which the point would be multiplied + * @returns New point + */ + multiply(scalar) { + const { endo: endo2 } = extraOpts; + if (!Fn2.isValidNot0(scalar)) + throw new RangeError("invalid scalar: out of range"); + let point, fake; + const mul3 = (n) => wnaf.cached(this, n, (p) => normalizeZ(_Point, p)); + if (endo2) { + const { k1neg, k1, k2neg, k2 } = splitEndoScalarN(scalar); + const { p: k1p, f: k1f } = mul3(k1); + const { p: k2p, f: k2f } = mul3(k2); + fake = k1f.add(k2f); + point = finishEndo(endo2.beta, k1p, k2p, k1neg, k2neg); + } else { + const { p, f } = mul3(scalar); + point = p; + fake = f; + } + return normalizeZ(_Point, [point, fake])[0]; + } + /** + * Non-constant-time multiplication. Uses double-and-add algorithm. + * It's faster, but should only be used when you don't care about + * an exposed secret key e.g. sig verification, which works over *public* keys. + */ + multiplyUnsafe(scalar) { + const { endo: endo2 } = extraOpts; + const p = this; + const sc = scalar; + if (!Fn2.isValid(sc)) + throw new RangeError("invalid scalar: out of range"); + if (sc === _0n4 || p.is0()) + return _Point.ZERO; + if (sc === _1n4) + return p; + if (wnaf.hasCache(this)) + return this.multiply(sc); + if (endo2) { + const { k1neg, k1, k2neg, k2 } = splitEndoScalarN(sc); + const { p1, p2 } = mulEndoUnsafe(_Point, p, k1, k2); + return finishEndo(endo2.beta, p1, p2, k1neg, k2neg); + } else { + return wnaf.unsafe(p, sc); + } + } + /** + * Converts Projective point to affine (x, y) coordinates. + * (X, Y, Z) ∋ (x=X/Z, y=Y/Z). + * @param invertedZ - Z^-1 (inverted zero) - optional, precomputation is useful for invertBatch + */ + toAffine(invertedZ) { + const p = this; + let iz = invertedZ; + const { X, Y, Z } = p; + if (Fp.eql(Z, Fp.ONE)) + return { x: X, y: Y }; + const is0 = p.is0(); + if (iz == null) + iz = is0 ? Fp.ONE : Fp.inv(Z); + const x = Fp.mul(X, iz); + const y = Fp.mul(Y, iz); + const zz = Fp.mul(Z, iz); + if (is0) + return { x: Fp.ZERO, y: Fp.ZERO }; + if (!Fp.eql(zz, Fp.ONE)) + throw new Error("invZ was invalid"); + return { x, y }; + } + /** + * Checks whether Point is free of torsion elements (is in prime subgroup). + * Always torsion-free for cofactor=1 curves. + */ + isTorsionFree() { + const { isTorsionFree } = extraOpts; + if (cofactor === _1n4) + return true; + if (isTorsionFree) + return isTorsionFree(_Point, this); + return wnaf.unsafe(this, CURVE_ORDER).is0(); + } + clearCofactor() { + const { clearCofactor } = extraOpts; + if (cofactor === _1n4) + return this; + if (clearCofactor) + return clearCofactor(_Point, this); + return this.multiplyUnsafe(cofactor); + } + isSmallOrder() { + if (cofactor === _1n4) + return this.is0(); + return this.clearCofactor().is0(); + } + toBytes(isCompressed = true) { + abool(isCompressed, "isCompressed"); + this.assertValidity(); + return encodePoint(_Point, this, isCompressed); + } + toHex(isCompressed = true) { + return bytesToHex2(this.toBytes(isCompressed)); + } + toString() { + return ``; + } + }; + // base / generator point + __publicField(_Point, "BASE", new _Point(CURVE.Gx, CURVE.Gy, Fp.ONE)); + // zero / infinity / identity point + __publicField(_Point, "ZERO", new _Point(Fp.ZERO, Fp.ONE, Fp.ZERO)); + // 0, 1, 0 + // math field + __publicField(_Point, "Fp", Fp); + // scalar field + __publicField(_Point, "Fn", Fn2); + let Point2 = _Point; + const bits = Fn2.BITS; + const wnaf = new wNAF(Point2, extraOpts.endo ? Math.ceil(bits / 2) : bits); + if (bits >= 8) + Point2.BASE.precompute(8); + Object.freeze(Point2.prototype); + Object.freeze(Point2); + return Point2; +} +function pprefix(hasEvenY) { + return Uint8Array.of(hasEvenY ? 2 : 3); +} +function getWLengths(Fp, Fn2) { + return { + secretKey: Fn2.BYTES, + publicKey: 1 + Fp.BYTES, + publicKeyUncompressed: 1 + 2 * Fp.BYTES, + publicKeyHasPrefix: true, + // Raw compact `(r || s)` signature width; DER and recovered signatures use + // different lengths outside this helper. + signature: 2 * Fn2.BYTES + }; +} +function ecdh(Point2, ecdhOpts = {}) { + const { Fn: Fn2 } = Point2; + const randomBytes_ = ecdhOpts.randomBytes === void 0 ? randomBytes2 : ecdhOpts.randomBytes; + const lengths = Object.assign(getWLengths(Point2.Fp, Fn2), { + seed: Math.max(getMinHashLength(Fn2.ORDER), 16) + }); + function isValidSecretKey(secretKey) { + try { + const num2 = Fn2.fromBytes(secretKey); + return Fn2.isValidNot0(num2); + } catch (error) { + return false; + } + } + function isValidPublicKey(publicKey, isCompressed) { + const { publicKey: comp, publicKeyUncompressed } = lengths; + try { + const l = publicKey.length; + if (isCompressed === true && l !== comp) + return false; + if (isCompressed === false && l !== publicKeyUncompressed) + return false; + return !!Point2.fromBytes(publicKey); + } catch (error) { + return false; + } + } + function randomSecretKey(seed) { + seed = seed === void 0 ? randomBytes_(lengths.seed) : seed; + return mapHashToField(abytes2(seed, lengths.seed, "seed"), Fn2.ORDER); + } + function getPublicKey(secretKey, isCompressed = true) { + return Point2.BASE.multiply(Fn2.fromBytes(secretKey)).toBytes(isCompressed); + } + function isProbPub(item) { + const { secretKey, publicKey, publicKeyUncompressed } = lengths; + const allowedLengths = Fn2._lengths; + if (!isBytes3(item)) + return void 0; + const l = abytes2(item, void 0, "key").length; + const isPub = l === publicKey || l === publicKeyUncompressed; + const isSec = l === secretKey || !!allowedLengths?.includes(l); + if (isPub && isSec) + return void 0; + return isPub; + } + function getSharedSecret(secretKeyA, publicKeyB, isCompressed = true) { + if (isProbPub(secretKeyA) === true) + throw new Error("first arg must be private key"); + if (isProbPub(publicKeyB) === false) + throw new Error("second arg must be public key"); + const s = Fn2.fromBytes(secretKeyA); + const b = Point2.fromBytes(publicKeyB); + return b.multiply(s).toBytes(isCompressed); + } + const utils2 = { + isValidSecretKey, + isValidPublicKey, + randomSecretKey + }; + const keygen = createKeygen(randomSecretKey, getPublicKey); + Object.freeze(utils2); + Object.freeze(lengths); + return Object.freeze({ getPublicKey, getSharedSecret, keygen, Point: Point2, utils: utils2, lengths }); +} +function ecdsa(Point2, hash, ecdsaOpts = {}) { + const hash_ = hash; + ahash(hash_); + validateObject(ecdsaOpts, {}, { + hmac: "function", + lowS: "boolean", + randomBytes: "function", + bits2int: "function", + bits2int_modN: "function" + }); + ecdsaOpts = Object.assign({}, ecdsaOpts); + const randomBytes5 = ecdsaOpts.randomBytes === void 0 ? randomBytes2 : ecdsaOpts.randomBytes; + const hmac2 = ecdsaOpts.hmac === void 0 ? (key, msg) => hmac(hash_, key, msg) : ecdsaOpts.hmac; + const { Fp, Fn: Fn2 } = Point2; + const { ORDER: CURVE_ORDER, BITS: fnBits } = Fn2; + const { keygen, getPublicKey, getSharedSecret, utils: utils2, lengths } = ecdh(Point2, ecdsaOpts); + const defaultSigOpts = { + prehash: true, + lowS: typeof ecdsaOpts.lowS === "boolean" ? ecdsaOpts.lowS : true, + format: "compact", + extraEntropy: false + }; + const hasLargeRecoveryLifts = CURVE_ORDER * _2n2 + _1n4 < Fp.ORDER; + function isBiggerThanHalfOrder(number) { + const HALF = CURVE_ORDER >> _1n4; + return number > HALF; + } + function validateRS(title, num2) { + if (!Fn2.isValidNot0(num2)) + throw new Error(`invalid signature ${title}: out of range 1..Point.Fn.ORDER`); + return num2; + } + function assertRecoverableCurve() { + if (hasLargeRecoveryLifts) + throw new Error('"recovered" sig type is not supported for cofactor >2 curves'); + } + function validateSigLength(bytes, format) { + validateSigFormat(format); + const size = lengths.signature; + const sizer = format === "compact" ? size : format === "recovered" ? size + 1 : void 0; + return abytes2(bytes, sizer); + } + class Signature { + constructor(r, s, recovery) { + __publicField(this, "r"); + __publicField(this, "s"); + __publicField(this, "recovery"); + this.r = validateRS("r", r); + this.s = validateRS("s", s); + if (recovery != null) { + assertRecoverableCurve(); + if (![0, 1, 2, 3].includes(recovery)) + throw new Error("invalid recovery id"); + this.recovery = recovery; + } + Object.freeze(this); + } + static fromBytes(bytes, format = defaultSigOpts.format) { + validateSigLength(bytes, format); + let recid; + if (format === "der") { + const { r: r2, s: s2 } = DER.toSig(abytes2(bytes)); + return new Signature(r2, s2); + } + if (format === "recovered") { + recid = bytes[0]; + format = "compact"; + bytes = bytes.subarray(1); + } + const L = lengths.signature / 2; + const r = bytes.subarray(0, L); + const s = bytes.subarray(L, L * 2); + return new Signature(Fn2.fromBytes(r), Fn2.fromBytes(s), recid); + } + static fromHex(hex, format) { + return this.fromBytes(hexToBytes2(hex), format); + } + assertRecovery() { + const { recovery } = this; + if (recovery == null) + throw new Error("invalid recovery id: must be present"); + return recovery; + } + addRecoveryBit(recovery) { + return new Signature(this.r, this.s, recovery); + } + // Unlike the top-level helper below, this method expects a digest that has + // already been hashed to the curve's message representative. + recoverPublicKey(messageHash) { + const { r, s } = this; + const recovery = this.assertRecovery(); + const radj = recovery === 2 || recovery === 3 ? r + CURVE_ORDER : r; + if (!Fp.isValid(radj)) + throw new Error("invalid recovery id: sig.r+curve.n != R.x"); + const x = Fp.toBytes(radj); + const R = Point2.fromBytes(concatBytes2(pprefix((recovery & 1) === 0), x)); + const ir = Fn2.inv(radj); + const h = bits2int_modN(abytes2(messageHash, void 0, "msgHash")); + const u1 = Fn2.create(-h * ir); + const u2 = Fn2.create(s * ir); + const Q4 = Point2.BASE.multiplyUnsafe(u1).add(R.multiplyUnsafe(u2)); + if (Q4.is0()) + throw new Error("invalid recovery: point at infinify"); + Q4.assertValidity(); + return Q4; + } + // Signatures should be low-s, to prevent malleability. + hasHighS() { + return isBiggerThanHalfOrder(this.s); + } + toBytes(format = defaultSigOpts.format) { + validateSigFormat(format); + if (format === "der") + return hexToBytes2(DER.hexFromSig(this)); + const { r, s } = this; + const rb = Fn2.toBytes(r); + const sb = Fn2.toBytes(s); + if (format === "recovered") { + assertRecoverableCurve(); + return concatBytes2(Uint8Array.of(this.assertRecovery()), rb, sb); + } + return concatBytes2(rb, sb); + } + toHex(format) { + return bytesToHex2(this.toBytes(format)); + } + } + Object.freeze(Signature.prototype); + Object.freeze(Signature); + const bits2int = ecdsaOpts.bits2int === void 0 ? function bits2int_def(bytes) { + if (bytes.length > 8192) + throw new Error("input is too large"); + const num2 = bytesToNumberBE(bytes); + const delta = bytes.length * 8 - fnBits; + return delta > 0 ? num2 >> BigInt(delta) : num2; + } : ecdsaOpts.bits2int; + const bits2int_modN = ecdsaOpts.bits2int_modN === void 0 ? function bits2int_modN_def(bytes) { + return Fn2.create(bits2int(bytes)); + } : ecdsaOpts.bits2int_modN; + const ORDER_MASK = bitMask(fnBits); + function int2octets(num2) { + aInRange("num < 2^" + fnBits, num2, _0n4, ORDER_MASK); + return Fn2.toBytes(num2); + } + function validateMsgAndHash(message, prehash) { + abytes2(message, void 0, "message"); + return prehash ? abytes2(hash_(message), void 0, "prehashed message") : message; + } + function prepSig(message, secretKey, opts2) { + const { lowS, prehash, extraEntropy } = validateSigOpts(opts2, defaultSigOpts); + message = validateMsgAndHash(message, prehash); + const h1int = bits2int_modN(message); + const d = Fn2.fromBytes(secretKey); + if (!Fn2.isValidNot0(d)) + throw new Error("invalid private key"); + const seedArgs = [int2octets(d), int2octets(h1int)]; + if (extraEntropy != null && extraEntropy !== false) { + const e = extraEntropy === true ? randomBytes5(lengths.secretKey) : extraEntropy; + seedArgs.push(abytes2(e, void 0, "extraEntropy")); + } + const seed = concatBytes2(...seedArgs); + const m = h1int; + function k2sig(kBytes) { + const k = bits2int(kBytes); + if (!Fn2.isValidNot0(k)) + return; + const ik = Fn2.inv(k); + const q = Point2.BASE.multiply(k).toAffine(); + const r = Fn2.create(q.x); + if (r === _0n4) + return; + const s = Fn2.create(ik * Fn2.create(m + r * d)); + if (s === _0n4) + return; + let recovery = (q.x === r ? 0 : 2) | Number(q.y & _1n4); + let normS = s; + if (lowS && isBiggerThanHalfOrder(s)) { + normS = Fn2.neg(s); + recovery ^= 1; + } + return new Signature(r, normS, hasLargeRecoveryLifts ? void 0 : recovery); + } + return { seed, k2sig }; + } + function sign(message, secretKey, opts2 = {}) { + const { seed, k2sig } = prepSig(message, secretKey, opts2); + const drbg = createHmacDrbg(hash_.outputLen, Fn2.BYTES, hmac2); + const sig = drbg(seed, k2sig); + return sig.toBytes(opts2.format); + } + function verify(signature, message, publicKey, opts2 = {}) { + const { lowS, prehash, format } = validateSigOpts(opts2, defaultSigOpts); + publicKey = abytes2(publicKey, void 0, "publicKey"); + message = validateMsgAndHash(message, prehash); + if (!isBytes3(signature)) { + const end = signature instanceof Signature ? ", use sig.toBytes()" : ""; + throw new Error("verify expects Uint8Array signature" + end); + } + validateSigLength(signature, format); + try { + const sig = Signature.fromBytes(signature, format); + const P = Point2.fromBytes(publicKey); + if (lowS && sig.hasHighS()) + return false; + const { r, s } = sig; + const h = bits2int_modN(message); + const is = Fn2.inv(s); + const u1 = Fn2.create(h * is); + const u2 = Fn2.create(r * is); + const R = Point2.BASE.multiplyUnsafe(u1).add(P.multiplyUnsafe(u2)); + if (R.is0()) + return false; + const v = Fn2.create(R.x); + return v === r; + } catch (e) { + return false; + } + } + function recoverPublicKey(signature, message, opts2 = {}) { + const { prehash } = validateSigOpts(opts2, defaultSigOpts); + message = validateMsgAndHash(message, prehash); + return Signature.fromBytes(signature, "recovered").recoverPublicKey(message).toBytes(); + } + return Object.freeze({ + keygen, + getPublicKey, + getSharedSecret, + utils: utils2, + lengths, + Point: Point2, + sign, + verify, + recoverPublicKey, + Signature, + hash: hash_ + }); +} + +// node_modules/@noble/curves/secp256k1.js +var secp256k1_CURVE = { + p: BigInt("0xfffffffffffffffffffffffffffffffffffffffffffffffffffffffefffffc2f"), + n: BigInt("0xfffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141"), + h: BigInt(1), + a: BigInt(0), + b: BigInt(7), + Gx: BigInt("0x79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798"), + Gy: BigInt("0x483ada7726a3c4655da4fbfc0e1108a8fd17b448a68554199c47d08ffb10d4b8") +}; +var secp256k1_ENDO = { + beta: BigInt("0x7ae96a2b657c07106e64479eac3434e99cf0497512f58995c1396c28719501ee"), + basises: [ + [BigInt("0x3086d221a7d46bcde86c90e49284eb15"), -BigInt("0xe4437ed6010e88286f547fa90abfe4c3")], + [BigInt("0x114ca50f7a8e2f3f657c1108d9d44cfd8"), BigInt("0x3086d221a7d46bcde86c90e49284eb15")] + ] +}; +var _0n5 = /* @__PURE__ */ BigInt(0); +var _2n3 = /* @__PURE__ */ BigInt(2); +function sqrtMod(y) { + const P = secp256k1_CURVE.p; + const _3n3 = BigInt(3), _6n = BigInt(6), _11n = BigInt(11), _22n = BigInt(22); + const _23n = BigInt(23), _44n = BigInt(44), _88n = BigInt(88); + const b2 = y * y * y % P; + const b3 = b2 * b2 * y % P; + const b6 = pow2(b3, _3n3, P) * b3 % P; + const b9 = pow2(b6, _3n3, P) * b3 % P; + const b11 = pow2(b9, _2n3, P) * b2 % P; + const b22 = pow2(b11, _11n, P) * b11 % P; + const b44 = pow2(b22, _22n, P) * b22 % P; + const b88 = pow2(b44, _44n, P) * b44 % P; + const b176 = pow2(b88, _88n, P) * b88 % P; + const b220 = pow2(b176, _44n, P) * b44 % P; + const b223 = pow2(b220, _3n3, P) * b3 % P; + const t1 = pow2(b223, _23n, P) * b22 % P; + const t2 = pow2(t1, _6n, P) * b2 % P; + const root = pow2(t2, _2n3, P); + if (!Fpk1.eql(Fpk1.sqr(root), y)) + throw new Error("Cannot find square root"); + return root; +} +var Fpk1 = Field(secp256k1_CURVE.p, { sqrt: sqrtMod }); +var Pointk1 = /* @__PURE__ */ weierstrass(secp256k1_CURVE, { + Fp: Fpk1, + endo: secp256k1_ENDO +}); +var secp256k1 = /* @__PURE__ */ ecdsa(Pointk1, sha256); +var TAGGED_HASH_PREFIXES = {}; +function taggedHash(tag, ...messages) { + let tagP = TAGGED_HASH_PREFIXES[tag]; + if (tagP === void 0) { + const tagH = sha256(asciiToBytes(tag)); + tagP = concatBytes2(tagH, tagH); + TAGGED_HASH_PREFIXES[tag] = tagP; + } + return sha256(concatBytes2(tagP, ...messages)); +} +var pointToBytes = (point) => point.toBytes(true).slice(1); +var hasEven = (y) => y % _2n3 === _0n5; +function schnorrGetExtPubKey(priv) { + const { Fn: Fn2, BASE } = Pointk1; + const d_ = Fn2.fromBytes(priv); + const p = BASE.multiply(d_); + const scalar = hasEven(p.y) ? d_ : Fn2.neg(d_); + return { scalar, bytes: pointToBytes(p) }; +} +function lift_x(x) { + const Fp = Fpk1; + if (!Fp.isValidNot0(x)) + throw new Error("invalid x: Fail if x \u2265 p"); + const xx = Fp.create(x * x); + const c = Fp.create(xx * x + BigInt(7)); + let y = Fp.sqrt(c); + if (!hasEven(y)) + y = Fp.neg(y); + const p = Pointk1.fromAffine({ x, y }); + p.assertValidity(); + return p; +} +var num = bytesToNumberBE; +function challenge(...args) { + return Pointk1.Fn.create(num(taggedHash("BIP0340/challenge", ...args))); +} +function schnorrGetPublicKey(secretKey) { + return schnorrGetExtPubKey(secretKey).bytes; +} +function schnorrSign(message, secretKey, auxRand = randomBytes(32)) { + const { Fn: Fn2, BASE } = Pointk1; + const m = abytes2(message, void 0, "message"); + const { bytes: px, scalar: d } = schnorrGetExtPubKey(secretKey); + const a = abytes2(auxRand, 32, "auxRand"); + const t = Fn2.toBytes(d ^ num(taggedHash("BIP0340/aux", a))); + const rand = taggedHash("BIP0340/nonce", t, px, m); + const k_ = Fn2.create(num(rand)); + if (k_ === 0n) + throw new Error("sign failed: k is zero"); + const p = BASE.multiply(k_); + const k = hasEven(p.y) ? k_ : Fn2.neg(k_); + const rx = pointToBytes(p); + const e = challenge(rx, px, m); + const sig = new Uint8Array(64); + sig.set(rx, 0); + sig.set(Fn2.toBytes(Fn2.create(k + e * d)), 32); + if (!schnorrVerify(sig, m, px)) + throw new Error("sign: Invalid signature produced"); + return sig; +} +function schnorrVerify(signature, message, publicKey) { + const { Fp, Fn: Fn2, BASE } = Pointk1; + const sig = abytes2(signature, 64, "signature"); + const m = abytes2(message, void 0, "message"); + const pub = abytes2(publicKey, 32, "publicKey"); + try { + const P = lift_x(num(pub)); + const r = num(sig.subarray(0, 32)); + if (!Fp.isValidNot0(r)) + return false; + const s = num(sig.subarray(32, 64)); + if (!Fn2.isValidNot0(s)) + return false; + const e = challenge(Fn2.toBytes(r), pointToBytes(P), m); + const R = BASE.multiplyUnsafe(s).add(P.multiplyUnsafe(Fn2.neg(e))); + const { x, y } = R.toAffine(); + if (R.is0() || !hasEven(y) || x !== r) + return false; + return true; + } catch (error) { + return false; + } +} +var schnorr = /* @__PURE__ */ (() => { + const size = 32; + const seedLength = 48; + const randomSecretKey = (seed) => { + seed = seed === void 0 ? randomBytes(seedLength) : seed; + return mapHashToField(seed, secp256k1_CURVE.n); + }; + return Object.freeze({ + keygen: createKeygen(randomSecretKey, schnorrGetPublicKey), + getPublicKey: schnorrGetPublicKey, + sign: schnorrSign, + verify: schnorrVerify, + Point: Pointk1, + utils: Object.freeze({ + randomSecretKey, + taggedHash, + lift_x, + pointToBytes + }), + lengths: Object.freeze({ + secretKey: size, + publicKey: size, + publicKeyHasPrefix: false, + signature: size * 2, + seed: seedLength + }) + }); +})(); + +// node_modules/@noble/hashes/legacy.js +var Rho160 = /* @__PURE__ */ Uint8Array.from([ + 7, + 4, + 13, + 1, + 10, + 6, + 15, + 3, + 12, + 0, + 9, + 5, + 2, + 14, + 11, + 8 +]); +var Id160 = /* @__PURE__ */ (() => Uint8Array.from(new Array(16).fill(0).map((_, i) => i)))(); +var Pi160 = /* @__PURE__ */ (() => Id160.map((i) => (9 * i + 5) % 16))(); +var idxLR = /* @__PURE__ */ (() => { + const L = [Id160]; + const R = [Pi160]; + const res = [L, R]; + for (let i = 0; i < 4; i++) + for (let j of res) + j.push(j[i].map((k) => Rho160[k])); + return res; +})(); +var idxL = /* @__PURE__ */ (() => idxLR[0])(); +var idxR = /* @__PURE__ */ (() => idxLR[1])(); +var shifts160 = /* @__PURE__ */ [ + [11, 14, 15, 12, 5, 8, 7, 9, 11, 13, 14, 15, 6, 7, 9, 8], + [12, 13, 11, 15, 6, 9, 9, 7, 12, 15, 11, 13, 7, 8, 7, 7], + [13, 15, 14, 11, 7, 7, 6, 8, 13, 14, 13, 12, 5, 5, 6, 9], + [14, 11, 12, 14, 8, 6, 5, 5, 15, 12, 15, 14, 9, 9, 8, 6], + [15, 12, 13, 13, 9, 5, 8, 6, 14, 11, 12, 11, 8, 6, 5, 5] +].map((i) => Uint8Array.from(i)); +var shiftsL160 = /* @__PURE__ */ idxL.map((idx, i) => idx.map((j) => shifts160[i][j])); +var shiftsR160 = /* @__PURE__ */ idxR.map((idx, i) => idx.map((j) => shifts160[i][j])); +var Kl160 = /* @__PURE__ */ Uint32Array.from([ + 0, + 1518500249, + 1859775393, + 2400959708, + 2840853838 +]); +var Kr160 = /* @__PURE__ */ Uint32Array.from([ + 1352829926, + 1548603684, + 1836072691, + 2053994217, + 0 +]); +function ripemd_f(group, x, y, z) { + if (group === 0) + return x ^ y ^ z; + if (group === 1) + return x & y | ~x & z; + if (group === 2) + return (x | ~y) ^ z; + if (group === 3) + return x & z | y & ~z; + return x ^ (y | ~z); +} +var BUF_160 = /* @__PURE__ */ new Uint32Array(16); +var _RIPEMD160 = class extends HashMD { + constructor() { + super(64, 20, 8, true); + __publicField(this, "h0", 1732584193 | 0); + __publicField(this, "h1", 4023233417 | 0); + __publicField(this, "h2", 2562383102 | 0); + __publicField(this, "h3", 271733878 | 0); + __publicField(this, "h4", 3285377520 | 0); + } + get() { + const { h0, h1, h2, h3, h4 } = this; + return [h0, h1, h2, h3, h4]; + } + set(h0, h1, h2, h3, h4) { + this.h0 = h0 | 0; + this.h1 = h1 | 0; + this.h2 = h2 | 0; + this.h3 = h3 | 0; + this.h4 = h4 | 0; + } + process(view, offset) { + for (let i = 0; i < 16; i++, offset += 4) + BUF_160[i] = view.getUint32(offset, true); + let al = this.h0 | 0, ar = al, bl = this.h1 | 0, br = bl, cl = this.h2 | 0, cr = cl, dl = this.h3 | 0, dr = dl, el = this.h4 | 0, er = el; + for (let group = 0; group < 5; group++) { + const rGroup = 4 - group; + const hbl = Kl160[group], hbr = Kr160[group]; + const rl = idxL[group], rr = idxR[group]; + const sl = shiftsL160[group], sr = shiftsR160[group]; + for (let i = 0; i < 16; i++) { + const tl = rotl(al + ripemd_f(group, bl, cl, dl) + BUF_160[rl[i]] + hbl, sl[i]) + el | 0; + al = el, el = dl, dl = rotl(cl, 10) | 0, cl = bl, bl = tl; + } + for (let i = 0; i < 16; i++) { + const tr = rotl(ar + ripemd_f(rGroup, br, cr, dr) + BUF_160[rr[i]] + hbr, sr[i]) + er | 0; + ar = er, er = dr, dr = rotl(cr, 10) | 0, cr = br, br = tr; + } + } + this.set(this.h1 + cl + dr | 0, this.h2 + dl + er | 0, this.h3 + el + ar | 0, this.h4 + al + br | 0, this.h0 + bl + cr | 0); + } + roundClean() { + clean(BUF_160); + } + destroy() { + this.destroyed = true; + clean(this.buffer); + this.set(0, 0, 0, 0, 0); + } +}; +var ripemd160 = /* @__PURE__ */ createHasher(() => new _RIPEMD160()); + +// node_modules/@scure/bip32/index.js +var Point = /* @__PURE__ */ (() => secp256k1.Point)(); +var Fn = /* @__PURE__ */ (() => Point.Fn)(); +var base58check = /* @__PURE__ */ createBase58check(sha256); +var MASTER_SECRET = /* @__PURE__ */ (() => { + return Uint8Array.from("Bitcoin seed".split(""), (char) => char.charCodeAt(0)); +})(); +var BITCOIN_VERSIONS = { private: 76066276, public: 76067358 }; +var HARDENED_OFFSET = 2147483648; +var hash160 = (data) => ripemd160(sha256(data)); +var fromU32 = (data) => createView(data).getUint32(0, false); +var toU32 = (n) => { + if (typeof n !== "number") + throw new TypeError("invalid number, should be from 0 to 2**32-1, got " + n); + if (!Number.isSafeInteger(n) || n < 0 || n > 2 ** 32 - 1) + throw new RangeError("invalid number, should be from 0 to 2**32-1, got " + n); + const buf = new Uint8Array(4); + createView(buf).setUint32(0, n, false); + return buf; +}; +var HDKey = class _HDKey { + constructor(opt) { + __publicField(this, "versions"); + __publicField(this, "depth", 0); + __publicField(this, "index", 0); + __publicField(this, "chainCode", null); + __publicField(this, "parentFingerprint", 0); + __publicField(this, "_privateKey"); + __publicField(this, "_publicKey"); + __publicField(this, "pubHash"); + if (!opt || typeof opt !== "object") { + throw new Error("HDKey.constructor must not be called directly"); + } + this.versions = opt.versions || BITCOIN_VERSIONS; + this.depth = opt.depth || 0; + this.chainCode = opt.chainCode ? Uint8Array.from(opt.chainCode) : null; + this.index = opt.index || 0; + this.parentFingerprint = opt.parentFingerprint || 0; + if (!this.depth) { + if (this.parentFingerprint || this.index) { + throw new Error("HDKey: zero depth with non-zero index/parent fingerprint"); + } + } + if (this.depth > 255) { + throw new Error("HDKey: depth exceeds the serializable value 255"); + } + if (opt.publicKey && opt.privateKey) { + throw new Error("HDKey: publicKey and privateKey at same time."); + } + if (opt.privateKey) { + if (!secp256k1.utils.isValidSecretKey(opt.privateKey)) + throw new Error("Invalid private key"); + this._privateKey = Uint8Array.from(opt.privateKey); + this._publicKey = secp256k1.getPublicKey(this._privateKey, true); + } else if (opt.publicKey) { + this._publicKey = Point.fromBytes(opt.publicKey).toBytes(true); + } else { + throw new Error("HDKey: no public or private key provided"); + } + this.pubHash = hash160(this._publicKey); + } + get fingerprint() { + if (!this.pubHash) { + throw new Error("No publicKey set!"); + } + return fromU32(this.pubHash); + } + get identifier() { + return this.pubHash; + } + get pubKeyHash() { + return this.pubHash; + } + // Returns the live private key buffer for this instance. + // Copy it first if you need an immutable snapshot. + get privateKey() { + return this._privateKey || null; + } + get publicKey() { + return this._publicKey || null; + } + get privateExtendedKey() { + const priv = this._privateKey; + if (!priv) { + throw new Error("No private key"); + } + return base58check.encode(this.serialize(this.versions.private, concatBytes(Uint8Array.of(0), priv))); + } + get publicExtendedKey() { + if (!this._publicKey) { + throw new Error("No public key"); + } + return base58check.encode(this.serialize(this.versions.public, this._publicKey)); + } + static fromMasterSeed(seed, versions = BITCOIN_VERSIONS) { + abytes(seed); + if (8 * seed.length < 128 || 8 * seed.length > 512) { + throw new RangeError("HDKey: seed length must be between 128 and 512 bits; 256 bits is advised, got " + seed.length); + } + const I = hmac(sha512, MASTER_SECRET, seed); + const privateKey = I.slice(0, 32); + const chainCode = I.slice(32); + return new _HDKey({ versions, chainCode, privateKey }); + } + static fromExtendedKey(base58key, versions = BITCOIN_VERSIONS) { + const keyBuffer = base58check.decode(base58key); + const keyView = createView(keyBuffer); + const version = keyView.getUint32(0, false); + const opt = { + versions, + depth: keyBuffer[4], + parentFingerprint: keyView.getUint32(5, false), + index: keyView.getUint32(9, false), + chainCode: keyBuffer.slice(13, 45) + }; + const key = keyBuffer.slice(45); + const isPriv = key[0] === 0; + if (version !== versions[isPriv ? "private" : "public"]) { + throw new Error("Version mismatch"); + } + if (isPriv) { + return new _HDKey({ ...opt, privateKey: key.slice(1) }); + } else { + return new _HDKey({ ...opt, publicKey: key }); + } + } + static fromJSON(json) { + return _HDKey.fromExtendedKey(json.xpriv); + } + derive(path) { + if (!/^[mM]'?/.test(path)) { + throw new Error('Path must start with "m" or "M"'); + } + if (/^[mM]'?$/.test(path)) { + return this; + } + const parts = path.replace(/^[mM]'?\//, "").split("/"); + let child = this; + for (const c of parts) { + const m = /^(\d+)('?)$/.exec(c); + const m1 = m && m[1]; + if (!m || m.length !== 3 || typeof m1 !== "string") + throw new Error("invalid child index: " + c); + let idx = +m1; + if (!Number.isSafeInteger(idx) || idx >= HARDENED_OFFSET) { + throw new Error("Invalid index"); + } + if (m[2] === "'") { + idx += HARDENED_OFFSET; + } + child = child.deriveChild(idx); + } + return child; + } + /** + * @param _I - Test-only override for the 64-byte HMAC-SHA512 output; normal callers must omit it. + */ + deriveChild(index, _I) { + if (!this._publicKey || !this.chainCode) { + throw new Error("No publicKey or chainCode set"); + } + let data = toU32(index); + if (index >= HARDENED_OFFSET) { + const priv = this._privateKey; + if (!priv) { + throw new Error("Could not derive hardened child key"); + } + data = concatBytes(Uint8Array.of(0), priv, data); + } else { + data = concatBytes(this._publicKey, data); + } + const out = _I || hmac(sha512, this.chainCode, data); + abytes(out, 64); + const childTweak = out.slice(0, 32); + const chainCode = out.slice(32); + const opt = { + versions: this.versions, + chainCode, + depth: this.depth + 1, + parentFingerprint: this.fingerprint, + index + }; + if (opt.depth > 255) { + throw new Error("HDKey: depth exceeds the serializable value 255"); + } + try { + const ctweak = Fn.fromBytes(childTweak); + if (this._privateKey) { + const added = Fn.create(Fn.fromBytes(this._privateKey) + ctweak); + if (!Fn.isValidNot0(added)) { + throw new Error("The tweak was out of range or the resulted private key is invalid"); + } + opt.privateKey = Fn.toBytes(added); + } else { + const point = Point.fromBytes(this._publicKey); + const added = ctweak === 0n ? point : point.add(Point.BASE.multiply(ctweak)); + if (added.equals(Point.ZERO)) { + throw new Error("The tweak was equal to negative P, which made the result key invalid"); + } + opt.publicKey = added.toBytes(true); + } + return new _HDKey(opt); + } catch (err) { + return this.deriveChild(index + 1); + } + } + sign(hash) { + if (!this._privateKey) { + throw new Error("No privateKey set!"); + } + abytes(hash, 32); + return secp256k1.sign(hash, this._privateKey, { prehash: false }); + } + verify(hash, signature) { + abytes(hash, 32); + abytes(signature, 64); + if (!this._publicKey) { + throw new Error("No publicKey set!"); + } + return secp256k1.verify(signature, hash, this._publicKey, { prehash: false }); + } + wipePrivateData() { + if (this._privateKey) { + this._privateKey.fill(0); + this._privateKey = void 0; + } + return this; + } + toJSON() { + return { + xpriv: this.privateExtendedKey, + xpub: this.publicExtendedKey + }; + } + serialize(version, key) { + if (!this.chainCode) { + throw new Error("No chainCode set"); + } + abytes(key, 33); + return concatBytes(toU32(version), new Uint8Array([this.depth]), toU32(this.parentFingerprint), toU32(this.index), this.chainCode, key); + } +}; + +// node_modules/@noble/hashes/sha3.js +var _0n6 = BigInt(0); +var _1n5 = BigInt(1); +var _2n4 = BigInt(2); +var _7n2 = BigInt(7); +var _256n = BigInt(256); +var _0x71n = BigInt(113); +var SHA3_PI = []; +var SHA3_ROTL = []; +var _SHA3_IOTA = []; +for (let round = 0, R = _1n5, x = 1, y = 0; round < 24; round++) { + [x, y] = [y, (2 * x + 3 * y) % 5]; + SHA3_PI.push(2 * (5 * y + x)); + SHA3_ROTL.push((round + 1) * (round + 2) / 2 % 64); + let t = _0n6; + for (let j = 0; j < 7; j++) { + R = (R << _1n5 ^ (R >> _7n2) * _0x71n) % _256n; + if (R & _2n4) + t ^= _1n5 << (_1n5 << BigInt(j)) - _1n5; + } + _SHA3_IOTA.push(t); +} +var IOTAS = split(_SHA3_IOTA, true); +var SHA3_IOTA_H = IOTAS[0]; +var SHA3_IOTA_L = IOTAS[1]; +var rotlH = (h, l, s) => s > 32 ? rotlBH(h, l, s) : rotlSH(h, l, s); +var rotlL = (h, l, s) => s > 32 ? rotlBL(h, l, s) : rotlSL(h, l, s); +function keccakP(s, rounds = 24) { + anumber(rounds, "rounds"); + if (rounds < 1 || rounds > 24) + throw new Error('"rounds" expected integer 1..24'); + const B = new Uint32Array(5 * 2); + for (let round = 24 - rounds; round < 24; round++) { + for (let x = 0; x < 10; x++) + B[x] = s[x] ^ s[x + 10] ^ s[x + 20] ^ s[x + 30] ^ s[x + 40]; + for (let x = 0; x < 10; x += 2) { + const idx1 = (x + 8) % 10; + const idx0 = (x + 2) % 10; + const B0 = B[idx0]; + const B1 = B[idx0 + 1]; + const Th = rotlH(B0, B1, 1) ^ B[idx1]; + const Tl = rotlL(B0, B1, 1) ^ B[idx1 + 1]; + for (let y = 0; y < 50; y += 10) { + s[x + y] ^= Th; + s[x + y + 1] ^= Tl; + } + } + let curH = s[2]; + let curL = s[3]; + for (let t = 0; t < 24; t++) { + const shift = SHA3_ROTL[t]; + const Th = rotlH(curH, curL, shift); + const Tl = rotlL(curH, curL, shift); + const PI = SHA3_PI[t]; + curH = s[PI]; + curL = s[PI + 1]; + s[PI] = Th; + s[PI + 1] = Tl; + } + for (let y = 0; y < 50; y += 10) { + const b0 = s[y], b1 = s[y + 1], b2 = s[y + 2], b3 = s[y + 3]; + s[y] ^= ~s[y + 2] & s[y + 4]; + s[y + 1] ^= ~s[y + 3] & s[y + 5]; + s[y + 2] ^= ~s[y + 4] & s[y + 6]; + s[y + 3] ^= ~s[y + 5] & s[y + 7]; + s[y + 4] ^= ~s[y + 6] & s[y + 8]; + s[y + 5] ^= ~s[y + 7] & s[y + 9]; + s[y + 6] ^= ~s[y + 8] & b0; + s[y + 7] ^= ~s[y + 9] & b1; + s[y + 8] ^= ~b0 & b2; + s[y + 9] ^= ~b1 & b3; + } + s[0] ^= SHA3_IOTA_H[round]; + s[1] ^= SHA3_IOTA_L[round]; + } + clean(B); +} +var Keccak = class _Keccak { + // NOTE: we accept arguments in bytes instead of bits here. + constructor(blockLen, suffix, outputLen, enableXOF = false, rounds = 24) { + __publicField(this, "state"); + __publicField(this, "pos", 0); + __publicField(this, "posOut", 0); + __publicField(this, "finished", false); + __publicField(this, "state32"); + __publicField(this, "destroyed", false); + __publicField(this, "blockLen"); + __publicField(this, "suffix"); + __publicField(this, "outputLen"); + __publicField(this, "canXOF"); + __publicField(this, "enableXOF", false); + __publicField(this, "rounds"); + this.blockLen = blockLen; + this.suffix = suffix; + this.outputLen = outputLen; + this.enableXOF = enableXOF; + this.canXOF = enableXOF; + this.rounds = rounds; + anumber(outputLen, "outputLen"); + if (!(0 < blockLen && blockLen < 200)) + throw new Error("only keccak-f1600 function is supported"); + this.state = new Uint8Array(200); + this.state32 = u32(this.state); + } + clone() { + return this._cloneInto(); + } + keccak() { + swap32IfBE(this.state32); + keccakP(this.state32, this.rounds); + swap32IfBE(this.state32); + this.posOut = 0; + this.pos = 0; + } + update(data) { + aexists(this); + abytes(data); + const { blockLen, state } = this; + const len = data.length; + for (let pos = 0; pos < len; ) { + const take = Math.min(blockLen - this.pos, len - pos); + for (let i = 0; i < take; i++) + state[this.pos++] ^= data[pos++]; + if (this.pos === blockLen) + this.keccak(); + } + return this; + } + finish() { + if (this.finished) + return; + this.finished = true; + const { state, suffix, pos, blockLen } = this; + state[pos] ^= suffix; + if ((suffix & 128) !== 0 && pos === blockLen - 1) + this.keccak(); + state[blockLen - 1] ^= 128; + this.keccak(); + } + writeInto(out) { + aexists(this, false); + abytes(out); + this.finish(); + const bufferOut = this.state; + const { blockLen } = this; + for (let pos = 0, len = out.length; pos < len; ) { + if (this.posOut >= blockLen) + this.keccak(); + const take = Math.min(blockLen - this.posOut, len - pos); + out.set(bufferOut.subarray(this.posOut, this.posOut + take), pos); + this.posOut += take; + pos += take; + } + return out; + } + xofInto(out) { + if (!this.enableXOF) + throw new Error("XOF is not possible for this instance"); + return this.writeInto(out); + } + xof(bytes) { + anumber(bytes); + return this.xofInto(new Uint8Array(bytes)); + } + digestInto(out) { + aoutput(out, this); + if (this.finished) + throw new Error("digest() was already called"); + this.writeInto(out.subarray(0, this.outputLen)); + this.destroy(); + } + digest() { + const out = new Uint8Array(this.outputLen); + this.digestInto(out); + return out; + } + destroy() { + this.destroyed = true; + clean(this.state); + } + _cloneInto(to) { + const { blockLen, suffix, outputLen, rounds, enableXOF } = this; + to || (to = new _Keccak(blockLen, suffix, outputLen, enableXOF, rounds)); + to.blockLen = blockLen; + to.state32.set(this.state32); + to.pos = this.pos; + to.posOut = this.posOut; + to.finished = this.finished; + to.rounds = rounds; + to.suffix = suffix; + to.outputLen = outputLen; + to.enableXOF = enableXOF; + to.canXOF = this.canXOF; + to.destroyed = this.destroyed; + return to; + } +}; +var genKeccak = (suffix, blockLen, outputLen, info = {}) => createHasher(() => new Keccak(blockLen, suffix, outputLen), info); +var sha3_256 = /* @__PURE__ */ genKeccak( + 6, + 136, + 32, + /* @__PURE__ */ oidNist(8) +); +var sha3_512 = /* @__PURE__ */ genKeccak( + 6, + 72, + 64, + /* @__PURE__ */ oidNist(10) +); +var genShake = (suffix, blockLen, outputLen, info = {}) => createHasher((opts2 = {}) => new Keccak(blockLen, suffix, opts2.dkLen === void 0 ? outputLen : opts2.dkLen, true), info); +var shake128 = /* @__PURE__ */ genShake(31, 168, 16, /* @__PURE__ */ oidNist(11)); +var shake256 = /* @__PURE__ */ genShake(31, 136, 32, /* @__PURE__ */ oidNist(12)); + +// node_modules/@noble/post-quantum/utils.js +var abytesDoc = abytes; +var randomBytes3 = randomBytes; +function equalBytes(a, b) { + if (a.length !== b.length) + return false; + let diff = 0; + for (let i = 0; i < a.length; i++) + diff |= a[i] ^ b[i]; + return diff === 0; +} +function copyBytes2(bytes) { + return Uint8Array.from(abytes(bytes)); +} +function byteSwap64(arr) { + const bytes = new Uint8Array(arr.buffer, arr.byteOffset, arr.byteLength); + for (let i = 0; i < bytes.length; i += 8) { + const a0 = bytes[i + 0]; + const a1 = bytes[i + 1]; + const a2 = bytes[i + 2]; + const a3 = bytes[i + 3]; + bytes[i + 0] = bytes[i + 7]; + bytes[i + 1] = bytes[i + 6]; + bytes[i + 2] = bytes[i + 5]; + bytes[i + 3] = bytes[i + 4]; + bytes[i + 4] = a3; + bytes[i + 5] = a2; + bytes[i + 6] = a1; + bytes[i + 7] = a0; + } + return arr; +} +var baswap64If = isLE ? (arr) => arr : byteSwap64; +function validateOpts(opts2) { + if (Object.prototype.toString.call(opts2) !== "[object Object]") + throw new TypeError("expected valid options object"); +} +function validateVerOpts(opts2) { + validateOpts(opts2); + if (opts2.context !== void 0) + abytes(opts2.context, void 0, "opts.context"); +} +function validateSigOpts2(opts2) { + validateVerOpts(opts2); + if (opts2.extraEntropy !== false && opts2.extraEntropy !== void 0) + abytes(opts2.extraEntropy, void 0, "opts.extraEntropy"); +} +function splitCoder(label, ...lengths) { + const getLength = (c) => typeof c === "number" ? c : c.bytesLen; + const bytesLen = lengths.reduce((sum, a) => sum + getLength(a), 0); + return { + bytesLen, + encode: (bufs) => { + const res = new Uint8Array(bytesLen); + for (let i = 0, pos = 0; i < lengths.length; i++) { + const c = lengths[i]; + const l = getLength(c); + const b = typeof c === "number" ? bufs[i] : c.encode(bufs[i]); + abytes(b, l, label); + res.set(b, pos); + if (typeof c !== "number") + b.fill(0); + pos += l; + } + return res; + }, + decode: (buf) => { + abytes(buf, bytesLen, label); + const res = []; + for (const c of lengths) { + const l = getLength(c); + const b = buf.subarray(0, l); + res.push(typeof c === "number" ? b : c.decode(b)); + buf = buf.subarray(l); + } + return res; + } + }; +} +function vecCoder(c, vecLen) { + const coder = c; + const bytesLen = vecLen * coder.bytesLen; + return { + bytesLen, + encode: (u) => { + if (u.length !== vecLen) + throw new RangeError(`vecCoder.encode: wrong length=${u.length}. Expected: ${vecLen}`); + const res = new Uint8Array(bytesLen); + for (let i = 0, pos = 0; i < u.length; i++) { + const b = coder.encode(u[i]); + res.set(b, pos); + b.fill(0); + pos += b.length; + } + return res; + }, + decode: (a) => { + abytes(a, bytesLen); + const r = []; + for (let i = 0; i < a.length; i += coder.bytesLen) + r.push(coder.decode(a.subarray(i, i + coder.bytesLen))); + return r; + } + }; +} +function cleanBytes(...list) { + for (const t of list) { + if (Array.isArray(t)) + for (const b of t) + b.fill(0); + else + t.fill(0); + } +} +function getMask(bits) { + if (!Number.isSafeInteger(bits) || bits < 0 || bits > 32) + throw new RangeError(`expected bits in [0..32], got ${bits}`); + return bits === 32 ? 4294967295 : ~(-1 << bits) >>> 0; +} +var EMPTY = /* @__PURE__ */ Uint8Array.of(); +function getMessage(msg, ctx = EMPTY) { + abytes(msg); + abytes(ctx); + if (ctx.length > 255) + throw new RangeError("context should be 255 bytes or less"); + return concatBytes(new Uint8Array([0, ctx.length]), ctx, msg); +} +var oidNistP = /* @__PURE__ */ Uint8Array.from([6, 9, 96, 134, 72, 1, 101, 3, 4, 2]); +function checkHash(hash, requiredStrength = 0) { + if (!hash.oid || !equalBytes(hash.oid.subarray(0, 10), oidNistP)) + throw new Error("hash.oid is invalid: expected NIST hash"); + const collisionResistance = hash.outputLen * 8 / 2; + if (requiredStrength > collisionResistance) { + throw new Error("Pre-hash security strength too low: " + collisionResistance + ", required: " + requiredStrength); + } +} +function getMessagePrehash(hash, msg, ctx = EMPTY) { + abytes(msg); + abytes(ctx); + if (ctx.length > 255) + throw new RangeError("context should be 255 bytes or less"); + const hashed = hash(msg); + return concatBytes(new Uint8Array([1, ctx.length]), ctx, hash.oid, hashed); +} + +// node_modules/@noble/post-quantum/_crystals.js +var genCrystals = (opts2) => { + const { newPoly: newPoly2, N: N3, Q: Q4, F: F3, ROOT_OF_UNITY: ROOT_OF_UNITY3, brvBits, isKyber } = opts2; + const mod2 = (a, modulo = Q4) => { + const result = a % modulo | 0; + return (result >= 0 ? result | 0 : modulo + result | 0) | 0; + }; + const smod = (a, modulo = Q4) => { + const r = mod2(a, modulo) | 0; + return (r > modulo >> 1 ? r - modulo | 0 : r) | 0; + }; + function getZettas() { + const out = newPoly2(N3); + for (let i = 0; i < N3; i++) { + const b = reverseBits(i, brvBits); + const p = BigInt(ROOT_OF_UNITY3) ** BigInt(b) % BigInt(Q4); + out[i] = Number(p) | 0; + } + return out; + } + const nttZetas = getZettas(); + const field = { + add: (a, b) => mod2((a | 0) + (b | 0)) | 0, + sub: (a, b) => mod2((a | 0) - (b | 0)) | 0, + mul: (a, b) => mod2((a | 0) * (b | 0)) | 0, + inv: (_a) => { + throw new Error("not implemented"); + } + }; + const nttOpts = { + N: N3, + roots: nttZetas, + invertButterflies: true, + skipStages: isKyber ? 1 : 0, + brp: false + }; + const dif = FFTCore(field, { dit: false, ...nttOpts }); + const dit = FFTCore(field, { dit: true, ...nttOpts }); + const NTT = { + encode: (r) => { + return dif(r); + }, + decode: (r) => { + dit(r); + for (let i = 0; i < r.length; i++) + r[i] = mod2(F3 * r[i]); + return r; + } + }; + const bitsCoder = (d, c) => { + const mask = getMask(d); + const bytesLen = d * (N3 / 8); + return { + bytesLen, + encode: (poly_) => { + const poly = poly_; + const r = new Uint8Array(bytesLen); + for (let i = 0, buf = 0, bufLen = 0, pos = 0; i < poly.length; i++) { + buf |= (c.encode(poly[i]) & mask) << bufLen; + bufLen += d; + for (; bufLen >= 8; bufLen -= 8, buf >>= 8) + r[pos++] = buf & getMask(bufLen); + } + return r; + }, + decode: (bytes) => { + const r = newPoly2(N3); + for (let i = 0, buf = 0, bufLen = 0, pos = 0; i < bytes.length; i++) { + buf |= bytes[i] << bufLen; + bufLen += 8; + for (; bufLen >= d; bufLen -= d, buf >>= d) + r[pos++] = c.decode(buf & mask); + } + return r; + } + }; + }; + return { + mod: mod2, + smod, + nttZetas, + NTT: { + encode: (r) => NTT.encode(r), + decode: (r) => NTT.decode(r) + }, + bitsCoder + }; +}; +var createXofShake = (shake) => (seed, blockLen) => { + if (!blockLen) + blockLen = shake.blockLen; + const _seed = new Uint8Array(seed.length + 2); + _seed.set(seed); + const seedLen = seed.length; + const buf = new Uint8Array(blockLen); + let h = shake.create({}); + let calls = 0; + let xofs = 0; + return { + stats: () => ({ calls, xofs }), + get: (x, y) => { + _seed[seedLen + 0] = x; + _seed[seedLen + 1] = y; + h.destroy(); + h = shake.create({}).update(_seed); + calls++; + return () => { + xofs++; + return h.xofInto(buf); + }; + }, + clean: () => { + h.destroy(); + cleanBytes(buf, _seed); + } + }; +}; +var XOF128 = /* @__PURE__ */ createXofShake(shake128); +var XOF256 = /* @__PURE__ */ createXofShake(shake256); + +// node_modules/@noble/post-quantum/ml-dsa.js +function validateInternalOpts(opts2) { + validateOpts(opts2); + if (opts2.externalMu !== void 0) + abool(opts2.externalMu, "opts.externalMu"); +} +var N = 256; +var Q = 8380417; +var ROOT_OF_UNITY = 1753; +var F = 8347681; +var D = 13; +var GAMMA2_1 = Math.floor((Q - 1) / 88) | 0; +var GAMMA2_2 = Math.floor((Q - 1) / 32) | 0; +var PARAMS = /* @__PURE__ */ (() => Object.freeze({ + 2: Object.freeze({ + K: 4, + L: 4, + D, + GAMMA1: 2 ** 17, + GAMMA2: GAMMA2_1, + TAU: 39, + ETA: 2, + OMEGA: 80 + }), + 3: Object.freeze({ + K: 6, + L: 5, + D, + GAMMA1: 2 ** 19, + GAMMA2: GAMMA2_2, + TAU: 49, + ETA: 4, + OMEGA: 55 + }), + 5: Object.freeze({ + K: 8, + L: 7, + D, + GAMMA1: 2 ** 19, + GAMMA2: GAMMA2_2, + TAU: 60, + ETA: 2, + OMEGA: 75 + }) +}))(); +var newPoly = (n) => new Int32Array(n); +var crystals = /* @__PURE__ */ genCrystals({ + N, + Q, + F, + ROOT_OF_UNITY, + newPoly, + isKyber: false, + brvBits: 8 +}); +var id = (n) => n; +var polyCoder = (d, compress2 = id, verify = id) => crystals.bitsCoder(d, { + encode: (i) => compress2(verify(i)), + decode: (i) => verify(compress2(i)) +}); +var polyAdd = (a_, b_) => { + const a = a_; + const b = b_; + for (let i = 0; i < a.length; i++) + a[i] = crystals.mod(a[i] + b[i]); + return a; +}; +var polySub = (a_, b_) => { + const a = a_; + const b = b_; + for (let i = 0; i < a.length; i++) + a[i] = crystals.mod(a[i] - b[i]); + return a; +}; +var polyShiftl = (p_) => { + const p = p_; + for (let i = 0; i < N; i++) + p[i] <<= D; + return p; +}; +var polyChknorm = (p_, B) => { + const p = p_; + for (let i = 0; i < N; i++) + if (Math.abs(crystals.smod(p[i])) >= B) + return true; + return false; +}; +var MultiplyNTTs = (a_, b_) => { + const a = a_; + const b = b_; + const c = newPoly(N); + for (let i = 0; i < a.length; i++) + c[i] = crystals.mod(a[i] * b[i]); + return c; +}; +function RejNTTPoly(xof_) { + const xof = xof_; + const r = newPoly(N); + for (let j = 0; j < N; ) { + const b = xof(); + if (b.length % 3) + throw new Error("RejNTTPoly: unaligned block"); + for (let i = 0; j < N && i <= b.length - 3; i += 3) { + const t = (b[i + 0] | b[i + 1] << 8 | b[i + 2] << 16) & 8388607; + if (t < Q) + r[j++] = t; + } + } + return r; +} +function getDilithium(opts_) { + const opts2 = opts_; + const { K, L, GAMMA1, GAMMA2, TAU, ETA, OMEGA } = opts2; + const { CRH_BYTES, TR_BYTES, C_TILDE_BYTES, XOF128: XOF1282, XOF256: XOF2562, securityLevel } = opts2; + if (![2, 4].includes(ETA)) + throw new Error("Wrong ETA"); + if (![1 << 17, 1 << 19].includes(GAMMA1)) + throw new Error("Wrong GAMMA1"); + if (![GAMMA2_1, GAMMA2_2].includes(GAMMA2)) + throw new Error("Wrong GAMMA2"); + const BETA = TAU * ETA; + const decompose = (r) => { + const rPlus = crystals.mod(r); + const r0 = crystals.smod(rPlus, 2 * GAMMA2) | 0; + if (rPlus - r0 === Q - 1) + return { r1: 0 | 0, r0: r0 - 1 | 0 }; + const r1 = Math.floor((rPlus - r0) / (2 * GAMMA2)) | 0; + return { r1, r0 }; + }; + const HighBits = (r) => decompose(r).r1; + const LowBits = (r) => decompose(r).r0; + const MakeHint = (z, r) => { + const res0 = z <= GAMMA2 || z > Q - GAMMA2 || z === Q - GAMMA2 && r === 0 ? 0 : 1; + return res0; + }; + const UseHint = (h, r) => { + const m = Math.floor((Q - 1) / (2 * GAMMA2)); + const { r1, r0 } = decompose(r); + if (h === 1) + return r0 > 0 ? crystals.mod(r1 + 1, m) | 0 : crystals.mod(r1 - 1, m) | 0; + return r1 | 0; + }; + const Power2Round = (r) => { + const rPlus = crystals.mod(r); + const r0 = crystals.smod(rPlus, 2 ** D) | 0; + return { r1: Math.floor((rPlus - r0) / 2 ** D) | 0, r0 }; + }; + const hintCoder = { + bytesLen: OMEGA + K, + encode: (h_) => { + const h = h_; + if (h === false) + throw new Error("hint.encode: hint is false"); + const res = new Uint8Array(OMEGA + K); + for (let i = 0, k = 0; i < K; i++) { + for (let j = 0; j < N; j++) + if (h[i][j] !== 0) + res[k++] = j; + res[OMEGA + i] = k; + } + return res; + }, + decode: (buf) => { + const h = []; + let k = 0; + for (let i = 0; i < K; i++) { + const hi = newPoly(N); + if (buf[OMEGA + i] < k || buf[OMEGA + i] > OMEGA) + return false; + for (let j = k; j < buf[OMEGA + i]; j++) { + if (j > k && buf[j] <= buf[j - 1]) + return false; + hi[buf[j]] = 1; + } + k = buf[OMEGA + i]; + h.push(hi); + } + for (let j = k; j < OMEGA; j++) + if (buf[j] !== 0) + return false; + return h; + } + }; + const ETACoder = polyCoder(ETA === 2 ? 3 : 4, (i) => ETA - i, (i) => { + if (!(-ETA <= i && i <= ETA)) + throw new Error(`malformed key s1/s3 ${i} outside of ETA range [${-ETA}, ${ETA}]`); + return i; + }); + const T0Coder = polyCoder(13, (i) => (1 << D - 1) - i); + const T1Coder = polyCoder(10); + const ZCoder = polyCoder(GAMMA1 === 1 << 17 ? 18 : 20, (i) => crystals.smod(GAMMA1 - i)); + const W1Coder = polyCoder(GAMMA2 === GAMMA2_1 ? 6 : 4); + const W1Vec = vecCoder(W1Coder, K); + const publicCoder = splitCoder("publicKey", 32, vecCoder(T1Coder, K)); + const secretCoder = splitCoder("secretKey", 32, 32, TR_BYTES, vecCoder(ETACoder, L), vecCoder(ETACoder, K), vecCoder(T0Coder, K)); + const sigCoder = splitCoder("signature", C_TILDE_BYTES, vecCoder(ZCoder, L), hintCoder); + const CoefFromHalfByte = ETA === 2 ? (n) => n < 15 ? 2 - n % 5 : false : (n) => n < 9 ? 4 - n : false; + function RejBoundedPoly(xof_) { + const xof = xof_; + const r = newPoly(N); + for (let j = 0; j < N; ) { + const b = xof(); + for (let i = 0; j < N && i < b.length; i += 1) { + const d1 = CoefFromHalfByte(b[i] & 15); + const d2 = CoefFromHalfByte(b[i] >> 4 & 15); + if (d1 !== false) + r[j++] = d1; + if (j < N && d2 !== false) + r[j++] = d2; + } + } + return r; + } + const SampleInBall = (seed) => { + const pre = newPoly(N); + const s = shake256.create({}).update(seed); + const buf = new Uint8Array(shake256.blockLen); + s.xofInto(buf); + const masks = buf.slice(0, 8); + for (let i = N - TAU, pos = 8, maskPos = 0, maskBit = 0; i < N; i++) { + let b = i + 1; + for (; b > i; ) { + b = buf[pos++]; + if (pos < shake256.blockLen) + continue; + s.xofInto(buf); + pos = 0; + } + pre[i] = pre[b]; + pre[b] = 1 - ((masks[maskPos] >> maskBit++ & 1) << 1); + if (maskBit >= 8) { + maskPos++; + maskBit = 0; + } + } + return pre; + }; + const polyPowerRound = (p_) => { + const p = p_; + const res0 = newPoly(N); + const res1 = newPoly(N); + for (let i = 0; i < p.length; i++) { + const { r0, r1 } = Power2Round(p[i]); + res0[i] = r0; + res1[i] = r1; + } + return { r0: res0, r1: res1 }; + }; + const polyUseHint = (u_, h_) => { + const u = u_; + const h = h_; + for (let i = 0; i < N; i++) + u[i] = UseHint(h[i], u[i]); + return u; + }; + const polyMakeHint = (a_, b_) => { + const a = a_; + const b = b_; + const v = newPoly(N); + let cnt = 0; + for (let i = 0; i < N; i++) { + const h = MakeHint(a[i], b[i]); + v[i] = h; + cnt += h; + } + return { v, cnt }; + }; + const signRandBytes = 32; + const seedCoder = splitCoder("seed", 32, 64, 32); + const internal = Object.freeze({ + info: Object.freeze({ type: "internal-ml-dsa" }), + lengths: Object.freeze({ + secretKey: secretCoder.bytesLen, + publicKey: publicCoder.bytesLen, + seed: 32, + signature: sigCoder.bytesLen, + signRand: signRandBytes + }), + keygen: (seed) => { + const seedDst = new Uint8Array(32 + 2); + const randSeed = seed === void 0; + if (randSeed) + seed = randomBytes3(32); + abytesDoc(seed, 32, "seed"); + seedDst.set(seed); + if (randSeed) + cleanBytes(seed); + seedDst[32] = K; + seedDst[33] = L; + const [rho, rhoPrime, K_] = seedCoder.decode(shake256(seedDst, { dkLen: seedCoder.bytesLen })); + const xofPrime = XOF2562(rhoPrime); + const s1 = []; + for (let i = 0; i < L; i++) + s1.push(RejBoundedPoly(xofPrime.get(i & 255, i >> 8 & 255))); + const s2 = []; + for (let i = L; i < L + K; i++) + s2.push(RejBoundedPoly(xofPrime.get(i & 255, i >> 8 & 255))); + const s1Hat = s1.map((i) => crystals.NTT.encode(i.slice())); + const t0 = []; + const t1 = []; + const xof = XOF1282(rho); + const t = newPoly(N); + for (let i = 0; i < K; i++) { + cleanBytes(t); + for (let j = 0; j < L; j++) { + const aij = RejNTTPoly(xof.get(j, i)); + polyAdd(t, MultiplyNTTs(aij, s1Hat[j])); + } + crystals.NTT.decode(t); + const { r0, r1 } = polyPowerRound(polyAdd(t, s2[i])); + t0.push(r0); + t1.push(r1); + } + const publicKey = publicCoder.encode([rho, t1]); + const tr = shake256(publicKey, { dkLen: TR_BYTES }); + const secretKey = secretCoder.encode([rho, K_, tr, s1, s2, t0]); + xof.clean(); + xofPrime.clean(); + cleanBytes(rho, rhoPrime, K_, s1, s2, s1Hat, t, t0, t1, tr, seedDst); + return { + publicKey, + secretKey + }; + }, + getPublicKey: (secretKey) => { + const [rho, _K, _tr, s1, s2, _t0] = secretCoder.decode(secretKey); + const xof = XOF1282(rho); + const s1Hat = s1.map((p) => crystals.NTT.encode(p.slice())); + const t1 = []; + const tmp = newPoly(N); + for (let i = 0; i < K; i++) { + tmp.fill(0); + for (let j = 0; j < L; j++) { + const aij = RejNTTPoly(xof.get(j, i)); + polyAdd(tmp, MultiplyNTTs(aij, s1Hat[j])); + } + crystals.NTT.decode(tmp); + polyAdd(tmp, s2[i]); + const { r1 } = polyPowerRound(tmp); + t1.push(r1); + } + xof.clean(); + cleanBytes(tmp, s1Hat, _t0, s1, s2); + return publicCoder.encode([rho, t1]); + }, + // NOTE: random is optional. + sign: (msg, secretKey, opts3 = {}) => { + validateSigOpts2(opts3); + validateInternalOpts(opts3); + let { extraEntropy: random, externalMu = false } = opts3; + const [rho, _K, tr, s1, s2, t0] = secretCoder.decode(secretKey); + const A = []; + const xof = XOF1282(rho); + for (let i = 0; i < K; i++) { + const pv = []; + for (let j = 0; j < L; j++) + pv.push(RejNTTPoly(xof.get(j, i))); + A.push(pv); + } + xof.clean(); + for (let i = 0; i < L; i++) + crystals.NTT.encode(s1[i]); + for (let i = 0; i < K; i++) { + crystals.NTT.encode(s2[i]); + crystals.NTT.encode(t0[i]); + } + const mu = externalMu ? msg : ( + // 6: µ ← H(tr||M, 512) + // ▷ Compute message representative µ + shake256.create({ dkLen: CRH_BYTES }).update(tr).update(msg).digest() + ); + const rnd = random === false ? new Uint8Array(32) : random === void 0 ? randomBytes3(signRandBytes) : random; + abytesDoc(rnd, 32, "extraEntropy"); + const rhoprime = shake256.create({ dkLen: CRH_BYTES }).update(_K).update(rnd).update(mu).digest(); + abytesDoc(rhoprime, CRH_BYTES); + const x256 = XOF2562(rhoprime, ZCoder.bytesLen); + main_loop: for (let kappa = 0; ; ) { + const y = []; + for (let i = 0; i < L; i++, kappa++) + y.push(ZCoder.decode(x256.get(kappa & 255, kappa >> 8)())); + const z = y.map((i) => crystals.NTT.encode(i.slice())); + const w = []; + for (let i = 0; i < K; i++) { + const wi = newPoly(N); + for (let j = 0; j < L; j++) + polyAdd(wi, MultiplyNTTs(A[i][j], z[j])); + crystals.NTT.decode(wi); + w.push(wi); + } + const w1 = w.map((j) => j.map(HighBits)); + const cTilde = shake256.create({ dkLen: C_TILDE_BYTES }).update(mu).update(W1Vec.encode(w1)).digest(); + const cHat = crystals.NTT.encode(SampleInBall(cTilde)); + const cs1 = s1.map((i) => MultiplyNTTs(i, cHat)); + for (let i = 0; i < L; i++) { + polyAdd(crystals.NTT.decode(cs1[i]), y[i]); + if (polyChknorm(cs1[i], GAMMA1 - BETA)) + continue main_loop; + } + let cnt = 0; + const h = []; + for (let i = 0; i < K; i++) { + const cs2 = crystals.NTT.decode(MultiplyNTTs(s2[i], cHat)); + const r0 = polySub(w[i], cs2).map(LowBits); + if (polyChknorm(r0, GAMMA2 - BETA)) + continue main_loop; + const ct0 = crystals.NTT.decode(MultiplyNTTs(t0[i], cHat)); + if (polyChknorm(ct0, GAMMA2)) + continue main_loop; + polyAdd(r0, ct0); + const hint = polyMakeHint(r0, w1[i]); + h.push(hint.v); + cnt += hint.cnt; + } + if (cnt > OMEGA) + continue; + x256.clean(); + const res = sigCoder.encode([cTilde, cs1, h]); + cleanBytes(cTilde, cs1, h, cHat, w1, w, z, y, rhoprime, s1, s2, t0, ...A); + if (!externalMu) + cleanBytes(mu); + return res; + } + throw new Error("Unreachable code path reached, report this error"); + }, + verify: (sig, msg, publicKey, opts3 = {}) => { + validateInternalOpts(opts3); + const { externalMu = false } = opts3; + const [rho, t1] = publicCoder.decode(publicKey); + const tr = shake256(publicKey, { dkLen: TR_BYTES }); + if (sig.length !== sigCoder.bytesLen) + return false; + const [cTilde, z, h] = sigCoder.decode(sig); + if (h === false) + return false; + for (let i = 0; i < L; i++) + if (polyChknorm(z[i], GAMMA1 - BETA)) + return false; + const mu = externalMu ? msg : ( + // 7: µ ← H(tr||M, 512) + shake256.create({ dkLen: CRH_BYTES }).update(tr).update(msg).digest() + ); + const c = crystals.NTT.encode(SampleInBall(cTilde)); + const zNtt = z.map((i) => i.slice()); + for (let i = 0; i < L; i++) + crystals.NTT.encode(zNtt[i]); + const wTick1 = []; + const xof = XOF1282(rho); + for (let i = 0; i < K; i++) { + const ct12d = MultiplyNTTs(crystals.NTT.encode(polyShiftl(t1[i])), c); + const Az = newPoly(N); + for (let j = 0; j < L; j++) { + const aij = RejNTTPoly(xof.get(j, i)); + polyAdd(Az, MultiplyNTTs(aij, zNtt[j])); + } + const wApprox = crystals.NTT.decode(polySub(Az, ct12d)); + wTick1.push(polyUseHint(wApprox, h[i])); + } + xof.clean(); + const c2 = shake256.create({ dkLen: C_TILDE_BYTES }).update(mu).update(W1Vec.encode(wTick1)).digest(); + for (const t of h) { + const sum = t.reduce((acc, i) => acc + i, 0); + if (!(sum <= OMEGA)) + return false; + } + for (const t of z) + if (polyChknorm(t, GAMMA1 - BETA)) + return false; + return equalBytes(cTilde, c2); + } + }); + return Object.freeze({ + info: Object.freeze({ type: "ml-dsa" }), + internal, + securityLevel, + keygen: internal.keygen, + lengths: internal.lengths, + getPublicKey: internal.getPublicKey, + sign: (msg, secretKey, opts3 = {}) => { + validateSigOpts2(opts3); + const M = getMessage(msg, opts3.context); + const res = internal.sign(M, secretKey, opts3); + cleanBytes(M); + return res; + }, + verify: (sig, msg, publicKey, opts3 = {}) => { + validateVerOpts(opts3); + return internal.verify(sig, getMessage(msg, opts3.context), publicKey); + }, + prehash: (hash) => { + checkHash(hash, securityLevel); + return Object.freeze({ + info: Object.freeze({ type: "hashml-dsa" }), + securityLevel, + lengths: internal.lengths, + keygen: internal.keygen, + getPublicKey: internal.getPublicKey, + sign: (msg, secretKey, opts3 = {}) => { + validateSigOpts2(opts3); + const M = getMessagePrehash(hash, msg, opts3.context); + const res = internal.sign(M, secretKey, opts3); + cleanBytes(M); + return res; + }, + verify: (sig, msg, publicKey, opts3 = {}) => { + validateVerOpts(opts3); + return internal.verify(sig, getMessagePrehash(hash, msg, opts3.context), publicKey); + } + }); + } + }); +} +var ml_dsa44 = /* @__PURE__ */ (() => getDilithium({ + ...PARAMS[2], + CRH_BYTES: 64, + TR_BYTES: 64, + C_TILDE_BYTES: 32, + XOF128, + XOF256, + securityLevel: 128 +}))(); +var ml_dsa65 = /* @__PURE__ */ (() => getDilithium({ + ...PARAMS[3], + CRH_BYTES: 64, + TR_BYTES: 64, + C_TILDE_BYTES: 48, + XOF128, + XOF256, + securityLevel: 192 +}))(); + +// node_modules/@noble/post-quantum/slh-dsa.js +var PARAMS2 = /* @__PURE__ */ (() => Object.freeze({ + "128f": Object.freeze({ W: 16, N: 16, H: 66, D: 22, K: 33, A: 6, securityLevel: 128 }), + "128s": Object.freeze({ W: 16, N: 16, H: 63, D: 7, K: 14, A: 12, securityLevel: 128 }), + "192f": Object.freeze({ W: 16, N: 24, H: 66, D: 22, K: 33, A: 8, securityLevel: 192 }), + "192s": Object.freeze({ W: 16, N: 24, H: 63, D: 7, K: 17, A: 14, securityLevel: 192 }), + "256f": Object.freeze({ W: 16, N: 32, H: 68, D: 17, K: 35, A: 9, securityLevel: 256 }), + "256s": Object.freeze({ W: 16, N: 32, H: 64, D: 8, K: 22, A: 14, securityLevel: 256 }) +}))(); +var AddressType = { + WOTS: 0, + WOTSPK: 1, + HASHTREE: 2, + FORSTREE: 3, + FORSPK: 4, + WOTSPRF: 5, + FORSPRF: 6 +}; +function hexToNumber2(hex) { + if (typeof hex !== "string") + throw new Error("hex string expected, got " + typeof hex); + return BigInt(hex === "" ? "0" : "0x" + hex); +} +function bytesToNumberBE2(bytes) { + return hexToNumber2(bytesToHex(bytes)); +} +function numberToBytesBE2(n, len) { + return hexToBytes(n.toString(16).padStart(len * 2, "0")); +} +var base2b = (outLen, b) => { + const mask = getMask(b); + return (bytes) => { + const baseB = new Uint32Array(outLen); + for (let out = 0, pos = 0, bits = 0, total = 0; out < outLen; out++) { + while (bits < b) { + total = total << 8 | bytes[pos++]; + bits += 8; + } + bits -= b; + baseB[out] = total >>> bits & mask; + } + return baseB; + }; +}; +function getMaskBig(bits) { + return (1n << BigInt(bits)) - 1n; +} +function gen(opts2, hashOpts_) { + const hashOpts = hashOpts_; + const { N: N3, W, H, D: D2, K, A, securityLevel } = opts2; + const getContext = hashOpts.getContext(opts2); + if (W !== 16) + throw new Error("Unsupported Winternitz parameter"); + const WOTS_LOGW = 4; + const WOTS_LEN1 = Math.floor(8 * N3 / WOTS_LOGW); + const WOTS_LEN2 = N3 <= 8 ? 2 : N3 <= 136 ? 3 : 4; + const TREE_HEIGHT = Math.floor(H / D2); + const WOTS_LEN = WOTS_LEN1 + WOTS_LEN2; + let ADDR_BYTES = 22; + let OFFSET_LAYER = 0; + let OFFSET_TREE = 1; + let OFFSET_TYPE = 9; + let OFFSET_KP_ADDR2 = 12; + let OFFSET_KP_ADDR1 = 13; + let OFFSET_CHAIN_ADDR = 17; + let OFFSET_TREE_INDEX = 18; + let OFFSET_HASH_ADDR = 21; + if (!hashOpts.isCompressed) { + ADDR_BYTES = 32; + OFFSET_LAYER += 3; + OFFSET_TREE += 7; + OFFSET_TYPE += 10; + OFFSET_KP_ADDR2 += 10; + OFFSET_KP_ADDR1 += 10; + OFFSET_CHAIN_ADDR += 10; + OFFSET_TREE_INDEX += 10; + OFFSET_HASH_ADDR += 10; + } + const setAddr = (opts3, addr = new Uint8Array(ADDR_BYTES)) => { + const { type, height, tree, layer, index, chain: chain2, hash, keypair } = opts3; + const { subtreeAddr, keypairAddr } = opts3; + const v = createView(addr); + if (height !== void 0) + addr[OFFSET_CHAIN_ADDR] = height; + if (layer !== void 0) + addr[OFFSET_LAYER] = layer; + if (type !== void 0) + addr[OFFSET_TYPE] = type; + if (chain2 !== void 0) + addr[OFFSET_CHAIN_ADDR] = chain2; + if (hash !== void 0) + addr[OFFSET_HASH_ADDR] = hash; + if (index !== void 0) + v.setUint32(OFFSET_TREE_INDEX, index, false); + if (subtreeAddr) + addr.set(subtreeAddr.subarray(0, OFFSET_TREE + 8)); + if (tree !== void 0) + v.setBigUint64(OFFSET_TREE, tree, false); + if (keypair !== void 0) { + addr[OFFSET_KP_ADDR1] = keypair; + if (TREE_HEIGHT > 8) + addr[OFFSET_KP_ADDR2] = keypair >>> 8; + } + if (keypairAddr) { + addr.set(keypairAddr.subarray(0, OFFSET_TREE + 8)); + addr[OFFSET_KP_ADDR1] = keypairAddr[OFFSET_KP_ADDR1]; + if (TREE_HEIGHT > 8) + addr[OFFSET_KP_ADDR2] = keypairAddr[OFFSET_KP_ADDR2]; + } + return addr; + }; + const chainCoder = base2b(WOTS_LEN2, WOTS_LOGW); + const chainLengths = (msg) => { + const W1 = base2b(WOTS_LEN1, WOTS_LOGW)(msg); + let csum = 0; + for (let i = 0; i < W1.length; i++) + csum += W - 1 - W1[i]; + csum <<= (8 - WOTS_LEN2 * WOTS_LOGW % 8) % 8; + const W2 = chainCoder(numberToBytesBE2(csum, Math.ceil(WOTS_LEN2 * WOTS_LOGW / 8))); + const lengths = new Uint32Array(WOTS_LEN); + lengths.set(W1); + lengths.set(W2, W1.length); + return lengths; + }; + const messageToIndices = base2b(K, A); + const TREE_BITS = TREE_HEIGHT * (D2 - 1); + const LEAF_BITS = TREE_HEIGHT; + const hashMsgCoder = splitCoder("hashedMessage", Math.ceil(A * K / 8), Math.ceil(TREE_BITS / 8), Math.ceil(TREE_HEIGHT / 8)); + const hashMessage = (R, pkSeed, msg, context) => { + const rawContext = context; + const digest = rawContext.Hmsg(R, pkSeed, msg, hashMsgCoder.bytesLen); + const [md, tmpIdxTree, tmpIdxLeaf] = hashMsgCoder.decode(digest); + const tree = bytesToNumberBE2(tmpIdxTree) & getMaskBig(TREE_BITS); + const leafIdx = Number(bytesToNumberBE2(tmpIdxLeaf)) & getMask(LEAF_BITS); + return { tree, leafIdx, md }; + }; + const treehash = (height, fn) => function treehash_i(context, leafIdx, idxOffset, treeAddr, info) { + const rawContext = context; + const leafFn = fn; + const maxIdx = (1 << height) - 1; + const stack = new Uint8Array(height * N3); + const authPath = new Uint8Array(height * N3); + for (let idx = 0; ; idx++) { + const current = new Uint8Array(2 * N3); + const cur0 = current.subarray(0, N3); + const cur1 = current.subarray(N3); + const addrOffset = idx + idxOffset; + cur1.set(leafFn(leafIdx, addrOffset, rawContext, info)); + let h = 0; + for (let i = idx, o = idxOffset, l = leafIdx; ; h++, i >>>= 1, l >>>= 1, o >>>= 1) { + if (h === height) + return { root: cur1, authPath }; + if ((i ^ l) === 1) + authPath.subarray(h * N3).set(cur1); + if ((i & 1) === 0 && idx < maxIdx) + break; + setAddr({ height: h + 1, index: (i >> 1) + (o >> 1) }, treeAddr); + cur0.set(stack.subarray(h * N3).subarray(0, N3)); + cur1.set(rawContext.thashN(2, current, treeAddr)); + } + stack.subarray(h * N3).set(cur1); + } + throw new Error("Unreachable code path reached, report this error"); + }; + const wotsTreehash = treehash(TREE_HEIGHT, (leafIdx, addrOffset, context, info) => { + const rawContext = context; + const wotsPk = new Uint8Array(WOTS_LEN * N3); + const wotsKmask = addrOffset === leafIdx ? 0 : ~0 >>> 0; + setAddr({ keypair: addrOffset }, info.leafAddr); + setAddr({ keypair: addrOffset }, info.pkAddr); + for (let i = 0; i < WOTS_LEN; i++) { + const wotsK = info.wotsSteps[i] | wotsKmask; + const pk = wotsPk.subarray(i * N3, (i + 1) * N3); + setAddr({ chain: i, hash: 0, type: AddressType.WOTSPRF }, info.leafAddr); + pk.set(rawContext.PRFaddr(info.leafAddr)); + setAddr({ type: AddressType.WOTS }, info.leafAddr); + for (let k = 0; ; k++) { + if (k === wotsK) + info.wotsSig.subarray(i * N3).set(pk); + if (k === W - 1) + break; + setAddr({ hash: k }, info.leafAddr); + pk.set(rawContext.thash1(pk, info.leafAddr)); + } + } + return rawContext.thashN(WOTS_LEN, wotsPk, info.pkAddr); + }); + const forsTreehash = treehash(A, (_, addrOffset, context, forsLeafAddr) => { + const rawContext = context; + setAddr({ type: AddressType.FORSPRF, index: addrOffset }, forsLeafAddr); + const prf = rawContext.PRFaddr(forsLeafAddr); + setAddr({ type: AddressType.FORSTREE }, forsLeafAddr); + return rawContext.thash1(prf, forsLeafAddr); + }); + const merkleSign = (context, wotsAddr, treeAddr, leafIdx, prevRoot = new Uint8Array(N3)) => { + setAddr({ type: AddressType.HASHTREE }, treeAddr); + const info = { + wotsSig: new Uint8Array(wotsCoder.bytesLen), + wotsSteps: chainLengths(prevRoot), + leafAddr: setAddr({ subtreeAddr: wotsAddr }), + pkAddr: setAddr({ type: AddressType.WOTSPK, subtreeAddr: wotsAddr }) + }; + const { root, authPath } = wotsTreehash(context, leafIdx, 0, treeAddr, info); + return { + root, + sigWots: info.wotsSig.subarray(0, WOTS_LEN * N3), + sigAuth: authPath + }; + }; + const computeRoot = (leaf, leafIdx, idxOffset, authPath, treeHeight, context, addr) => { + const rawContext = context; + const buffer = new Uint8Array(2 * N3); + const b0 = buffer.subarray(0, N3); + const b1 = buffer.subarray(N3, 2 * N3); + if ((leafIdx & 1) !== 0) { + b1.set(leaf.subarray(0, N3)); + b0.set(authPath.subarray(0, N3)); + } else { + b0.set(leaf.subarray(0, N3)); + b1.set(authPath.subarray(0, N3)); + } + leafIdx >>>= 1; + idxOffset >>>= 1; + for (let i = 0; i < treeHeight - 1; i++, leafIdx >>= 1, idxOffset >>= 1) { + setAddr({ height: i + 1, index: leafIdx + idxOffset }, addr); + const a = authPath.subarray((i + 1) * N3, (i + 2) * N3); + if ((leafIdx & 1) !== 0) { + b1.set(rawContext.thashN(2, buffer, addr)); + b0.set(a); + } else { + buffer.set(rawContext.thashN(2, buffer, addr)); + b1.set(a); + } + } + setAddr({ height: treeHeight, index: leafIdx + idxOffset }, addr); + return rawContext.thashN(2, buffer, addr); + }; + const seedCoder = splitCoder("seed", N3, N3, N3); + const publicCoder = splitCoder("publicKey", N3, N3); + const secretCoder = splitCoder("secretKey", N3, N3, publicCoder.bytesLen); + const forsCoder = vecCoder(splitCoder("fors", N3, N3 * A), K); + const wotsCoder = vecCoder(splitCoder("wots", WOTS_LEN * N3, TREE_HEIGHT * N3), D2); + const sigCoder = splitCoder("signature", N3, forsCoder, wotsCoder); + const internal = Object.freeze({ + info: Object.freeze({ type: "internal-slh-dsa" }), + lengths: Object.freeze({ + publicKey: publicCoder.bytesLen, + secretKey: secretCoder.bytesLen, + signature: sigCoder.bytesLen, + seed: seedCoder.bytesLen, + signRand: N3 + }), + keygen(seed) { + if (seed !== void 0) + abytesDoc(seed, seedCoder.bytesLen, "seed"); + seed = seed === void 0 ? randomBytes3(seedCoder.bytesLen) : copyBytes2(seed); + const [secretSeed, secretPRF, publicSeed] = seedCoder.decode(seed); + const context = getContext(publicSeed, secretSeed); + const topTreeAddr = setAddr({ layer: D2 - 1 }); + const wotsAddr = setAddr({ layer: D2 - 1 }); + const { root } = merkleSign(context, wotsAddr, topTreeAddr, ~0 >>> 0); + const publicKey = publicCoder.encode([publicSeed, root]); + const secretKey = secretCoder.encode([secretSeed, secretPRF, publicKey]); + context.clean(); + cleanBytes(secretSeed, secretPRF, root, wotsAddr, topTreeAddr); + return { + publicKey, + secretKey + }; + }, + getPublicKey: (secretKey) => { + const [_skSeed, _skPRF, pk] = secretCoder.decode(secretKey); + return Uint8Array.from(pk); + }, + sign: (msg, sk, opts3 = {}) => { + validateSigOpts2(opts3); + let { extraEntropy: random } = opts3; + const [skSeed, skPRF, pk] = secretCoder.decode(sk); + const [pkSeed, _] = publicCoder.decode(pk); + if (random === false) + random = copyBytes2(pkSeed); + else if (random === void 0) + random = randomBytes3(N3); + else + random = copyBytes2(random); + abytesDoc(random, N3); + const context = getContext(pkSeed, skSeed); + const R = context.PRFmsg(skPRF, random, msg); + let { tree, leafIdx, md } = hashMessage(R, pk, msg, context); + const wotsAddr = setAddr({ + type: AddressType.WOTS, + tree, + keypair: leafIdx + }); + const roots = []; + const forsLeaf = setAddr({ keypairAddr: wotsAddr }); + const forsTreeAddr = setAddr({ keypairAddr: wotsAddr }); + const indices = messageToIndices(md); + const fors = []; + for (let i = 0; i < indices.length; i++) { + const idxOffset = i << A; + setAddr({ + type: AddressType.FORSPRF, + height: 0, + index: indices[i] + idxOffset + }, forsTreeAddr); + const prf = context.PRFaddr(forsTreeAddr); + setAddr({ type: AddressType.FORSTREE }, forsTreeAddr); + const { root: root2, authPath } = forsTreehash(context, indices[i], idxOffset, forsTreeAddr, forsLeaf); + roots.push(root2); + fors.push([prf, authPath]); + } + const forsPkAddr = setAddr({ + type: AddressType.FORSPK, + keypairAddr: wotsAddr + }); + const root = context.thashN(K, concatBytes(...roots), forsPkAddr); + const treeAddr = setAddr({ type: AddressType.HASHTREE }); + const wots = []; + for (let i = 0; i < D2; i++, tree >>= BigInt(TREE_HEIGHT)) { + setAddr({ tree, layer: i }, treeAddr); + setAddr({ subtreeAddr: treeAddr, keypair: leafIdx }, wotsAddr); + const { sigWots, sigAuth, root: r } = merkleSign(context, wotsAddr, treeAddr, leafIdx, root); + root.set(r); + cleanBytes(r); + wots.push([sigWots, sigAuth]); + leafIdx = Number(tree & getMaskBig(TREE_HEIGHT)); + } + context.clean(); + const SIG = sigCoder.encode([R, fors, wots]); + cleanBytes(R, random, treeAddr, wotsAddr, forsLeaf, forsTreeAddr, indices, roots); + return SIG; + }, + verify: (sig, msg, publicKey) => { + const [pkSeed, pubRoot] = publicCoder.decode(publicKey); + const [random, forsVec, wotsVec] = sigCoder.decode(sig); + const pk = publicKey; + if (sig.length !== sigCoder.bytesLen) + return false; + const context = getContext(pkSeed); + let { tree, leafIdx, md } = hashMessage(random, pk, msg, context); + const wotsAddr = setAddr({ + type: AddressType.WOTS, + tree, + keypair: leafIdx + }); + const roots = []; + const forsTreeAddr = setAddr({ + type: AddressType.FORSTREE, + keypairAddr: wotsAddr + }); + const indices = messageToIndices(md); + for (let i = 0; i < forsVec.length; i++) { + const [prf, authPath] = forsVec[i]; + const idxOffset = i << A; + setAddr({ height: 0, index: indices[i] + idxOffset }, forsTreeAddr); + const leaf = context.thash1(prf, forsTreeAddr); + roots.push(computeRoot(leaf, indices[i], idxOffset, authPath, A, context, forsTreeAddr)); + } + const forsPkAddr = setAddr({ + type: AddressType.FORSPK, + keypairAddr: wotsAddr + }); + let root = context.thashN(K, concatBytes(...roots), forsPkAddr); + const treeAddr = setAddr({ type: AddressType.HASHTREE }); + const wotsPkAddr = setAddr({ type: AddressType.WOTSPK }); + const wotsPk = new Uint8Array(WOTS_LEN * N3); + for (let i = 0; i < wotsVec.length; i++, tree >>= BigInt(TREE_HEIGHT)) { + const [wots, sigAuth] = wotsVec[i]; + setAddr({ tree, layer: i }, treeAddr); + setAddr({ subtreeAddr: treeAddr, keypair: leafIdx }, wotsAddr); + setAddr({ keypairAddr: wotsAddr }, wotsPkAddr); + const lengths = chainLengths(root); + for (let i2 = 0; i2 < WOTS_LEN; i2++) { + setAddr({ chain: i2 }, wotsAddr); + const steps = W - 1 - lengths[i2]; + const start = lengths[i2]; + const out = wotsPk.subarray(i2 * N3); + out.set(wots.subarray(i2 * N3, (i2 + 1) * N3)); + for (let j = start; j < start + steps && j < W; j++) { + setAddr({ hash: j }, wotsAddr); + out.set(context.thash1(out, wotsAddr)); + } + } + const leaf = context.thashN(WOTS_LEN, wotsPk, wotsPkAddr); + root = computeRoot(leaf, leafIdx, 0, sigAuth, TREE_HEIGHT, context, treeAddr); + leafIdx = Number(tree & getMaskBig(TREE_HEIGHT)); + } + return equalBytes(root, pubRoot); + } + }); + return Object.freeze({ + info: Object.freeze({ type: "slh-dsa" }), + internal, + securityLevel, + lengths: internal.lengths, + keygen: internal.keygen, + getPublicKey: internal.getPublicKey, + sign: (msg, secretKey, opts3 = {}) => { + validateSigOpts2(opts3); + const M = getMessage(msg, opts3.context); + const res = internal.sign(M, secretKey, opts3); + cleanBytes(M); + return res; + }, + verify: (sig, msg, publicKey, opts3 = {}) => { + validateVerOpts(opts3); + return internal.verify(sig, getMessage(msg, opts3.context), publicKey); + }, + prehash: (hash) => { + checkHash(hash, securityLevel); + const rawHash = hash; + return Object.freeze({ + info: Object.freeze({ type: "hashslh-dsa" }), + lengths: internal.lengths, + keygen: internal.keygen, + getPublicKey: internal.getPublicKey, + sign: (msg, secretKey, opts3 = {}) => { + validateSigOpts2(opts3); + const M = getMessagePrehash(rawHash, msg, opts3.context); + const res = internal.sign(M, secretKey, opts3); + cleanBytes(M); + return res; + }, + verify: (sig, msg, publicKey, opts3 = {}) => { + validateVerOpts(opts3); + return internal.verify(sig, getMessagePrehash(rawHash, msg, opts3.context), publicKey); + } + }); + } + }); +} +var genSha = (h0, h1) => (opts2) => (pub_seed, sk_seed) => { + const { N: N3 } = opts2; + const stats = { prf: 0, thash: 0, hmsg: 0, gen_message_random: 0, mgf1: 0 }; + const counterB = new Uint8Array(4); + const counterV = createView(counterB); + const h0ps = h0.create().update(pub_seed).update(new Uint8Array(h0.blockLen - N3)); + const h1ps = h1.create().update(pub_seed).update(new Uint8Array(h1.blockLen - N3)); + const h0tmp = h0ps.clone(); + const h1tmp = h1ps.clone(); + function mgf1(seed, length, hash) { + stats.mgf1++; + const out = new Uint8Array(Math.ceil(length / hash.outputLen) * hash.outputLen); + if (length > 2 ** 32) + throw new Error("mask too long"); + for (let counter = 0, o = out; o.length; counter++) { + counterV.setUint32(0, counter, false); + hash.create().update(seed).update(counterB).digestInto(o); + o = o.subarray(hash.outputLen); + } + cleanBytes(out.subarray(length)); + return out.subarray(0, length); + } + const thash = (_, h, hTmp) => (blocks, input, addr) => { + stats.thash++; + const d = h._cloneInto(hTmp).update(addr).update(input.subarray(0, blocks * N3)).digest(); + return d.subarray(0, N3); + }; + return { + PRFaddr: (addr) => { + if (!sk_seed) + throw new Error("No sk seed"); + stats.prf++; + const res = h0ps._cloneInto(h0tmp).update(addr).update(sk_seed).digest().subarray(0, N3); + return res; + }, + PRFmsg: (skPRF, random, msg) => { + stats.gen_message_random++; + return hmac.create(h1, skPRF).update(random).update(msg).digest().subarray(0, N3); + }, + Hmsg: (R, pk, m, outLen) => { + stats.hmsg++; + const seed = concatBytes(R.subarray(0, N3), pk.subarray(0, N3), h1.create().update(R.subarray(0, N3)).update(pk).update(m).digest()); + return mgf1(seed, outLen, h1); + }, + thash1: thash(h0, h0ps, h0tmp).bind(null, 1), + thashN: thash(h1, h1ps, h1tmp), + clean: () => { + h0ps.destroy(); + h1ps.destroy(); + h0tmp.destroy(); + h1tmp.destroy(); + } + }; +}; +var SHA256_SIMPLE = /* @__PURE__ */ (() => ({ + isCompressed: true, + getContext: genSha(sha256, sha256) +}))(); +var slh_dsa_sha2_128s = /* @__PURE__ */ (() => gen(PARAMS2["128s"], SHA256_SIMPLE))(); + +// node_modules/@noble/ciphers/utils.js +function isBytes4(a) { + return a instanceof Uint8Array || ArrayBuffer.isView(a) && a.constructor.name === "Uint8Array" && "BYTES_PER_ELEMENT" in a && a.BYTES_PER_ELEMENT === 1; +} +function abool2(b) { + if (typeof b !== "boolean") + throw new TypeError(`boolean expected, not ${b}`); +} +function anumber4(n) { + if (typeof n !== "number") + throw new TypeError("number expected, got " + typeof n); + if (!Number.isSafeInteger(n) || n < 0) + throw new RangeError("positive integer expected, got " + n); +} +function abytes3(value, length, title = "") { + const bytes = isBytes4(value); + const len = value?.length; + const needsLen = length !== void 0; + if (!bytes || needsLen && len !== length) { + const prefix = title && `"${title}" `; + const ofLen = needsLen ? ` of length ${length}` : ""; + const got = bytes ? `length=${len}` : `type=${typeof value}`; + const message = prefix + "expected Uint8Array" + ofLen + ", got " + got; + if (!bytes) + throw new TypeError(message); + throw new RangeError(message); + } + return value; +} +function u82(arr) { + return new Uint8Array(arr.buffer, arr.byteOffset, arr.byteLength); +} +function u322(arr) { + return new Uint32Array(arr.buffer, arr.byteOffset, Math.floor(arr.byteLength / 4)); +} +function clean2(...arrays) { + for (let i = 0; i < arrays.length; i++) { + arrays[i].fill(0); + } +} +var isLE2 = /* @__PURE__ */ (() => new Uint8Array(new Uint32Array([287454020]).buffer)[0] === 68)(); +var byteSwap2 = (word) => word << 24 & 4278190080 | word << 8 & 16711680 | word >>> 8 & 65280 | word >>> 24 & 255; +var swap8IfBE = isLE2 ? (n) => n : (n) => byteSwap2(n) >>> 0; +var byteSwap322 = (arr) => { + for (let i = 0; i < arr.length; i++) + arr[i] = byteSwap2(arr[i]); + return arr; +}; +var swap32IfBE2 = isLE2 ? (u) => u : byteSwap322; +function overlapBytes(a, b) { + if (!a.byteLength || !b.byteLength) + return false; + return a.buffer === b.buffer && // best we can do, may fail with an obscure Proxy + a.byteOffset < b.byteOffset + b.byteLength && // a starts before b end + b.byteOffset < a.byteOffset + a.byteLength; +} +function complexOverlapBytes(input, output) { + if (overlapBytes(input, output) && input.byteOffset < output.byteOffset) + throw new Error("complex overlap of input and output is not supported"); +} +function checkOpts2(defaults, opts2) { + if (opts2 == null || typeof opts2 !== "object") + throw new Error("options must be defined"); + const merged = Object.assign(defaults, opts2); + return merged; +} +var wrapCipher = /* @__NO_SIDE_EFFECTS__ */ (params, constructor) => { + function wrappedCipher(key, ...args) { + abytes3(key, void 0, "key"); + if (params.nonceLength !== void 0) { + const nonce = args[0]; + abytes3(nonce, params.varSizeNonce ? void 0 : params.nonceLength, "nonce"); + } + const tagl = params.tagLength; + if (tagl && args[1] !== void 0) + abytes3(args[1], void 0, "AAD"); + const cipher = constructor(key, ...args); + const checkOutput = (fnLength, output) => { + if (output !== void 0) { + if (fnLength !== 2) + throw new Error("cipher output not supported"); + abytes3(output, void 0, "output"); + } + }; + let called = false; + const wrCipher = { + encrypt(data, output) { + if (called) + throw new Error("cannot encrypt() twice with same key + nonce"); + called = true; + abytes3(data); + checkOutput(cipher.encrypt.length, output); + return cipher.encrypt(data, output); + }, + decrypt(data, output) { + abytes3(data); + if (tagl && data.length < tagl) + throw new Error('"ciphertext" expected length bigger than tagLength=' + tagl); + checkOutput(cipher.decrypt.length, output); + return cipher.decrypt(data, output); + } + }; + return wrCipher; + } + Object.assign(wrappedCipher, params); + return wrappedCipher; +}; +function getOutput(expectedLength, out, onlyAligned = true) { + if (out === void 0) + return new Uint8Array(expectedLength); + abytes3(out, void 0, "output"); + if (out.length !== expectedLength) + throw new Error('"output" expected Uint8Array of length ' + expectedLength + ", got: " + out.length); + if (onlyAligned && !isAligned32(out)) + throw new Error("invalid output, must be aligned"); + return out; +} +function isAligned32(bytes) { + return bytes.byteOffset % 4 === 0; +} +function copyBytes3(bytes) { + return Uint8Array.from(abytes3(bytes)); +} + +// node_modules/@noble/ciphers/aes.js +var BLOCK_SIZE = 16; +var BLOCK_SIZE32 = 4; +var POLY = 283; +function validateKeyLength(key) { + if (![16, 24, 32].includes(key.length)) + throw new Error('"aes key" expected Uint8Array of length 16/24/32, got length=' + key.length); +} +function mul2(n) { + return n << 1 ^ POLY & -(n >> 7); +} +function mul(a, b) { + let res = 0; + for (; b > 0; b >>= 1) { + res ^= a & -(b & 1); + a = mul2(a); + } + return res; +} +var incBytes = (data, isLE3, carry = 1) => { + if (!Number.isSafeInteger(carry) || carry > 4294967040) + throw new Error("incBytes: wrong carry " + carry); + abytes3(data); + for (let i = 0; i < data.length; i++) { + const pos = !isLE3 ? data.length - 1 - i : i; + carry = carry + (data[pos] & 255) | 0; + data[pos] = carry & 255; + carry >>>= 8; + } +}; +var sbox = /* @__PURE__ */ (() => { + const t = new Uint8Array(256); + for (let i = 0, x = 1; i < 256; i++, x ^= mul2(x)) + t[i] = x; + const box = new Uint8Array(256); + box[0] = 99; + for (let i = 0; i < 255; i++) { + let x = t[255 - i]; + x |= x << 8; + box[t[i]] = (x ^ x >> 4 ^ x >> 5 ^ x >> 6 ^ x >> 7 ^ 99) & 255; + } + clean2(t); + return box; +})(); +var rotr32_8 = (n) => n << 24 | n >>> 8; +var rotl32_8 = (n) => n << 8 | n >>> 24; +function genTtable(sbox2, fn) { + if (sbox2.length !== 256) + throw new Error("Wrong sbox length"); + const T0 = new Uint32Array(256).map((_, j) => fn(sbox2[j])); + const T1 = T0.map(rotl32_8); + const T2 = T1.map(rotl32_8); + const T3 = T2.map(rotl32_8); + const T01 = new Uint32Array(256 * 256); + const T23 = new Uint32Array(256 * 256); + const sbox22 = new Uint16Array(256 * 256); + for (let i = 0; i < 256; i++) { + for (let j = 0; j < 256; j++) { + const idx = i * 256 + j; + T01[idx] = T0[i] ^ T1[j]; + T23[idx] = T2[i] ^ T3[j]; + sbox22[idx] = sbox2[i] << 8 | sbox2[j]; + } + } + return { sbox: sbox2, sbox2: sbox22, T0, T1, T2, T3, T01, T23 }; +} +var tableEncoding = /* @__PURE__ */ genTtable(sbox, (s) => mul(s, 3) << 24 | s << 16 | s << 8 | mul(s, 2)); +var xPowers = /* @__PURE__ */ (() => { + const p = new Uint8Array(16); + for (let i = 0, x = 1; i < 16; i++, x = mul2(x)) + p[i] = x; + return p; +})(); +function expandKeyLE(key) { + abytes3(key); + const len = key.length; + validateKeyLength(key); + const { sbox2 } = tableEncoding; + const toClean = []; + if (!isLE2 || !isAligned32(key)) + toClean.push(key = copyBytes3(key)); + const k32 = swap32IfBE2(u322(key)); + const Nk = k32.length; + const subByte = (n) => applySbox(sbox2, n, n, n, n); + const xk = new Uint32Array(len + 28); + xk.set(k32); + for (let i = Nk; i < xk.length; i++) { + let t = xk[i - 1]; + if (i % Nk === 0) + t = subByte(rotr32_8(t)) ^ xPowers[i / Nk - 1]; + else if (Nk > 6 && i % Nk === 4) + t = subByte(t); + xk[i] = xk[i - Nk] ^ t; + } + clean2(...toClean); + return xk; +} +function apply0123(T01, T23, s0, s1, s2, s3) { + return T01[s0 << 8 & 65280 | s1 >>> 8 & 255] ^ T23[s2 >>> 8 & 65280 | s3 >>> 24 & 255]; +} +function applySbox(sbox2, s0, s1, s2, s3) { + return sbox2[s0 & 255 | s1 & 65280] | sbox2[s2 >>> 16 & 255 | s3 >>> 16 & 65280] << 16; +} +function encrypt(xk, s0, s1, s2, s3) { + const { sbox2, T01, T23 } = tableEncoding; + let k = 0; + s0 ^= xk[k++], s1 ^= xk[k++], s2 ^= xk[k++], s3 ^= xk[k++]; + const rounds = xk.length / 4 - 2; + for (let i = 0; i < rounds; i++) { + const t02 = xk[k++] ^ apply0123(T01, T23, s0, s1, s2, s3); + const t12 = xk[k++] ^ apply0123(T01, T23, s1, s2, s3, s0); + const t22 = xk[k++] ^ apply0123(T01, T23, s2, s3, s0, s1); + const t32 = xk[k++] ^ apply0123(T01, T23, s3, s0, s1, s2); + s0 = t02, s1 = t12, s2 = t22, s3 = t32; + } + const t0 = xk[k++] ^ applySbox(sbox2, s0, s1, s2, s3); + const t1 = xk[k++] ^ applySbox(sbox2, s1, s2, s3, s0); + const t2 = xk[k++] ^ applySbox(sbox2, s2, s3, s0, s1); + const t3 = xk[k++] ^ applySbox(sbox2, s3, s0, s1, s2); + return { s0: t0, s1: t1, s2: t2, s3: t3 }; +} +function ctrCounter(xk, nonce, src, dst) { + abytes3(nonce, BLOCK_SIZE, "nonce"); + abytes3(src); + const srcLen = src.length; + dst = getOutput(srcLen, dst); + complexOverlapBytes(src, dst); + const ctr2 = nonce; + const c32 = u322(ctr2); + const src32 = u322(src); + const dst32 = u322(dst); + let { s0, s1, s2, s3 } = encrypt(xk, swap8IfBE(c32[0]), swap8IfBE(c32[1]), swap8IfBE(c32[2]), swap8IfBE(c32[3])); + for (let i = 0; i + 4 <= src32.length; i += 4) { + dst32[i + 0] = src32[i + 0] ^ swap8IfBE(s0); + dst32[i + 1] = src32[i + 1] ^ swap8IfBE(s1); + dst32[i + 2] = src32[i + 2] ^ swap8IfBE(s2); + dst32[i + 3] = src32[i + 3] ^ swap8IfBE(s3); + incBytes(ctr2, false, 1); + ({ s0, s1, s2, s3 } = encrypt(xk, swap8IfBE(c32[0]), swap8IfBE(c32[1]), swap8IfBE(c32[2]), swap8IfBE(c32[3]))); + } + const start = BLOCK_SIZE * Math.floor(src32.length / BLOCK_SIZE32); + if (start < srcLen) { + const b32 = new Uint32Array([s0, s1, s2, s3]); + swap32IfBE2(b32); + const buf = u82(b32); + for (let i = start, pos = 0; i < srcLen; i++, pos++) + dst[i] = src[i] ^ buf[pos]; + clean2(b32); + } + return dst; +} +var ctr = /* @__PURE__ */ wrapCipher({ blockSize: 16, nonceLength: 16 }, function aesctr(key, nonce) { + function processCtr(buf, dst) { + abytes3(buf); + if (dst !== void 0) { + abytes3(dst); + if (!isAligned32(dst)) + throw new Error("unaligned destination"); + } + const xk = expandKeyLE(key); + const n = copyBytes3(nonce); + const toClean = [xk, n]; + if (!isAligned32(buf)) + toClean.push(buf = copyBytes3(buf)); + const out = ctrCounter(xk, n, buf, dst); + clean2(...toClean); + return out; + } + return { + encrypt: (plaintext, dst) => processCtr(plaintext, dst), + decrypt: (ciphertext, dst) => processCtr(ciphertext, dst) + }; +}); +var _AesCtrDRBG = class { + constructor(keyLen, seed, personalization) { + __publicField(this, "blockLen"); + __publicField(this, "key"); + __publicField(this, "nonce"); + __publicField(this, "state"); + __publicField(this, "reseedCnt"); + this.blockLen = ctr.blockSize; + const keyLenBytes = keyLen / 8; + const nonceLen = 16; + this.state = new Uint8Array(keyLenBytes + nonceLen); + this.key = this.state.subarray(0, keyLenBytes); + this.nonce = this.state.subarray(keyLenBytes, keyLenBytes + nonceLen); + this.reseedCnt = 1; + incBytes(this.nonce, false, 1); + this.addEntropy(seed, personalization); + } + update(data) { + ctr(this.key, this.nonce).encrypt(new Uint8Array(this.state.length), this.state); + if (data) { + abytes3(data); + for (let i = 0; i < data.length; i++) + this.state[i] ^= data[i]; + } + incBytes(this.nonce, false, 1); + } + // Optional `info` is additional input XORed into the reseed block and is + // limited to the internal state width. + addEntropy(seed, info) { + abytes3(seed, this.state.length, "seed"); + const _seed = seed.slice(); + if (info) { + abytes3(info); + if (info.length > _seed.length) + throw new Error("info length is too big"); + for (let i = 0; i < info.length; i++) + _seed[i] ^= info[i]; + } + this.update(_seed); + _seed.fill(0); + this.reseedCnt = 1; + } + // Optional `info` is additional input for the pre/post-update steps; bytes + // SP 800-90A Rev. 1 CTR_DRBG without a derivation function limits + // additional_input to seedlen, which is exactly this internal state width. + randomBytes(len, info) { + anumber4(len); + if (len > 2 ** 16) + throw new Error("requested output is too big"); + if (this.reseedCnt > 2 ** 48) + throw new Error("entropy exhausted"); + if (info) { + abytes3(info); + if (info.length > this.state.length) + throw new Error("info length is too big"); + this.update(info); + } + const res = new Uint8Array(len); + ctr(this.key, this.nonce).encrypt(res, res); + incBytes(this.nonce, false, Math.ceil(len / this.blockLen)); + this.update(info); + this.reseedCnt++; + return res; + } + // Zeroes the current state and resets the counter, but does not make the + // instance unusable: later calls continue from the zeroed state. + clean() { + this.state.fill(0); + this.reseedCnt = 0; + } +}; +var createAesDrbg = (keyLen) => { + return (seed, personalization = void 0) => new _AesCtrDRBG(keyLen, seed, personalization); +}; +var rngAesCtrDrbg256 = /* @__PURE__ */ createAesDrbg(256); + +// node_modules/@noble/ciphers/_arx.js +var encodeStr = (str) => Uint8Array.from(str.split(""), (c) => c.charCodeAt(0)); +var sigma16_32 = /* @__PURE__ */ (() => swap32IfBE2(u322(encodeStr("expand 16-byte k"))))(); +var sigma32_32 = /* @__PURE__ */ (() => swap32IfBE2(u322(encodeStr("expand 32-byte k"))))(); +function rotl2(a, b) { + return a << b | a >>> 32 - b; +} +var BLOCK_LEN = 64; +var BLOCK_LEN32 = 16; +var MAX_COUNTER = /* @__PURE__ */ (() => 2 ** 32 - 1)(); +var U32_EMPTY = /* @__PURE__ */ Uint32Array.of(); +function runCipher(core, sigma, key, nonce, data, output, counter, rounds) { + const len = data.length; + const block = new Uint8Array(BLOCK_LEN); + const b32 = u322(block); + const isAligned = isLE2 && isAligned32(data) && isAligned32(output); + const d32 = isAligned ? u322(data) : U32_EMPTY; + const o32 = isAligned ? u322(output) : U32_EMPTY; + if (!isLE2) { + for (let pos = 0; pos < len; counter++) { + core(sigma, key, nonce, b32, counter, rounds); + swap32IfBE2(b32); + if (counter >= MAX_COUNTER) + throw new Error("arx: counter overflow"); + const take = Math.min(BLOCK_LEN, len - pos); + for (let j = 0, posj; j < take; j++) { + posj = pos + j; + output[posj] = data[posj] ^ block[j]; + } + pos += take; + } + return; + } + for (let pos = 0; pos < len; counter++) { + core(sigma, key, nonce, b32, counter, rounds); + if (counter >= MAX_COUNTER) + throw new Error("arx: counter overflow"); + const take = Math.min(BLOCK_LEN, len - pos); + if (isAligned && take === BLOCK_LEN) { + const pos32 = pos / 4; + if (pos % 4 !== 0) + throw new Error("arx: invalid block position"); + for (let j = 0, posj; j < BLOCK_LEN32; j++) { + posj = pos32 + j; + o32[posj] = d32[posj] ^ b32[j]; + } + pos += BLOCK_LEN; + continue; + } + for (let j = 0, posj; j < take; j++) { + posj = pos + j; + output[posj] = data[posj] ^ block[j]; + } + pos += take; + } +} +function createCipher(core, opts2) { + const { allowShortKeys, extendNonceFn, counterLength, counterRight, rounds } = checkOpts2({ allowShortKeys: false, counterLength: 8, counterRight: false, rounds: 20 }, opts2); + if (typeof core !== "function") + throw new Error("core must be a function"); + anumber4(counterLength); + anumber4(rounds); + abool2(counterRight); + abool2(allowShortKeys); + return (key, nonce, data, output, counter = 0) => { + abytes3(key, void 0, "key"); + abytes3(nonce, void 0, "nonce"); + abytes3(data, void 0, "data"); + const len = data.length; + output = getOutput(len, output, false); + anumber4(counter); + if (counter < 0 || counter >= MAX_COUNTER) + throw new Error("arx: counter overflow"); + const toClean = []; + let l = key.length; + let k; + let sigma; + if (l === 32) { + toClean.push(k = copyBytes3(key)); + sigma = sigma32_32; + } else if (l === 16 && allowShortKeys) { + k = new Uint8Array(32); + k.set(key); + k.set(key, 16); + sigma = sigma16_32; + toClean.push(k); + } else { + abytes3(key, 32, "arx key"); + throw new Error("invalid key size"); + } + if (!isLE2 || !isAligned32(nonce)) + toClean.push(nonce = copyBytes3(nonce)); + let k32 = u322(k); + if (extendNonceFn) { + if (nonce.length !== 24) + throw new Error(`arx: extended nonce must be 24 bytes`); + const n16 = nonce.subarray(0, 16); + if (isLE2) + extendNonceFn(sigma, k32, u322(n16), k32); + else { + const sigmaRaw = swap32IfBE2(Uint32Array.from(sigma)); + extendNonceFn(sigmaRaw, k32, u322(n16), k32); + clean2(sigmaRaw); + swap32IfBE2(k32); + } + nonce = nonce.subarray(16); + } else if (!isLE2) + swap32IfBE2(k32); + const nonceNcLen = 16 - counterLength; + if (nonceNcLen !== nonce.length) + throw new Error(`arx: nonce must be ${nonceNcLen} or 16 bytes`); + if (nonceNcLen !== 12) { + const nc = new Uint8Array(12); + nc.set(nonce, counterRight ? 0 : 12 - nonce.length); + nonce = nc; + toClean.push(nonce); + } + const n32 = swap32IfBE2(u322(nonce)); + try { + runCipher(core, sigma, k32, n32, data, output, counter, rounds); + return output; + } finally { + clean2(...toClean); + } + }; +} + +// node_modules/@noble/ciphers/chacha.js +function chachaCore(s, k, n, out, cnt, rounds = 20) { + let y00 = s[0], y01 = s[1], y02 = s[2], y03 = s[3], y04 = k[0], y05 = k[1], y06 = k[2], y07 = k[3], y08 = k[4], y09 = k[5], y10 = k[6], y11 = k[7], y12 = cnt, y13 = n[0], y14 = n[1], y15 = n[2]; + let x00 = y00, x01 = y01, x02 = y02, x03 = y03, x04 = y04, x05 = y05, x06 = y06, x07 = y07, x08 = y08, x09 = y09, x10 = y10, x11 = y11, x12 = y12, x13 = y13, x14 = y14, x15 = y15; + for (let r = 0; r < rounds; r += 2) { + x00 = x00 + x04 | 0; + x12 = rotl2(x12 ^ x00, 16); + x08 = x08 + x12 | 0; + x04 = rotl2(x04 ^ x08, 12); + x00 = x00 + x04 | 0; + x12 = rotl2(x12 ^ x00, 8); + x08 = x08 + x12 | 0; + x04 = rotl2(x04 ^ x08, 7); + x01 = x01 + x05 | 0; + x13 = rotl2(x13 ^ x01, 16); + x09 = x09 + x13 | 0; + x05 = rotl2(x05 ^ x09, 12); + x01 = x01 + x05 | 0; + x13 = rotl2(x13 ^ x01, 8); + x09 = x09 + x13 | 0; + x05 = rotl2(x05 ^ x09, 7); + x02 = x02 + x06 | 0; + x14 = rotl2(x14 ^ x02, 16); + x10 = x10 + x14 | 0; + x06 = rotl2(x06 ^ x10, 12); + x02 = x02 + x06 | 0; + x14 = rotl2(x14 ^ x02, 8); + x10 = x10 + x14 | 0; + x06 = rotl2(x06 ^ x10, 7); + x03 = x03 + x07 | 0; + x15 = rotl2(x15 ^ x03, 16); + x11 = x11 + x15 | 0; + x07 = rotl2(x07 ^ x11, 12); + x03 = x03 + x07 | 0; + x15 = rotl2(x15 ^ x03, 8); + x11 = x11 + x15 | 0; + x07 = rotl2(x07 ^ x11, 7); + x00 = x00 + x05 | 0; + x15 = rotl2(x15 ^ x00, 16); + x10 = x10 + x15 | 0; + x05 = rotl2(x05 ^ x10, 12); + x00 = x00 + x05 | 0; + x15 = rotl2(x15 ^ x00, 8); + x10 = x10 + x15 | 0; + x05 = rotl2(x05 ^ x10, 7); + x01 = x01 + x06 | 0; + x12 = rotl2(x12 ^ x01, 16); + x11 = x11 + x12 | 0; + x06 = rotl2(x06 ^ x11, 12); + x01 = x01 + x06 | 0; + x12 = rotl2(x12 ^ x01, 8); + x11 = x11 + x12 | 0; + x06 = rotl2(x06 ^ x11, 7); + x02 = x02 + x07 | 0; + x13 = rotl2(x13 ^ x02, 16); + x08 = x08 + x13 | 0; + x07 = rotl2(x07 ^ x08, 12); + x02 = x02 + x07 | 0; + x13 = rotl2(x13 ^ x02, 8); + x08 = x08 + x13 | 0; + x07 = rotl2(x07 ^ x08, 7); + x03 = x03 + x04 | 0; + x14 = rotl2(x14 ^ x03, 16); + x09 = x09 + x14 | 0; + x04 = rotl2(x04 ^ x09, 12); + x03 = x03 + x04 | 0; + x14 = rotl2(x14 ^ x03, 8); + x09 = x09 + x14 | 0; + x04 = rotl2(x04 ^ x09, 7); + } + let oi = 0; + out[oi++] = y00 + x00 | 0; + out[oi++] = y01 + x01 | 0; + out[oi++] = y02 + x02 | 0; + out[oi++] = y03 + x03 | 0; + out[oi++] = y04 + x04 | 0; + out[oi++] = y05 + x05 | 0; + out[oi++] = y06 + x06 | 0; + out[oi++] = y07 + x07 | 0; + out[oi++] = y08 + x08 | 0; + out[oi++] = y09 + x09 | 0; + out[oi++] = y10 + x10 | 0; + out[oi++] = y11 + x11 | 0; + out[oi++] = y12 + x12 | 0; + out[oi++] = y13 + x13 | 0; + out[oi++] = y14 + x14 | 0; + out[oi++] = y15 + x15 | 0; +} +var chacha20 = /* @__PURE__ */ createCipher(chachaCore, { + counterRight: false, + counterLength: 4, + allowShortKeys: false +}); + +// node_modules/@noble/post-quantum/falcon.js +var bitsCoderMSB = (newPoly2, N3, d, c) => { + const mask = getMask(d); + const bytesLen = d * (N3 / 8); + return { + bytesLen, + encode: (poly) => { + if (poly.length !== N3) + throw new Error(`wrong length: expected ${N3}, got ${poly.length}`); + const r = new Uint8Array(bytesLen); + for (let i = 0, buf = 0, bufLen = 0, pos = 0; i < poly.length; i++) { + buf = buf << d | c.encode(poly[i]) & mask; + bufLen += d; + for (; bufLen >= 8; bufLen -= 8) + r[pos++] = buf >>> bufLen - 8 & 255; + } + return r; + }, + decode: (bytes) => { + const r = newPoly2(N3); + for (let i = 0, buf = 0, bufLen = 0, pos = 0; i < bytes.length; i++) { + buf = buf << 8 | bytes[i]; + bufLen += 8; + for (; bufLen >= d; bufLen -= d) + r[pos++] = c.decode(buf >>> bufLen - d & mask); + } + return r; + } + }; +}; +var headerCoder = (tag, restCoder) => { + const coder = restCoder; + return { + bytesLen: 1 + coder.bytesLen, + encode(value) { + const body = coder.encode(value); + const out = new Uint8Array(1 + body.length); + out[0] = tag; + out.set(body, 1); + cleanBytes(body); + return out; + }, + decode(data) { + if (data[0] !== tag) + throw new Error(`wrong tag: expected ${tag}, got 0x${data[0]}`); + return coder.decode(data.subarray(1)); + } + }; +}; +var compCoder = (n) => { + const LIMIT = 2047; + return { + encode(data) { + if (data.length !== n) + throw new Error("wrong length"); + const res = []; + let buf = 0; + let bufLen = 0; + const writeBits = (n2, v) => { + bufLen += n2; + buf = buf << n2 | v; + for (; bufLen >= 8; buf &= getMask(bufLen)) { + bufLen -= 8; + res.push(buf >>> bufLen & 255); + } + }; + for (let i = 0; i < n; i++) { + let v = data[i]; + if (!Number.isInteger(v) || v < -LIMIT || v > LIMIT) + throw new Error(`data[${i}]=${v} out of range`); + const sign = v < 0 ? 1 : 0; + v = Math.abs(v); + writeBits(1, sign); + writeBits(7, v & 127); + writeBits((v >>> 7) + 1, 1); + } + if (bufLen > 0) + res.push(buf << 8 - bufLen & 255); + return new Uint8Array(res); + }, + decode(data) { + const res = new Int16Array(n); + let buf = 0; + let bufLen = 0; + let pos = 0; + const readBits = (n2) => { + for (; bufLen < n2 && pos < data.length; bufLen += 8) + buf = buf << 8 | data[pos++]; + if (bufLen < n2) + throw new Error(`end of buffer: len=${bufLen} buf=${buf} lastByte=${data[pos]}`); + bufLen -= n2; + const val = buf >>> bufLen; + buf &= getMask(bufLen); + return val; + }; + for (let resPos = 0; resPos < n; resPos++) { + const sign = readBits(1); + const low = readBits(7); + let high = 0; + for (; !readBits(1); high++) + ; + const v = low | high << 7; + if (sign && v === 0) + throw new Error("negative zero encoding"); + if (v > LIMIT) + throw new Error(`limit: ${v} > ${LIMIT}`); + res[resPos] = sign ? -v : v; + } + if (buf) + throw new Error("non-empty accumulator"); + return res; + } + }; +}; +var pad = (len) => ({ + encode(data) { + const res = new Uint8Array(len); + res.set(data); + return res; + }, + decode(data) { + let end = data.length; + while (end > 0 && data[end - 1] === 0) + end--; + return data.subarray(0, end); + } +}); +var cleanCPoly = (...list) => { + for (const p of list) { + for (let i = 0; i < p.length; i++) { + p[i].re = 0; + p[i].im = 0; + } + } +}; +function getComplex(field) { + const F3 = field; + return { + lift: (x) => { + if (x.re !== void 0 && x.im !== void 0) + return x; + return { re: x, im: F3.ZERO }; + }, + add: (a, b) => ({ + re: F3.add(a.re, b.re), + im: F3.add(a.im, b.im) + }), + sub: (a, b) => ({ + re: F3.sub(a.re, b.re), + im: F3.sub(a.im, b.im) + }), + mul: (a, b) => ({ + re: F3.sub(F3.mul(a.re, b.re), F3.mul(a.im, b.im)), + im: F3.add(F3.mul(a.re, b.im), F3.mul(a.im, b.re)) + }), + div: (a, b) => { + const denom = F3.add(F3.mul(b.re, b.re), F3.mul(b.im, b.im)); + return { + re: F3.div(F3.add(F3.mul(a.re, b.re), F3.mul(a.im, b.im)), denom), + im: F3.div(F3.sub(F3.mul(a.im, b.re), F3.mul(a.re, b.im)), denom) + }; + }, + neg: (a) => ({ re: F3.neg(a.re), im: F3.neg(a.im) }), + conj: (a) => ({ re: a.re, im: F3.neg(a.im) }), + scale: (a, x) => ({ + re: F3.mul(a.re, x), + im: F3.mul(a.im, x) + }), + // a.re * a.re + a.im * a.im + b.re * b.re + b.im * b.im; + magSqSum: (a, b) => F3.add(F3.add(F3.add(F3.mul(a.re, a.re), F3.mul(a.im, a.im)), F3.mul(b.re, b.re)), F3.mul(b.im, b.im)), + eql: (a, b) => F3.eql(a.re, b.re) && F3.eql(a.im, b.im), + clone: (a) => ({ re: a.re, im: a.im }), + inv: () => { + throw new Error("not implemented"); + } + }; +} +var ComplexArr = { + decode(lst) { + const N3 = lst.length; + const hn = N3 >> 1; + const len = lst.length; + if (len === 0) + return []; + if (len % 2 !== 0) + throw new Error("Array length must be even to pair real and imaginary parts."); + const res = []; + for (let i = 0; i < hn; i++) { + res.push({ re: lst[i], im: lst[i + hn] }); + } + return res; + }, + encode(lst) { + const re = []; + const im = []; + for (const i of lst) { + re.push(i.re); + im.push(i.im); + } + return [...re, ...im]; + } +}; +var ComplexArrInterleaved = { + decode(lst) { + const len = lst.length; + if (len === 0) + return []; + if (len % 2 !== 0) + throw new Error("Array length must be even to pair real and imaginary parts."); + const res = []; + for (let i = 0; i < len; i += 2) { + res.push({ re: lst[i], im: lst[i + 1] }); + } + return res; + }, + encode(lst) { + const res = []; + for (const complexNum of lst) { + res.push(complexNum.re); + res.push(complexNum.im); + } + return res; + } +}; +var u8f = (arr) => new Uint8Array(arr.buffer, arr.byteOffset, arr.byteLength); +var f64a = (arr) => new Float64Array(baswap64If(Uint8Array.from(arr.subarray(0, Math.floor(arr.byteLength / 8) * 8))).buffer); +var Float = /* @__PURE__ */ Object.freeze({ + encode(n) { + const bytes = new Uint8Array(8); + const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength); + view.setFloat64(0, n, false); + return bytesToHex(bytes); + }, + decode(s) { + const bytes = hexToBytes(s); + const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength); + return view.getFloat64(0, false); + } +}); +var f64b = (n) => Float.decode(numberToHexUnpadded(n)); +var EMPTY_CHACHA20_BLOCK = /* @__PURE__ */ new Uint8Array(64); +var NONCELEN = 40; +var Q2 = 12289; +var Qhalf = Q2 >> 1; +var QBig = BigInt(Q2); +var R2 = 10952; +var Q0I = 12287; +var F_INV_Q = 1 / Q2; +var F_MINUS_INV_Q = -F_INV_Q; +var MAX_BL_SMALL = [1, 1, 2, 2, 4, 7, 14, 27, 53, 106, 209]; +var MAX_BL_LARGE = [2, 2, 5, 7, 12, 21, 40, 78, 157, 308]; +var BNORM_MAX = f64b(BigInt("4670353323383631276")); +var BITLENGTH = [ + { avg: 4, std: 0 }, + { avg: 11, std: 1 }, + { avg: 24, std: 1 }, + { avg: 50, std: 1 }, + { avg: 102, std: 1 }, + { avg: 202, std: 2 }, + { avg: 401, std: 4 }, + { avg: 794, std: 5 }, + { avg: 1577, std: 8 }, + { avg: 3138, std: 13 }, + { avg: 6308, std: 25 } +]; +var gauss_1024_12289 = [ + 1283868770400643928n, + 6416574995475331444n, + 4078260278032692663n, + 2353523259288686585n, + 1227179971273316331n, + 575931623374121527n, + 242543240509105209n, + 91437049221049666n, + 30799446349977173n, + 9255276791179340n, + 2478152334826140n, + 590642893610164n, + 125206034929641n, + 23590435911403n, + 3948334035941n, + 586753615614n, + 77391054539n, + 9056793210n, + 940121950n, + 86539696n, + 7062824n, + 510971n, + 32764n, + 1862n, + 94n, + 4n, + 0n +]; +var INV_SIGMA = /* @__PURE__ */ Object.freeze([ + 0, + // unused + f64b(BigInt("4574611497772390042")), + f64b(BigInt("4574501679055810265")), + f64b(BigInt("4574396282908341804")), + f64b(BigInt("4574245855758572086")), + f64b(BigInt("4574103865040221165")), + f64b(BigInt("4573969550563515544")), + f64b(BigInt("4573842244705920822")), + f64b(BigInt("4573721358406441454")), + f64b(BigInt("4573606369665796042")), + f64b(BigInt("4573496814039276259")) +]); +var SIGMA_MIN = /* @__PURE__ */ Object.freeze([ + 0, + // unused + f64b(BigInt("4607707126469777035")), + f64b(BigInt("4607777455861499430")), + f64b(BigInt("4607846828256951418")), + f64b(BigInt("4607949175006100261")), + f64b(BigInt("4608049571757433526")), + f64b(BigInt("4608148125896792003")), + f64b(BigInt("4608244935301382692")), + f64b(BigInt("4608340089478362016")), + f64b(BigInt("4608433670533905013")), + f64b(BigInt("4608525754002622308")) +]); +var GAUSS0 = new Uint32Array([ + 10745844, + 3068844, + 3741698, + 5559083, + 1580863, + 8248194, + 2260429, + 13669192, + 2736639, + 708981, + 4421575, + 10046180, + 169348, + 7122675, + 4136815, + 30538, + 13063405, + 7650655, + 4132, + 14505003, + 7826148, + 417, + 16768101, + 11363290, + 31, + 8444042, + 8086568, + 1, + 12844466, + 265321, + 0, + 1232676, + 13644283, + 0, + 38047, + 9111839, + 0, + 870, + 6138264, + 0, + 14, + 12545723, + 0, + 0, + 3104126, + 0, + 0, + 28824, + 0, + 0, + 198, + 0, + 0, + 1 +]); +var L2BOUND = [ + 0, + // unused + 101498, + 208714, + 428865, + 892039, + 1852696, + 3842630, + 7959734, + 16468416, + 34034726, + 70265242 +]; +var COMPLEX_ROOTS = /* @__PURE__ */ (() => { + const roots = f64a(hexToBytes("000000000000000000000000000000000000000000000080000000000000f03fcd3b7f669ea0e63fcd3b7f669ea0e63fcd3b7f669ea0e6bfcd3b7f669ea0e63f468d32cf6b90ed3f63a9aea6e27dd83f63a9aea6e27dd8bf468d32cf6b90ed3f63a9aea6e27dd83f468d32cf6b90ed3f468d32cf6b90edbf63a9aea6e27dd83fb05cf7cf9762ef3f0ba6693cb8f8c83f0ba6693cb8f8c8bfb05cf7cf9762ef3fc868ae393bc7e13fa3a10e29669bea3fa3a10e29669beabfc868ae393bc7e13fa3a10e29669bea3fc868ae393bc7e13fc868ae393bc7e1bfa3a10e29669bea3f0ba6693cb8f8c83fb05cf7cf9762ef3fb05cf7cf9762efbf0ba6693cb8f8c83f2625d1a38dd8ef3f2cb429bca617b93f2cb429bca617b9bf2625d1a38dd8ef3fd61d0925f34ce43f4117156b80bce83f4117156b80bce8bfd61d0925f34ce43fb1bd80f1b238ec3f3bf606385d2bde3f3bf606385d2bdebfb1bd80f1b238ec3f069fd52e0694d23fda2dc656419fee3fda2dc656419feebf069fd52e0694d23fda2dc656419fee3f069fd52e0694d23f069fd52e0694d2bfda2dc656419fee3f3bf606385d2bde3fb1bd80f1b238ec3fb1bd80f1b238ecbf3bf606385d2bde3f4117156b80bce83fd61d0925f34ce43fd61d0925f34ce4bf4117156b80bce83f2cb429bca617b93f2625d1a38dd8ef3f2625d1a38dd8efbf2cb429bca617b93f7e6d79e321f6ef3f14d80df1651fa93f14d80df1651fa9bf7e6d79e321f6ef3fa0ec8c34697de53fafaf6a22dfb5e73fafaf6a22dfb5e7bfa0ec8c34697de53f73c73cf47aedec3fc05ce109105ddb3fc05ce109105ddbbf73c73cf47aedec3fdd1fab759a8fd53fe586f6042121ee3fe586f6042121eebfdd1fab759a8fd53fd73092fb7e0aef3f1b5f217bf919cf3f1b5f217bf919cfbfd73092fb7e0aef3feeff22998773e03f3e6e19458372eb3f3e6e19458372ebbfeeff22998773e03f4187f347e0b3e93f3570e1fcf70fe33f3570e1fcf70fe3bf4187f347e0b3e93f3a618e6e10c8c23f17a5087f55a7ef3f17a5087f55a7efbf3a618e6e10c8c23f17a5087f55a7ef3f3a618e6e10c8c23f3a618e6e10c8c2bf17a5087f55a7ef3f3570e1fcf70fe33f4187f347e0b3e93f4187f347e0b3e9bf3570e1fcf70fe33f3e6e19458372eb3feeff22998773e03feeff22998773e0bf3e6e19458372eb3f1b5f217bf919cf3fd73092fb7e0aef3fd73092fb7e0aefbf1b5f217bf919cf3fe586f6042121ee3fdd1fab759a8fd53fdd1fab759a8fd5bfe586f6042121ee3fc05ce109105ddb3f73c73cf47aedec3f73c73cf47aedecbfc05ce109105ddb3fafaf6a22dfb5e73fa0ec8c34697de53fa0ec8c34697de5bfafaf6a22dfb5e73f14d80df1651fa93f7e6d79e321f6ef3f7e6d79e321f6efbf14d80df1651fa93f0dcd846088fdef3f7e66a3f75521993f7e66a3f7552199bf0dcd846088fdef3fdf2c1d55b710e63f96ffef37082de73f96ffef37082de7bfdf2c1d55b710e63f3ac94dd13441ed3f8aeda84379efd93f8aeda84379efd9bf3ac94dd13441ed3f9f45fa308508d73f3cc2ccb613dbed3f3cc2ccb613dbedbf9f45fa308508d73f89e564acf338ef3f634f7e6a820bcc3f634f7e6a820bccbf89e564acf338ef3f234b1b54b31ee13f000215580a09eb3f000215580a09ebbf234b1b54b31ee13f822746a0a729ea3fdf12dd4c056de23fdf12dd4c056de2bf822746a0a729ea3fc63f8b4414e2c53fa94b71fa6487ef3fa94b71fa6487efbfc63f8b4414e2c53fd39fe17064c2ef3f0e73a9564e56bf3f0e73a9564e56bfbfd39fe17064c2ef3fb9502029faafe33ffb639249223ae93ffb639249223ae9bfb9502029faafe33f2a956facc0d7eb3fba9af8dba48bdf3fba9af8dba48bdfbf2a956facc0d7eb3f77f6b162d211d13f634968e740d7ee3f634968e740d7eebf77f6b162d211d13f12e148ec8862ee3f016617945c13d43f016617945c13d4bf12e148ec8862ee3f5ec431996ec6dc3ff51134214b95ec3ff51134214b95ecbf5ec431996ec6dc3f6e97ff0b0e3be83fe9e5e3bbcae6e43fe9e5e3bbcae6e4bf6e97ff0b0e3be83ff619ce9220d5b23f3a8801adcde9ef3f3a8801adcde9efbff619ce9220d5b23f3a8801adcde9ef3ff619ce9220d5b23ff619ce9220d5b2bf3a8801adcde9ef3fe9e5e3bbcae6e43f6e97ff0b0e3be83f6e97ff0b0e3be8bfe9e5e3bbcae6e43ff51134214b95ec3f5ec431996ec6dc3f5ec431996ec6dcbff51134214b95ec3f016617945c13d43f12e148ec8862ee3f12e148ec8862eebf016617945c13d43f634968e740d7ee3f77f6b162d211d13f77f6b162d211d1bf634968e740d7ee3fba9af8dba48bdf3f2a956facc0d7eb3f2a956facc0d7ebbfba9af8dba48bdf3ffb639249223ae93fb9502029faafe33fb9502029faafe3bffb639249223ae93f0e73a9564e56bf3fd39fe17064c2ef3fd39fe17064c2efbf0e73a9564e56bf3fa94b71fa6487ef3fc63f8b4414e2c53fc63f8b4414e2c5bfa94b71fa6487ef3fdf12dd4c056de23f822746a0a729ea3f822746a0a729eabfdf12dd4c056de23f000215580a09eb3f234b1b54b31ee13f234b1b54b31ee1bf000215580a09eb3f634f7e6a820bcc3f89e564acf338ef3f89e564acf338efbf634f7e6a820bcc3f3cc2ccb613dbed3f9f45fa308508d73f9f45fa308508d7bf3cc2ccb613dbed3f8aeda84379efd93f3ac94dd13441ed3f3ac94dd13441edbf8aeda84379efd93f96ffef37082de73fdf2c1d55b710e63fdf2c1d55b710e6bf96ffef37082de73f7e66a3f75521993f0dcd846088fdef3f0dcd846088fdefbf7e66a3f75521993fdb929b1662ffef3f84c7defcd121893f84c7defcd12189bfdb929b1662ffef3f3d78f0251959e63fafa8ea5444e7e63fafa8ea5444e7e6bf3d78f0251959e63f8be6c9736169ed3fd793bc632a37d93fd793bc632a37d9bf8be6c9736169ed3fe7cc1d31a9c3d73f9ba0386252b6ed3f9ba0386252b6edbfe7cc1d31a9c3d73f2d2f0b3b604eef3f5104b025a082ca3f5104b025a082cabf2d2f0b3b604eef3f49dbde634d73e13f11d5219ebcd2ea3f11d5219ebcd2eabf49dbde634d73e13fe2fa021b0963ea3f59eb3399791ae23f59eb3399791ae2bfe2fa021b0963ea3f31bf50ded96dc73f7720a1a39975ef3f7720a1a39975efbf31bf50ded96dc73f7ba66dfd15ceef3fd5c29ec78537bc3fd5c29ec78537bcbf7ba66dfd15ceef3fd4564553d9fee33f0d94efa3ccfbe83f0d94efa3ccfbe8bfd4564553d9fee33f49557226c408ec3fd678ef5219dcde3fd678ef5219dcdebf49557226c408ec3f3edb4c3f44d3d13f740bdfc8d8bbee3f740bdfc8d8bbeebf3edb4c3f44d3d13f0dd14cab7b81ee3f5281e1c21054d33f5281e1c21054d3bf0dd14cab7b81ee3f89e3865b7779dd3f9b7388348b67ec3f9b7388348b67ecbf89e3865b7779dd3fbf2eba0f407ce83f39099b9b449ae43f39099b9b449ae4bfbf2eba0f407ce83f19a49a0ad0f6b53f095bbdfccae1ef3f095bbdfccae1efbf19a49a0ad0f6b53fad718e6595f0ef3fe020f8796e65af3fe020f8796e65afbfad718e6595f0ef3f9655a3928232e53f711757e3ecf8e73f711757e3ecf8e7bf9655a3928232e53f5cfcfcf3f0c1ec3fe71e01d84912dc3fe71e01d84912dcbf5cfcfcf3f0c1ec3f6ae77842e2d1d43f7ec12b4b6a42ee3f7ec12b4b6a42eebf6ae77842e2d1d43fc273e4a378f1ee3faefd370eb84fd03faefd370eb84fd0bfc273e4a378f1ee3fb73e4c87fc1ce03fd2903567aaa5eb3fd2903567aaa5ebbfb73e4c87fc1ce03f42d7c7f47e77e93ff35906b15860e33ff35906b15860e3bf42d7c7f47e77e93f77f5dacef039c13f41d7957179b5ef3f41d7957179b5efbf77f5dacef039c13f9b09c924f997ef3f5a3e29b17655c43f5a3e29b17655c4bf9b09c924f997ef3feaf3fa25dbbee23f94af29ef43efe93f94af29ef43efe9bfeaf3fa25dbbee23f1257f53e4d3eeb3f8f895d4d70c9e03f8f895d4d70c9e0bf1257f53e4d3eeb3f114345e54f93cd3fda3a76f75222ef3fda3a76f75222efbf114345e54f93cd3f2bbe2d62aefeed3fc6273fdd7d4cd63fc6273fdd7d4cd6bf2bbe2d62aefeed3fca3f6d2bc8a6da3fdc353e74e717ed3fdc353e74e717edbfca3f6d2bc8a6da3f6172035fe771e73f8c0165be7bc7e53f8c0165be7bc7e5bf6172035fe771e73fcd55947565d8a23f5df7feef72faef3f5df7feef72faefbfcd55947565d8a23f5df7feef72faef3fcd55947565d8a23fcd55947565d8a2bf5df7feef72faef3f8c0165be7bc7e53f6172035fe771e73f6172035fe771e7bf8c0165be7bc7e53fdc353e74e717ed3fca3f6d2bc8a6da3fca3f6d2bc8a6dabfdc353e74e717ed3fc6273fdd7d4cd63f2bbe2d62aefeed3f2bbe2d62aefeedbfc6273fdd7d4cd63fda3a76f75222ef3f114345e54f93cd3f114345e54f93cdbfda3a76f75222ef3f8f895d4d70c9e03f1257f53e4d3eeb3f1257f53e4d3eebbf8f895d4d70c9e03f94af29ef43efe93feaf3fa25dbbee23feaf3fa25dbbee2bf94af29ef43efe93f5a3e29b17655c43f9b09c924f997ef3f9b09c924f997efbf5a3e29b17655c43f41d7957179b5ef3f77f5dacef039c13f77f5dacef039c1bf41d7957179b5ef3ff35906b15860e33f42d7c7f47e77e93f42d7c7f47e77e9bff35906b15860e33fd2903567aaa5eb3fb73e4c87fc1ce03fb73e4c87fc1ce0bfd2903567aaa5eb3faefd370eb84fd03fc273e4a378f1ee3fc273e4a378f1eebfaefd370eb84fd03f7ec12b4b6a42ee3f6ae77842e2d1d43f6ae77842e2d1d4bf7ec12b4b6a42ee3fe71e01d84912dc3f5cfcfcf3f0c1ec3f5cfcfcf3f0c1ecbfe71e01d84912dc3f711757e3ecf8e73f9655a3928232e53f9655a3928232e5bf711757e3ecf8e73fe020f8796e65af3fad718e6595f0ef3fad718e6595f0efbfe020f8796e65af3f095bbdfccae1ef3f19a49a0ad0f6b53f19a49a0ad0f6b5bf095bbdfccae1ef3f39099b9b449ae43fbf2eba0f407ce83fbf2eba0f407ce8bf39099b9b449ae43f9b7388348b67ec3f89e3865b7779dd3f89e3865b7779ddbf9b7388348b67ec3f5281e1c21054d33f0dd14cab7b81ee3f0dd14cab7b81eebf5281e1c21054d33f740bdfc8d8bbee3f3edb4c3f44d3d13f3edb4c3f44d3d1bf740bdfc8d8bbee3fd678ef5219dcde3f49557226c408ec3f49557226c408ecbfd678ef5219dcde3f0d94efa3ccfbe83fd4564553d9fee33fd4564553d9fee3bf0d94efa3ccfbe83fd5c29ec78537bc3f7ba66dfd15ceef3f7ba66dfd15ceefbfd5c29ec78537bc3f7720a1a39975ef3f31bf50ded96dc73f31bf50ded96dc7bf7720a1a39975ef3f59eb3399791ae23fe2fa021b0963ea3fe2fa021b0963eabf59eb3399791ae23f11d5219ebcd2ea3f49dbde634d73e13f49dbde634d73e1bf11d5219ebcd2ea3f5104b025a082ca3f2d2f0b3b604eef3f2d2f0b3b604eefbf5104b025a082ca3f9ba0386252b6ed3fe7cc1d31a9c3d73fe7cc1d31a9c3d7bf9ba0386252b6ed3fd793bc632a37d93f8be6c9736169ed3f8be6c9736169edbfd793bc632a37d93fafa8ea5444e7e63f3d78f0251959e63f3d78f0251959e6bfafa8ea5444e7e63f84c7defcd121893fdb929b1662ffef3fdb929b1662ffefbf84c7defcd121893f928a8e85d8ffef3f710067fef021793f710067fef02179bf928a8e85d8ffef3f10af9184f77ce63f7582c1730dc4e63f7582c1730dc4e6bf10af9184f77ce63ff9ecb8020b7ded3fb0a4c82ea5dad83fb0a4c82ea5dad8bff9ecb8020b7ded3fc4aa4eb0e320d83f888966a983a3ed3f888966a983a3edbfc4aa4eb0e320d83f849e78b1a258ef3f6643dcf2cbbdc93f6643dcf2cbbdc9bf849e78b1a258ef3fb8b9f2095a9de13fd4c0165932b7ea3fd4c0165932b7eabfb8b9f2095a9de13f9de69f52587fea3f1b86bc8bf0f0e13f1b86bc8bf0f0e1bf9de69f52587fea3fc6649ce86633c83fb7bbf57d3f6cef3fb7bbf57d3f6cefbfc6649ce86633c83f840b221479d3ef3f035c4924b7a7ba3f035c4924b7a7babf840b221479d3ef3fb16b8e17ff25e43fcc98163345dce83fcc98163345dce8bfb16b8e17ff25e43fb071a93fde20ec3f1451f8eae083de3f1451f8eae083debfb071a93fde20ec3f71bbc3abbb33d23f8ea8e7e8b2adee3f8ea8e7e8b2adeebf71bbc3abbb33d23ff2f71d368490ee3f8703ecda22f4d23f8703ecda22f4d2bff2f71d368490ee3f58cc81148fd2dd3f07692b014250ec3f07692b014250ecbf58cc81148fd2dd3faad44d9a7e9ce83f4773981bb573e43f4773981bb573e4bfaad44d9a7e9ce83f215b5d6a5887b73f56f4f19f53ddef3f56f4f19f53ddefbf215b5d6a5887b73f5c578d0f83f3ef3fe3d7c0128d42ac3fe3d7c0128d42acbf5c578d0f83f3ef3f375197381058e53fb23dc36c83d7e73fb23dc36c83d7e7bf375197381058e53ff6328b89d9d7ec3f01bd0423cfb7db3f01bd0423cfb7dbbff6328b89d9d7ec3f243caf80d830d53f25ce70e8ea31ee3f25ce70e8ea31eebf243caf80d830d53fec950b0c22feee3ff9eddf1adcdccf3ff9eddf1adcdccfbfec950b0c22feee3f1a22ae265648e03fe90475d2388ceb3fe90475d2388cebbf1a22ae265648e03f220dd82ecf95e93f578e0c0d4038e33f578e0c0d4038e3bf220dd82ecf95e93fcf7becd41601c23fbbcf468e8eaeef3fbbcf468e8eaeefbfcf7becd41601c23fc8b2ad55ce9fef3f148dcdb0db8ec33f148dcdb0db8ec3bfc8b2ad55ce9fef3f17eae8e380e7e23fd580eaf5b1d1e93fd580eaf5b1d1e9bf17eae8e380e7e23f051492fe8958eb3fe1c51774909ee03fe1c51774909ee0bf051492fe8958eb3f1b1a101eca56ce3f5d20f7538f16ef3f5d20f7538f16efbf1b1a101eca56ce3fac8029ca0c10ee3f93a69e3727eed53f93a69e3727eed5bfac8029ca0c10ee3f09407f6c0d02db3f92bdb2fed402ed3f92bdb2fed402edbf09407f6c0d02db3fe5554f570094e73f50725d2a8da2e53f50725d2a8da2e5bfe5554f570094e73f43cd90d200fca53fdf81dbda71f8ef3fdf81dbda71f8efbf43cd90d200fca53ff8d3f11d25fcef3f01cfd13137699f3f01cfd13137699fbff8d3f11d25fcef3f7470839534ece53f8dd2a88d944fe73f8dd2a88d944fe7bf7470839534ece53f9fefe020b22ced3fe5a1de27414bda3fe5a1de27414bdabf9fefe020b22ced3f177ec77d9daad63fda47def705eded3fda47def705ededbf177ec77d9daad63f9d9a08c9c92def3f86b212b38ccfcc3f86b212b38ccfccbf9d9a08c9c92def3f7e8e2abb26f4e03fb4130047cd23eb3fb4130047cd23ebbf7e8e2abb26f4e03f37f9baea950cea3fa89c62270796e23fa89c62270796e2bf37f9baea950cea3ff2c59785df1bc53fdb41aeffd58fef3fdb41aeffd58fefbff2c59785df1bc53f8641e41716bcef3f1d83ba47a072c03f1d83ba47a072c0bf8641e41716bcef3f22ebdf854188e33fd76d8ee4ef58e93fd76d8ee4ef58e9bf22ebdf854188e33fea8093c4d7beeb3f1012e74bf6e2df3f1012e74bf6e2dfbfea8093c4d7beeb3f90dbdbcfd9b0d03fbc9d5ae282e4ee3fbc9d5ae282e4eebf90dbdbcfd9b0d03ffc9f72049f52ee3f541057a5b872d43f541057a5b872d4bffc9f72049f52ee3f0b0097497f6cdc3f00b9a069c1abec3f00b9a069c1abecbf0b0097497f6cdc3fcc7ab5331b1ae83f9ba0599fc00ce53f9ba0599fc00ce5bfcc7ab5331b1ae83fb309d7340144b13fc473b6ec58edef3fc473b6ec58edefbfb309d7340144b13f40392eaff3e5ef3f962027791166b43f962027791166b4bf40392eaff3e5ef3f0400ec45a1c0e43fcc58e91ac55be83fcc58e91ac55be8bf0400ec45a1c0e43ff33c23528e7eec3f5bdbe9e81620dd3f5bdbe9e81620ddbff33c23528e7eec3fb71404faceb3d33f44976adb2772ee3f44976adb2772eebfb71404faceb3d33f84bfc3d3b2c9ee3f775176d7a072d13f775176d7a072d1bf84bfc3d3b2c9ee3f67d03f960534df3fdd7753e164f0eb3fdd7753e164f0ebbf67d03f960534df3fa29dd46f161be93f4483c53882d7e33f4483c53882d7e3bfa29dd46f161be93fc99faecb0ec7bd3f21b7fe6c64c8ef3f21b7fe6c64c8efbfc99faecb0ec7bd3f6e3de629a67eef3fb24af60413a8c63fb24af60413a8c6bf6e3de629a67eef3f1fac98fbd543e23fc89a11c87846ea3fc89a11c87846eabf1fac98fbd543e23f74143cb404eeea3feb6c33af1549e13feb6c33af1549e1bf74143cb404eeea3f22673def3247cb3fdd92ff85d043ef3fdd92ff85d043efbf22673def3247cb3f600241cbd7c8ed3ff618240f3466d73ff618240f3466d7bf600241cbd7c8ed3fffbd41617193d93fb13ee9526f55ed3fb13ee9526f55edbfffbd41617193d93f7a6d17b3420ae73fe91b1ca30335e63fe91b1ca30335e6bf7a6d17b3420ae73ffd0ee3bb36d9923fa1514bb49cfeef3fa1514bb49cfeefbffd0ee3bb36d9923fa1514bb49cfeef3ffd0ee3bb36d9923ffd0ee3bb36d992bfa1514bb49cfeef3fe91b1ca30335e63f7a6d17b3420ae73f7a6d17b3420ae7bfe91b1ca30335e63fb13ee9526f55ed3fffbd41617193d93fffbd41617193d9bfb13ee9526f55ed3ff618240f3466d73f600241cbd7c8ed3f600241cbd7c8edbff618240f3466d73fdd92ff85d043ef3f22673def3247cb3f22673def3247cbbfdd92ff85d043ef3feb6c33af1549e13f74143cb404eeea3f74143cb404eeeabfeb6c33af1549e13fc89a11c87846ea3f1fac98fbd543e23f1fac98fbd543e2bfc89a11c87846ea3fb24af60413a8c63f6e3de629a67eef3f6e3de629a67eefbfb24af60413a8c63f21b7fe6c64c8ef3fc99faecb0ec7bd3fc99faecb0ec7bdbf21b7fe6c64c8ef3f4483c53882d7e33fa29dd46f161be93fa29dd46f161be9bf4483c53882d7e33fdd7753e164f0eb3f67d03f960534df3f67d03f960534dfbfdd7753e164f0eb3f775176d7a072d13f84bfc3d3b2c9ee3f84bfc3d3b2c9eebf775176d7a072d13f44976adb2772ee3fb71404faceb3d33fb71404faceb3d3bf44976adb2772ee3f5bdbe9e81620dd3ff33c23528e7eec3ff33c23528e7eecbf5bdbe9e81620dd3fcc58e91ac55be83f0400ec45a1c0e43f0400ec45a1c0e4bfcc58e91ac55be83f962027791166b43f40392eaff3e5ef3f40392eaff3e5efbf962027791166b43fc473b6ec58edef3fb309d7340144b13fb309d7340144b1bfc473b6ec58edef3f9ba0599fc00ce53fcc7ab5331b1ae83fcc7ab5331b1ae8bf9ba0599fc00ce53f00b9a069c1abec3f0b0097497f6cdc3f0b0097497f6cdcbf00b9a069c1abec3f541057a5b872d43ffc9f72049f52ee3ffc9f72049f52eebf541057a5b872d43fbc9d5ae282e4ee3f90dbdbcfd9b0d03f90dbdbcfd9b0d0bfbc9d5ae282e4ee3f1012e74bf6e2df3fea8093c4d7beeb3fea8093c4d7beebbf1012e74bf6e2df3fd76d8ee4ef58e93f22ebdf854188e33f22ebdf854188e3bfd76d8ee4ef58e93f1d83ba47a072c03f8641e41716bcef3f8641e41716bcefbf1d83ba47a072c03fdb41aeffd58fef3ff2c59785df1bc53ff2c59785df1bc5bfdb41aeffd58fef3fa89c62270796e23f37f9baea950cea3f37f9baea950ceabfa89c62270796e23fb4130047cd23eb3f7e8e2abb26f4e03f7e8e2abb26f4e0bfb4130047cd23eb3f86b212b38ccfcc3f9d9a08c9c92def3f9d9a08c9c92defbf86b212b38ccfcc3fda47def705eded3f177ec77d9daad63f177ec77d9daad6bfda47def705eded3fe5a1de27414bda3f9fefe020b22ced3f9fefe020b22cedbfe5a1de27414bda3f8dd2a88d944fe73f7470839534ece53f7470839534ece5bf8dd2a88d944fe73f01cfd13137699f3ff8d3f11d25fcef3ff8d3f11d25fcefbf01cfd13137699f3fdf81dbda71f8ef3f43cd90d200fca53f43cd90d200fca5bfdf81dbda71f8ef3f50725d2a8da2e53fe5554f570094e73fe5554f570094e7bf50725d2a8da2e53f92bdb2fed402ed3f09407f6c0d02db3f09407f6c0d02dbbf92bdb2fed402ed3f93a69e3727eed53fac8029ca0c10ee3fac8029ca0c10eebf93a69e3727eed53f5d20f7538f16ef3f1b1a101eca56ce3f1b1a101eca56cebf5d20f7538f16ef3fe1c51774909ee03f051492fe8958eb3f051492fe8958ebbfe1c51774909ee03fd580eaf5b1d1e93f17eae8e380e7e23f17eae8e380e7e2bfd580eaf5b1d1e93f148dcdb0db8ec33fc8b2ad55ce9fef3fc8b2ad55ce9fefbf148dcdb0db8ec33fbbcf468e8eaeef3fcf7becd41601c23fcf7becd41601c2bfbbcf468e8eaeef3f578e0c0d4038e33f220dd82ecf95e93f220dd82ecf95e9bf578e0c0d4038e33fe90475d2388ceb3f1a22ae265648e03f1a22ae265648e0bfe90475d2388ceb3ff9eddf1adcdccf3fec950b0c22feee3fec950b0c22feeebff9eddf1adcdccf3f25ce70e8ea31ee3f243caf80d830d53f243caf80d830d5bf25ce70e8ea31ee3f01bd0423cfb7db3ff6328b89d9d7ec3ff6328b89d9d7ecbf01bd0423cfb7db3fb23dc36c83d7e73f375197381058e53f375197381058e5bfb23dc36c83d7e73fe3d7c0128d42ac3f5c578d0f83f3ef3f5c578d0f83f3efbfe3d7c0128d42ac3f56f4f19f53ddef3f215b5d6a5887b73f215b5d6a5887b7bf56f4f19f53ddef3f4773981bb573e43faad44d9a7e9ce83faad44d9a7e9ce8bf4773981bb573e43f07692b014250ec3f58cc81148fd2dd3f58cc81148fd2ddbf07692b014250ec3f8703ecda22f4d23ff2f71d368490ee3ff2f71d368490eebf8703ecda22f4d23f8ea8e7e8b2adee3f71bbc3abbb33d23f71bbc3abbb33d2bf8ea8e7e8b2adee3f1451f8eae083de3fb071a93fde20ec3fb071a93fde20ecbf1451f8eae083de3fcc98163345dce83fb16b8e17ff25e43fb16b8e17ff25e4bfcc98163345dce83f035c4924b7a7ba3f840b221479d3ef3f840b221479d3efbf035c4924b7a7ba3fb7bbf57d3f6cef3fc6649ce86633c83fc6649ce86633c8bfb7bbf57d3f6cef3f1b86bc8bf0f0e13f9de69f52587fea3f9de69f52587feabf1b86bc8bf0f0e13fd4c0165932b7ea3fb8b9f2095a9de13fb8b9f2095a9de1bfd4c0165932b7ea3f6643dcf2cbbdc93f849e78b1a258ef3f849e78b1a258efbf6643dcf2cbbdc93f888966a983a3ed3fc4aa4eb0e320d83fc4aa4eb0e320d8bf888966a983a3ed3fb0a4c82ea5dad83ff9ecb8020b7ded3ff9ecb8020b7dedbfb0a4c82ea5dad83f7582c1730dc4e63f10af9184f77ce63f10af9184f77ce6bf7582c1730dc4e63f710067fef021793f928a8e85d8ffef3f928a8e85d8ffefbf710067fef021793f021d6221f6ffef3fbaa4ccbef821693fbaa4ccbef82169bf021d6221f6ffef3f719ca1ead18ee63f9ce22fed5cb2e63f9ce22fed5cb2e6bf719ca1ead18ee63f4fa44584c486ed3f44edd5864bacd83f44edd5864bacd8bf4fa44584c486ed3f3f90f3aa6a4fd83f463d8bdd009aed3f463d8bdd009aedbf3f90f3aa6a4fd83f5d6843eda65def3ffa2ab6e9495bc93ffa2ab6e9495bc9bf5d6843eda65def3fbf73131750b2e13f8eb92c7a54a9ea3f8eb92c7a54a9eabfbf73131750b2e13fd25a546e678dea3f7248dc641bdce13f7248dc641bdce1bfd25a546e678dea3f0418c4271796c83fee3c88567567ef3fee3c88567567efbf0418c4271796c83f9e5ca72d0dd6ef3f5ca824ebb6dfb93f5ca824ebb6dfb9bf9e5ca72d0dd6ef3f80432a5b7f39e43f554618756acce83f554618756acce8bf80432a5b7f39e43ff1e33149d12cec3f25d83c6da857de3f25d83c6da857debff1e33149d12cec3fba545599e663d23f0058e69383a6ee3f0058e69383a6eebfba545599e663d23f306b0136ec97ee3f2045954e1ac4d23f2045954e1ac4d2bf306b0136ec97ee3fde41a966fffedd3f04c041318344ec3f04c041318344ecbfde41a966fffedd3f881dde1e87ace83fa2322b695a60e43fa2322b695a60e4bf881dde1e87ace83fa130c112874fb83f8c531475fadaef3f8c531475fadaefbfa130c112874fb83fd3beb154dcf4ef3f17835fbd01b1aa3f17835fbd01b1aabfd3beb154dcf4ef3f9f649751c36ae53f33d3e29cb8c6e73f33d3e29cb8c6e7bf9f649751c36ae53f60a09927b3e2ec3f9356fd14788adb3f9356fd14788adbbf60a09927b3e2ec3fb467f4124060d53f7a1939448f29ee3f7a1939448f29eebfb467f4124060d53f8c73cf145a04ef3f0238bd80747bcf3f0238bd80747bcfbf8c73cf145a04ef3fb7b831ecf35de03fe992e786667feb3fe992e786667febbfb7b831ecf35de03fb2062ba4dfa4e93f1fa649ec2124e33f1fa649ec2124e3bfb2062ba4dfa4e93f0934fd4d9964c23fdcfd0ccbfbaaef3fdcfd0ccbfbaaefbf0934fd4d9964c23f91177aac9ba3ef3fa71645f97b2bc33fa71645f97b2bc3bf91177aac9ba3ef3f1510444bc2fbe23fc275f010d1c2e93fc275f010d1c2e9bf1510444bc2fbe23f47bcfd148f65eb3f8cb032201189e03f8cb032201189e0bf47bcfd148f65eb3f48e32d466bb8ce3f5f8f89bc9010ef3f5f8f89bc9010efbf48e32d466bb8ce3fd966dc2fa018ee3fb6b39d8be7bed53fb6b39d8be7bed5bfd966dc2fa018ee3f7219b31d972fdb3f7b46cee830f8ec3f7b46cee830f8ecbf7219b31d972fdb3fd297bf07f7a4e73fdf23f7d50190e53fdf23f7d50190e5bfd297bf07f7a4e73f864687a5ba8da73f64911bbb53f7ef3f64911bbb53f7efbf864687a5ba8da73f79a6e29ce0fcef3f1d3be54c4f459c3f1d3be54c4f459cbf79a6e29ce0fcef3f106ae5bd7cfee53f4299078e553ee73f4299078e553ee7bf106ae5bd7cfee53fdcfbcb7bfc36ed3fc00ab543651dda3fc00ab543651ddabfdcfbcb7bfc36ed3fb60c8a6398d9d63f818d6d0f16e4ed3f818d6d0f16e4edbfb60c8a6398d9d63ff0ae3a5a6833ef3fdd745d53906dcc3fdd745d53906dccbff0ae3a5a6833ef3f57a9d0487209e13ff5a24c2a7416eb3ff5a24c2a7416ebbf57a9d0487209e13f5ea7c0d2261bea3fba3c4def8b81e23fba3c4def8b81e2bf5ea7c0d2261bea3fdecb5486007fc53f784bcb37a78bef3f784bcb37a78befbfdecb5486007fc53f888d0a0f47bfef3f5bb86fade80ec03f5bb86fade80ec0bf888d0a0f47bfef3f2930d6e3239ce33f6c4aace39049e93f6c4aace39049e9bf2930d6e3239ce33f27230dcb54cbeb3fded2245c57b7df3fded2245c57b7dfbf27230dcb54cbeb3fce49174e5be1d03f5186076aebddee3f5186076aebddeebfce49174e5be1d03fd36704559d5aee3ff03689dc1043d43ff03689dc1043d4bfd36704559d5aee3f895386c37f99dc3f49c4b9198fa0ec3f49c4b9198fa0ecbf895386c37f99dc3fff45f5139c2ae83f86a4cc25ccf9e43f86a4cc25ccf9e4bfff45f5139c2ae83f4d44ed74960cb23f0f4130259debef3f0f4130259debefbf4d44ed74960cb23f602d4885eae7ef3f99a2c5129f9db33f99a2c5129f9db3bf602d4885eae7ef3f7f9f586dbcd3e43ffa83af11714be83ffa83af11714be8bf7f9f586dbcd3e43f139c0287f589ec3f21cde1ae4bf3dc3f21cde1ae4bf3dcbf139c0287f589ec3f71c26ee99be3d33fa7535dc5616aee3fa7535dc5616aeebf71c26ee99be3d33f0990995e83d0ee3f7893c6ef3e42d13f7893c6ef3e42d1bf0990995e83d0ee3fa3cd56e6de5fdf3fc15411611be4eb3fc15411611be4ebbfa3cd56e6de5fdf3f15a8c51fa42ae93f18c58149c4c3e33f18c58149c4c3e3bf15a8c51fa42ae93f3faae4fdb78ebe3ff69a7d3b6ec5ef3ff69a7d3b6ec5efbf3faae4fdb78ebe3f0cc6404a0f83ef3f0d831d831a45c63f0d831d831a45c6bf0cc6404a0f83ef3f1071bb4c7358e23fc63b594a1838ea3fc63b594a1838eabf1071bb4c7358e23fb6579fd88ffbea3f4f25eecfe933e13f4f25eecfe933e1bfb6579fd88ffbea3fad5df13463a9cb3f65bc1bbc6b3eef3f65bc1bbc6b3eefbfad5df13463a9cb3f5a918af3fed1ed3f921026c96337d73f921026c96337d7bf5a918af3fed1ed3ff2f90d447dc1d93f2475181b5b4bed3f2475181b5b4bedbff2f90d447dc1d93fbf410e96ac1be73fff22ec4fe422e63fff22ec4fe422e6bfbf410e96ac1be73f26b2fa214dfd953f77cb70681cfeef3f77cb70681cfeefbf26b2fa214dfd953fd13bc54309ffef3fcb97b96a296a8f3fcb97b96a296a8fbfd13bc54309ffef3f5b537f431547e63f755bc999caf8e63f755bc999caf8e6bf5b537f431547e63f7f8a8872715fed3f8f94abb75565d93f8f94abb75565d9bf7f8a8872715fed3faedf13e6f594d73f9a7595439ebfed3f9a7595439ebfedbfaedf13e6f594d73fb4abbc062249ef3fabb9f3d5f1e4ca3fabb9f3d5f1e4cabfb4abbc062249ef3fbce2dbe4365ee13fefec45f368e0ea3fefec45f368e0eabfbce2dbe4365ee13f23f59010c954ea3fe2132c662d2fe23fe2132c662d2fe2bf23f59010c954ea3fffc4088dfd0ac73f2a321a9c297aef3f2a321a9c297aefbfffc4088dfd0ac73f5443910347cbef3fc17d303b53ffbc3fc17d303b53ffbcbf5443910347cbef3f8006beea33ebe33ffe5e5743790be93ffe5e5743790be9bf8006beea33ebe33f47b1a1259dfceb3ffef7bf061908df3ffef7bf061908dfbf47b1a1259dfceb3f43f2e8fbf7a2d13fb2f61a4bcfc2ee3fb2f61a4bcfc2eebf43f2e8fbf7a2d13f5a16a529db79ee3fabb653e3f583d33fabb653e3f583d3bf5a16a529db79ee3f9d60a82bd04cdd3fd7aa9e891573ec3fd7aa9e891573ecbf9d60a82bd04cdd3f95a19a1d0a6ce83ff122675179ade43ff122675179ade4bf95a19a1d0a6ce83f0a4d4d4a772eb53f86d8e92be9e3ef3f86d8e92be9e3efbf0a4d4d4a772eb53f9161820201efef3f6430464e617bb03f6430464e617bb0bf9161820201efef3fa69ad91ca81fe53ffa526e758b09e83ffa526e758b09e8bfa69ad91ca81fe53f99da000ae2b6ec3f293126476d3fdc3f293126476d3fdcbf99da000ae2b6ec3ff3821bd153a2d43f5ece81ff8d4aee3f5ece81ff8d4aeebff3821bd153a2d43f44a5504c07ebee3f1e66eb054e80d03f1e66eb054e80d0bf44a5504c07ebee3fe1822bc84007e03f0dc4b6a049b2eb3f0dc4b6a049b2ebbfe1822bc84007e03fe17fbd423f68e93f8d7f811b5374e33f8d7f811b5374e3bfe17fbd423f68e93f8667b2bc4dd6c03fb7ad668dd1b8ef3fb7ad668dd1b8efbf8667b2bc4dd6c03f08ac854ff193ef3f88fa797fb1b8c43f88fa797fb1b8c4bf08ac854ff193ef3f58eb7ae876aae23fde4931f1f4fde93fde4931f1f4fde9bf58eb7ae876aae23ff37bf3a51531eb3fb6c44bb8d0dee03fb6c44bb8d0dee0bff37bf3a51531eb3feebd2c4d7731cd3fce0946fc1728ef3fce0946fc1728efbfeebd2c4d7731cd3f9ca59b6ae3f5ed3fcb63ad9c947bd63fcb63ad9c947bd6bf9ca59b6ae3f5ed3f1bf3dbd30c79da3fe1a4e5c65522ed3fe1a4e5c65522edbf1bf3dbd30c79da3f6447302cc560e73f5c343ee7ded9e53f5c343ee7ded9e5bf6447302cc560e73f7fc142db8546a13faefd25e455fbef3faefd25e455fbefbf7fc142db8546a13f14c008427cf9ef3f7961f86f396aa43f7961f86f396aa4bf14c008427cf9ef3f48744f260bb5e53f5bb3901bfb82e73f5bb3901bfb82e7bf48744f260bb5e53fb9d2592f670ded3f09dc5c1273d4da3f09dc5c1273d4dabfb9d2592f670ded3f02c2885c591dd63f540f28d96607ee3f540f28d96607eebf02c2885c591dd63f084728be7a1cef3f9a09013f16f5cd3f9a09013f16f5cdbf084728be7a1cef3fec858f8705b4e03f2579de09744beb3f2579de09744bebbfec858f8705b4e03f7224b4ed82e0e93fb89b4ed333d3e23fb89b4ed333d3e2bf7224b4ed82e0e93f9348db572ff2c33f29defb7ced9bef3f29defb7ced9befbf9348db572ff2c33f4dd581c60db2ef3fe724be40899dc13fe724be40899dc1bf4dd581c60db2ef3fe14dc152524ce33f947545f1ae86e93f947545f1ae86e9bfe14dc152524ce33f5e15d91ffa98eb3f96bded55ae32e03f96bded55ae32e0bf5e15d91ffa98eb3fd2fdb906181fd03fc0a31ce5d6f7ee3fc0a31ce5d6f7eebfd2fdb906181fd03f85ce75ec333aee3f487019dc6301d53f487019dc6301d5bf85ce75ec333aee3fd9c0ff1715e5db3fa0dec220eeccec3fa0dec220eeccecbfd9c0ff1715e5db3f8636b0873fe8e73ffc9d15f54f45e53ffc9d15f54f45e5bf8636b0873fe8e73fc98e80f906d4ad3fed31e11416f2ef3fed31e11416f2efbfc98e80f906d4ad3f0733f72299dfef3f29b1793e1bbfb63f29b1793e1bbfb6bf0733f72299dfef3fff9160300387e43fa11b48e7668ce83fa11b48e7668ce8bfff9160300387e43f5af8fe59ef5bec3fd910fa5c0ca6dd3fd910fa5c0ca6ddbf5af8fe59ef5bec3fafba38b61f24d33f2560ad5b0989ee3f2560ad5b0989eebfafba38b61f24d33f11885b51cfb4ee3fbe27d7838503d23fbe27d7838503d2bf11885b51cfb4ee3f2056f29506b0de3f575e46dcd914ec3f575e46dcd914ecbf2056f29506b0de3f496c489b10ece83f8c103d667212e43f8c103d667212e4bf496c489b10ece83f4cf638eca66fbb3f8760d858d1d0ef3f8760d858d1d0efbf4cf638eca66fbb3fb77e4b43f670ef3f1ccbd2bba7d0c73f1ccbd2bba7d0c7bfb77e4b43f670ef3fd66075a1ba05e23ff5609dde3871ea3ff5609dde3871eabfd66075a1ba05e23fc8fa3ebdffc4ea3fe5463a1f5988e13fe5463a1f5988e1bfc8fa3ebdffc4ea3fda31181b3e20ca3f072daf1f8b53ef3f072daf1f8b53efbfda31181b3e20ca3fb98ae62cf4aced3fe44173d34df2d73fe44173d34df2d7bfb98ae62cf4aced3fd17bef81ef08d93fff0d8c503f73ed3fff0d8c503f73edbfd17bef81ef08d93fcdaf4aefafd5e63f86b3523f0f6be63f86b3523f0f6be6bfcdaf4aefafd5e63f0397500e6bd9823f4f8c972ca7ffef3f4f8c972ca7ffefbf0397500e6bd9823f4f8c972ca7ffef3f0397500e6bd9823f0397500e6bd982bf4f8c972ca7ffef3f86b3523f0f6be63fcdaf4aefafd5e63fcdaf4aefafd5e6bf86b3523f0f6be63fff0d8c503f73ed3fd17bef81ef08d93fd17bef81ef08d9bfff0d8c503f73ed3fe44173d34df2d73fb98ae62cf4aced3fb98ae62cf4acedbfe44173d34df2d73f072daf1f8b53ef3fda31181b3e20ca3fda31181b3e20cabf072daf1f8b53ef3fe5463a1f5988e13fc8fa3ebdffc4ea3fc8fa3ebdffc4eabfe5463a1f5988e13ff5609dde3871ea3fd66075a1ba05e23fd66075a1ba05e2bff5609dde3871ea3f1ccbd2bba7d0c73fb77e4b43f670ef3fb77e4b43f670efbf1ccbd2bba7d0c73f8760d858d1d0ef3f4cf638eca66fbb3f4cf638eca66fbbbf8760d858d1d0ef3f8c103d667212e43f496c489b10ece83f496c489b10ece8bf8c103d667212e43f575e46dcd914ec3f2056f29506b0de3f2056f29506b0debf575e46dcd914ec3fbe27d7838503d23f11885b51cfb4ee3f11885b51cfb4eebfbe27d7838503d23f2560ad5b0989ee3fafba38b61f24d33fafba38b61f24d3bf2560ad5b0989ee3fd910fa5c0ca6dd3f5af8fe59ef5bec3f5af8fe59ef5becbfd910fa5c0ca6dd3fa11b48e7668ce83fff9160300387e43fff9160300387e4bfa11b48e7668ce83f29b1793e1bbfb63f0733f72299dfef3f0733f72299dfefbf29b1793e1bbfb63fed31e11416f2ef3fc98e80f906d4ad3fc98e80f906d4adbfed31e11416f2ef3ffc9d15f54f45e53f8636b0873fe8e73f8636b0873fe8e7bffc9d15f54f45e53fa0dec220eeccec3fd9c0ff1715e5db3fd9c0ff1715e5dbbfa0dec220eeccec3f487019dc6301d53f85ce75ec333aee3f85ce75ec333aeebf487019dc6301d53fc0a31ce5d6f7ee3fd2fdb906181fd03fd2fdb906181fd0bfc0a31ce5d6f7ee3f96bded55ae32e03f5e15d91ffa98eb3f5e15d91ffa98ebbf96bded55ae32e03f947545f1ae86e93fe14dc152524ce33fe14dc152524ce3bf947545f1ae86e93fe724be40899dc13f4dd581c60db2ef3f4dd581c60db2efbfe724be40899dc13f29defb7ced9bef3f9348db572ff2c33f9348db572ff2c3bf29defb7ced9bef3fb89b4ed333d3e23f7224b4ed82e0e93f7224b4ed82e0e9bfb89b4ed333d3e23f2579de09744beb3fec858f8705b4e03fec858f8705b4e0bf2579de09744beb3f9a09013f16f5cd3f084728be7a1cef3f084728be7a1cefbf9a09013f16f5cd3f540f28d96607ee3f02c2885c591dd63f02c2885c591dd6bf540f28d96607ee3f09dc5c1273d4da3fb9d2592f670ded3fb9d2592f670dedbf09dc5c1273d4da3f5bb3901bfb82e73f48744f260bb5e53f48744f260bb5e5bf5bb3901bfb82e73f7961f86f396aa43f14c008427cf9ef3f14c008427cf9efbf7961f86f396aa43faefd25e455fbef3f7fc142db8546a13f7fc142db8546a1bfaefd25e455fbef3f5c343ee7ded9e53f6447302cc560e73f6447302cc560e7bf5c343ee7ded9e53fe1a4e5c65522ed3f1bf3dbd30c79da3f1bf3dbd30c79dabfe1a4e5c65522ed3fcb63ad9c947bd63f9ca59b6ae3f5ed3f9ca59b6ae3f5edbfcb63ad9c947bd63fce0946fc1728ef3feebd2c4d7731cd3feebd2c4d7731cdbfce0946fc1728ef3fb6c44bb8d0dee03ff37bf3a51531eb3ff37bf3a51531ebbfb6c44bb8d0dee03fde4931f1f4fde93f58eb7ae876aae23f58eb7ae876aae2bfde4931f1f4fde93f88fa797fb1b8c43f08ac854ff193ef3f08ac854ff193efbf88fa797fb1b8c43fb7ad668dd1b8ef3f8667b2bc4dd6c03f8667b2bc4dd6c0bfb7ad668dd1b8ef3f8d7f811b5374e33fe17fbd423f68e93fe17fbd423f68e9bf8d7f811b5374e33f0dc4b6a049b2eb3fe1822bc84007e03fe1822bc84007e0bf0dc4b6a049b2eb3f1e66eb054e80d03f44a5504c07ebee3f44a5504c07ebeebf1e66eb054e80d03f5ece81ff8d4aee3ff3821bd153a2d43ff3821bd153a2d4bf5ece81ff8d4aee3f293126476d3fdc3f99da000ae2b6ec3f99da000ae2b6ecbf293126476d3fdc3ffa526e758b09e83fa69ad91ca81fe53fa69ad91ca81fe5bffa526e758b09e83f6430464e617bb03f9161820201efef3f9161820201efefbf6430464e617bb03f86d8e92be9e3ef3f0a4d4d4a772eb53f0a4d4d4a772eb5bf86d8e92be9e3ef3ff122675179ade43f95a19a1d0a6ce83f95a19a1d0a6ce8bff122675179ade43fd7aa9e891573ec3f9d60a82bd04cdd3f9d60a82bd04cddbfd7aa9e891573ec3fabb653e3f583d33f5a16a529db79ee3f5a16a529db79eebfabb653e3f583d33fb2f61a4bcfc2ee3f43f2e8fbf7a2d13f43f2e8fbf7a2d1bfb2f61a4bcfc2ee3ffef7bf061908df3f47b1a1259dfceb3f47b1a1259dfcebbffef7bf061908df3ffe5e5743790be93f8006beea33ebe33f8006beea33ebe3bffe5e5743790be93fc17d303b53ffbc3f5443910347cbef3f5443910347cbefbfc17d303b53ffbc3f2a321a9c297aef3fffc4088dfd0ac73fffc4088dfd0ac7bf2a321a9c297aef3fe2132c662d2fe23f23f59010c954ea3f23f59010c954eabfe2132c662d2fe23fefec45f368e0ea3fbce2dbe4365ee13fbce2dbe4365ee1bfefec45f368e0ea3fabb9f3d5f1e4ca3fb4abbc062249ef3fb4abbc062249efbfabb9f3d5f1e4ca3f9a7595439ebfed3faedf13e6f594d73faedf13e6f594d7bf9a7595439ebfed3f8f94abb75565d93f7f8a8872715fed3f7f8a8872715fedbf8f94abb75565d93f755bc999caf8e63f5b537f431547e63f5b537f431547e6bf755bc999caf8e63fcb97b96a296a8f3fd13bc54309ffef3fd13bc54309ffefbfcb97b96a296a8f3f77cb70681cfeef3f26b2fa214dfd953f26b2fa214dfd95bf77cb70681cfeef3fff22ec4fe422e63fbf410e96ac1be73fbf410e96ac1be7bfff22ec4fe422e63f2475181b5b4bed3ff2f90d447dc1d93ff2f90d447dc1d9bf2475181b5b4bed3f921026c96337d73f5a918af3fed1ed3f5a918af3fed1edbf921026c96337d73f65bc1bbc6b3eef3fad5df13463a9cb3fad5df13463a9cbbf65bc1bbc6b3eef3f4f25eecfe933e13fb6579fd88ffbea3fb6579fd88ffbeabf4f25eecfe933e13fc63b594a1838ea3f1071bb4c7358e23f1071bb4c7358e2bfc63b594a1838ea3f0d831d831a45c63f0cc6404a0f83ef3f0cc6404a0f83efbf0d831d831a45c63ff69a7d3b6ec5ef3f3faae4fdb78ebe3f3faae4fdb78ebebff69a7d3b6ec5ef3f18c58149c4c3e33f15a8c51fa42ae93f15a8c51fa42ae9bf18c58149c4c3e33fc15411611be4eb3fa3cd56e6de5fdf3fa3cd56e6de5fdfbfc15411611be4eb3f7893c6ef3e42d13f0990995e83d0ee3f0990995e83d0eebf7893c6ef3e42d13fa7535dc5616aee3f71c26ee99be3d33f71c26ee99be3d3bfa7535dc5616aee3f21cde1ae4bf3dc3f139c0287f589ec3f139c0287f589ecbf21cde1ae4bf3dc3ffa83af11714be83f7f9f586dbcd3e43f7f9f586dbcd3e4bffa83af11714be83f99a2c5129f9db33f602d4885eae7ef3f602d4885eae7efbf99a2c5129f9db33f0f4130259debef3f4d44ed74960cb23f4d44ed74960cb2bf0f4130259debef3f86a4cc25ccf9e43fff45f5139c2ae83fff45f5139c2ae8bf86a4cc25ccf9e43f49c4b9198fa0ec3f895386c37f99dc3f895386c37f99dcbf49c4b9198fa0ec3ff03689dc1043d43fd36704559d5aee3fd36704559d5aeebff03689dc1043d43f5186076aebddee3fce49174e5be1d03fce49174e5be1d0bf5186076aebddee3fded2245c57b7df3f27230dcb54cbeb3f27230dcb54cbebbfded2245c57b7df3f6c4aace39049e93f2930d6e3239ce33f2930d6e3239ce3bf6c4aace39049e93f5bb86fade80ec03f888d0a0f47bfef3f888d0a0f47bfefbf5bb86fade80ec03f784bcb37a78bef3fdecb5486007fc53fdecb5486007fc5bf784bcb37a78bef3fba3c4def8b81e23f5ea7c0d2261bea3f5ea7c0d2261beabfba3c4def8b81e23ff5a24c2a7416eb3f57a9d0487209e13f57a9d0487209e1bff5a24c2a7416eb3fdd745d53906dcc3ff0ae3a5a6833ef3ff0ae3a5a6833efbfdd745d53906dcc3f818d6d0f16e4ed3fb60c8a6398d9d63fb60c8a6398d9d6bf818d6d0f16e4ed3fc00ab543651dda3fdcfbcb7bfc36ed3fdcfbcb7bfc36edbfc00ab543651dda3f4299078e553ee73f106ae5bd7cfee53f106ae5bd7cfee5bf4299078e553ee73f1d3be54c4f459c3f79a6e29ce0fcef3f79a6e29ce0fcefbf1d3be54c4f459c3f64911bbb53f7ef3f864687a5ba8da73f864687a5ba8da7bf64911bbb53f7ef3fdf23f7d50190e53fd297bf07f7a4e73fd297bf07f7a4e7bfdf23f7d50190e53f7b46cee830f8ec3f7219b31d972fdb3f7219b31d972fdbbf7b46cee830f8ec3fb6b39d8be7bed53fd966dc2fa018ee3fd966dc2fa018eebfb6b39d8be7bed53f5f8f89bc9010ef3f48e32d466bb8ce3f48e32d466bb8cebf5f8f89bc9010ef3f8cb032201189e03f47bcfd148f65eb3f47bcfd148f65ebbf8cb032201189e03fc275f010d1c2e93f1510444bc2fbe23f1510444bc2fbe2bfc275f010d1c2e93fa71645f97b2bc33f91177aac9ba3ef3f91177aac9ba3efbfa71645f97b2bc33fdcfd0ccbfbaaef3f0934fd4d9964c23f0934fd4d9964c2bfdcfd0ccbfbaaef3f1fa649ec2124e33fb2062ba4dfa4e93fb2062ba4dfa4e9bf1fa649ec2124e33fe992e786667feb3fb7b831ecf35de03fb7b831ecf35de0bfe992e786667feb3f0238bd80747bcf3f8c73cf145a04ef3f8c73cf145a04efbf0238bd80747bcf3f7a1939448f29ee3fb467f4124060d53fb467f4124060d5bf7a1939448f29ee3f9356fd14788adb3f60a09927b3e2ec3f60a09927b3e2ecbf9356fd14788adb3f33d3e29cb8c6e73f9f649751c36ae53f9f649751c36ae5bf33d3e29cb8c6e73f17835fbd01b1aa3fd3beb154dcf4ef3fd3beb154dcf4efbf17835fbd01b1aa3f8c531475fadaef3fa130c112874fb83fa130c112874fb8bf8c531475fadaef3fa2322b695a60e43f881dde1e87ace83f881dde1e87ace8bfa2322b695a60e43f04c041318344ec3fde41a966fffedd3fde41a966fffeddbf04c041318344ec3f2045954e1ac4d23f306b0136ec97ee3f306b0136ec97eebf2045954e1ac4d23f0058e69383a6ee3fba545599e663d23fba545599e663d2bf0058e69383a6ee3f25d83c6da857de3ff1e33149d12cec3ff1e33149d12cecbf25d83c6da857de3f554618756acce83f80432a5b7f39e43f80432a5b7f39e4bf554618756acce83f5ca824ebb6dfb93f9e5ca72d0dd6ef3f9e5ca72d0dd6efbf5ca824ebb6dfb93fee3c88567567ef3f0418c4271796c83f0418c4271796c8bfee3c88567567ef3f7248dc641bdce13fd25a546e678dea3fd25a546e678deabf7248dc641bdce13f8eb92c7a54a9ea3fbf73131750b2e13fbf73131750b2e1bf8eb92c7a54a9ea3ffa2ab6e9495bc93f5d6843eda65def3f5d6843eda65defbffa2ab6e9495bc93f463d8bdd009aed3f3f90f3aa6a4fd83f3f90f3aa6a4fd8bf463d8bdd009aed3f44edd5864bacd83f4fa44584c486ed3f4fa44584c486edbf44edd5864bacd83f9ce22fed5cb2e63f719ca1ead18ee63f719ca1ead18ee6bf9ce22fed5cb2e63fbaa4ccbef821693f021d6221f6ffef3f021d6221f6ffefbfbaa4ccbef821693f")); + const rootBytes = u8f(baswap64If(Float64Array.from(roots))); + if (bytesToHex(shake256(rootBytes)) !== "f45a496cf56ccc6e3e3395a20209206d81d71a7905a661447bd5bc0e24e0af1e") { + throw new Error("COMPLEX_ROOTS mismatch"); + } + return roots; +})(); +var intField = { + mul(x, y) { + let z = Math.imul(x, y); + let w = Math.imul(Q2, Math.imul(z, Q0I) & 65535); + z = (z + w >>> 16) - Q2; + z += Q2 & z >> 31; + return z >>> 0; + }, + inv(y) { + if (y === 0) + throw new Error("divison by zero"); + const e00 = this.mul(y, R2); + const e01 = this.mul(e00, e00); + const e02 = this.mul(e01, e00); + const e03 = this.mul(e02, e01); + const e04 = this.mul(e03, e03); + const e05 = this.mul(e04, e04); + const e06 = this.mul(e05, e05); + const e07 = this.mul(e06, e06); + const e08 = this.mul(e07, e07); + const e09 = this.mul(e08, e02); + const e10 = this.mul(e09, e08); + const e11 = this.mul(e10, e10); + const e12 = this.mul(e11, e11); + const e13 = this.mul(e12, e09); + const e14 = this.mul(e13, e13); + const e15 = this.mul(e14, e14); + const e16 = this.mul(e15, e10); + const e17 = this.mul(e16, e16); + const e18 = this.mul(e17, e00); + return e18; + }, + div: (x, y) => intField.mul(x, intField.inv(y)) +}; +function getIntPoly(logn) { + const n = 1 << logn; + const newPoly2 = (n2) => new Uint16Array(n2); + const F3 = Number(invert(BigInt(n), QBig)); + const { mod: mod2, smod, NTT } = genCrystals({ + N: n, + Q: Q2, + F: F3, + ROOT_OF_UNITY: 7, + newPoly: newPoly2, + isKyber: false, + brvBits: 10 + }); + const ntt = (r) => NTT.encode(r); + const intt = (r) => NTT.decode(r); + const signedCoder = { + encode: (p) => Int16Array.from(p, (x) => smod(x)), + decode: (p) => Uint16Array.from(p, (x) => mod2(x)) + }; + const intPoly = { + create: newPoly2, + smallSqnorm(f) { + let s = 0; + let ng = 0; + for (let u = 0; u < n; u++) { + const z = f[u]; + s = s + z * z >>> 0; + ng |= s; + } + return (s | -(ng >>> 31)) >>> 0; + }, + isShort(s1, s2) { + let s = 0 >>> 0; + let ng = 0 >>> 0; + for (let u = 0; u < n; u++) { + let z1 = s1[u] << 16 >> 16; + s = s + (z1 * z1 >>> 0) >>> 0; + ng |= s; + let z2 = s2[u] << 16 >> 16; + s = s + (z2 * z2 >>> 0) >>> 0; + ng |= s; + } + if (ng & 2147483648) + s = 4294967295; + return s <= L2BOUND[logn]; + }, + sub(a, b) { + for (let i = 0; i < n; i++) + a[i] = mod2(a[i] - b[i]); + return a; + }, + ntt, + intt, + toMontgomery(d) { + for (let i = 0; i < n; i++) + d[i] = intField.mul(d[i], R2); + return d; + }, + mul(f, d) { + for (let i = 0; i < n; i++) + f[i] = intField.mul(f[i], d[i]); + return f; + }, + div(f, d) { + for (let i = 0; i < n; i++) + f[i] = intField.div(f[i], d[i]); + this.intt(f); + return f; + } + }; + return { newPoly: newPoly2, intPoly, signedCoder }; +} +var fComplex = getComplex({ + ZERO: 0, + ONE: 1, + add: (x, y) => x + y, + sub: (x, y) => x - y, + mul: (x, y) => x * y, + div: (x, y) => x / y, + eql: (x, y) => x === y, + inv: (x) => 1 / x, + neg: (x) => -x +}); +var COMPLEX_ROOTS_O = ComplexArrInterleaved.decode(COMPLEX_ROOTS); +var FFTCoreRoots = {}; +var FFTCoreRootsConj = {}; +for (let logn = 0; logn < 10; logn++) { + const out = new Array(1 << logn); + const outC = new Array(1 << logn); + for (let i = 0, g1 = 1, g2 = 1; i < logn; i++) { + const ng = 1 << i; + for (let k = 0; k < ng; k++) + out[g1++] = COMPLEX_ROOTS_O[(ng << 1) + k]; + const ng2 = 1 << logn - i; + for (let k = 0; k < ng2 >> 1; k++) + outC[out.length - g2++] = fComplex.neg(fComplex.conj(COMPLEX_ROOTS_O[ng2 + k])); + } + FFTCoreRoots[logn] = out; + FFTCoreRootsConj[logn] = outC; +} +function getFloatPoly(logn) { + const n = 1 << logn; + const N_COMPLEX = n >> 1; + const hn = Math.log2(N_COMPLEX); + const fftOpts = { N: N_COMPLEX, invertButterflies: true, skipStages: 0, brp: false }; + const inv = 1 / N_COMPLEX; + return { + to: (f) => ComplexArr.decode(Array.from(f)), + from: (f) => new Float64Array(ComplexArr.encode(f)), + // Runtime callers also pass HashToPoint's Uint16Array output here; + // the implementation only needs a numeric typed-array shape, + // even though the local type is narrower. + convSmall: (f) => ComplexArr.decode(Array.from(f)), + add: (a, b) => a.map((i, j) => fComplex.add(i, b[j])), + sub: (a, b) => a.map((i, j) => fComplex.sub(i, b[j])), + neg: (a) => a.map((i) => fComplex.neg(i)), + mul: (a, b) => a.map((i, j) => fComplex.mul(i, b[j])), + conj: (a) => a.map((i) => fComplex.conj(i)), + mulConst: (a, x) => a.map((i) => fComplex.scale(i, x)), + scaleNorm: (a, b) => a.map((i, j) => fComplex.scale(i, b[j])), + invNorm: (a, b) => new Float64Array(a.map((i, j) => 1 / fComplex.magSqSum(i, b[j]))), + FFT: (f) => FFTCore(fComplex, { ...fftOpts, dit: false, roots: FFTCoreRoots[hn] })(f), + iFFT(f) { + FFTCore(fComplex, { ...fftOpts, dit: true, roots: FFTCoreRootsConj[hn] })(f); + for (let i = 0; i < f.length; i++) + f[i] = fComplex.scale(f[i], inv); + return f; + } + }; +} +function ApproxExp(x, ccs) { + const ev = [ + 0.9999999999999949, + 0.5000000000000192, + 0.16666666666698401, + 0.04166666666611049, + 0.008333333327800835, + 0.001388888894063187, + 1984127392773119e-19, + 2480156683358538e-20, + 27555863502191225e-22, + 2756073561604778e-22, + 2529950637944207e-23, + 2073772366009083e-24 + ]; + const y = -x; + let z = ev[ev.length - 1]; + for (let i = ev.length - 2; i >= 0; i--) + z = z * y + ev[i]; + return ccs * (1 + z * y); +} +function genFalcon(opts2) { + const { N: N3 } = opts2; + const logn = Math.log2(N3); + const id2 = (n) => n; + const { newPoly: newPoly2, intPoly, signedCoder } = getIntPoly(logn); + const floatPoly = getFloatPoly(logn); + class NTRU { + constructor(logn2, seed) { + __publicField(this, "logn"); + __publicField(this, "shake"); + this.logn = logn2; + this.shake = shake256.create().update(seed); + } + gaussSingle() { + const g = 1 << 10 - this.logn; + let val = 0; + for (let i = 0; i < g; i++) { + const r128 = bytesToNumberLE(this.shake.xof(16)); + const r1 = r128 & 0x7fffffffffffffffn; + const r2 = r128 >> 64n & 0x7fffffffffffffffn; + const sign2 = Number(r128 >> 63n & 1n); + let f = r1 < gauss_1024_12289[0] ? 1 : 0; + let v = 0; + for (let k = 1; k < gauss_1024_12289.length; k++) { + const tBit = r2 >= gauss_1024_12289[k] ? 1 : 0; + v |= k & -(tBit & (f ^ 1)); + f |= tBit; + } + val += sign2 === 1 ? -v : v; + } + return val; + } + polyGauss() { + const n = 1 << this.logn; + let mod2 = 0; + const f = new Int8Array(n); + for (let u = 0; u < n; u++) { + let s; + while (true) { + s = this.gaussSingle(); + if (s < -127 || s > 127) + continue; + if (u === n - 1) { + if ((mod2 ^ s & 1) === 0) + continue; + } + break; + } + if (u < n - 1) + mod2 ^= s & 1; + f[u] = s; + } + return f; + } + galoisNorm(logn2, a) { + const n = 1 << logn2; + const d = new Array(n >> 1); + for (let k = 0; k < n; k += 2) { + let s = 0n; + for (let i = 0; i <= k; i += 2) + s += a[i] * a[k - i]; + for (let i = k + 2; i < n; i += 2) + s -= a[i] * a[k + n - i]; + d[k >>> 1] = s; + } + for (let k = 0; k < n; k += 2) { + let s = 0n; + for (let i = 1; i < k; i += 2) + s += a[i] * a[k - i]; + for (let i = k + 1; i < n; i += 2) + s -= a[i] * a[k + n - i]; + d[k >>> 1] -= s; + } + return d; + } + mulConjD(logn2, d, a, b) { + const n = 1 << logn2; + for (let k = 0; k < n; k++) { + let s = 0n; + for (let i = 0; i <= k; i += 2) + s += b[i >>> 1] * a[k - i]; + for (let i = k + 2 - (k & 1); i < n; i += 2) + s -= b[i >>> 1] * a[k + n - i]; + if ((k & 1) === 0) + d[k] = s; + else + d[k] = -s; + } + return d; + } + subMul(logn2, a, b, c, e) { + const n = 1 << logn2; + for (let k = 0; k < n; k++) { + let s = 0n; + for (let i = 0; i <= k; i++) + s += b[i] * c[k - i]; + for (let i = k + 1; i < n; i++) + s -= b[i] * c[k + n - i]; + a[k] -= s << e; + } + return a; + } + reduce(logn2, f, g, F3, G, logn_top) { + const n = 1 << logn2; + const depth = logn_top - logn2; + const floatPoly2 = getFloatPoly(logn2); + const slen = MAX_BL_SMALL[depth]; + const llen = MAX_BL_LARGE[depth]; + let maxFGBits = BigInt(31 * llen); + let FGlen = BigInt(llen); + const scalefg = BigInt(31 * (slen - 10)); + const fgMaxBits = BITLENGTH[depth].avg + 6 * BITLENGTH[depth].std; + const fgMinBits = BITLENGTH[depth].avg - 6 * BITLENGTH[depth].std; + let scaleK = BigInt(Math.round(31 * llen - fgMinBits)); + let fx = new Float64Array(n); + let gx = new Float64Array(n); + for (let i = 0; i < n; i++) { + fx[i] = Number(f[i] >> scalefg); + gx[i] = Number(g[i] >> scalefg); + } + const rt3 = floatPoly2.conj(floatPoly2.FFT(floatPoly2.to(fx))); + const rt4 = floatPoly2.conj(floatPoly2.FFT(floatPoly2.to(gx))); + const rt5 = floatPoly2.invNorm(rt3, rt4); + const Fx = new Float64Array(n); + const Gx = new Float64Array(n); + const k = new Array(n); + while (true) { + let scaleFG = 31n * (FGlen - 10n); + for (let i = 0; i < n; i++) { + Fx[i] = Number(F3[i] >> scaleFG); + Gx[i] = Number(G[i] >> scaleFG); + } + const rt2 = floatPoly2.mul(floatPoly2.FFT(floatPoly2.to(Gx)), rt4); + const rt1 = floatPoly2.mul(floatPoly2.FFT(floatPoly2.to(Fx)), rt3); + const rt2f = floatPoly2.from(floatPoly2.iFFT(floatPoly2.scaleNorm(floatPoly2.add(rt2, rt1), rt5))); + const pdc = 2 ** Number(scaleFG - scalefg - scaleK); + for (let i = 0; i < n; i++) { + const BOUND = 2147483647; + const val = rt2f[i] * pdc; + if (val <= -BOUND || val >= BOUND) + return false; + k[i] = BigInt(Math.round(val)); + } + F3 = this.subMul(logn2, F3, f, k, scaleK); + G = this.subMul(logn2, G, g, k, scaleK); + const maxfgNew = scaleK + BigInt(Math.round(fgMaxBits)) + 10n; + if (maxfgNew < maxFGBits) + maxFGBits = maxfgNew; + if (FGlen > 1n && FGlen * 31n >= maxFGBits + 31n) + FGlen--; + if (scaleK <= 0n) + break; + scaleK -= 25n; + if (scaleK < 0n) + scaleK = 0n; + } + return true; + } + // This is recursive thing that goes from logn to 0 + solveBranch(logn2, f, g, F3, G, logn_top) { + if (logn2 === 0) { + const xf = f[0]; + const xg = g[0]; + if (xf <= 0n || xg <= 0n) + return false; + try { + const u1 = invert(xf, xg); + const v1 = (1n - u1 * xf) / xg; + F3[0] = -v1 * QBig; + G[0] = u1 * QBig; + return true; + } catch (e) { + return false; + } + } + if (logn_top === void 0) + logn_top = logn2; + const n = 1 << logn2; + const hn = n >>> 1; + if (!f || f.length < n || !g || g.length < n) + return false; + const fp = this.galoisNorm(logn2, f); + const gp = this.galoisNorm(logn2, g); + const Fp = new Array(hn); + const Gp = new Array(hn); + if (!this.solveBranch(logn2 - 1, fp, gp, Fp, Gp, logn_top)) + return false; + F3 = this.mulConjD(logn2, F3, g, Fp); + G = this.mulConjD(logn2, G, f, Gp); + return this.reduce(logn2, f, g, F3, G, logn_top); + } + solve(f, g) { + const n = 1 << logn; + const bf = Array.from(f).map(BigInt); + const bg = Array.from(g).map(BigInt); + const bF = new Array(n); + const bG = new Array(n); + if (!this.solveBranch(logn, bf, bg, bF, bG)) + return false; + const F3 = new Int8Array(n); + const G = new Int8Array(n); + for (let i = 0; i < n; i++) { + const x = bF[i]; + const y = bG[i]; + if (x < -127 || x > 127 || y < -127 || y > 127) + return false; + F3[i] = Number(x); + G[i] = Number(y); + } + return [F3, G]; + } + generate() { + let max = 1e6; + let curr = 0; + while (true) { + if (curr++ === max) + throw new Error("can't generate key"); + const f = this.polyGauss(); + const g = this.polyGauss(); + let lim = 1 << opts2.fgBits - 1; + for (let u = 0; u < N3; u++) { + if (f[u] >= lim || f[u] <= -lim || g[u] >= lim || g[u] <= -lim) { + lim = -1; + break; + } + } + if (lim < 0) + continue; + const normf = intPoly.smallSqnorm(f); + const normg = intPoly.smallSqnorm(g); + const norm = normf + normg | -((normf | normg) >>> 31); + if (norm >= 16823) + continue; + let rt1 = floatPoly.FFT(floatPoly.convSmall(f)); + let rt2 = floatPoly.FFT(floatPoly.convSmall(g)); + const rt3 = floatPoly.invNorm(rt1, rt2); + rt1 = floatPoly.iFFT(floatPoly.scaleNorm(floatPoly.mulConst(floatPoly.conj(rt1), Q2), rt3)); + rt2 = floatPoly.iFFT(floatPoly.scaleNorm(floatPoly.mulConst(floatPoly.conj(rt2), Q2), rt3)); + let bnorm = 0; + for (let u = 0; u < rt1.length; u++) { + bnorm += rt1[u].re * rt1[u].re; + bnorm += rt2[u].re * rt2[u].re; + } + for (let u = 0; u < rt1.length; u++) { + bnorm += rt1[u].im * rt1[u].im; + bnorm += rt2[u].im * rt2[u].im; + } + if (!(bnorm < BNORM_MAX)) + continue; + let pub; + try { + pub = computePublic(f, g); + } catch (_) { + continue; + } + const solved = this.solve(f, g); + if (solved === false) + continue; + return [f, g, solved[0], solved[1], pub]; + } + } + } + const modqCoder = () => { + const coder = bitsCoderMSB(newPoly2, N3, 14, { + encode: id2, + decode: id2 + }); + return { + bytesLen: coder.bytesLen, + encode(poly) { + for (let i = 0; i < poly.length; i++) + if (poly[i] >= 12289) + throw new Error("public key coeff out of range"); + return coder.encode(poly); + }, + decode(bytes) { + if (bytes.length !== coder.bytesLen) + throw new Error("wrong public key length"); + const poly = coder.decode(bytes); + for (let i = 0; i < poly.length; i++) + if (poly[i] >= 12289) + throw new Error("public key coeff out of range"); + const normalized = coder.encode(poly); + if (normalized.length !== bytes.length) + throw new Error("wrong public key length"); + for (let i = 0; i < bytes.length; i++) + if (bytes[i] !== normalized[i]) + throw new Error("wrong public key encoding"); + return poly; + } + }; + }; + const trimI8Coder = (bits) => { + const shift = 32 - bits; + const coder = bitsCoderMSB((len) => new Int8Array(len), N3, bits, { + encode: (v) => v & (1 << bits) - 1, + decode: (w) => (w & getMask(bits)) << shift >> shift + }); + return { + bytesLen: coder.bytesLen, + encode(poly) { + const max = (1 << bits - 1) - 1; + const min = -max; + for (let i = 0; i < poly.length; i++) + if (poly[i] < min || poly[i] > max) + throw new Error("private key coeff out of range"); + return coder.encode(poly); + }, + decode(bytes) { + const poly = coder.decode(bytes); + const min = -(1 << bits - 1); + for (let i = 0; i < poly.length; i++) + if (poly[i] === min) + throw new Error("forbidden private key coeff"); + return poly; + } + }; + }; + const fgCoder = trimI8Coder(opts2.fgBits); + const FGCoder = trimI8Coder(opts2.FGBits); + const secretKeyCoder = headerCoder(80 + logn, splitCoder("falcon.secretKey", fgCoder, fgCoder, FGCoder)); + const publicKeyCoder = headerCoder(0 + logn, modqCoder()); + const decodePaddedSig = (s2) => { + const normalized = compCoder(N3).encode(compCoder(N3).decode(s2)); + for (let i = normalized.length; i < s2.length; i++) + if (s2[i] !== 0) + throw new Error("non-zero padding"); + return normalized; + }; + const decodeUnpaddedSig = (s2) => { + const normalized = compCoder(N3).encode(compCoder(N3).decode(s2)); + if (normalized.length !== s2.length) + throw new Error("wrong signature length"); + return s2; + }; + const decodeSig = opts2.padded ? decodePaddedSig : decodeUnpaddedSig; + const SignatureCoderBasic = (logn2) => { + const TYPE_BYTE = 32 + logn2; + return { + encode({ msg, nonce, s2 }) { + let compressed = s2; + const payloadLen = 1 + compressed.length; + const totalLen = 2 + NONCELEN + msg.length + payloadLen; + const out = new Uint8Array(totalLen); + let i = 0; + out[i++] = payloadLen >> 8 & 255; + out[i++] = payloadLen & 255; + out.set(nonce, i); + i += NONCELEN; + out.set(msg, i); + i += msg.length; + out[i++] = TYPE_BYTE; + out.set(compressed, i); + return out; + }, + decode(data) { + if (!data || data.length < NONCELEN + 3) + throw new Error("signature coder: wrong length"); + const len = data[0] << 8 | data[1]; + const s2Len = len - 1; + const msgLen = data.length - NONCELEN - 3 - s2Len; + if (msgLen < 0) + throw new Error("signature coder: wrong msg length"); + const typeByte = data[2 + NONCELEN + msgLen]; + if (typeByte !== TYPE_BYTE) + throw new Error("signature coder: wrong type byte"); + const nonce = data.subarray(2, 2 + NONCELEN); + const msg = data.subarray(2 + NONCELEN, 2 + NONCELEN + msgLen); + const s2 = decodeUnpaddedSig(data.subarray(2 + NONCELEN + msgLen + 1)); + if (s2.length !== s2Len) + throw new Error("signature coder: wrong s2 length"); + return { msg, nonce, s2 }; + } + }; + }; + const SignatureCoderPadded = (logn2) => { + const sigLen = opts2.paddedLen; + return { + encode({ msg, nonce, s2 }) { + return headerCoder(48 + logn2, splitCoder("falcon.signature", NONCELEN, sigLen, msg.length)).encode([nonce, pad(sigLen).encode(s2), msg]); + }, + decode(data) { + const msgLen = data.length - NONCELEN - sigLen - 1; + const [nonce, s2, msg] = headerCoder(48 + logn2, splitCoder("falcon.signature", NONCELEN, sigLen, msgLen)).decode(data); + return { nonce, s2: decodeSig(s2), msg }; + } + }; + }; + const SignatureCoderDetached = (logn2) => { + const sigLen = opts2.padded ? opts2.sigLen - 1 - NONCELEN : opts2.detachedLen; + const getSigLen = (s2) => opts2.padded ? sigLen : s2.length; + return { + encode({ nonce, s2 }) { + return headerCoder(48 + logn2, splitCoder("falcon.detachedSignature", NONCELEN, getSigLen(s2))).encode([nonce, opts2.padded ? pad(sigLen).encode(s2) : s2]); + }, + decode(data) { + const [nonce, raw] = headerCoder(48 + logn2, splitCoder("falcon.detachedSignature", NONCELEN, data.length - NONCELEN - 1)).decode(data); + const s2 = decodeSig(raw); + return { nonce, s2 }; + } + }; + }; + const SignatureCoder = (opts2.padded ? SignatureCoderPadded : SignatureCoderBasic)(logn); + const invertF = (f) => { + const tt = intPoly.ntt(signedCoder.decode(f)); + for (let u = 0; u < N3; u++) + if (tt[u] === 0) + throw new Error("invalid secretKey: non-invertible f"); + return tt; + }; + function computePublic(f, g) { + const tt = invertF(f); + const h = intPoly.ntt(signedCoder.decode(g)); + const res2 = intPoly.div(h, tt); + cleanBytes(tt); + return res2; + } + function completePrivate(f, g, F3) { + let t1 = intPoly.toMontgomery(intPoly.ntt(signedCoder.decode(g))); + const t2 = intPoly.ntt(signedCoder.decode(F3)); + const tt = invertF(f); + t1 = intPoly.div(intPoly.mul(t1, t2), tt); + const G = new Int8Array(N3); + for (let u = 0; u < N3; u++) { + let w = t1[u]; + w -= Q2 & ~-(w - Qhalf >>> 31); + const gi = w | 0; + if (gi < -127 || gi > 127) { + cleanBytes(t1, t2, tt, G); + throw new Error("Coefficient out of bounds"); + } + G[u] = gi; + } + cleanBytes(t1, t2, tt); + return G; + } + function HashToPoint(nonce, msg) { + const h = shake256.create().update(nonce).update(msg); + const c = new Uint16Array(N3); + const kQ = 5 * Q2; + for (let i = 0; i < N3; ) { + const tmp = h.xof(2); + let w = tmp[0] << 8 | tmp[1]; + if (w < kQ) + c[i++] = w % Q2; + } + return c; + } + class FFSampler { + constructor(logn2, seed, b00, b01, b10, b11) { + __publicField(this, "logn"); + // Shake + __publicField(this, "shake"); + __publicField(this, "shakeBuf"); + __publicField(this, "ctrView"); + // ChaCha + __publicField(this, "ctr", 0n); + __publicField(this, "buf"); + __publicField(this, "buf32"); + __publicField(this, "pos"); + __publicField(this, "key"); + __publicField(this, "nonce32"); + __publicField(this, "curBlock"); + __publicField(this, "curBlock32"); + __publicField(this, "view"); + // Sampler + __publicField(this, "b01"); + __publicField(this, "b11"); + __publicField(this, "g00"); + __publicField(this, "g01"); + __publicField(this, "g11"); + this.logn = logn2; + this.shake = shake256.create().update(seed); + this.shakeBuf = new Uint8Array(56); + this.key = this.shakeBuf.subarray(0, 32); + this.nonce32 = u32(this.shakeBuf.subarray(32, 48)); + this.ctrView = createView(this.shakeBuf.subarray(48, 56)); + this.curBlock = new Uint8Array(64); + this.curBlock32 = u32(this.curBlock); + this.buf = new Uint8Array(8 * this.curBlock.length); + this.buf32 = u32(this.buf); + this.pos = this.buf.length; + this.view = createView(this.buf); + this.b01 = b01; + this.b11 = b11; + const { g00, g01, g11 } = this.gramFFT(b00, b10); + this.g00 = g00; + this.g01 = g01; + this.g11 = g11; + } + destroy() { + this.shake.destroy(); + cleanBytes(this.shakeBuf, this.curBlock, this.buf); + cleanCPoly(this.b01, this.b11, this.g00, this.g01, this.g11); + } + refill(minBytes) { + if (this.buf.length - this.pos >= minBytes) + return; + const out32 = swap32IfBE(this.buf32); + for (let i = 0; i < 8; i++, this.ctr++) { + const n = swap32IfBE(this.nonce32.slice()); + n[2] ^= Number(this.ctr & 0xffffffffn); + n[3] ^= Number(this.ctr >> 32n); + swap32IfBE(n.subarray(1)); + chacha20(this.key, u8(n.subarray(1)), EMPTY_CHACHA20_BLOCK, this.curBlock, n[0]); + const block32 = swap32IfBE(this.curBlock32); + for (let j = 0; j < 16; j++) + out32[i + j * 8] = block32[j]; + swap32IfBE(block32); + } + swap32IfBE(out32); + this.pos = 0; + } + // Sampler + gaussian0() { + this.refill(9); + const t0 = this.view.getUint32(this.pos, true); + const t1 = this.view.getUint32(this.pos + 4, true); + const t2 = this.buf[this.pos + 8]; + this.pos += 9; + const v0 = t0 & 16777215; + const v1 = t0 >>> 24 & 255 | (t1 & 65535) << 8; + const v2 = t1 >>> 16 & 65535 | t2 << 16; + let z = 0; + for (let i = 0; i < GAUSS0.length; i += 3) { + let cc = v0 - GAUSS0[i + 2] >>> 31; + cc = (v1 - GAUSS0[i + 1] | 0) - cc >>> 31; + cc = (v2 - GAUSS0[i + 0] | 0) - cc >>> 31; + z += cc; + } + return z; + } + berExp(x, ccs) { + let s = Math.trunc(x * 1.4426950408889634); + const r = x - s * 0.6931471805599453; + let e = ApproxExp(r, ccs); + e *= 2147483648; + let z1 = e | 0; + e = (e - z1) * 4294967296; + let z0 = e | 0; + z1 = z1 << 1 | z0 >>> 31; + z0 <<= 1; + s = (s | 63 - s >>> 26) & 63; + const sm = -(s >>> 5) | 0; + z0 ^= sm & (z0 ^ z1); + z1 &= ~sm; + s &= 31; + z0 = z0 >>> s | z1 << 31 - s << 1; + z1 >>>= s; + for (let j = 0; j < 2; j++) { + for (let i = 24; i >= 0; i -= 8) { + this.refill(1); + const w = this.buf[this.pos++]; + const bz = z1 >>> i & 255; + if (w !== bz) + return w < bz; + } + z1 = z0; + } + return false; + } + samplerZ(mu, isigma) { + const s = Math.floor(mu); + const r = mu - s; + const dss = isigma * isigma * 0.5; + const ccs = isigma * SIGMA_MIN[this.logn]; + for (; ; ) { + const z0 = this.gaussian0(); + this.refill(1); + const b = this.buf[this.pos++] & 1; + const z = ((z0 << 1) + 1 & -b) - z0; + let x = z - r; + x = x * x * dss - z0 * z0 * 0.15086504887537272; + if (this.berExp(x, ccs)) + return s + z; + } + } + ldlFFT(logn2, g00t, g01t, g11t) { + g00t = g00t.slice(); + const hn = 1 << logn2 - 1; + for (let i = 0; i < hn; i++) { + const g01 = g01t[i]; + const g11 = g11t[i]; + const mu = fComplex.scale(g01, 1 / g00t[i].re); + g11t[i] = { re: g11.re - (mu.re * g01.re + mu.im * g01.im), im: g11.im }; + g01t[i] = fComplex.conj(mu); + } + return { g00: g00t, g01: g01t, g11: g11t }; + } + splitFFT(logn2, f) { + const hn = 1 << logn2 - 1; + const qn = hn >> 1; + if (logn2 === 1) + return { f0: [{ re: f[0].re, im: 0 }], f1: [{ re: f[0].im, im: 0 }] }; + const f0t = new Array(qn); + const f1t = new Array(qn); + const ft = f; + for (let i = 0; i < qn; i++) { + const a = ft[(i << 1) + 0]; + const b = ft[(i << 1) + 1]; + f0t[i] = fComplex.scale(fComplex.add(a, b), 0.5); + f1t[i] = fComplex.scale(fComplex.mul(fComplex.sub(a, b), fComplex.conj(COMPLEX_ROOTS_O[i + hn])), 0.5); + } + return { f0: f0t, f1: f1t }; + } + splitSelfAdjFFT(logn2, f) { + const hn = 1 << logn2 - 1; + const qn = hn >> 1; + if (logn2 === 1) + return { f0: [{ re: f[0].re, im: 0 }], f1: [{ re: 0, im: 0 }] }; + const f0t = new Array(qn); + const f1t = new Array(qn); + const ft = f; + for (let i = 0; i < qn; i++) { + const a = ft[(i << 1) + 0]; + const b = ft[(i << 1) + 1]; + f0t[i] = fComplex.scale(fComplex.add(a, b), 0.5); + f1t[i] = fComplex.scale(fComplex.scale(fComplex.conj(COMPLEX_ROOTS_O[i + hn]), fComplex.sub(a, b).re), 0.5); + } + return { f0: f0t, f1: f1t }; + } + mergeFFT(logn2, f0, f1) { + const hn = 1 << logn2 - 1; + const qn = hn >> 1; + if (logn2 === 1) + return [{ re: f0[0].re, im: f1[0].re }]; + const ft = new Array(2 * qn); + for (let i = 0; i < qn; i++) { + const a = f0[i]; + const c = fComplex.mul(f1[i], COMPLEX_ROOTS_O[i + hn]); + ft[(i << 1) + 0] = fComplex.add(a, c); + ft[(i << 1) + 1] = fComplex.sub(a, c); + } + return ft; + } + gramFFT(b00, b10) { + const { b01, b11 } = this; + const hn = 1 << this.logn >> 1; + const g00 = new Array(hn); + const g01 = new Array(hn); + const g11 = new Array(hn); + for (let i = 0; i < hn; i++) { + const b00t = b00[i]; + const b01t = b01[i]; + const b10t = b10[i]; + const b11t = b11[i]; + const u = fComplex.mul(b00t, fComplex.conj(b10t)); + const v = fComplex.mul(b01t, fComplex.conj(b11t)); + g00[i] = { re: fComplex.magSqSum(b00t, b01t), im: 0 }; + g01[i] = fComplex.add(u, v); + g11[i] = { re: fComplex.magSqSum(b10t, b11t), im: 0 }; + } + return { g00, g01, g11 }; + } + ffsampRec(logn2, t0, t1, g00i, g01i, g11i) { + if (logn2 === 0) { + const leaf = Math.sqrt(g00i[0].re) * INV_SIGMA[this.logn]; + const t0re = this.samplerZ(t0[0].re, leaf); + const t1re = this.samplerZ(t1[0].re, leaf); + return { t0: [{ re: t0re, im: 0 }], t1: [{ re: t1re, im: 0 }] }; + } + const { g00, g01, g11 } = this.ldlFFT(logn2, g00i, g01i, g11i); + const { f0: g00f0, f1: g00f1 } = this.splitSelfAdjFFT(logn2, g00); + const { f0: g11f0, f1: g11f1 } = this.splitSelfAdjFFT(logn2, g11); + const { f0: t1f0in, f1: t1f1in } = this.splitFFT(logn2, t1); + const { t0: t1f0out, t1: t1f1out } = this.ffsampRec(logn2 - 1, t1f0in, t1f1in, g11f0, g11f1, g11f0); + const t1new = this.mergeFFT(logn2, t1f0out, t1f1out); + const t0tmp = floatPoly.add(t0, floatPoly.mul(g01, floatPoly.sub(t1, t1new))); + const { f0: t0f0in, f1: t0f1in } = this.splitFFT(logn2, t0tmp); + const { t0: t0f0out, t1: t0f1out } = this.ffsampRec(logn2 - 1, t0f0in, t0f1in, g00f0, g00f1, g00f0); + const z1 = this.mergeFFT(logn2, t0f0out, t0f1out); + return { t0: z1, t1: t1new }; + } + // sampling a preimage in FFT domain + sample(hm) { + const t0t = floatPoly.FFT(floatPoly.convSmall(hm)); + const t0f = floatPoly.mulConst(floatPoly.mul(t0t, this.b11), F_INV_Q); + const t1f = floatPoly.mulConst(floatPoly.mul(t0t, this.b01), F_MINUS_INV_Q); + this.shake.xofInto(this.shakeBuf); + this.ctr = this.ctrView.getBigUint64(0, true); + return this.ffsampRec(this.logn, t0f, t1f, this.g00, this.g01, this.g11); + } + } + const signRaw = (sk, msg, maxLen, rnd = randomBytes) => { + abytes(msg); + const nonce = rnd(40); + abytes(nonce, 40, "nonce"); + const hm = HashToPoint(nonce, msg); + const seed = rnd(48); + abytes(seed, 48, "seed"); + try { + const [f, g, F3] = secretKeyCoder.decode(sk); + try { + const G = completePrivate(f, g, F3); + const b00 = floatPoly.FFT(floatPoly.convSmall(g)); + const b01 = floatPoly.FFT(floatPoly.neg(floatPoly.convSmall(f))); + const b10 = floatPoly.FFT(floatPoly.convSmall(G)); + const b11 = floatPoly.FFT(floatPoly.neg(floatPoly.convSmall(F3))); + const sampler = new FFSampler(logn, seed, b00, b01, b10, b11); + const s2 = new Int16Array(N3); + try { + while (true) { + const { t0, t1 } = sampler.sample(hm); + const t2 = floatPoly.add(floatPoly.mul(t0, b00), floatPoly.mul(t1, b10)); + const t3 = floatPoly.mul(t0, b01); + const t4 = floatPoly.iFFT(t2); + const t5 = floatPoly.iFFT(floatPoly.add(floatPoly.mul(t1, b11), t3)); + const hn = N3 >> 1; + let sqn = 0; + for (let i = 0; i < hn; i++) { + sqn += (hm[i] - (Math.round(t4[i].re) | 0)) ** 2; + sqn += (hm[hn + i] - (Math.round(t4[i].im) | 0)) ** 2; + const z = -Math.round(t5[i].re); + sqn += z * z; + s2[i] = z & 65535; + const z2 = -Math.round(t5[i].im); + sqn += z2 * z2; + s2[i + hn] = z2 & 65535; + } + cleanCPoly(t0, t1, t2, t3, t4, t5); + if (!(sqn <= L2BOUND[logn])) + continue; + const s2comp = compCoder(N3).encode(s2); + if (s2comp.length > maxLen) { + cleanBytes(s2comp); + continue; + } + return { s2: s2comp, nonce, msg }; + } + } finally { + cleanBytes(s2); + sampler.destroy(); + cleanCPoly(b00, b01, b10, b11); + cleanBytes(G); + } + } finally { + cleanBytes(f, g, F3); + } + } finally { + cleanBytes(seed); + } + }; + const verifyRaw = (pk, s2comp, nonce, msg) => { + const s2 = compCoder(N3).decode(s2comp); + const c0 = HashToPoint(nonce, msg); + const h = intPoly.toMontgomery(intPoly.ntt(publicKeyCoder.decode(pk))); + const s1 = intPoly.intt(intPoly.mul(intPoly.ntt(signedCoder.decode(s2)), h)); + intPoly.sub(s1, c0); + return intPoly.isShort(signedCoder.encode(s1), s2); + }; + const info = Object.freeze({ type: "falcon" }); + const keyLengths = Object.freeze({ + seed: 48, + publicKey: publicKeyCoder.bytesLen, + secretKey: secretKeyCoder.bytesLen + }); + const getRnd = (opts3 = {}) => { + validateSigOpts2(opts3); + if (opts3.context !== void 0) + throw new Error("context is not supported"); + if (opts3.random !== void 0) + return opts3.random; + if (opts3.extraEntropy === void 0) + return randomBytes; + const seed = opts3.extraEntropy === false ? new Uint8Array(48) : opts3.extraEntropy; + abytes(seed, 48, "opts.extraEntropy"); + const drbg = rngAesCtrDrbg256(seed); + return (len = 0) => drbg.randomBytes(len); + }; + const checkVerOpts = (opts3 = {}) => { + validateVerOpts(opts3); + if (opts3.context !== void 0) + throw new Error("context is not supported"); + }; + const tests = Object.freeze({ + publicKeyCoder: Object.freeze(publicKeyCoder), + privateKeyCoder: Object.freeze(secretKeyCoder), + maxS2Len: opts2.maxS2Len + }); + const attachedLengths = Object.freeze({ ...keyLengths, signRand: 48 }); + const lengths = opts2.padded ? Object.freeze({ ...attachedLengths, signature: opts2.sigLen }) : attachedLengths; + const keygen = (seed) => { + const randSeed = seed === void 0; + if (randSeed) + seed = randomBytes(48); + abytes(seed, 48, "seed"); + const [f, g, F3, _G, pub] = new NTRU(logn, seed).generate(); + const sk = secretKeyCoder.encode([f, g, F3]); + const pk = publicKeyCoder.encode(pub); + if (randSeed) + cleanBytes(seed); + cleanBytes(f, g, F3, _G); + return { publicKey: pk, secretKey: sk }; + }; + const getPublicKey = (sk) => { + const [f, g, F3] = secretKeyCoder.decode(sk); + try { + const h = computePublic(f, g); + cleanBytes(f, g, F3); + return publicKeyCoder.encode(h); + } catch (e) { + cleanBytes(f, g, F3); + throw e; + } + }; + const sign = (msg, sk, sigOpts = {}) => { + const { s2, nonce } = signRaw(sk, msg, opts2.maxS2Len, getRnd(sigOpts)); + return SignatureCoderDetached(logn).encode({ nonce, s2 }); + }; + const verify = (sig, msg, pk, verOpts = {}) => { + checkVerOpts(verOpts); + abytes(sig); + abytes(msg); + abytes(pk); + try { + const { s2, nonce } = SignatureCoderDetached(logn).decode(sig); + return verifyRaw(pk, s2, nonce, msg); + } catch { + return false; + } + }; + const attached = Object.freeze({ + info, + lengths: attachedLengths, + keygen, + getPublicKey, + seal(msg, sk, sigOpts = {}) { + const { s2, nonce } = signRaw(sk, msg, opts2.maxS2Len, getRnd(sigOpts)); + return SignatureCoder.encode({ msg, nonce, s2 }); + }, + open(sig, pk, verOpts = {}) { + checkVerOpts(verOpts); + const { s2, nonce, msg } = SignatureCoder.decode(sig); + if (verifyRaw(pk, s2, nonce, msg)) + return msg; + throw new Error("invalid signature"); + } + }); + const res = { + info, + lengths, + attached, + keygen, + getPublicKey, + sign, + verify + }; + res.__test = tests; + return Object.freeze(res); +} +var falcon512opts = { + N: 512, + // Table 3.3 fixed padded detached bytes, including the detached header byte and 40-byte nonce. + sigLen: 666, + fgBits: 6, + FGBits: 8, + // Compressed-s payload bytes only, excluding the detached header byte and 40-byte nonce. + paddedLen: 625, + // Payload-only budget: genFalcon() adds the detached header byte and 40-byte nonce around it. + detachedLen: 690 +}; +var falcon512 = /* @__PURE__ */ (() => genFalcon({ ...falcon512opts, maxS2Len: 711 }))(); + +// node_modules/@noble/post-quantum/ml-kem.js +var N2 = 256; +var Q3 = 3329; +var F2 = 3303; +var ROOT_OF_UNITY2 = 17; +var crystals2 = /* @__PURE__ */ genCrystals({ + N: N2, + Q: Q3, + F: F2, + ROOT_OF_UNITY: ROOT_OF_UNITY2, + newPoly: (n) => new Uint16Array(n), + brvBits: 7, + isKyber: true +}); +var PARAMS3 = /* @__PURE__ */ (() => Object.freeze({ + 512: Object.freeze({ N: N2, Q: Q3, K: 2, ETA1: 3, ETA2: 2, du: 10, dv: 4, RBGstrength: 128 }), + 768: Object.freeze({ N: N2, Q: Q3, K: 3, ETA1: 2, ETA2: 2, du: 10, dv: 4, RBGstrength: 192 }), + 1024: Object.freeze({ N: N2, Q: Q3, K: 4, ETA1: 2, ETA2: 2, du: 11, dv: 5, RBGstrength: 256 }) +}))(); +var compress = (d) => { + if (d >= 12) + return { encode: (i) => i, decode: (i) => i >= Q3 ? i - Q3 : i }; + const a = 2 ** (d - 1); + return { + // This only matches standalone Compress_d after bitsCoder masks the result into Z_(2^d). + encode: (i) => ((i << d) + Q3 / 2) / Q3, + // const decompress = (i: number) => round((Q / 2 ** d) * i); + decode: (i) => i * Q3 + a >>> d + }; +}; +var byteCoder = (d) => crystals2.bitsCoder(d, d === 12 ? { encode: (i) => i, decode: (i) => i >= Q3 ? i - Q3 : i } : { encode: (i) => i, decode: (i) => i }); +var polyCoder2 = (d) => d === 12 ? byteCoder(12) : crystals2.bitsCoder(d, compress(d)); +function polyAdd2(a_, b_) { + const a = a_; + const b = b_; + for (let i = 0; i < N2; i++) + a[i] = crystals2.mod(a[i] + b[i]); +} +function polySub2(a_, b_) { + const a = a_; + const b = b_; + for (let i = 0; i < N2; i++) + a[i] = crystals2.mod(a[i] - b[i]); +} +function BaseCaseMultiply(a0, a1, b0, b1, zeta) { + const c0 = crystals2.mod(a1 * b1 * zeta + a0 * b0); + const c1 = crystals2.mod(a0 * b1 + a1 * b0); + return { c0, c1 }; +} +function MultiplyNTTs2(f_, g_) { + const f = f_; + const g = g_; + for (let i = 0; i < N2 / 2; i++) { + let z = crystals2.nttZetas[64 + (i >> 1)]; + if (i & 1) + z = -z; + const { c0, c1 } = BaseCaseMultiply(f[2 * i + 0], f[2 * i + 1], g[2 * i + 0], g[2 * i + 1], z); + f[2 * i + 0] = c0; + f[2 * i + 1] = c1; + } + return f; +} +function SampleNTT(xof_) { + const xof = xof_; + const r = new Uint16Array(N2); + for (let j = 0; j < N2; ) { + const b = xof(); + if (b.length % 3) + throw new Error("SampleNTT: unaligned block"); + for (let i = 0; j < N2 && i + 3 <= b.length; i += 3) { + const d1 = (b[i + 0] >> 0 | b[i + 1] << 8) & 4095; + const d2 = (b[i + 1] >> 4 | b[i + 2] << 4) & 4095; + if (d1 < Q3) + r[j++] = d1; + if (j < N2 && d2 < Q3) + r[j++] = d2; + } + } + return r; +} +var sampleCBDBytes = (buf, eta) => { + const r = new Uint16Array(N2); + const b32 = u32(buf); + swap32IfBE(b32); + let len = 0; + for (let i = 0, p = 0, bb = 0, t0 = 0; i < b32.length; i++) { + let b = b32[i]; + for (let j = 0; j < 32; j++) { + bb += b & 1; + b >>= 1; + len += 1; + if (len === eta) { + t0 = bb; + bb = 0; + } else if (len === 2 * eta) { + r[p++] = crystals2.mod(t0 - bb); + bb = 0; + len = 0; + } + } + } + swap32IfBE(b32); + if (len) + throw new Error(`sampleCBD: leftover bits: ${len}`); + return r; +}; +function sampleCBD(PRF_, seed, nonce, eta) { + const PRF = PRF_; + return sampleCBDBytes(PRF(eta * N2 / 4, seed, nonce), eta); +} +var genKPKE = (opts_) => { + const opts2 = opts_; + const { K, PRF, XOF, HASH512, ETA1, ETA2, du, dv } = opts2; + const poly1 = polyCoder2(1); + const polyV = polyCoder2(dv); + const polyU = polyCoder2(du); + const publicCoder = splitCoder("publicKey", vecCoder(polyCoder2(12), K), 32); + const secretCoder = vecCoder(polyCoder2(12), K); + const cipherCoder = splitCoder("ciphertext", vecCoder(polyU, K), polyV); + const seedCoder = splitCoder("seed", 32, 32); + return { + secretCoder, + lengths: { + secretKey: secretCoder.bytesLen, + publicKey: publicCoder.bytesLen, + cipherText: cipherCoder.bytesLen + }, + keygen: (seed) => { + abytesDoc(seed, 32, "seed"); + const seedDst = new Uint8Array(33); + seedDst.set(seed); + seedDst[32] = K; + const seedHash = HASH512(seedDst); + const [rho, sigma] = seedCoder.decode(seedHash); + const sHat = []; + const tHat = []; + for (let i = 0; i < K; i++) + sHat.push(crystals2.NTT.encode(sampleCBD(PRF, sigma, i, ETA1))); + const x = XOF(rho); + for (let i = 0; i < K; i++) { + const e = crystals2.NTT.encode(sampleCBD(PRF, sigma, K + i, ETA1)); + for (let j = 0; j < K; j++) { + const aji = SampleNTT(x.get(j, i)); + polyAdd2(e, MultiplyNTTs2(aji, sHat[j])); + } + tHat.push(e); + } + x.clean(); + const res = { + publicKey: publicCoder.encode([tHat, rho]), + secretKey: secretCoder.encode(sHat) + }; + cleanBytes(rho, sigma, sHat, tHat, seedDst, seedHash); + return res; + }, + encrypt: (publicKey, msg, seed) => { + const [tHat, rho] = publicCoder.decode(publicKey); + const rHat = []; + for (let i = 0; i < K; i++) + rHat.push(crystals2.NTT.encode(sampleCBD(PRF, seed, i, ETA1))); + const x = XOF(rho); + const tmp2 = new Uint16Array(N2); + const u = []; + for (let i = 0; i < K; i++) { + const e1 = sampleCBD(PRF, seed, K + i, ETA2); + const tmp = new Uint16Array(N2); + for (let j = 0; j < K; j++) { + const aij = SampleNTT(x.get(i, j)); + polyAdd2(tmp, MultiplyNTTs2(aij, rHat[j])); + } + polyAdd2(e1, crystals2.NTT.decode(tmp)); + u.push(e1); + polyAdd2(tmp2, MultiplyNTTs2(tHat[i], rHat[i])); + cleanBytes(tmp); + } + x.clean(); + const e2 = sampleCBD(PRF, seed, 2 * K, ETA2); + polyAdd2(e2, crystals2.NTT.decode(tmp2)); + const v = poly1.decode(msg); + polyAdd2(v, e2); + cleanBytes(tHat, rHat, tmp2, e2); + return cipherCoder.encode([u, v]); + }, + decrypt: (cipherText, privateKey) => { + const [u, v] = cipherCoder.decode(cipherText); + const sk = secretCoder.decode(privateKey); + const tmp = new Uint16Array(N2); + for (let i = 0; i < K; i++) + polyAdd2(tmp, MultiplyNTTs2(sk[i], crystals2.NTT.encode(u[i]))); + polySub2(v, crystals2.NTT.decode(tmp)); + cleanBytes(tmp, sk, u); + return poly1.encode(v); + } + }; +}; +function createKyber(opts2) { + const rawOpts = opts2; + const KPKE = genKPKE(rawOpts); + const { HASH256, HASH512, KDF } = rawOpts; + const { secretCoder: KPKESecretCoder, lengths } = KPKE; + const secretCoder = splitCoder("secretKey", lengths.secretKey, lengths.publicKey, 32, 32); + const msgLen = 32; + const seedLen = 64; + const kemLengths = Object.freeze({ + ...lengths, + seed: 64, + msg: msgLen, + msgRand: msgLen, + secretKey: secretCoder.bytesLen + }); + return Object.freeze({ + info: Object.freeze({ type: "ml-kem" }), + lengths: kemLengths, + keygen: (seed = randomBytes3(seedLen)) => { + abytesDoc(seed, seedLen, "seed"); + const { publicKey, secretKey: sk } = KPKE.keygen(seed.subarray(0, 32)); + const publicKeyHash = HASH256(publicKey); + const secretKey = secretCoder.encode([sk, publicKey, publicKeyHash, seed.subarray(32)]); + cleanBytes(sk, publicKeyHash); + return { + publicKey, + secretKey + }; + }, + getPublicKey: (secretKey) => { + const [_sk, publicKey, _publicKeyHash, _z] = secretCoder.decode(secretKey); + return Uint8Array.from(publicKey); + }, + encapsulate: (publicKey, msg = randomBytes3(msgLen)) => { + abytesDoc(publicKey, lengths.publicKey, "publicKey"); + abytesDoc(msg, msgLen, "message"); + const eke = publicKey.subarray(0, 384 * opts2.K); + const ek = KPKESecretCoder.encode(KPKESecretCoder.decode(copyBytes2(eke))); + if (!equalBytes(ek, eke)) { + cleanBytes(ek); + throw new Error("ML-KEM.encapsulate: wrong publicKey modulus"); + } + cleanBytes(ek); + const kr = HASH512.create().update(msg).update(HASH256(publicKey)).digest(); + const cipherText = KPKE.encrypt(publicKey, msg, kr.subarray(32, 64)); + cleanBytes(kr.subarray(32)); + return { + cipherText, + sharedSecret: kr.subarray(0, 32) + }; + }, + decapsulate: (cipherText, secretKey) => { + abytesDoc(secretKey, secretCoder.bytesLen, "secretKey"); + abytesDoc(cipherText, lengths.cipherText, "cipherText"); + const k768 = secretCoder.bytesLen - 96; + const start = k768 + 32; + const test = HASH256(secretKey.subarray(k768 / 2, start)); + if (!equalBytes(test, secretKey.subarray(start, start + 32))) + throw new Error("invalid secretKey: hash check failed"); + const [sk, publicKey, publicKeyHash, z] = secretCoder.decode(secretKey); + const msg = KPKE.decrypt(cipherText, sk); + const kr = HASH512.create().update(msg).update(publicKeyHash).digest(); + const Khat = kr.subarray(0, 32); + const cipherText2 = KPKE.encrypt(publicKey, msg, kr.subarray(32, 64)); + const isValid = equalBytes(cipherText, cipherText2); + const Kbar = KDF.create({ dkLen: 32 }).update(z).update(cipherText).digest(); + cleanBytes(msg, cipherText2, !isValid ? Khat : Kbar); + return isValid ? Khat : Kbar; + } + }); +} +function shakePRF(dkLen, key, nonce) { + return shake256.create({ dkLen }).update(key).update(new Uint8Array([nonce])).digest(); +} +var opts = /* @__PURE__ */ (() => ({ + HASH256: sha3_256, + HASH512: sha3_512, + KDF: shake256, + XOF: XOF128, + PRF: shakePRF +}))(); +var mk = (params) => createKyber({ + ...opts, + ...params +}); +var ml_kem768 = /* @__PURE__ */ (() => mk(PARAMS3[768]))(); + +// www/js/pq-crypto.mjs +var PQ_DERIVATION_PATHS = { + secp256k1: [0], + // 32 bytes (standard NIP-06) + mlDsa44: [1], + // 32 bytes + mlDsa65: [2], + // 32 bytes + slhDsa: [3, 4], + // 64 bytes concatenated, take first 48 + falcon512: [5, 6], + // 64 bytes concatenated, take first 48 + mlKem: [7, 8] + // 64 bytes concatenated +}; +var PQ_SEED_LENGTHS = { + mlDsa44: 32, + mlDsa65: 32, + slhDsa: 48, + falcon512: 48, + mlKem: 64 +}; +function generateSeedPhrase() { + return generateMnemonic(wordlist, 128); +} +function mnemonicToSeed(mnemonic, passphrase = "") { + if (!validateMnemonic(mnemonic, wordlist)) { + throw new Error("Invalid mnemonic"); + } + return mnemonicToSeedSync(mnemonic, passphrase); +} +function isValidMnemonic(mnemonic) { + return validateMnemonic(mnemonic, wordlist); +} +function deriveBIP32Child(bip39Seed, childIndices) { + const hdKey = HDKey.fromMasterSeed(bip39Seed); + const path = `m/44'/1237'/0'/0/${childIndices.join("/")}`; + const child = hdKey.derive(path); + if (!child.privateKey) { + throw new Error(`Failed to derive private key at path ${path}`); + } + return child.privateKey; +} +function derivePQSeedFromBIP32(bip39Seed, childIndices, requiredLength) { + if (childIndices.length === 1) { + const seed = deriveBIP32Child(bip39Seed, childIndices); + if (seed.length !== requiredLength) { + throw new Error(`Seed length mismatch: got ${seed.length}, expected ${requiredLength}`); + } + return seed; + } else { + let combined = new Uint8Array(0); + for (const idx of childIndices) { + const child = deriveBIP32Child(bip39Seed, [idx]); + const newCombined = new Uint8Array(combined.length + child.length); + newCombined.set(combined); + newCombined.set(child, combined.length); + combined = newCombined; + } + if (combined.length < requiredLength) { + throw new Error(`Combined seed too short: got ${combined.length}, expected ${requiredLength}`); + } + return combined.slice(0, requiredLength); + } +} +function deriveSecp256k1FromSeed(seed, accountIndex = 0) { + const hdKey = HDKey.fromMasterSeed(seed); + const path = `m/44'/1237'/${accountIndex}'/0/0`; + const child = hdKey.derive(path); + if (!child.privateKey) { + throw new Error("Failed to derive private key"); + } + return { + privateKey: child.privateKey, + publicKey: child.publicKey + }; +} +function derivePQKeysFromSeed(bip39Seed) { + const mlDsa44Seed = derivePQSeedFromBIP32(bip39Seed, PQ_DERIVATION_PATHS.mlDsa44, PQ_SEED_LENGTHS.mlDsa44); + const mlDsa44Keys = ml_dsa44.keygen(mlDsa44Seed); + const mlDsa65Seed = derivePQSeedFromBIP32(bip39Seed, PQ_DERIVATION_PATHS.mlDsa65, PQ_SEED_LENGTHS.mlDsa65); + const mlDsa65Keys = ml_dsa65.keygen(mlDsa65Seed); + const slhDsaSeed = derivePQSeedFromBIP32(bip39Seed, PQ_DERIVATION_PATHS.slhDsa, PQ_SEED_LENGTHS.slhDsa); + const slhDsaKeys = slh_dsa_sha2_128s.keygen(slhDsaSeed); + const falconSeed = derivePQSeedFromBIP32(bip39Seed, PQ_DERIVATION_PATHS.falcon512, PQ_SEED_LENGTHS.falcon512); + const falconKeys = falcon512.keygen(falconSeed); + const mlKemSeed = derivePQSeedFromBIP32(bip39Seed, PQ_DERIVATION_PATHS.mlKem, PQ_SEED_LENGTHS.mlKem); + const mlKemKeys = ml_kem768.keygen(mlKemSeed); + return { + mlDsa44: mlDsa44Keys, + mlDsa65: mlDsa65Keys, + slhDsa: slhDsaKeys, + falcon512: falconKeys, + mlKem: mlKemKeys + }; +} +function signWithMLDSA44(message, secretKey) { + return ml_dsa44.sign(message, secretKey); +} +function verifyMLDSA44(signature, message, publicKey) { + return ml_dsa44.verify(signature, message, publicKey); +} +function signWithMLDSA65(message, secretKey) { + return ml_dsa65.sign(message, secretKey); +} +function verifyMLDSA65(signature, message, publicKey) { + return ml_dsa65.verify(signature, message, publicKey); +} +function signWithSLHDSA(message, secretKey) { + return slh_dsa_sha2_128s.sign(message, secretKey); +} +function verifySLHDSA(signature, message, publicKey) { + return slh_dsa_sha2_128s.verify(signature, message, publicKey); +} +function signWithFalcon(message, secretKey) { + return falcon512.sign(message, secretKey); +} +function verifyFalcon(signature, message, publicKey) { + return falcon512.verify(signature, message, publicKey); +} +function bytesToBase64(bytes) { + let binary = ""; + for (let i = 0; i < bytes.length; i++) { + binary += String.fromCharCode(bytes[i]); + } + return btoa(binary); +} +function base64ToBytes(base64) { + const binary = atob(base64); + const bytes = new Uint8Array(binary.length); + for (let i = 0; i < binary.length; i++) { + bytes[i] = binary.charCodeAt(i); + } + return bytes; +} +function bytesToHex3(bytes) { + return Array.from(bytes).map((b) => b.toString(16).padStart(2, "0")).join(""); +} +function hexToBytes3(hex) { + const bytes = new Uint8Array(hex.length / 2); + for (let i = 0; i < hex.length; i += 2) { + bytes[i / 2] = parseInt(hex.substr(i, 2), 16); + } + return bytes; +} +function hexToNpub(hexPubkey) { + const bytes = hexToBytes3(hexPubkey); + return bech32.encode("npub", bech32.toWords(bytes)); +} +function verifyNostrEvent(event) { + const serialized = JSON.stringify([ + 0, + event.pubkey, + event.created_at, + event.kind, + event.tags, + event.content + ]); + const hash = sha256(new TextEncoder().encode(serialized)); + const sig = hexToBytes3(event.sig); + const pubkey = hexToBytes3(event.pubkey); + return schnorr.verify(sig, hash, pubkey); +} +function buildNIPQRContent(hexPubkey, blockHeight, pqKeys) { + const npub = hexToNpub(hexPubkey); + const content = `NOSTR identity: [hex] ${hexPubkey} [npub] ${npub} is signaling migration to successor post quantum pubkeys listed in the tags of this event. This link is established pre-quantum at blockheight ${blockHeight}.`; + const statementBytes = new TextEncoder().encode(content); + const mlDsa44Sig = signWithMLDSA44(statementBytes, pqKeys.mlDsa44.secretKey); + const mlDsa65Sig = signWithMLDSA65(statementBytes, pqKeys.mlDsa65.secretKey); + const slhDsaSig = signWithSLHDSA(statementBytes, pqKeys.slhDsa.secretKey); + const falconSig = signWithFalcon(statementBytes, pqKeys.falcon512.secretKey); + const tags = [ + ["d", "nip-qr-migration"], + ["block_height", String(blockHeight)], + ["algorithm", "ml-dsa-44", bytesToBase64(pqKeys.mlDsa44.publicKey), bytesToBase64(mlDsa44Sig)], + ["algorithm", "ml-dsa-65", bytesToBase64(pqKeys.mlDsa65.publicKey), bytesToBase64(mlDsa65Sig)], + ["algorithm", "slh-dsa-128s", bytesToBase64(pqKeys.slhDsa.publicKey), bytesToBase64(slhDsaSig)], + ["algorithm", "falcon-512", bytesToBase64(pqKeys.falcon512.publicKey), bytesToBase64(falconSig)], + ["algorithm", "ml-kem-768", bytesToBase64(pqKeys.mlKem.publicKey)] + ]; + return { content, tags, statementBytes }; +} +function verifyNIPQRContent(contentText, tags) { + const results = []; + const msg = new TextEncoder().encode(contentText); + for (const tag of tags) { + if (tag[0] !== "algorithm") continue; + const algorithm = tag[1]; + const pubKeyBase64 = tag[2]; + const sigBase64 = tag[3]; + if (algorithm === "ml-kem-768" || !sigBase64) { + results.push({ algorithm, valid: true, note: "KEM (no signature to verify)" }); + continue; + } + const pubKey = base64ToBytes(pubKeyBase64); + const sig = base64ToBytes(sigBase64); + let valid = false; + if (algorithm === "ml-dsa-44") { + valid = verifyMLDSA44(sig, msg, pubKey); + } else if (algorithm === "ml-dsa-65") { + valid = verifyMLDSA65(sig, msg, pubKey); + } else if (algorithm === "slh-dsa-128s") { + valid = verifySLHDSA(sig, msg, pubKey); + } else if (algorithm === "falcon-512") { + valid = verifyFalcon(sig, msg, pubKey); + } + results.push({ algorithm, valid }); + } + return { + valid: results.every((r) => r.valid), + results + }; +} +var PQ_KEY_INFO = { + "ml-dsa-44": { + name: "ML-DSA-44 (Dilithium)", + publicKeySize: 1312, + signatureSize: 2420, + fips: "FIPS 204", + type: "signature", + securityLevel: "Category 2 (~AES-128)", + derivationPath: "m/44'/1237'/0'/0/1" + }, + "ml-dsa-65": { + name: "ML-DSA-65 (Dilithium)", + publicKeySize: 1952, + signatureSize: 3309, + fips: "FIPS 204", + type: "signature", + securityLevel: "Category 3 (~AES-192)", + derivationPath: "m/44'/1237'/0'/0/2" + }, + "slh-dsa-128s": { + name: "SLH-DSA-128s (SPHINCS+)", + publicKeySize: 32, + signatureSize: 7856, + fips: "FIPS 205", + type: "signature", + securityLevel: "Category 1 (~AES-128, hash-based)", + derivationPath: "m/44'/1237'/0'/0/3+4" + }, + "falcon-512": { + name: "Falcon-512", + publicKeySize: 897, + signatureSize: 666, + fips: "FIPS 206 (draft)", + type: "signature", + securityLevel: "Category 1 (~AES-128, lattice-based)", + derivationPath: "m/44'/1237'/0'/0/5+6" + }, + "ml-kem-768": { + name: "ML-KEM-768 (Kyber)", + publicKeySize: 1184, + ciphertextSize: 1088, + fips: "FIPS 203", + type: "kem", + securityLevel: "Category 3 (~AES-192)", + derivationPath: "m/44'/1237'/0'/0/7+8" + } +}; +var OTS_CALENDAR_SERVERS = [ + "https://alice.btc.calendar.opentimestamps.org", + "https://bob.btc.calendar.opentimestamps.org" +]; +var OTS_DETACHED_PREFIX = hexToBytes3( + "004f70656e54696d657374616d7073000050726f6f6600bf89e2e884e892940108" +); +function concatBytes4(...arrays) { + const length = arrays.reduce((sum, bytes) => sum + bytes.length, 0); + const result = new Uint8Array(length); + let offset = 0; + for (const bytes of arrays) { + result.set(bytes, offset); + offset += bytes.length; + } + return result; +} +function isDetachedOtsFile(otsBytes) { + if (!(otsBytes instanceof Uint8Array) || otsBytes.length < OTS_DETACHED_PREFIX.length) return false; + for (let i = 0; i < OTS_DETACHED_PREFIX.length; i++) { + if (otsBytes[i] !== OTS_DETACHED_PREFIX[i]) return false; + } + return true; +} +async function timestampEvent(eventIdHex) { + const hashBytes = hexToBytes3(eventIdHex); + for (const server of OTS_CALENDAR_SERVERS) { + try { + const response = await fetch(`${server}/digest`, { + method: "POST", + body: hashBytes, + headers: { + "Accept": "application/vnd.opentimestamps.v1", + "Content-Type": "application/x-www-form-urlencoded" + } + }); + if (!response.ok) { + console.warn(`[ots] Calendar ${server} returned ${response.status}`); + continue; + } + const fragmentBuffer = await response.arrayBuffer(); + const fragmentBytes = new Uint8Array(fragmentBuffer); + const otsBytes = concatBytes4(OTS_DETACHED_PREFIX, hashBytes, fragmentBytes); + console.log(`[ots] Received calendar fragment (${fragmentBytes.length} bytes); built detached .ots file (${otsBytes.length} bytes) from ${server}`); + return otsBytes; + } catch (e) { + console.warn(`[ots] Calendar ${server} failed:`, e.message); + } + } + throw new Error("All OpenTimestamps calendar servers failed"); +} +async function upgradeOts(otsBytes) { + const upgradeUrl = typeof window !== "undefined" && window.location ? `${window.location.origin}/ots-upgrade` : "https://laantungir.net/ots-upgrade"; + const response = await fetch(upgradeUrl, { + method: "POST", + body: otsBytes, + headers: { + "Content-Type": "application/octet-stream", + "Accept": "application/json" + } + }); + if (!response.ok) { + throw new Error(`OTS upgrade helper returned HTTP ${response.status}`); + } + const result = await response.json(); + if (!result.proof) { + throw new Error(result.error || "OTS upgrade helper returned no proof"); + } + return { + proof: base64ToBytes(result.proof), + changed: Boolean(result.changed), + confirmed: Boolean(result.confirmed), + detail: String(result.stderr || result.stdout || "").trim() + }; +} +function isOtsConfirmed(otsBytes) { + const bitcoinTag = hexToBytes3("0588960d73d71901"); + outer: for (let i = 0; i <= otsBytes.length - bitcoinTag.length; i++) { + for (let j = 0; j < bitcoinTag.length; j++) { + if (otsBytes[i + j] !== bitcoinTag[j]) continue outer; + } + return true; + } + return false; +} +function savePendingOts(eventId, otsBytes, metadata = {}) { + let existing = {}; + try { + existing = JSON.parse(localStorage.getItem("pq-pending-ots") || "{}"); + } catch (e) { + existing = {}; + } + const data = { + ...existing, + ...metadata, + eventId, + ots: bytesToBase64(otsBytes), + timestamp: existing.timestamp || Date.now(), + updatedAt: Date.now() + }; + localStorage.setItem("pq-pending-ots", JSON.stringify(data)); +} +function loadPendingOts() { + const data = localStorage.getItem("pq-pending-ots"); + if (!data) return null; + try { + const parsed = JSON.parse(data); + return { + ...parsed, + eventId: parsed.eventId, + ots: base64ToBytes(parsed.ots), + timestamp: parsed.timestamp, + pendingPublished: Boolean(parsed.pendingPublished), + confirmedPublished: Boolean(parsed.confirmedPublished) + }; + } catch (e) { + return null; + } +} +function clearPendingOts() { + localStorage.removeItem("pq-pending-ots"); +} +export { + PQ_KEY_INFO, + base64ToBytes, + buildNIPQRContent, + bytesToBase64, + bytesToHex3 as bytesToHex, + clearPendingOts, + derivePQKeysFromSeed, + deriveSecp256k1FromSeed, + generateSeedPhrase, + hexToBytes3 as hexToBytes, + hexToNpub, + isDetachedOtsFile, + isOtsConfirmed, + isValidMnemonic, + loadPendingOts, + mnemonicToSeed, + savePendingOts, + signWithFalcon, + signWithMLDSA44, + signWithMLDSA65, + signWithSLHDSA, + timestampEvent, + upgradeOts, + verifyFalcon, + verifyMLDSA44, + verifyMLDSA65, + verifyNIPQRContent, + verifyNostrEvent, + verifySLHDSA +}; +/*! Bundled license information: + +@scure/base/index.js: + (*! scure-base - MIT License (c) 2022 Paul Miller (paulmillr.com) *) + +@scure/bip39/index.js: + (*! scure-bip39 - MIT License (c) 2022 Patricio Palladino, Paul Miller (paulmillr.com) *) + +@noble/curves/utils.js: +@noble/curves/abstract/modular.js: +@noble/curves/abstract/curve.js: +@noble/curves/abstract/weierstrass.js: +@noble/curves/secp256k1.js: + (*! noble-curves - MIT License (c) 2022 Paul Miller (paulmillr.com) *) + +@scure/bip32/index.js: + (*! scure-bip32 - MIT License (c) 2022 Patricio Palladino, Paul Miller (paulmillr.com) *) + +@noble/post-quantum/utils.js: +@noble/post-quantum/_crystals.js: +@noble/post-quantum/ml-dsa.js: +@noble/post-quantum/slh-dsa.js: +@noble/post-quantum/falcon.js: +@noble/post-quantum/ml-kem.js: + (*! noble-post-quantum - MIT License (c) 2024 Paul Miller (paulmillr.com) *) + +@noble/ciphers/utils.js: + (*! noble-ciphers - MIT License (c) 2023 Paul Miller (paulmillr.com) *) +*/ +//# sourceMappingURL=pq-crypto.bundle.js.map diff --git a/www/verify.html b/www/verify.html new file mode 100644 index 0000000..2c96289 --- /dev/null +++ b/www/verify.html @@ -0,0 +1,299 @@ + + + + + + + Verify NIP-QR Event + + + + + + + + + +
+
+
+
Verify NIP-QR Event
+
+
+
+ + +
+
+ +
+
Verify NIP-QR Migration Event
+
+ Paste a NIP-QR event JSON below to verify all signatures. + This will check the secp256k1 signature and all 5 post-quantum signatures (ML-DSA-44, ML-DSA-65, SLH-DSA-128s, Falcon-512, and ML-KEM-768). +
+ + + + + + +
+ + + + +
+ +
+
+ + +
+
+
+
+
+ + + + + +