app

Local-first trade for farms and co-ops
git clone https://radroots.dev/git/app.git
Log | Files | Refs | README | LICENSE

commit 1bac8ff9dfce7eae8afb2edaef630bdf26d89cb7
parent f62325ac8832f455a820b62c050657fdc7923a8f
Author: triesap <tyson@radroots.org>
Date:   Sun,  2 Aug 2026 18:55:33 +0000

core(nostr): add key generation and parsing adapter

- pin the approved upstream Nostr 0.44.1 source revision
- isolate random key generation behind Radroots-owned types
- derive canonical pubkey npub secret-hex and nsec values
- verify known nsec and secret-hex vectors with redaction

Diffstat:
Mcore/Cargo.lock | 375+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mcore/Cargo.toml | 1+
Mcore/crates/nostr/Cargo.toml | 4++++
Acore/crates/nostr/src/keys.rs | 151++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mcore/crates/nostr/src/lib.rs | 10+++-------
Mdocs/implementation/nostr-runtime-rcld.md | 8+++++++-
6 files changed, 541 insertions(+), 8 deletions(-)

diff --git a/core/Cargo.lock b/core/Cargo.lock @@ -3,6 +3,16 @@ version = 4 [[package]] +name = "aead" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0" +dependencies = [ + "crypto-common", + "generic-array", +] + +[[package]] name = "aes" version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -34,6 +44,12 @@ dependencies = [ ] [[package]] +name = "arrayvec" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3fb67a6e08acf24fdeccbac2cb6ac4305825bd1f117462e0e6f2f193345ad56" + +[[package]] name = "async-broadcast" version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -177,6 +193,72 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" [[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "base64ct" +version = "1.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" + +[[package]] +name = "bech32" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32637268377fc7b10a8c6d51de3e7fba1ce5dd371a96e342b34e6078db558e7f" + +[[package]] +name = "bip39" +version = "2.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90dbd31c98227229239363921e60fcf5e558e43ec69094d46fc4996f08d1d5bc" +dependencies = [ + "bitcoin_hashes", + "serde", + "unicode-normalization", +] + +[[package]] +name = "bitcoin-consensus-encoding" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "207311705279250ba465076a1bac4b1ac982855fff73fc5f67e22158ac58cdc9" +dependencies = [ + "bitcoin-internals", + "hex-conservative 1.2.0", + "serde", +] + +[[package]] +name = "bitcoin-internals" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d573f4cf32996a8dce612e4348cece65a241f1882ed594047c9ba348e8869fa5" + +[[package]] +name = "bitcoin-io" +version = "0.1.101" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb5de036369d1ac59d3c1819ebc4d850f89466f5401c571a285b6ed564a4cb78" +dependencies = [ + "bitcoin-consensus-encoding", +] + +[[package]] +name = "bitcoin_hashes" +version = "0.14.101" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bca4c7abb40c8817d77403c880988cfd484f23ab2365726afb2f798363e2c4a2" +dependencies = [ + "bitcoin-io", + "hex-conservative 0.2.2", + "serde", +] + +[[package]] name = "bitflags" version = "2.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -251,6 +333,30 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" [[package]] +name = "chacha20" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3613f74bd2eac03dad61bd53dbe620703d4371614fe0bc3b9f04dd36fe4e818" +dependencies = [ + "cfg-if", + "cipher", + "cpufeatures", +] + +[[package]] +name = "chacha20poly1305" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10cd79432192d1c0f4e1a0fef9527696cc039165d729fb41b3f4f4f354c2dc35" +dependencies = [ + "aead", + "chacha20", + "cipher", + "poly1305", + "zeroize", +] + +[[package]] name = "cipher" version = "0.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -258,6 +364,7 @@ checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" dependencies = [ "crypto-common", "inout", + "zeroize", ] [[package]] @@ -307,6 +414,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" dependencies = [ "generic-array", + "rand_core", "typenum", ] @@ -512,8 +620,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" dependencies = [ "cfg-if", + "js-sys", "libc", "wasi", + "wasm-bindgen", ] [[package]] @@ -565,6 +675,24 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" [[package]] +name = "hex-conservative" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fda06d18ac606267c40c04e41b9947729bf8b9efe74bd4e82b61a5f26a510b9f" +dependencies = [ + "arrayvec", +] + +[[package]] +name = "hex-conservative" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35431185f361ccf3ffc58254628af5f1f5d5f28531da2e02e5d6c82bbc282a10" +dependencies = [ + "arrayvec", +] + +[[package]] name = "hkdf" version = "0.12.4" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -706,6 +834,24 @@ dependencies = [ ] [[package]] +name = "instant" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0242819d153cba4b4b05a5a8f2a7e9bbf97b6055b2a002b395c96b5ff3c0222" +dependencies = [ + "cfg-if", + "js-sys", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] name = "js-sys" version = "0.3.103" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -788,6 +934,29 @@ dependencies = [ ] [[package]] +name = "nostr" +version = "0.44.1" +source = "git+https://github.com/rust-nostr/nostr.git?rev=5bba5163eb77107f82c4a8262cf29d7f33a73219#5bba5163eb77107f82c4a8262cf29d7f33a73219" +dependencies = [ + "base64", + "bech32", + "bip39", + "bitcoin_hashes", + "cbc", + "chacha20", + "chacha20poly1305", + "getrandom 0.2.17", + "hex", + "instant", + "scrypt", + "secp256k1", + "serde", + "serde_json", + "unicode-normalization", + "url", +] + +[[package]] name = "num" version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -872,6 +1041,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" [[package]] +name = "opaque-debug" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" + +[[package]] name = "ordered-stream" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -888,6 +1063,27 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" [[package]] +name = "password-hash" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "346f04948ba92c43e8469c1ee6736c7563d71012b17d40745260fe106aac2166" +dependencies = [ + "base64ct", + "rand_core", + "subtle", +] + +[[package]] +name = "pbkdf2" +version = "0.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ed6a7761f76e3b9f92dfb0a60a6a6477c61024b775147ff0973a02653abaf2" +dependencies = [ + "digest", + "hmac", +] + +[[package]] name = "percent-encoding" version = "2.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -931,6 +1127,17 @@ dependencies = [ ] [[package]] +name = "poly1305" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8159bd90725d2df49889a078b54f4f79e87f1f8a8444194cdca81d38f5393abf" +dependencies = [ + "cpufeatures", + "opaque-debug", + "universal-hash", +] + +[[package]] name = "potential_utf" version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -946,6 +1153,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" [[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] name = "proc-macro-crate" version = "3.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1001,6 +1217,10 @@ version = "0.1.0-alpha.0" [[package]] name = "radroots-studio-nostr" version = "0.1.0-alpha.0" +dependencies = [ + "nostr", + "radroots-studio-domain", +] [[package]] name = "radroots-studio-storage" @@ -1019,6 +1239,36 @@ name = "radroots-studio-uniffi-bindgen" version = "0.1.0-alpha.0" [[package]] +name = "rand" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22f6172bdec972074665ed81ed53b71da00bfc44b65a753cfde883ec4c702a1a" +dependencies = [ + "libc", + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.17", +] + +[[package]] name = "refinery" version = "0.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1133,6 +1383,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f" [[package]] +name = "salsa20" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97a22f5af31f73a954c10289c93e8a50cc23d971e80ee446f1f6f7137a088213" +dependencies = [ + "cipher", +] + +[[package]] name = "same-file" version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1142,6 +1401,38 @@ dependencies = [ ] [[package]] +name = "scrypt" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0516a385866c09368f0b5bcd1caff3366aace790fcd46e2bb032697bb172fd1f" +dependencies = [ + "password-hash", + "pbkdf2", + "salsa20", + "sha2", +] + +[[package]] +name = "secp256k1" +version = "0.29.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9465315bc9d4566e1724f0fffcbcc446268cb522e60f9a27bcded6b19c108113" +dependencies = [ + "rand", + "secp256k1-sys", + "serde", +] + +[[package]] +name = "secp256k1-sys" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4387882333d3aa8cb20530a17c69a3752e97837832f34f6dccc760e715001d9" +dependencies = [ + "cc", +] + +[[package]] name = "secrecy" version = "0.10.3" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1223,6 +1514,19 @@ dependencies = [ ] [[package]] +name = "serde_json" +version = "1.0.151" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] name = "serde_repr" version = "0.1.21" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1409,6 +1713,21 @@ dependencies = [ ] [[package]] +name = "tinyvec" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb4ebadaa0af04fab11ae01eb5f9fdb5f9c5b875506e210e71c07873528baa7f" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] name = "toml_datetime" version = "1.1.1+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1493,6 +1812,25 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" [[package]] +name = "unicode-normalization" +version = "0.1.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fd4f6878c9cb28d874b009da9e8d183b5abc80117c40bbd187a1fde336be6e8" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "universal-hash" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea" +dependencies = [ + "crypto-common", + "subtle", +] + +[[package]] name = "url" version = "2.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1502,6 +1840,7 @@ dependencies = [ "idna", "percent-encoding", "serde", + "serde_derive", ] [[package]] @@ -1604,6 +1943,16 @@ dependencies = [ ] [[package]] +name = "web-sys" +version = "0.3.103" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8622dcb61c0bcc9fffa6938bed81210af2da9a7e4a1a834b2e37a59b6dfb6141" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] name = "winapi-util" version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1757,6 +2106,26 @@ dependencies = [ ] [[package]] +name = "zerocopy" +version = "0.8.55" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5a105cd7b140f6eeec8acff2ea38135d3cab283ada58540f629fe51e46696eb" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.55" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fe976fb70c78cd64cccfe3a6fc142244e8a77b70959b30faf9d0ac37ee228eb" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] name = "zerofrom" version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1817,6 +2186,12 @@ dependencies = [ ] [[package]] +name = "zmij" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" + +[[package]] name = "zvariant" version = "5.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" diff --git a/core/Cargo.toml b/core/Cargo.toml @@ -25,6 +25,7 @@ pedantic = "deny" [workspace.dependencies] keyring = "=4.1.6" +nostr = { git = "https://github.com/rust-nostr/nostr.git", rev = "5bba5163eb77107f82c4a8262cf29d7f33a73219", package = "nostr" } refinery = { version = "=0.9.2", default-features = false, features = ["rusqlite"] } rusqlite = { version = "=0.39.0", features = ["bundled"] } secrecy = "=0.10.3" diff --git a/core/crates/nostr/Cargo.toml b/core/crates/nostr/Cargo.toml @@ -6,5 +6,9 @@ rust-version.workspace = true license.workspace = true repository.workspace = true +[dependencies] +nostr.workspace = true +radroots-studio-domain = { path = "../domain" } + [lints] workspace = true diff --git a/core/crates/nostr/src/keys.rs b/core/crates/nostr/src/keys.rs @@ -0,0 +1,151 @@ +use nostr::{Keys, ToBech32}; +use radroots_studio_domain::{ + Npub, Nsec, PublicKey, SafeError, SafeErrorCode, SafeMessage, SecretKeyInput, +}; + +pub struct GeneratedKeyMaterial { + public_key: PublicKey, + npub: Npub, + secret: SecretKeyInput, + nsec: Nsec, +} + +impl GeneratedKeyMaterial { + #[must_use] + pub fn into_parts(self) -> (PublicKey, Npub, SecretKeyInput, Nsec) { + (self.public_key, self.npub, self.secret, self.nsec) + } +} + +pub struct ImportedKeyMaterial { + public_key: PublicKey, + npub: Npub, + secret: SecretKeyInput, +} + +impl ImportedKeyMaterial { + #[must_use] + pub fn into_parts(self) -> (PublicKey, Npub, SecretKeyInput) { + (self.public_key, self.npub, self.secret) + } +} + +/// Generates one cryptographically random local Nostr keypair. +/// +/// # Errors +/// +/// Returns a safe key error if an upstream encoding cannot be represented by +/// the stricter Radroots domain boundary. +pub fn generate_local_keypair() -> Result<GeneratedKeyMaterial, SafeError> { + let keys = Keys::generate(); + let (public_key, npub, secret, nsec) = encode_keys(&keys)?; + Ok(GeneratedKeyMaterial { + public_key, + npub, + secret, + nsec, + }) +} + +/// Parses nsec or canonical secret hex and derives public Nostr identity. +/// +/// # Errors +/// +/// Returns a safe invalid-secret-key error for checksum, scalar, or encoding +/// failures without exposing the rejected input. +pub fn import_secret(input: SecretKeyInput) -> Result<ImportedKeyMaterial, SafeError> { + let keys = input + .with_exposed_secret(Keys::parse) + .map_err(|_| invalid_secret_key())?; + drop(input); + let public_key = PublicKey::from_bytes(keys.public_key().to_bytes()); + let npub = keys + .public_key() + .to_bech32() + .map_err(|_| invalid_public_key()) + .and_then(Npub::from_encoded)?; + let secret = SecretKeyInput::parse(keys.secret_key().to_secret_hex())?; + Ok(ImportedKeyMaterial { + public_key, + npub, + secret, + }) +} + +fn encode_keys(keys: &Keys) -> Result<(PublicKey, Npub, SecretKeyInput, Nsec), SafeError> { + let public_key = PublicKey::from_bytes(keys.public_key().to_bytes()); + let npub = keys + .public_key() + .to_bech32() + .map_err(|_| invalid_public_key()) + .and_then(Npub::from_encoded)?; + let secret = SecretKeyInput::parse(keys.secret_key().to_secret_hex())?; + let nsec = keys + .secret_key() + .to_bech32() + .map_err(|_| invalid_secret_key()) + .and_then(Nsec::from_encoded)?; + Ok((public_key, npub, secret, nsec)) +} + +const fn invalid_secret_key() -> SafeError { + SafeError::new( + SafeErrorCode::InvalidSecretKey, + SafeMessage::new("The Nostr secret key is invalid."), + ) +} + +const fn invalid_public_key() -> SafeError { + SafeError::new( + SafeErrorCode::InvalidPublicKey, + SafeMessage::new("The Nostr public key is invalid."), + ) +} + +#[cfg(test)] +mod tests { + use radroots_studio_domain::{SafeErrorCode, SecretKeyInput}; + + use super::{generate_local_keypair, import_secret}; + + const SECRET_HEX: &str = "7e7e9c42a91bfef19fa7ea99d52d8afdb67d893a8fefba1f5cb9793f2107f6d7"; + const NSEC: &str = "nsec1vl029mgpspedva04g90vltkh6fvh240zqtv9k0t9af8935ke9laqsnlfe5"; + const NSEC_PUBLIC_HEX: &str = + "7e7e9c42a91bfef19fa929e5fda1b72e0ebc1a4c1141673e2794234d86addf4e"; + const HEX_PUBLIC_HEX: &str = "0cfda0afa91cc2fbbd6050c285802fe95c7a1755e0f68323999e13760501dc40"; + + #[test] + fn keys_generate_valid_redacted_material() { + let generated = generate_local_keypair().expect("generated"); + let (public_key, npub, secret, nsec) = generated.into_parts(); + assert_eq!(public_key.to_hex().len(), 64); + assert!(npub.as_str().starts_with("npub1")); + assert_eq!(secret.with_exposed_secret(str::len), 64); + assert_eq!(nsec.with_exposed_secret(str::len), 63); + assert!(!format!("{secret:?} {nsec:?}").contains("nsec1")); + } + + #[test] + fn keys_import_known_nsec_and_hex_vectors() { + let from_nsec = import_secret(SecretKeyInput::parse(NSEC.to_owned()).expect("nsec")) + .expect("import nsec"); + let from_hex = import_secret(SecretKeyInput::parse(SECRET_HEX.to_owned()).expect("hex")) + .expect("import hex"); + let (nsec_public, nsec_npub, _) = from_nsec.into_parts(); + let (hex_public, hex_npub, _) = from_hex.into_parts(); + assert_eq!(nsec_public.to_hex(), NSEC_PUBLIC_HEX); + assert_eq!(hex_public.to_hex(), HEX_PUBLIC_HEX); + assert!(nsec_npub.as_str().starts_with("npub1")); + assert!(hex_npub.as_str().starts_with("npub1")); + } + + #[test] + fn keys_reject_structurally_plausible_nsec_with_invalid_checksum() { + let input = SecretKeyInput::parse( + "nsec1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq".to_owned(), + ) + .expect("domain shape"); + let error = import_secret(input).err().expect("invalid checksum"); + assert_eq!(error.code(), SafeErrorCode::InvalidSecretKey); + } +} diff --git a/core/crates/nostr/src/lib.rs b/core/crates/nostr/src/lib.rs @@ -1,9 +1,5 @@ #![doc = "Radroots Studio Nostr protocol adapters."] -#[cfg(test)] -mod tests { - #[test] - fn crate_is_available_to_the_workspace() { - assert_eq!(env!("CARGO_PKG_NAME"), "radroots-studio-nostr"); - } -} +pub mod keys; + +pub use keys::{GeneratedKeyMaterial, ImportedKeyMaterial, generate_local_keypair, import_secret}; diff --git a/docs/implementation/nostr-runtime-rcld.md b/docs/implementation/nostr-runtime-rcld.md @@ -216,6 +216,12 @@ licenses and primary-source provenance: - `zeroize` `1.9.0`; - `directories` `6.0.0`. +Dependency-source deviation: crates.io has yanked `nostr` 0.44.1 and refuses a +new exact registry resolution. The `nostr` crate is therefore pinned to the +upstream rust-nostr commit `5bba5163eb77107f82c4a8262cf29d7f33a73219`, +whose crate manifest declares version 0.44.1. This preserves the approved +version and uses the upstream source rather than substituting a newer release. + Notedeck and nostrdb are GPL clean-room references only. Do not copy their code. Reject optional disk-secret storage, nondeterministic account fallback, cache-before-persist publication, and delete-without-recovery patterns. @@ -490,7 +496,7 @@ handoff commit sequence. ### RCLD-06 -- [ ] 25. Pin Nostr dependency and implement key generation/derivation adapter. +- [x] 25. Pin Nostr dependency and implement key generation/derivation adapter. - [ ] 26. Implement generate account command with in-memory storage. - [ ] 27. Implement import secret key command. - [ ] 28. Define and test duplicate import and credential-repair handling.