commit b943892c0c085e839d289c000b7b8be1907bc2aa
parent 7f5b979d04b65bd1847b0cc2c317a2daa9273127
Author: triesap <tyson@radroots.org>
Date: Fri, 31 Jul 2026 09:51:27 +0000
nostr: move Nostr key and NIP-19 conversion out of identity
- Convert canonical identity public keys explicitly to and from Nostr and npub.
- Add opaque nsec and NIP-49 adapters behind the signing feature.
- Redact secret values, passwords, ciphertexts, and normalized error reports.
- Retire legacy key helpers and enforce identity and package ownership.
Diffstat:
8 files changed, 437 insertions(+), 55 deletions(-)
diff --git a/crates/nostr/Cargo.toml b/crates/nostr/Cargo.toml
@@ -34,7 +34,12 @@ events = [
"radroots_event_codec/json",
]
nip17 = ["std", "codec", "nostr/nip44", "nostr/nip59"]
-signing = ["events", "dep:radroots_signing", "radroots_signing/std"]
+signing = [
+ "events",
+ "dep:radroots_signing",
+ "nostr/nip49",
+ "radroots_signing/std",
+]
[dependencies]
base64 = { workspace = true, optional = true }
diff --git a/crates/nostr/src/error.rs b/crates/nostr/src/error.rs
@@ -3,6 +3,28 @@ use thiserror::Error;
#[derive(Debug, Error)]
pub enum RadrootsNostrError {
+ #[error("invalid Nostr public key")]
+ InvalidPublicKey,
+
+ #[error("invalid NIP-19 npub public key")]
+ InvalidNpub,
+
+ #[cfg(feature = "signing")]
+ #[error("invalid Nostr secret key")]
+ InvalidSecretKey,
+
+ #[cfg(feature = "signing")]
+ #[error("invalid NIP-49 encrypted secret key")]
+ InvalidEncryptedSecretKey,
+
+ #[cfg(feature = "signing")]
+ #[error("NIP-49 secret-key encryption failed")]
+ SecretKeyEncryption,
+
+ #[cfg(feature = "signing")]
+ #[error("NIP-49 secret-key decryption failed")]
+ SecretKeyDecryption,
+
#[error("Nostr event kind {kind} exceeds {max}")]
KindOutOfRange { kind: u32, max: u16 },
diff --git a/crates/nostr/src/key.rs b/crates/nostr/src/key.rs
@@ -2,3 +2,336 @@
//!
//! Step 125 moves the existing conversion behavior to this durable public
//! module without returning Nostr representation policy to identity.
+
+use alloc::string::String;
+
+use nostr::nips::nip19::{FromBech32, ToBech32};
+use radroots_identity::PublicKey;
+
+use crate::Error;
+
+/// Converts a canonical Radroots public key into its Nostr representation.
+pub fn public_key_to_nostr(public_key: PublicKey) -> Result<nostr::PublicKey, Error> {
+ nostr::PublicKey::from_slice(public_key.as_bytes()).map_err(|_| Error::InvalidPublicKey)
+}
+
+/// Converts a Nostr public key into the canonical Radroots representation.
+pub fn public_key_from_nostr(public_key: nostr::PublicKey) -> Result<PublicKey, Error> {
+ PublicKey::from_bytes(public_key.to_bytes()).map_err(|_| Error::InvalidPublicKey)
+}
+
+/// Encodes a canonical Radroots public key as a NIP-19 `npub`.
+pub fn public_key_to_npub(public_key: PublicKey) -> Result<String, Error> {
+ let public_key = public_key_to_nostr(public_key)?;
+ match public_key.to_bech32() {
+ Ok(encoded) => Ok(encoded),
+ Err(error) => match error {},
+ }
+}
+
+/// Decodes a NIP-19 `npub` into the canonical Radroots public-key value.
+pub fn public_key_from_npub(encoded: &str) -> Result<PublicKey, Error> {
+ nostr::PublicKey::from_bech32(encoded)
+ .map_err(|_| Error::InvalidNpub)
+ .and_then(public_key_from_nostr)
+}
+
+/// Parses a canonical hexadecimal public key or a NIP-19 `npub`.
+pub fn parse_public_key(encoded: &str) -> Result<PublicKey, Error> {
+ if encoded.starts_with("npub1") {
+ public_key_from_npub(encoded)
+ } else {
+ PublicKey::from_hex(encoded).map_err(|_| Error::InvalidPublicKey)
+ }
+}
+
+/// An opaque local Nostr secret key.
+///
+/// The value does not implement `Clone`, serialization, or unrestricted
+/// plaintext access. Debug output is always redacted; the concrete local
+/// signing adapter consumes the value through crate-private integration.
+///
+/// ```compile_fail
+/// use radroots_nostr::key::SecretKey;
+///
+/// let key = SecretKey::parse(
+/// "0000000000000000000000000000000000000000000000000000000000000001",
+/// )?;
+/// let _duplicate = key.clone();
+/// # Ok::<(), radroots_nostr::Error>(())
+/// ```
+#[cfg(feature = "signing")]
+pub struct SecretKey {
+ inner: nostr::SecretKey,
+}
+
+#[cfg(feature = "signing")]
+impl SecretKey {
+ /// Parses exact hexadecimal or NIP-19 `nsec` text.
+ ///
+ /// Errors never retain or render the supplied secret material.
+ pub fn parse(encoded: &str) -> Result<Self, Error> {
+ nostr::SecretKey::parse(encoded)
+ .map(|inner| Self { inner })
+ .map_err(|_| Error::InvalidSecretKey)
+ }
+
+ /// Derives the canonical public identity without exposing secret bytes.
+ pub fn public_key(&self) -> Result<PublicKey, Error> {
+ let public_key = nostr::Keys::new(self.inner.clone()).public_key();
+ public_key_from_nostr(public_key)
+ }
+}
+
+#[cfg(feature = "signing")]
+impl core::fmt::Debug for SecretKey {
+ fn fmt(&self, formatter: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
+ formatter
+ .debug_tuple("SecretKey")
+ .field(&"[redacted]")
+ .finish()
+ }
+}
+
+/// NIP-49 metadata describing how the plaintext key was previously handled.
+#[cfg(feature = "signing")]
+#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
+pub enum Nip49KeySecurity {
+ /// The plaintext key is known to have been handled insecurely.
+ Weak,
+ /// The plaintext key is not known to have been handled insecurely.
+ Medium,
+ /// The caller does not track plaintext-key handling.
+ #[default]
+ Unknown,
+}
+
+#[cfg(feature = "signing")]
+impl From<Nip49KeySecurity> for nostr::nips::nip49::KeySecurity {
+ fn from(value: Nip49KeySecurity) -> Self {
+ match value {
+ Nip49KeySecurity::Weak => Self::Weak,
+ Nip49KeySecurity::Medium => Self::Medium,
+ Nip49KeySecurity::Unknown => Self::Unknown,
+ }
+ }
+}
+
+/// Explicit NIP-49 encryption parameters.
+#[cfg(feature = "signing")]
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+pub struct Nip49Options {
+ log_n: u8,
+ key_security: Nip49KeySecurity,
+}
+
+#[cfg(feature = "signing")]
+impl Nip49Options {
+ /// Creates NIP-49 options with an explicit scrypt `log2(N)` work factor.
+ #[must_use]
+ pub const fn new(log_n: u8, key_security: Nip49KeySecurity) -> Self {
+ Self {
+ log_n,
+ key_security,
+ }
+ }
+
+ /// Returns the scrypt `log2(N)` work factor.
+ #[must_use]
+ pub const fn log_n(self) -> u8 {
+ self.log_n
+ }
+
+ /// Returns the NIP-49 plaintext-key handling metadata.
+ #[must_use]
+ pub const fn key_security(self) -> Nip49KeySecurity {
+ self.key_security
+ }
+}
+
+#[cfg(feature = "signing")]
+impl Default for Nip49Options {
+ fn default() -> Self {
+ Self::new(16, Nip49KeySecurity::Unknown)
+ }
+}
+
+/// Parses a Nostr secret key from exact hexadecimal or NIP-19 `nsec` text.
+///
+/// Errors never retain or render the supplied secret material.
+#[cfg(feature = "signing")]
+pub fn parse_secret_key(encoded: &str) -> Result<SecretKey, Error> {
+ SecretKey::parse(encoded)
+}
+
+/// Encodes a Nostr secret key as NIP-19 `nsec` text.
+#[cfg(feature = "signing")]
+pub fn secret_key_to_nsec(secret_key: &SecretKey) -> String {
+ match secret_key.inner.to_bech32() {
+ Ok(encoded) => encoded,
+ Err(error) => match error {},
+ }
+}
+
+/// Encrypts a Nostr secret key into a NIP-49 `ncryptsec` payload.
+#[cfg(feature = "signing")]
+pub fn encrypt_secret_key_nip49(secret_key: &SecretKey, password: &str) -> Result<String, Error> {
+ encrypt_secret_key_nip49_with_options(secret_key, password, Nip49Options::default())
+}
+
+/// Encrypts a Nostr secret key with explicit NIP-49 parameters.
+#[cfg(feature = "signing")]
+pub fn encrypt_secret_key_nip49_with_options(
+ secret_key: &SecretKey,
+ password: &str,
+ options: Nip49Options,
+) -> Result<String, Error> {
+ let encrypted = nostr::nips::nip49::EncryptedSecretKey::new(
+ &secret_key.inner,
+ password,
+ options.log_n,
+ options.key_security.into(),
+ )
+ .map_err(|_| Error::SecretKeyEncryption)?;
+ encrypted
+ .to_bech32()
+ .map_err(|_| Error::SecretKeyEncryption)
+}
+
+/// Decrypts a NIP-49 `ncryptsec` payload into a Nostr secret key.
+///
+/// Parse, password, and ciphertext failures are deliberately normalized so
+/// diagnostics never retain the encrypted payload, password, or plaintext.
+#[cfg(feature = "signing")]
+pub fn decrypt_secret_key_nip49(encrypted: &str, password: &str) -> Result<SecretKey, Error> {
+ nostr::nips::nip49::EncryptedSecretKey::from_bech32(encrypted)
+ .map_err(|_| Error::InvalidEncryptedSecretKey)?
+ .decrypt(password)
+ .map(|inner| SecretKey { inner })
+ .map_err(|_| Error::SecretKeyDecryption)
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use crate::test_fixtures::FIXTURE_ALICE;
+
+ #[cfg(feature = "signing")]
+ const NCRYPTSEC: &str = "ncryptsec1qgg9947rlpvqu76pj5ecreduf9jxhselq2nae2kghhvd5g7dgjtcxfqtd67p9m0w57lspw8gsq6yphnm8623nsl8xn9j4jdzz84zm3frztj3z7s35vpzmqf6ksu8r89qk5z2zxfmu5gv8th8wclt0h4p";
+ #[cfg(feature = "signing")]
+ const NCRYPTSEC_SECRET_HEX: &str =
+ "3501454135014541350145413501453fefb02227e449e57cf4d3a3ce05378683";
+
+ #[test]
+ fn native_public_key_round_trips_through_nostr_hex_and_npub() {
+ let native =
+ PublicKey::from_hex(FIXTURE_ALICE.public_key_hex).expect("native public key fixture");
+ let nostr = public_key_to_nostr(native).expect("Nostr public key");
+
+ assert_eq!(nostr.to_hex(), FIXTURE_ALICE.public_key_hex);
+ assert_eq!(
+ public_key_from_nostr(nostr).expect("native public key"),
+ native
+ );
+ assert_eq!(
+ public_key_to_npub(native).expect("npub"),
+ FIXTURE_ALICE.npub
+ );
+ assert_eq!(
+ public_key_from_npub(FIXTURE_ALICE.npub).expect("native npub"),
+ native
+ );
+ assert_eq!(
+ parse_public_key(FIXTURE_ALICE.public_key_hex).expect("hex public key"),
+ native
+ );
+ assert_eq!(
+ parse_public_key(FIXTURE_ALICE.npub).expect("npub public key"),
+ native
+ );
+ }
+
+ #[test]
+ fn public_key_parsing_rejects_wrong_nip19_kinds_without_echoing_input() {
+ let invalid = "nsec1-do-not-disclose-public-key-input";
+ let error = parse_public_key(invalid).expect_err("secret HRP is not a public key");
+
+ assert!(matches!(error, Error::InvalidPublicKey));
+ assert!(!error.to_string().contains(invalid));
+ assert!(!format!("{error:?}").contains(invalid));
+
+ let malformed_npub = "npub1-do-not-disclose-public-key-input";
+ let error = public_key_from_npub(malformed_npub).expect_err("malformed npub");
+ assert!(matches!(error, Error::InvalidNpub));
+ assert!(!error.to_string().contains(malformed_npub));
+ assert!(!format!("{error:?}").contains(malformed_npub));
+ }
+
+ #[cfg(feature = "signing")]
+ #[test]
+ fn secret_key_hex_and_nsec_vectors_round_trip() {
+ let from_hex = parse_secret_key(FIXTURE_ALICE.secret_key_hex).expect("hex secret key");
+ let from_nsec = parse_secret_key(FIXTURE_ALICE.nsec).expect("nsec secret key");
+
+ assert_eq!(secret_key_to_nsec(&from_hex), FIXTURE_ALICE.nsec);
+ assert_eq!(secret_key_to_nsec(&from_nsec), FIXTURE_ALICE.nsec);
+ assert_eq!(
+ from_hex.public_key().expect("public key").to_hex(),
+ FIXTURE_ALICE.public_key_hex
+ );
+ for rendered in [format!("{from_hex:?}"), format!("{from_nsec:?}")] {
+ assert!(rendered.contains("[redacted]"));
+ assert!(!rendered.contains(FIXTURE_ALICE.secret_key_hex));
+ assert!(!rendered.contains(FIXTURE_ALICE.nsec));
+ }
+ }
+
+ #[cfg(feature = "signing")]
+ #[test]
+ fn nip49_known_vector_decrypts_and_round_trips_with_explicit_options() {
+ let decrypted = decrypt_secret_key_nip49(NCRYPTSEC, "nostr").expect("known ncryptsec");
+ let expected = parse_secret_key(NCRYPTSEC_SECRET_HEX).expect("known secret key");
+ assert_eq!(
+ secret_key_to_nsec(&decrypted),
+ secret_key_to_nsec(&expected)
+ );
+
+ let options = Nip49Options::new(10, Nip49KeySecurity::Medium);
+ assert_eq!(options.log_n(), 10);
+ assert_eq!(options.key_security(), Nip49KeySecurity::Medium);
+ let encrypted = encrypt_secret_key_nip49_with_options(&decrypted, "test-password", options)
+ .expect("encrypt ncryptsec");
+ let round_trip =
+ decrypt_secret_key_nip49(&encrypted, "test-password").expect("decrypt ncryptsec");
+ assert_eq!(
+ secret_key_to_nsec(&round_trip),
+ secret_key_to_nsec(&decrypted)
+ );
+ }
+
+ #[cfg(feature = "signing")]
+ #[test]
+ fn secret_failures_are_redacted() {
+ let invalid_secret = "nsec1-do-not-disclose-secret-input";
+ let parse_error = parse_secret_key(invalid_secret).expect_err("invalid secret");
+ assert!(matches!(parse_error, Error::InvalidSecretKey));
+ assert!(!parse_error.to_string().contains(invalid_secret));
+ assert!(!format!("{parse_error:?}").contains(invalid_secret));
+
+ let password = "do-not-disclose-password";
+ let decrypt_error =
+ decrypt_secret_key_nip49(NCRYPTSEC, password).expect_err("wrong password");
+ assert!(matches!(decrypt_error, Error::SecretKeyDecryption));
+ for rendered in [decrypt_error.to_string(), format!("{decrypt_error:?}")] {
+ assert!(!rendered.contains(password));
+ assert!(!rendered.contains(NCRYPTSEC));
+ assert!(!rendered.contains(NCRYPTSEC_SECRET_HEX));
+ }
+
+ let encrypted_error =
+ decrypt_secret_key_nip49(invalid_secret, password).expect_err("invalid ncryptsec");
+ assert!(matches!(encrypted_error, Error::InvalidEncryptedSecretKey));
+ assert!(!encrypted_error.to_string().contains(invalid_secret));
+ assert!(!format!("{encrypted_error:?}").contains(invalid_secret));
+ }
+}
diff --git a/crates/nostr/src/lib.rs b/crates/nostr/src/lib.rs
@@ -12,7 +12,6 @@ pub mod event;
pub mod events;
pub mod filter;
pub mod key;
-pub mod parse;
pub mod tag;
pub mod tags;
pub mod types;
@@ -106,7 +105,12 @@ pub mod prelude {
radroots_nostr_metadata_has_fields,
};
- pub use crate::parse::{radroots_nostr_parse_pubkey, radroots_nostr_parse_pubkeys};
+ #[cfg(feature = "nip17")]
+ pub use crate::nip17::{
+ RadrootsNip17Error, RadrootsNip17Rumor, RadrootsNip17WrapOptions,
+ radroots_nostr_unwrap_gift_wrap, radroots_nostr_wrap_message,
+ radroots_nostr_wrap_message_file,
+ };
pub use crate::tags::*;
pub use crate::types::{
RadrootsNostrCoordinate, RadrootsNostrEvent, RadrootsNostrEventId,
@@ -117,14 +121,6 @@ pub mod prelude {
RadrootsNostrTag, RadrootsNostrTagKind, RadrootsNostrTagStandard, RadrootsNostrTimestamp,
RadrootsNostrToBech32, RadrootsNostrUrl,
};
- pub use crate::util::radroots_nostr_npub_string;
-
- #[cfg(feature = "nip17")]
- pub use crate::nip17::{
- RadrootsNip17Error, RadrootsNip17Rumor, RadrootsNip17WrapOptions,
- radroots_nostr_unwrap_gift_wrap, radroots_nostr_wrap_message,
- radroots_nostr_wrap_message_file,
- };
#[cfg(feature = "events")]
pub use crate::event_adapters::{to_post_event_metadata, to_profile_event_metadata};
diff --git a/crates/nostr/src/parse.rs b/crates/nostr/src/parse.rs
@@ -1,23 +0,0 @@
-use crate::types::{RadrootsNostrFromBech32, RadrootsNostrPublicKey};
-use alloc::{string::String, string::ToString, vec::Vec};
-
-#[derive(Debug, thiserror::Error)]
-pub enum ParseError {
- #[error("invalid pubkey format: {0}")]
- Invalid(String),
-}
-
-pub fn radroots_nostr_parse_pubkey(s: &str) -> Result<RadrootsNostrPublicKey, ParseError> {
- RadrootsNostrPublicKey::from_bech32(s)
- .or_else(|_| RadrootsNostrPublicKey::from_hex(s))
- .map_err(|_| ParseError::Invalid(s.to_string()))
-}
-
-pub fn radroots_nostr_parse_pubkeys(
- input: &[String],
-) -> Result<Vec<RadrootsNostrPublicKey>, ParseError> {
- input
- .iter()
- .map(|s| radroots_nostr_parse_pubkey(s))
- .collect()
-}
diff --git a/crates/nostr/src/util.rs b/crates/nostr/src/util.rs
@@ -1,11 +1,4 @@
-use crate::types::{
- RadrootsNostrEvent, RadrootsNostrPublicKey, RadrootsNostrTimestamp, RadrootsNostrToBech32,
-};
-use alloc::string::String;
-
-pub fn radroots_nostr_npub_string(pk: &RadrootsNostrPublicKey) -> Option<String> {
- pk.to_bech32().ok()
-}
+use crate::types::{RadrootsNostrEvent, RadrootsNostrTimestamp};
pub fn created_at_u32_saturating(ts: RadrootsNostrTimestamp) -> u32 {
u32::try_from(ts.as_secs()).unwrap_or(u32::MAX)
diff --git a/crates/nostr/tests/coverage.rs b/crates/nostr/tests/coverage.rs
@@ -17,7 +17,7 @@ use radroots_nostr::filter::{
radroots_nostr_filter_kind, radroots_nostr_filter_new_events, radroots_nostr_filter_tag,
radroots_nostr_kind,
};
-use radroots_nostr::parse::{radroots_nostr_parse_pubkey, radroots_nostr_parse_pubkeys};
+use radroots_nostr::key::{parse_public_key, public_key_from_nostr, public_key_to_npub};
use radroots_nostr::tags::{
radroots_nostr_tag_at_value, radroots_nostr_tag_first_value, radroots_nostr_tag_match_geohash,
radroots_nostr_tag_match_l, radroots_nostr_tag_match_location,
@@ -29,9 +29,7 @@ use radroots_nostr::types::{
RadrootsNostrKeys, RadrootsNostrKind, RadrootsNostrRelayUrl, RadrootsNostrTag,
RadrootsNostrTagKind, RadrootsNostrTagStandard, RadrootsNostrTimestamp,
};
-use radroots_nostr::util::{
- created_at_u32_saturating, event_created_at_u32_saturating, radroots_nostr_npub_string,
-};
+use radroots_nostr::util::{created_at_u32_saturating, event_created_at_u32_saturating};
use test_fixtures::RELAY_PRIMARY_WSS;
fn make_keys() -> RadrootsNostrKeys {
@@ -189,17 +187,14 @@ fn filter_helpers_cover_all_paths() {
fn parse_helpers_cover_success_and_failure() {
let keys = make_keys();
let pubkey_hex = keys.public_key().to_hex();
- let ok = radroots_nostr_parse_pubkey(pubkey_hex.as_str());
+ let ok = parse_public_key(pubkey_hex.as_str());
assert!(ok.is_ok());
- let invalid = radroots_nostr_parse_pubkey("invalid");
+ let invalid = parse_public_key("invalid");
assert!(invalid.is_err());
- let parsed = radroots_nostr_parse_pubkeys(std::slice::from_ref(&pubkey_hex));
- assert!(parsed.is_ok());
-
- let parse_err = radroots_nostr_parse_pubkeys(&[pubkey_hex, "invalid".to_string()]);
- assert!(parse_err.is_err());
+ let npub = public_key_to_npub(ok.expect("public key")).expect("npub");
+ assert!(parse_public_key(&npub).is_ok());
}
#[test]
@@ -392,8 +387,9 @@ fn tag_helpers_cover_matchers_and_resolve_paths() {
#[test]
fn util_helpers_cover_conversion_paths() {
let keys = make_keys();
- let npub = radroots_nostr_npub_string(&keys.public_key());
- assert!(npub.is_some());
+ let native = public_key_from_nostr(keys.public_key()).expect("native public key");
+ let npub = public_key_to_npub(native).expect("npub");
+ assert!(npub.starts_with("npub1"));
let max = RadrootsNostrTimestamp::from(u64::from(u32::MAX));
let overflow = RadrootsNostrTimestamp::from(u64::from(u32::MAX) + 1);
diff --git a/crates/nostr/tests/package_boundary.rs b/crates/nostr/tests/package_boundary.rs
@@ -7,6 +7,9 @@ use radroots_nostr::{Error as _, event as _, filter as _, key as _, tag as _};
const MANIFEST: &str = include_str!("../Cargo.toml");
const ROOT: &str = include_str!("../src/lib.rs");
+const KEY_MODULE: &str = include_str!("../src/key.rs");
+const IDENTITY_MANIFEST: &str = include_str!("../../identity/Cargo.toml");
+const IDENTITY_KEY_MODULE: &str = include_str!("../../identity/src/key.rs");
const TRANSPORT_MANIFEST: &str = include_str!("../../transport_nostr/Cargo.toml");
const TRANSPORT_ROOT: &str = include_str!("../../transport_nostr/src/lib.rs");
@@ -161,6 +164,63 @@ fn live_client_and_http_ownership_belongs_to_transport_nostr() {
}
}
+#[test]
+fn nostr_key_conversion_is_explicit_and_identity_remains_public_only() {
+ for required in [
+ "nostr/nip49",
+ "pub fn public_key_to_nostr",
+ "pub fn public_key_from_nostr",
+ "pub fn public_key_to_npub",
+ "pub fn public_key_from_npub",
+ "pub fn parse_public_key",
+ "pub fn parse_secret_key",
+ "pub fn secret_key_to_nsec",
+ "pub fn encrypt_secret_key_nip49",
+ "pub fn encrypt_secret_key_nip49_with_options",
+ "pub fn decrypt_secret_key_nip49",
+ ] {
+ let authority = if required == "nostr/nip49" {
+ MANIFEST
+ } else {
+ KEY_MODULE
+ };
+ assert!(
+ authority.contains(required),
+ "Nostr key authority is missing `{required}`"
+ );
+ }
+
+ for forbidden in ["nostr =", "nip49", "nsec", "ncryptsec", "SecretKey"] {
+ assert!(
+ !IDENTITY_MANIFEST.contains(forbidden),
+ "identity manifest regained Nostr secret ownership `{forbidden}`"
+ );
+ assert!(
+ !IDENTITY_KEY_MODULE.contains(forbidden),
+ "identity key module regained Nostr secret ownership `{forbidden}`"
+ );
+ }
+
+ for secret_function in [
+ "parse_secret_key",
+ "secret_key_to_nsec",
+ "encrypt_secret_key_nip49",
+ "encrypt_secret_key_nip49_with_options",
+ "decrypt_secret_key_nip49",
+ ] {
+ let signature = format!("pub fn {secret_function}");
+ let position = KEY_MODULE
+ .find(&signature)
+ .unwrap_or_else(|| panic!("missing secret adapter `{secret_function}`"));
+ let prefix = &KEY_MODULE[..position];
+ let nearby = &prefix[prefix.len().saturating_sub(160)..];
+ assert!(
+ nearby.contains("#[cfg(feature = \"signing\")]"),
+ "secret adapter `{secret_function}` is not signing-gated"
+ );
+ }
+}
+
fn rust_sources(root: &Path) -> Vec<PathBuf> {
let mut pending = vec![root.to_path_buf()];
let mut sources = Vec::new();