5364854881
Introduces PublicKey and SecretKey as distinct type-safe wrappers around secp256k1, with hex and NIP-19 bech32 (npub/nsec) encoding. SecretKey has a redacted Debug impl and no Display to reduce accidental leakage; it exposes material only through explicit to_hex / to_nsec. FromStr on both types auto-detects hex vs. bech32. Eight round-trip tests cover encoding, auto-detection, prefix validation, debug redaction, and generation.
149 lines
5.0 KiB
Rust
149 lines
5.0 KiB
Rust
use coracle_lib::keys::{KeyError, PublicKey, SecretKey};
|
|
|
|
fn fixed_secret() -> SecretKey {
|
|
let bytes: [u8; 32] = [
|
|
1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25,
|
|
26, 27, 28, 29, 30, 31, 32,
|
|
];
|
|
SecretKey::from_hex(&hex::encode(bytes)).unwrap()
|
|
}
|
|
|
|
#[test]
|
|
fn public_key_hex_roundtrip() {
|
|
let pk = fixed_secret().public_key();
|
|
let hex = pk.to_hex();
|
|
assert_eq!(hex.len(), 64);
|
|
assert_eq!(PublicKey::from_hex(&hex).unwrap(), pk);
|
|
}
|
|
|
|
#[test]
|
|
fn public_key_npub_roundtrip() {
|
|
let pk = fixed_secret().public_key();
|
|
let npub = pk.to_npub();
|
|
assert!(npub.starts_with("npub1"));
|
|
assert_eq!(PublicKey::from_npub(&npub).unwrap(), pk);
|
|
}
|
|
|
|
#[test]
|
|
fn secret_key_hex_roundtrip() {
|
|
let sk = fixed_secret();
|
|
let parsed = SecretKey::from_hex(&sk.to_hex()).unwrap();
|
|
assert_eq!(sk.to_hex(), parsed.to_hex());
|
|
}
|
|
|
|
#[test]
|
|
fn secret_key_nsec_roundtrip() {
|
|
let sk = fixed_secret();
|
|
let nsec = sk.to_nsec();
|
|
assert!(nsec.starts_with("nsec1"));
|
|
let parsed = SecretKey::from_nsec(&nsec).unwrap();
|
|
assert_eq!(sk.to_hex(), parsed.to_hex());
|
|
}
|
|
|
|
#[test]
|
|
fn from_str_auto_detects() {
|
|
let sk = fixed_secret();
|
|
let pk = sk.public_key();
|
|
|
|
let from_hex: PublicKey = pk.to_hex().parse().unwrap();
|
|
let from_npub: PublicKey = pk.to_npub().parse().unwrap();
|
|
assert_eq!(from_hex, pk);
|
|
assert_eq!(from_npub, pk);
|
|
|
|
let sk_from_hex: SecretKey = sk.to_hex().parse().unwrap();
|
|
let sk_from_nsec: SecretKey = sk.to_nsec().parse().unwrap();
|
|
assert_eq!(sk_from_hex.to_hex(), sk.to_hex());
|
|
assert_eq!(sk_from_nsec.to_hex(), sk.to_hex());
|
|
}
|
|
|
|
#[test]
|
|
fn wrong_prefix_rejected() {
|
|
let npub = fixed_secret().public_key().to_npub();
|
|
match SecretKey::from_nsec(&npub) {
|
|
Err(KeyError::WrongPrefix { expected: "nsec", .. }) => (),
|
|
other => panic!("expected WrongPrefix error, got {other:?}"),
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn debug_is_redacted() {
|
|
let sk = fixed_secret();
|
|
let rendered = format!("{sk:?}");
|
|
assert_eq!(rendered, "SecretKey(<redacted>)");
|
|
assert!(!rendered.contains(&sk.to_hex()));
|
|
}
|
|
|
|
#[test]
|
|
fn generate_produces_distinct_keys() {
|
|
let a = SecretKey::generate();
|
|
let b = SecretKey::generate();
|
|
assert_ne!(a.to_hex(), b.to_hex());
|
|
}
|
|
|
|
/// Test vector from NIP-49:
|
|
/// <https://github.com/nostr-protocol/nips/blob/master/49.md>
|
|
#[test]
|
|
fn ncryptsec_spec_vector() {
|
|
let ncryptsec = "ncryptsec1qgg9947rlpvqu76pj5ecreduf9jxhselq2nae2kghhvd5g7dgjtcxfqtd67p9m0w57lspw8gsq6yphnm8623nsl8xn9j4jdzz84zm3frztj3z7s35vpzmqf6ksu8r89qk5z2zxfmu5gv8th8wclt0h4p";
|
|
let expected = "3501454135014541350145413501453fefb02227e449e57cf4d3a3ce05378683";
|
|
let sk = SecretKey::from_ncryptsec(ncryptsec, "nostr").unwrap();
|
|
assert_eq!(sk.to_hex(), expected);
|
|
}
|
|
|
|
#[test]
|
|
fn ncryptsec_wrong_password_rejected() {
|
|
let ncryptsec = "ncryptsec1qgg9947rlpvqu76pj5ecreduf9jxhselq2nae2kghhvd5g7dgjtcxfqtd67p9m0w57lspw8gsq6yphnm8623nsl8xn9j4jdzz84zm3frztj3z7s35vpzmqf6ksu8r89qk5z2zxfmu5gv8th8wclt0h4p";
|
|
match SecretKey::from_ncryptsec(ncryptsec, "wrong-password") {
|
|
Err(KeyError::DecryptionFailed) => (),
|
|
other => panic!("expected DecryptionFailed, got {other:?}"),
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn ncryptsec_wrong_prefix_rejected() {
|
|
let nsec = fixed_secret().to_nsec();
|
|
match SecretKey::from_ncryptsec(&nsec, "nostr") {
|
|
Err(KeyError::WrongPrefix { expected: "ncryptsec", .. }) => (),
|
|
other => panic!("expected WrongPrefix error, got {other:?}"),
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn ncryptsec_roundtrip() {
|
|
let sk = fixed_secret();
|
|
// log_n = 16 is the low end of NIP-49's recommended range; picked here for
|
|
// test speed. security_byte = 0x01 means "has not been handled insecurely".
|
|
let ncryptsec = sk.to_ncryptsec("hunter2", 16, 0x01).unwrap();
|
|
assert!(ncryptsec.starts_with("ncryptsec1"));
|
|
|
|
let decrypted = SecretKey::from_ncryptsec(&ncryptsec, "hunter2").unwrap();
|
|
assert_eq!(decrypted.to_hex(), sk.to_hex());
|
|
}
|
|
|
|
#[test]
|
|
fn ncryptsec_roundtrip_is_nondeterministic() {
|
|
// Encrypting the same key twice must yield different strings, since salt
|
|
// and nonce are sampled fresh. Both must still decrypt to the same secret.
|
|
let sk = fixed_secret();
|
|
let a = sk.to_ncryptsec("hunter2", 16, 0x01).unwrap();
|
|
let b = sk.to_ncryptsec("hunter2", 16, 0x01).unwrap();
|
|
assert_ne!(a, b);
|
|
assert_eq!(
|
|
SecretKey::from_ncryptsec(&a, "hunter2").unwrap().to_hex(),
|
|
SecretKey::from_ncryptsec(&b, "hunter2").unwrap().to_hex(),
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn ncryptsec_nfkc_normalizes_password() {
|
|
// The character "ñ" can be encoded either as U+00F1 (precomposed) or as
|
|
// U+006E U+0303 (n + combining tilde). NFKC collapses them to the same
|
|
// form, so both strings should unlock the same ncryptsec.
|
|
let sk = fixed_secret();
|
|
let precomposed = "ma\u{00F1}ana";
|
|
let decomposed = "man\u{0303}ana";
|
|
let ncryptsec = sk.to_ncryptsec(precomposed, 16, 0x01).unwrap();
|
|
let decrypted = SecretKey::from_ncryptsec(&ncryptsec, decomposed).unwrap();
|
|
assert_eq!(decrypted.to_hex(), sk.to_hex());
|
|
}
|