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:
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.