use core::fmt;
use k256::elliptic_curve::rand_core::OsRng;
use k256::elliptic_curve::sec1::ToEncodedPoint;
use k256::schnorr::signature::hazmat::{PrehashSigner, PrehashVerifier};
use k256::schnorr::{Signature, SigningKey, VerifyingKey};
#[cfg(feature = "serde")]
use serde::{Deserialize, Deserializer, Serialize, Serializer};
#[cfg(feature = "nostr")]
use sha2::Digest;
#[cfg(any(feature = "nostr", feature = "nip44"))]
use sha2::Sha256;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum SecpError {
InvalidSecretKey,
InvalidPublicKey,
InvalidSignature,
ExhaustedAttempts,
InvalidHex(&'static str),
InvalidEvent(&'static str),
InvalidNip19(&'static str),
InvalidNip04(&'static str),
InvalidNip44(&'static str),
Json(String),
}
impl fmt::Display for SecpError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::InvalidSecretKey => f.write_str("invalid secret key"),
Self::InvalidPublicKey => f.write_str("invalid public key"),
Self::InvalidSignature => f.write_str("invalid signature"),
Self::ExhaustedAttempts => f.write_str("exhausted attempts"),
Self::InvalidHex(message) => f.write_str(message),
Self::InvalidEvent(message) => f.write_str(message),
Self::InvalidNip19(message) => f.write_str(message),
Self::InvalidNip04(message) => f.write_str(message),
Self::InvalidNip44(message) => f.write_str(message),
Self::Json(error) => write!(f, "json error: {error}"),
}
}
}
impl std::error::Error for SecpError {}
#[cfg(feature = "nostr")]
impl From<serde_json::Error> for SecpError {
fn from(value: serde_json::Error) -> Self {
Self::Json(value.to_string())
}
}
fn hex_encode(bytes: &[u8]) -> String {
const HEX: &[u8; 16] = b"0123456789abcdef";
let mut s = String::with_capacity(bytes.len() * 2);
for &b in bytes {
s.push(HEX[(b >> 4) as usize] as char);
s.push(HEX[(b & 0x0f) as usize] as char);
}
s
}
fn hex_decode(hex: &str) -> Result<Vec<u8>, SecpError> {
let hex = hex.as_bytes();
if hex.len() % 2 != 0 {
return Err(SecpError::InvalidHex("odd length"));
}
let mut out = Vec::with_capacity(hex.len() / 2);
for chunk in hex.chunks(2) {
let high = hex_nibble(chunk[0])?;
let low = hex_nibble(chunk[1])?;
out.push((high << 4) | low);
}
Ok(out)
}
fn hex_nibble(b: u8) -> Result<u8, SecpError> {
match b {
b'0'..=b'9' => Ok(b - b'0'),
b'a'..=b'f' => Ok(b - b'a' + 10),
b'A'..=b'F' => Ok(b - b'A' + 10),
_ => Err(SecpError::InvalidHex("invalid character")),
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct SecretKey {
bytes: [u8; 32],
}
impl SecretKey {
pub fn generate() -> Result<Self, SecpError> {
let signing_key = SigningKey::random(&mut OsRng);
let mut bytes = [0u8; 32];
bytes.copy_from_slice(&signing_key.to_bytes());
Self::from_bytes(bytes)
}
pub fn from_hex(hex: &str) -> Result<Self, SecpError> {
let bytes = hex_decode(hex)?;
if bytes.len() != 32 {
return Err(SecpError::InvalidHex("expected 64 hex characters"));
}
let mut arr = [0u8; 32];
arr.copy_from_slice(&bytes);
Self::from_bytes(arr)
}
pub fn from_bytes(bytes: [u8; 32]) -> Result<Self, SecpError> {
let _ = SigningKey::from_bytes(&bytes).map_err(|_| SecpError::InvalidSecretKey)?;
Ok(Self { bytes })
}
pub fn to_hex(&self) -> String {
hex_encode(&self.bytes)
}
pub fn to_bytes(&self) -> [u8; 32] {
self.bytes
}
pub fn public_key(&self) -> Result<PublicKey, SecpError> {
let verifying = self.verifying_key()?;
Ok(PublicKey {
sec1_bytes: verifying
.as_affine()
.to_encoded_point(true)
.as_bytes()
.try_into()
.map_err(|_| SecpError::InvalidPublicKey)?,
})
}
pub fn xonly_public_key(&self) -> Result<XOnlyPublicKey, SecpError> {
let verifying = self.verifying_key()?;
Ok(XOnlyPublicKey {
bytes: *verifying.to_bytes().as_ref(),
})
}
pub fn sign_schnorr_prehash(&self, digest32: [u8; 32]) -> Result<SchnorrSignature, SecpError> {
let signing_key = self.signing_key()?;
let signature = signing_key
.sign_prehash(&digest32)
.map_err(|_| SecpError::InvalidSignature)?;
Ok(SchnorrSignature {
bytes: signature.to_bytes(),
})
}
fn signing_key(&self) -> Result<SigningKey, SecpError> {
SigningKey::from_bytes(&self.bytes).map_err(|_| SecpError::InvalidSecretKey)
}
fn verifying_key(&self) -> Result<VerifyingKey, SecpError> {
let signing_key = self.signing_key()?;
Ok(*signing_key.verifying_key())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct PublicKey {
sec1_bytes: [u8; 33],
}
impl PublicKey {
pub fn from_hex(hex: &str) -> Result<Self, SecpError> {
let bytes = hex_decode(hex)?;
if bytes.len() != 33 {
return Err(SecpError::InvalidHex("expected 66 hex characters"));
}
Self::from_sec1_bytes(&bytes)
}
pub fn from_sec1_bytes(bytes: &[u8]) -> Result<Self, SecpError> {
let key =
k256::PublicKey::from_sec1_bytes(bytes).map_err(|_| SecpError::InvalidPublicKey)?;
let sec1 = key.to_sec1_bytes();
let mut sec1_bytes = [0u8; 33];
sec1_bytes.copy_from_slice(sec1.as_ref());
Ok(Self { sec1_bytes })
}
pub fn to_hex(&self) -> String {
hex_encode(&self.sec1_bytes)
}
pub fn to_sec1_bytes(&self) -> [u8; 33] {
self.sec1_bytes
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct XOnlyPublicKey {
bytes: [u8; 32],
}
impl XOnlyPublicKey {
pub fn from_hex(hex: &str) -> Result<Self, SecpError> {
let bytes = hex_decode(hex)?;
if bytes.len() != 32 {
return Err(SecpError::InvalidHex("expected 64 hex characters"));
}
let mut arr = [0u8; 32];
arr.copy_from_slice(&bytes);
Self::from_bytes(arr)
}
pub fn from_bytes(bytes: [u8; 32]) -> Result<Self, SecpError> {
let _ = VerifyingKey::from_bytes(&bytes).map_err(|_| SecpError::InvalidPublicKey)?;
Ok(Self { bytes })
}
pub fn to_hex(&self) -> String {
hex_encode(&self.bytes)
}
pub fn to_bytes(&self) -> [u8; 32] {
self.bytes
}
pub fn verify_schnorr_prehash(
&self,
digest32: [u8; 32],
sig: &SchnorrSignature,
) -> Result<(), SecpError> {
let verifying_key =
VerifyingKey::from_bytes(&self.bytes).map_err(|_| SecpError::InvalidPublicKey)?;
let signature =
Signature::try_from(sig.bytes.as_slice()).map_err(|_| SecpError::InvalidSignature)?;
verifying_key
.verify_prehash(&digest32, &signature)
.map_err(|_| SecpError::InvalidSignature)
}
}
#[cfg(feature = "serde")]
impl Serialize for XOnlyPublicKey {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.serialize_str(&self.to_hex())
}
}
#[cfg(feature = "serde")]
impl<'de> Deserialize<'de> for XOnlyPublicKey {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let hex = String::deserialize(deserializer)?;
Self::from_hex(&hex).map_err(serde::de::Error::custom)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct SchnorrSignature {
bytes: [u8; 64],
}
impl SchnorrSignature {
pub fn to_bytes(&self) -> [u8; 64] {
self.bytes
}
}
#[cfg(feature = "serde")]
impl Serialize for SchnorrSignature {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.serialize_str(&hex_encode(&self.bytes))
}
}
#[cfg(feature = "serde")]
impl<'de> Deserialize<'de> for SchnorrSignature {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let hex = String::deserialize(deserializer)?;
let bytes = hex_decode(&hex).map_err(serde::de::Error::custom)?;
if bytes.len() != 64 {
return Err(serde::de::Error::custom("expected 128 hex characters"));
}
let mut arr = [0u8; 64];
arr.copy_from_slice(&bytes);
Ok(Self { bytes: arr })
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct KeyBundle {
secret: SecretKey,
xonly: XOnlyPublicKey,
}
impl KeyBundle {
pub fn generate() -> Result<Self, SecpError> {
let secret = SecretKey::generate()?;
let xonly = secret.xonly_public_key()?;
Ok(Self { secret, xonly })
}
pub fn secret(&self) -> &SecretKey {
&self.secret
}
pub fn xonly_public_key(&self) -> &XOnlyPublicKey {
&self.xonly
}
#[cfg(feature = "batch")]
pub fn generate_batch(count: usize) -> Result<Vec<Self>, SecpError> {
let mut bundles = Vec::with_capacity(count);
for _ in 0..count {
bundles.push(Self::generate()?);
}
Ok(bundles)
}
#[cfg(feature = "nip19")]
pub fn npub(&self) -> Result<String, SecpError> {
nip19::encode_npub(&self.xonly)
}
#[cfg(feature = "nip19")]
pub fn nsec(&self) -> Result<String, SecpError> {
nip19::encode_nsec(&self.secret)
}
}
#[cfg(all(feature = "batch", feature = "nip19"))]
pub fn mine_vanity_npub(prefix: &str, max_attempts: u64) -> Result<KeyBundle, SecpError> {
for _ in 0..max_attempts {
let bundle = KeyBundle::generate()?;
if count_npub_prefix_matches(&bundle.xonly_public_key().to_bytes(), prefix)? == prefix.len()
{
return Ok(bundle);
}
}
Err(SecpError::ExhaustedAttempts)
}
#[cfg(all(feature = "batch", feature = "nip19"))]
#[derive(Debug, Clone)]
pub struct VanityCandidate {
bundle: KeyBundle,
matched_len: usize,
}
#[cfg(all(feature = "batch", feature = "nip19"))]
impl VanityCandidate {
pub fn bundle(&self) -> &KeyBundle {
&self.bundle
}
pub fn matched_len(&self) -> usize {
self.matched_len
}
}
#[cfg(all(feature = "batch", feature = "nip19"))]
pub fn mine_vanity_npub_candidates(
prefix: &str,
max_attempts: u64,
top_k: usize,
) -> Result<Vec<VanityCandidate>, SecpError> {
if top_k == 0 {
return Ok(vec![]);
}
let mut candidates: Vec<VanityCandidate> = Vec::new();
let mut min_matched = 0usize;
for _ in 0..max_attempts {
let bundle = KeyBundle::generate()?;
let matched = count_npub_prefix_matches(&bundle.xonly_public_key().to_bytes(), prefix)?;
if matched == 0 {
continue;
}
if matched == prefix.len() || matched > min_matched || candidates.len() < top_k {
candidates.push(VanityCandidate {
bundle,
matched_len: matched,
});
}
if candidates.len() > top_k {
candidates.sort_by(|a, b| b.matched_len.cmp(&a.matched_len));
candidates.truncate(top_k);
min_matched = candidates.last().map_or(0, |c| c.matched_len);
}
}
candidates.sort_by(|a, b| b.matched_len.cmp(&a.matched_len));
candidates.truncate(top_k);
Ok(candidates)
}
#[cfg(all(feature = "batch", feature = "nip19"))]
const BECH32_CHARSET: &[u8; 32] = b"qpzry9x8gf2tvdw0s3jn54khce6mua7l";
#[cfg(all(feature = "batch", feature = "nip19"))]
fn bech32_value(byte: u8) -> Result<u8, SecpError> {
match byte {
b'q' => Ok(0),
b'p' => Ok(1),
b'z' => Ok(2),
b'r' => Ok(3),
b'y' => Ok(4),
b'9' => Ok(5),
b'x' => Ok(6),
b'8' => Ok(7),
b'g' => Ok(8),
b'f' => Ok(9),
b'2' => Ok(10),
b't' => Ok(11),
b'v' => Ok(12),
b'd' => Ok(13),
b'w' => Ok(14),
b'0' => Ok(15),
b's' => Ok(16),
b'3' => Ok(17),
b'j' => Ok(18),
b'n' => Ok(19),
b'5' => Ok(20),
b'4' => Ok(21),
b'k' => Ok(22),
b'h' => Ok(23),
b'c' => Ok(24),
b'e' => Ok(25),
b'6' => Ok(26),
b'm' => Ok(27),
b'u' => Ok(28),
b'a' => Ok(29),
b'7' => Ok(30),
b'l' => Ok(31),
_ => Err(SecpError::InvalidNip19("invalid npub vanity prefix")),
}
}
#[cfg(all(feature = "batch", feature = "nip19"))]
fn count_npub_prefix_matches(xonly_bytes: &[u8; 32], prefix: &str) -> Result<usize, SecpError> {
let prefix_bytes = prefix.as_bytes();
if prefix_bytes.is_empty() {
return Ok(0);
}
for &byte in prefix_bytes {
bech32_value(byte)?;
}
let mut matched = 0usize;
let mut acc = 0u16;
let mut bits = 0u8;
for &byte in xonly_bytes {
acc = (acc << 8) | u16::from(byte);
bits += 8;
while bits >= 5 && matched < prefix_bytes.len() {
bits -= 5;
let value = ((acc >> bits) & 0x1f) as usize;
if BECH32_CHARSET[value] != prefix_bytes[matched] {
return Ok(matched);
}
matched += 1;
}
if matched == prefix_bytes.len() {
return Ok(matched);
}
}
if bits > 0 && matched < prefix_bytes.len() {
let value = ((acc << (5 - bits)) & 0x1f) as usize;
if BECH32_CHARSET[value] == prefix_bytes[matched] {
matched += 1;
}
}
Ok(matched)
}
#[cfg(feature = "batch")]
pub fn mine_pow(difficulty: u8, max_attempts: u64) -> Result<KeyBundle, SecpError> {
for _ in 0..max_attempts {
let bundle = KeyBundle::generate()?;
if count_leading_zero_nibbles(&bundle.xonly_public_key().to_bytes()) >= difficulty {
return Ok(bundle);
}
}
Err(SecpError::ExhaustedAttempts)
}
#[cfg(feature = "batch")]
pub fn mine_pow_best(min_difficulty: u8, max_attempts: u64) -> Result<(KeyBundle, u8), SecpError> {
let mut best: Option<(KeyBundle, u8)> = None;
for _ in 0..max_attempts {
let bundle = KeyBundle::generate()?;
let diff = count_leading_zero_nibbles(&bundle.xonly_public_key().to_bytes());
if diff >= min_difficulty {
match best {
Some((_, best_diff)) if diff <= best_diff => {}
_ => best = Some((bundle, diff)),
}
}
}
best.ok_or(SecpError::ExhaustedAttempts)
}
#[cfg(feature = "batch")]
fn count_leading_zero_nibbles(bytes: &[u8]) -> u8 {
let mut count = 0u8;
for &byte in bytes {
let high = byte >> 4;
if high == 0 {
count += 1;
} else {
break;
}
let low = byte & 0x0f;
if low == 0 {
count += 1;
} else {
break;
}
}
count
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct EventId {
bytes: [u8; 32],
}
impl EventId {
pub fn from_hex(hex: &str) -> Result<Self, SecpError> {
let bytes = hex_decode(hex)?;
if bytes.len() != 32 {
return Err(SecpError::InvalidHex("expected 64 hex characters"));
}
let mut arr = [0u8; 32];
arr.copy_from_slice(&bytes);
Ok(Self::from_bytes(arr))
}
pub fn from_bytes(bytes: [u8; 32]) -> Self {
Self { bytes }
}
pub fn to_hex(&self) -> String {
hex_encode(&self.bytes)
}
pub fn to_bytes(&self) -> [u8; 32] {
self.bytes
}
}
#[cfg(feature = "serde")]
impl Serialize for EventId {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.serialize_str(&self.to_hex())
}
}
#[cfg(feature = "serde")]
impl<'de> Deserialize<'de> for EventId {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let hex = String::deserialize(deserializer)?;
Self::from_hex(&hex).map_err(serde::de::Error::custom)
}
}
#[cfg(feature = "nip19")]
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Nip19 {
Npub(XOnlyPublicKey),
Nsec(SecretKey),
Note(EventId),
NProfile(NProfile),
NEvent(NEvent),
NAddr(NAddr),
NRelay(NRelay),
}
#[cfg(feature = "nip19")]
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct NProfile {
pub pubkey: XOnlyPublicKey,
pub relays: Vec<String>,
}
#[cfg(feature = "nip19")]
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct NEvent {
pub id: EventId,
pub relays: Vec<String>,
pub author: Option<XOnlyPublicKey>,
pub kind: Option<u32>,
}
#[cfg(feature = "nip19")]
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct NAddr {
pub identifier: String,
pub relays: Vec<String>,
pub author: XOnlyPublicKey,
pub kind: u32,
}
#[cfg(feature = "nip19")]
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct NRelay {
pub relay: String,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct UnsignedEvent {
pub created_at: u64,
pub kind: u32,
pub tags: Vec<Vec<String>>,
pub content: String,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct SignedEvent {
pub id: EventId,
pub pubkey: XOnlyPublicKey,
pub created_at: u64,
pub kind: u32,
pub tags: Vec<Vec<String>>,
pub content: String,
pub sig: SchnorrSignature,
}
#[cfg(feature = "nostr")]
pub mod nostr {
use super::*;
use serde_json::{Map, Value};
pub fn serialize_event(
pubkey: &XOnlyPublicKey,
event: &UnsignedEvent,
) -> Result<String, SecpError> {
let payload = serde_json::json!([
0,
hex_encode(&pubkey.to_bytes()),
event.created_at,
event.kind,
event.tags,
event.content
]);
serde_json::to_string(&payload).map_err(SecpError::from)
}
pub fn compute_event_id(
pubkey: &XOnlyPublicKey,
event: &UnsignedEvent,
) -> Result<EventId, SecpError> {
let serialized = serialize_event(pubkey, event)?;
let mut hasher = Sha256::new();
hasher.update(serialized.as_bytes());
let mut bytes = [0u8; 32];
bytes.copy_from_slice(&hasher.finalize());
Ok(EventId { bytes })
}
pub fn finalize_event(
event: UnsignedEvent,
secret: &SecretKey,
) -> Result<SignedEvent, SecpError> {
let pubkey = secret.xonly_public_key()?;
let id = compute_event_id(&pubkey, &event)?;
let sig = secret.sign_schnorr_prehash(id.to_bytes())?;
Ok(SignedEvent {
id,
pubkey,
created_at: event.created_at,
kind: event.kind,
tags: event.tags,
content: event.content,
sig,
})
}
pub fn serialize_signed_event(event: &SignedEvent) -> Result<String, SecpError> {
let tags = serde_json::to_string(&event.tags).map_err(SecpError::from)?;
let content = serde_json::to_string(&event.content).map_err(SecpError::from)?;
Ok(format!(
"{{\"id\":\"{}\",\"pubkey\":\"{}\",\"created_at\":{},\"kind\":{},\"tags\":{},\"content\":{},\"sig\":\"{}\"}}",
hex_encode(&event.id.to_bytes()),
hex_encode(&event.pubkey.to_bytes()),
event.created_at,
event.kind,
tags,
content,
hex_encode(&event.sig.to_bytes())
))
}
pub fn parse_signed_event(json: &str) -> Result<SignedEvent, SecpError> {
let value: Value = serde_json::from_str(json).map_err(SecpError::from)?;
let object = value.as_object().ok_or(SecpError::InvalidEvent(
"signed event must be a JSON object",
))?;
let id = parse_hex32(required_string(object, "id")?, "id")?;
let pubkey = parse_hex32(required_string(object, "pubkey")?, "pubkey")?;
let created_at = required_u64(object, "created_at")?;
let kind = required_u32(object, "kind")?;
let tags = parse_tags(required_value(object, "tags")?)?;
let content = required_string(object, "content")?.to_string();
let sig = parse_hex64(required_string(object, "sig")?, "sig")?;
Ok(SignedEvent {
id: EventId::from_bytes(id),
pubkey: XOnlyPublicKey::from_bytes(pubkey)?,
created_at,
kind,
tags,
content,
sig: SchnorrSignature { bytes: sig },
})
}
pub fn verify_event(event: &SignedEvent) -> Result<(), SecpError> {
let unsigned = UnsignedEvent {
created_at: event.created_at,
kind: event.kind,
tags: event.tags.clone(),
content: event.content.clone(),
};
let expected = compute_event_id(&event.pubkey, &unsigned)?;
if expected != event.id {
return Err(SecpError::InvalidEvent("event id mismatch"));
}
event
.pubkey
.verify_schnorr_prehash(event.id.to_bytes(), &event.sig)
}
fn required_value<'a>(
object: &'a Map<String, Value>,
field: &'static str,
) -> Result<&'a Value, SecpError> {
object
.get(field)
.ok_or(SecpError::InvalidEvent(missing_field(field)))
}
fn required_string<'a>(
object: &'a Map<String, Value>,
field: &'static str,
) -> Result<&'a str, SecpError> {
required_value(object, field)?
.as_str()
.ok_or(SecpError::InvalidEvent(expected_field(field)))
}
fn required_u64(object: &Map<String, Value>, field: &'static str) -> Result<u64, SecpError> {
required_value(object, field)?
.as_u64()
.ok_or(SecpError::InvalidEvent(expected_field(field)))
}
fn required_u32(object: &Map<String, Value>, field: &'static str) -> Result<u32, SecpError> {
required_u64(object, field)?
.try_into()
.map_err(|_| SecpError::InvalidEvent(expected_field(field)))
}
fn parse_tags(value: &Value) -> Result<Vec<Vec<String>>, SecpError> {
let tags = value
.as_array()
.ok_or(SecpError::InvalidEvent("tags must be an array"))?;
tags.iter()
.map(|tag| {
let tag = tag
.as_array()
.ok_or(SecpError::InvalidEvent("tag must be an array"))?;
tag.iter()
.map(|entry| {
entry
.as_str()
.map(ToOwned::to_owned)
.ok_or(SecpError::InvalidEvent("tag entry must be a string"))
})
.collect()
})
.collect()
}
fn parse_hex32(hex: &str, field: &'static str) -> Result<[u8; 32], SecpError> {
let bytes =
hex_decode(hex).map_err(|_| SecpError::InvalidEvent(invalid_hex_field(field)))?;
bytes
.try_into()
.map_err(|_| SecpError::InvalidEvent(invalid_hex_field(field)))
}
fn parse_hex64(hex: &str, field: &'static str) -> Result<[u8; 64], SecpError> {
let bytes =
hex_decode(hex).map_err(|_| SecpError::InvalidEvent(invalid_hex_field(field)))?;
bytes
.try_into()
.map_err(|_| SecpError::InvalidEvent(invalid_hex_field(field)))
}
fn missing_field(field: &'static str) -> &'static str {
match field {
"id" => "missing id",
"pubkey" => "missing pubkey",
"created_at" => "missing created_at",
"kind" => "missing kind",
"tags" => "missing tags",
"content" => "missing content",
"sig" => "missing sig",
_ => "missing field",
}
}
fn expected_field(field: &'static str) -> &'static str {
match field {
"id" => "id must be a string",
"pubkey" => "pubkey must be a string",
"created_at" => "created_at must be an integer",
"kind" => "kind must be an integer",
"content" => "content must be a string",
"sig" => "sig must be a string",
_ => "invalid field type",
}
}
fn invalid_hex_field(field: &'static str) -> &'static str {
match field {
"id" => "id must be 64 hex characters",
"pubkey" => "pubkey must be 64 hex characters",
"sig" => "sig must be 128 hex characters",
_ => "invalid hex field",
}
}
}
#[cfg(all(feature = "nostr", feature = "nip44"))]
pub mod nip17 {
use super::*;
use k256::elliptic_curve::rand_core::RngCore;
pub fn create_seal(
inner: UnsignedEvent,
sender: &SecretKey,
recipient: &XOnlyPublicKey,
) -> Result<SignedEvent, SecpError> {
let signed_inner = nostr::finalize_event(inner, sender)?;
let json = nostr::serialize_signed_event(&signed_inner)?;
let conversation_key = nip44::get_conversation_key(sender, recipient)?;
let encrypted = nip44::encrypt(&json, &conversation_key, None)?;
let seal = UnsignedEvent {
created_at: randomized_timestamp(signed_inner.created_at),
kind: 13,
tags: Vec::new(),
content: encrypted,
};
nostr::finalize_event(seal, sender)
}
pub fn open_seal(seal: &SignedEvent, recipient: &SecretKey) -> Result<SignedEvent, SecpError> {
if seal.kind != 13 {
return Err(SecpError::InvalidEvent("seal must have kind 13"));
}
nostr::verify_event(seal)?;
let conversation_key = nip44::get_conversation_key(recipient, &seal.pubkey)?;
let json = nip44::decrypt(&seal.content, &conversation_key)?;
let inner = nostr::parse_signed_event(&json)?;
nostr::verify_event(&inner)?;
Ok(inner)
}
pub fn create_gift_wrap(
seal: &SignedEvent,
recipient: &XOnlyPublicKey,
) -> Result<SignedEvent, SecpError> {
if seal.kind != 13 {
return Err(SecpError::InvalidEvent("seal must have kind 13"));
}
nostr::verify_event(seal)?;
let ephemeral = SecretKey::generate()?;
let json = nostr::serialize_signed_event(seal)?;
let conversation_key = nip44::get_conversation_key(&ephemeral, recipient)?;
let encrypted = nip44::encrypt(&json, &conversation_key, None)?;
let wrap = UnsignedEvent {
created_at: randomized_timestamp(seal.created_at),
kind: 1059,
tags: vec![vec!["p".to_string(), recipient.to_hex()]],
content: encrypted,
};
nostr::finalize_event(wrap, &ephemeral)
}
pub fn open_gift_wrap(
gift_wrap: &SignedEvent,
recipient: &SecretKey,
) -> Result<SignedEvent, SecpError> {
if gift_wrap.kind != 1059 {
return Err(SecpError::InvalidEvent("gift wrap must have kind 1059"));
}
nostr::verify_event(gift_wrap)?;
let conversation_key = nip44::get_conversation_key(recipient, &gift_wrap.pubkey)?;
let json = nip44::decrypt(&gift_wrap.content, &conversation_key)?;
let seal = nostr::parse_signed_event(&json)?;
open_seal(&seal, recipient)
}
fn randomized_timestamp(base: u64) -> u64 {
let offset = (OsRng.next_u32() % 345_601) as u64;
base.saturating_sub(172_800) + offset
}
}
#[cfg(feature = "nostr")]
pub mod nip42 {
use super::*;
pub fn create_auth_event(
challenge: &str,
relay_url: &str,
signer: &SecretKey,
now_unix_seconds: u64,
) -> Result<SignedEvent, SecpError> {
let event = UnsignedEvent {
created_at: now_unix_seconds,
kind: 22_242,
tags: vec![
vec!["relay".to_string(), relay_url.to_string()],
vec!["challenge".to_string(), challenge.to_string()],
],
content: String::new(),
};
nostr::finalize_event(event, signer)
}
pub fn validate_auth_event(
event: &SignedEvent,
challenge: &str,
relay_url: &str,
) -> Result<XOnlyPublicKey, SecpError> {
nostr::verify_event(event)?;
if event.kind != 22_242 {
return Err(SecpError::InvalidEvent("auth event must have kind 22242"));
}
if tag_value(&event.tags, "relay") != Some(relay_url) {
return Err(SecpError::InvalidEvent("auth event relay tag mismatch"));
}
if tag_value(&event.tags, "challenge") != Some(challenge) {
return Err(SecpError::InvalidEvent("auth event challenge tag mismatch"));
}
Ok(event.pubkey)
}
fn tag_value<'a>(tags: &'a [Vec<String>], name: &str) -> Option<&'a str> {
tags.iter()
.find(|tag| tag.first().is_some_and(|value| value == name))
.and_then(|tag| tag.get(1))
.map(String::as_str)
}
}
#[cfg(feature = "nip19")]
pub mod nip19 {
use super::*;
use bech32::{self, FromBase32, ToBase32, Variant};
pub fn encode_npub(pubkey: &XOnlyPublicKey) -> Result<String, SecpError> {
bech32::encode("npub", pubkey.to_bytes().to_base32(), Variant::Bech32)
.map_err(|_| SecpError::InvalidNip19("failed to encode npub"))
}
pub fn encode_nsec(secret: &SecretKey) -> Result<String, SecpError> {
bech32::encode("nsec", secret.to_bytes().to_base32(), Variant::Bech32)
.map_err(|_| SecpError::InvalidNip19("failed to encode nsec"))
}
pub fn encode_note(id: &EventId) -> Result<String, SecpError> {
bech32::encode("note", id.to_bytes().to_base32(), Variant::Bech32)
.map_err(|_| SecpError::InvalidNip19("failed to encode note"))
}
pub fn encode_nprofile(profile: &NProfile) -> Result<String, SecpError> {
encode_tlv_entity(
"nprofile",
&[
(0, vec![profile.pubkey.to_bytes().to_vec()]),
(
1,
profile
.relays
.iter()
.map(|relay| relay.as_bytes().to_vec())
.collect(),
),
],
)
}
pub fn encode_nevent(event: &NEvent) -> Result<String, SecpError> {
let mut fields = vec![
(0, vec![event.id.to_bytes().to_vec()]),
(
1,
event
.relays
.iter()
.map(|relay| relay.as_bytes().to_vec())
.collect(),
),
];
if let Some(author) = event.author {
fields.push((2, vec![author.to_bytes().to_vec()]));
}
if let Some(kind) = event.kind {
fields.push((3, vec![kind.to_be_bytes().to_vec()]));
}
encode_tlv_entity("nevent", &fields)
}
pub fn encode_naddr(addr: &NAddr) -> Result<String, SecpError> {
encode_tlv_entity(
"naddr",
&[
(0, vec![addr.identifier.as_bytes().to_vec()]),
(
1,
addr.relays
.iter()
.map(|relay| relay.as_bytes().to_vec())
.collect(),
),
(2, vec![addr.author.to_bytes().to_vec()]),
(3, vec![addr.kind.to_be_bytes().to_vec()]),
],
)
}
pub fn encode_nrelay(relay: &NRelay) -> Result<String, SecpError> {
encode_tlv_entity("nrelay", &[(0, vec![relay.relay.as_bytes().to_vec()])])
}
pub fn decode(s: &str) -> Result<Nip19, SecpError> {
let (hrp, data, variant) =
bech32::decode(s).map_err(|_| SecpError::InvalidNip19("invalid bech32 string"))?;
if variant != Variant::Bech32 {
return Err(SecpError::InvalidNip19("unexpected bech32 variant"));
}
let bytes = Vec::<u8>::from_base32(&data)
.map_err(|_| SecpError::InvalidNip19("invalid bech32 payload"))?;
match hrp.as_str() {
"npub" => {
let payload: [u8; 32] = bytes
.as_slice()
.try_into()
.map_err(|_| SecpError::InvalidNip19("expected 32-byte payload"))?;
Ok(Nip19::Npub(XOnlyPublicKey::from_bytes(payload)?))
}
"nsec" => {
let payload: [u8; 32] = bytes
.as_slice()
.try_into()
.map_err(|_| SecpError::InvalidNip19("expected 32-byte payload"))?;
Ok(Nip19::Nsec(SecretKey::from_bytes(payload)?))
}
"note" => {
let payload: [u8; 32] = bytes
.as_slice()
.try_into()
.map_err(|_| SecpError::InvalidNip19("expected 32-byte payload"))?;
Ok(Nip19::Note(EventId::from_bytes(payload)))
}
"nprofile" => decode_nprofile(&bytes),
"nevent" => decode_nevent(&bytes),
"naddr" => decode_naddr(&bytes),
"nrelay" => decode_nrelay(&bytes),
_ => Err(SecpError::InvalidNip19("unsupported nip19 prefix")),
}
}
fn decode_nprofile(bytes: &[u8]) -> Result<Nip19, SecpError> {
let tlv = parse_tlv(bytes)?;
let pubkey = required_bytes32(&tlv, 0, "nprofile")?;
let relays = utf8_entries(&tlv, 1, "nprofile")?;
Ok(Nip19::NProfile(NProfile {
pubkey: XOnlyPublicKey::from_bytes(pubkey)?,
relays,
}))
}
fn decode_nevent(bytes: &[u8]) -> Result<Nip19, SecpError> {
let tlv = parse_tlv(bytes)?;
let id = required_bytes32(&tlv, 0, "nevent")?;
let relays = utf8_entries(&tlv, 1, "nevent")?;
let author = optional_bytes32(&tlv, 2, "nevent")?
.map(XOnlyPublicKey::from_bytes)
.transpose()?;
let kind = optional_u32(&tlv, 3, "nevent")?;
Ok(Nip19::NEvent(NEvent {
id: EventId::from_bytes(id),
relays,
author,
kind,
}))
}
fn decode_naddr(bytes: &[u8]) -> Result<Nip19, SecpError> {
let tlv = parse_tlv(bytes)?;
let identifier = required_utf8(&tlv, 0, "naddr")?;
let relays = utf8_entries(&tlv, 1, "naddr")?;
let author = required_bytes32(&tlv, 2, "naddr")?;
let kind = required_u32(&tlv, 3, "naddr")?;
Ok(Nip19::NAddr(NAddr {
identifier,
relays,
author: XOnlyPublicKey::from_bytes(author)?,
kind,
}))
}
fn decode_nrelay(bytes: &[u8]) -> Result<Nip19, SecpError> {
let tlv = parse_tlv(bytes)?;
let relay = required_utf8(&tlv, 0, "nrelay")?;
Ok(Nip19::NRelay(NRelay { relay }))
}
fn encode_tlv_entity(prefix: &str, fields: &[(u8, Vec<Vec<u8>>)]) -> Result<String, SecpError> {
let tlv = encode_tlv(fields)?;
bech32::encode(prefix, tlv.to_base32(), Variant::Bech32)
.map_err(|_| SecpError::InvalidNip19("failed to encode tlv entity"))
}
fn encode_tlv(fields: &[(u8, Vec<Vec<u8>>)]) -> Result<Vec<u8>, SecpError> {
let mut out = Vec::new();
for (tag, values) in fields.iter().rev() {
for value in values {
let len: u8 = value
.len()
.try_into()
.map_err(|_| SecpError::InvalidNip19("tlv value too long"))?;
out.push(*tag);
out.push(len);
out.extend_from_slice(value);
}
}
Ok(out)
}
fn parse_tlv(bytes: &[u8]) -> Result<Vec<Vec<Vec<u8>>>, SecpError> {
let mut tlv = vec![Vec::new(); 256];
let mut offset = 0usize;
while offset < bytes.len() {
if offset + 2 > bytes.len() {
return Err(SecpError::InvalidNip19("truncated tlv header"));
}
let tag = bytes[offset] as usize;
let len = bytes[offset + 1] as usize;
offset += 2;
if offset + len > bytes.len() {
return Err(SecpError::InvalidNip19("not enough data for tlv entry"));
}
tlv[tag].push(bytes[offset..offset + len].to_vec());
offset += len;
}
Ok(tlv)
}
fn required_bytes32(
tlv: &[Vec<Vec<u8>>],
tag: usize,
entity: &'static str,
) -> Result<[u8; 32], SecpError> {
let value = tlv[tag]
.first()
.ok_or(SecpError::InvalidNip19(missing_required_field(entity, tag)))?;
value
.as_slice()
.try_into()
.map_err(|_| SecpError::InvalidNip19(expected_length(entity, tag, 32)))
}
fn optional_bytes32(
tlv: &[Vec<Vec<u8>>],
tag: usize,
entity: &'static str,
) -> Result<Option<[u8; 32]>, SecpError> {
tlv[tag]
.first()
.map(|value| {
value
.as_slice()
.try_into()
.map_err(|_| SecpError::InvalidNip19(expected_length(entity, tag, 32)))
})
.transpose()
}
fn required_u32(
tlv: &[Vec<Vec<u8>>],
tag: usize,
entity: &'static str,
) -> Result<u32, SecpError> {
optional_u32(tlv, tag, entity)?
.ok_or(SecpError::InvalidNip19(missing_required_field(entity, tag)))
}
fn optional_u32(
tlv: &[Vec<Vec<u8>>],
tag: usize,
entity: &'static str,
) -> Result<Option<u32>, SecpError> {
tlv[tag]
.first()
.map(|value| {
let bytes: [u8; 4] = value
.as_slice()
.try_into()
.map_err(|_| SecpError::InvalidNip19(expected_length(entity, tag, 4)))?;
Ok(u32::from_be_bytes(bytes))
})
.transpose()
}
fn required_utf8(
tlv: &[Vec<Vec<u8>>],
tag: usize,
entity: &'static str,
) -> Result<String, SecpError> {
let value = tlv[tag]
.first()
.ok_or(SecpError::InvalidNip19(missing_required_field(entity, tag)))?;
String::from_utf8(value.clone())
.map_err(|_| SecpError::InvalidNip19("invalid utf-8 payload"))
}
fn utf8_entries(
tlv: &[Vec<Vec<u8>>],
tag: usize,
_entity: &'static str,
) -> Result<Vec<String>, SecpError> {
tlv[tag]
.iter()
.map(|value| {
String::from_utf8(value.clone())
.map_err(|_| SecpError::InvalidNip19("invalid utf-8 payload"))
})
.collect()
}
fn missing_required_field(entity: &'static str, tag: usize) -> &'static str {
match (entity, tag) {
("nprofile", 0) => "missing TLV 0 for nprofile",
("nevent", 0) => "missing TLV 0 for nevent",
("naddr", 0) => "missing TLV 0 for naddr",
("naddr", 2) => "missing TLV 2 for naddr",
("naddr", 3) => "missing TLV 3 for naddr",
("nrelay", 0) => "missing TLV 0 for nrelay",
_ => "missing required tlv field",
}
}
fn expected_length(entity: &'static str, tag: usize, len: usize) -> &'static str {
match (entity, tag, len) {
("nprofile", 0, 32) => "TLV 0 should be 32 bytes",
("nevent", 0, 32) => "TLV 0 should be 32 bytes",
("nevent", 2, 32) => "TLV 2 should be 32 bytes",
("nevent", 3, 4) => "TLV 3 should be 4 bytes",
("naddr", 2, 32) => "TLV 2 should be 32 bytes",
("naddr", 3, 4) => "TLV 3 should be 4 bytes",
_ => "invalid tlv length",
}
}
}
#[cfg(feature = "nip44")]
pub mod nip44 {
use super::*;
use base64::engine::general_purpose::STANDARD;
use base64::Engine;
use chacha20::cipher::{KeyIvInit, StreamCipher};
use hkdf::Hkdf;
use hmac::{Hmac, Mac};
use k256::ecdh::diffie_hellman;
use k256::elliptic_curve::rand_core::RngCore;
type HmacSha256 = Hmac<Sha256>;
const VERSION_V2: u8 = 2;
const MIN_PLAINTEXT_SIZE: usize = 1;
const MAX_PLAINTEXT_SIZE: usize = 65_535;
const MIN_RAW_PAYLOAD_SIZE: usize = 99;
const MAX_RAW_PAYLOAD_SIZE: usize = 65_603;
const MIN_ENCODED_PAYLOAD_SIZE: usize = 132;
const MAX_ENCODED_PAYLOAD_SIZE: usize = 87_472;
pub fn get_conversation_key(
secret: &SecretKey,
pubkey: &XOnlyPublicKey,
) -> Result<[u8; 32], SecpError> {
let signing_key = secret.signing_key()?;
let secp_public = super::decode_xonly_pubkey(pubkey)?;
let shared = diffie_hellman(signing_key.as_nonzero_scalar(), secp_public.as_affine());
let hk = Hkdf::<Sha256>::extract(Some(b"nip44-v2"), shared.raw_secret_bytes().as_ref()).0;
Ok(hk.into())
}
pub fn calc_padded_len(len: usize) -> Result<usize, SecpError> {
if len < MIN_PLAINTEXT_SIZE {
return Err(SecpError::InvalidNip44("expected positive integer"));
}
if len <= 32 {
return Ok(32);
}
let next_power = 1usize << (usize::BITS as usize - (len - 1).leading_zeros() as usize);
let chunk = if next_power <= 256 {
32
} else {
next_power / 8
};
Ok(chunk * ((len - 1) / chunk + 1))
}
pub fn encrypt(
plaintext: &str,
conversation_key: &[u8; 32],
nonce: Option<[u8; 32]>,
) -> Result<String, SecpError> {
let nonce = nonce.unwrap_or_else(random_nonce);
let padded = pad(plaintext)?;
let keys = get_message_keys(conversation_key, &nonce)?;
let ciphertext = chacha20_xor(&keys.chacha_key, &keys.chacha_nonce, &padded);
let mac = hmac_aad(&keys.hmac_key, &ciphertext, &nonce)?;
let mut payload = Vec::with_capacity(1 + nonce.len() + ciphertext.len() + mac.len());
payload.push(VERSION_V2);
payload.extend_from_slice(&nonce);
payload.extend_from_slice(&ciphertext);
payload.extend_from_slice(&mac);
Ok(STANDARD.encode(payload))
}
pub fn decrypt(payload: &str, conversation_key: &[u8; 32]) -> Result<String, SecpError> {
let decoded = decode_payload(payload)?;
let keys = get_message_keys(conversation_key, &decoded.nonce)?;
let calculated_mac = hmac_aad(&keys.hmac_key, &decoded.ciphertext, &decoded.nonce)?;
if calculated_mac != decoded.mac {
return Err(SecpError::InvalidNip44("invalid MAC"));
}
let padded = chacha20_xor(&keys.chacha_key, &keys.chacha_nonce, &decoded.ciphertext);
unpad(&padded)
}
struct MessageKeys {
chacha_key: [u8; 32],
chacha_nonce: [u8; 12],
hmac_key: [u8; 32],
}
struct DecodedPayload {
nonce: [u8; 32],
ciphertext: Vec<u8>,
mac: [u8; 32],
}
fn get_message_keys(
conversation_key: &[u8; 32],
nonce: &[u8; 32],
) -> Result<MessageKeys, SecpError> {
let hk = Hkdf::<Sha256>::from_prk(conversation_key)
.map_err(|_| SecpError::InvalidNip44("invalid conversation key"))?;
let mut keys = [0u8; 76];
hk.expand(nonce, &mut keys)
.map_err(|_| SecpError::InvalidNip44("failed to derive message keys"))?;
let mut chacha_key = [0u8; 32];
chacha_key.copy_from_slice(&keys[..32]);
let mut chacha_nonce = [0u8; 12];
chacha_nonce.copy_from_slice(&keys[32..44]);
let mut hmac_key = [0u8; 32];
hmac_key.copy_from_slice(&keys[44..76]);
Ok(MessageKeys {
chacha_key,
chacha_nonce,
hmac_key,
})
}
fn random_nonce() -> [u8; 32] {
let mut nonce = [0u8; 32];
OsRng.fill_bytes(&mut nonce);
nonce
}
fn pad(plaintext: &str) -> Result<Vec<u8>, SecpError> {
let unpadded = plaintext.as_bytes();
let len = unpadded.len();
if !(MIN_PLAINTEXT_SIZE..=MAX_PLAINTEXT_SIZE).contains(&len) {
return Err(SecpError::InvalidNip44(
"invalid plaintext size: must be between 1 and 65535 bytes",
));
}
let padded_len = calc_padded_len(len)?;
let mut out = Vec::with_capacity(2 + padded_len);
let len_u16 = u16::try_from(len).expect("validated plaintext size <= u16::MAX");
out.extend_from_slice(&len_u16.to_be_bytes());
out.extend_from_slice(unpadded);
out.resize(2 + padded_len, 0);
Ok(out)
}
fn unpad(padded: &[u8]) -> Result<String, SecpError> {
if padded.len() < 2 {
return Err(SecpError::InvalidNip44("invalid padding"));
}
let len = u16::from_be_bytes([padded[0], padded[1]]) as usize;
if !(MIN_PLAINTEXT_SIZE..=MAX_PLAINTEXT_SIZE).contains(&len) {
return Err(SecpError::InvalidNip44("invalid padding"));
}
let expected = 2 + calc_padded_len(len)?;
if padded.len() != expected || 2 + len > padded.len() {
return Err(SecpError::InvalidNip44("invalid padding"));
}
let unpadded = &padded[2..2 + len];
String::from_utf8(unpadded.to_vec())
.map_err(|_| SecpError::InvalidNip44("invalid utf-8 payload"))
}
fn hmac_aad(key: &[u8; 32], message: &[u8], aad: &[u8; 32]) -> Result<[u8; 32], SecpError> {
let mut mac = HmacSha256::new_from_slice(key)
.map_err(|_| SecpError::InvalidNip44("invalid HMAC key"))?;
mac.update(aad);
mac.update(message);
let bytes = mac.finalize().into_bytes();
let mut out = [0u8; 32];
out.copy_from_slice(&bytes);
Ok(out)
}
fn chacha20_xor(key: &[u8; 32], nonce: &[u8; 12], data: &[u8]) -> Vec<u8> {
let mut out = data.to_vec();
let mut cipher = chacha20::ChaCha20::new(key.into(), nonce.into());
cipher.apply_keystream(&mut out);
out
}
fn decode_payload(payload: &str) -> Result<DecodedPayload, SecpError> {
let len = payload.len();
if !(MIN_ENCODED_PAYLOAD_SIZE..=MAX_ENCODED_PAYLOAD_SIZE).contains(&len) {
return Err(SecpError::InvalidNip44("invalid payload length"));
}
if payload.starts_with('#') {
return Err(SecpError::InvalidNip44("unknown encryption version"));
}
let data = STANDARD
.decode(payload)
.map_err(|_| SecpError::InvalidNip44("invalid base64"))?;
if !(MIN_RAW_PAYLOAD_SIZE..=MAX_RAW_PAYLOAD_SIZE).contains(&data.len()) {
return Err(SecpError::InvalidNip44("invalid data length"));
}
if data[0] != VERSION_V2 {
return Err(SecpError::InvalidNip44("unknown encryption version"));
}
if data.len() < 65 {
return Err(SecpError::InvalidNip44("invalid data length"));
}
let mut nonce = [0u8; 32];
nonce.copy_from_slice(&data[1..33]);
let mut mac = [0u8; 32];
mac.copy_from_slice(&data[data.len() - 32..]);
let ciphertext = data[33..data.len() - 32].to_vec();
Ok(DecodedPayload {
nonce,
ciphertext,
mac,
})
}
}
#[cfg(feature = "nip04")]
pub mod nip04 {
use super::*;
use aes::Aes256;
use base64::engine::general_purpose::STANDARD;
use base64::Engine;
use cbc::cipher::block_padding::Pkcs7;
use cbc::cipher::{BlockDecryptMut, BlockEncryptMut, KeyIvInit};
use k256::ecdh::diffie_hellman;
use k256::elliptic_curve::rand_core::RngCore;
type Aes256CbcEnc = cbc::Encryptor<Aes256>;
type Aes256CbcDec = cbc::Decryptor<Aes256>;
pub fn encrypt(
secret: &SecretKey,
pubkey: &XOnlyPublicKey,
plaintext: &str,
iv: Option<[u8; 16]>,
) -> Result<String, SecpError> {
let key = get_shared_secret_x(secret, pubkey)?;
let iv = iv.unwrap_or_else(random_iv);
let plaintext = plaintext.as_bytes();
let mut buf = plaintext.to_vec();
let msg_len = buf.len();
buf.resize(msg_len + 16, 0);
let ciphertext = Aes256CbcEnc::new((&key).into(), (&iv).into())
.encrypt_padded_mut::<Pkcs7>(&mut buf, msg_len)
.map_err(|_| SecpError::InvalidNip04("failed to encrypt"))?;
Ok(format!(
"{}?iv={}",
STANDARD.encode(ciphertext),
STANDARD.encode(iv)
))
}
pub fn decrypt(
secret: &SecretKey,
pubkey: &XOnlyPublicKey,
payload: &str,
) -> Result<String, SecpError> {
let (ciphertext_b64, iv_b64) = payload
.split_once("?iv=")
.ok_or(SecpError::InvalidNip04("invalid payload"))?;
let key = get_shared_secret_x(secret, pubkey)?;
let iv = STANDARD
.decode(iv_b64)
.map_err(|_| SecpError::InvalidNip04("invalid iv"))?;
let ciphertext = STANDARD
.decode(ciphertext_b64)
.map_err(|_| SecpError::InvalidNip04("invalid ciphertext"))?;
let iv: [u8; 16] = iv
.try_into()
.map_err(|_| SecpError::InvalidNip04("invalid iv"))?;
let mut buf = ciphertext;
let plaintext = Aes256CbcDec::new((&key).into(), (&iv).into())
.decrypt_padded_mut::<Pkcs7>(&mut buf)
.map_err(|_| SecpError::InvalidNip04("failed to decrypt"))?;
String::from_utf8(plaintext.to_vec())
.map_err(|_| SecpError::InvalidNip04("invalid utf-8 payload"))
}
fn get_shared_secret_x(
secret: &SecretKey,
pubkey: &XOnlyPublicKey,
) -> Result<[u8; 32], SecpError> {
let signing_key = secret.signing_key()?;
let secp_public = super::decode_xonly_pubkey(pubkey)?;
let shared = diffie_hellman(signing_key.as_nonzero_scalar(), secp_public.as_affine());
let mut out = [0u8; 32];
out.copy_from_slice(shared.raw_secret_bytes().as_ref());
Ok(out)
}
fn random_iv() -> [u8; 16] {
let mut iv = [0u8; 16];
OsRng.fill_bytes(&mut iv);
iv
}
}
#[cfg(any(feature = "nip04", feature = "nip44"))]
fn decode_xonly_pubkey(pubkey: &XOnlyPublicKey) -> Result<k256::PublicKey, SecpError> {
let mut sec1 = [0u8; 33];
sec1[0] = 0x02;
sec1[1..].copy_from_slice(&pubkey.to_bytes());
k256::PublicKey::from_sec1_bytes(&sec1).map_err(|_| SecpError::InvalidPublicKey)
}
#[cfg(test)]
mod tests {
use super::*;
#[cfg(feature = "nip04")]
const ORACLE_NIP04_PLAINTEXT: &str = "cyphercat nip04 oracle";
#[cfg(feature = "nip44")]
const ORACLE_NIP44_PLAINTEXT: &str = "cyphercat nip44 oracle";
#[test]
fn secret_key_roundtrip() {
let secret = SecretKey::generate().expect("secret key");
let restored = SecretKey::from_bytes(secret.to_bytes()).expect("restore");
assert_eq!(secret.to_bytes(), restored.to_bytes());
}
#[test]
fn schnorr_sign_and_verify() {
let secret = SecretKey::generate().expect("secret key");
let pubkey = secret.xonly_public_key().expect("pubkey");
let digest = [7u8; 32];
let sig = secret.sign_schnorr_prehash(digest).expect("sign");
pubkey.verify_schnorr_prehash(digest, &sig).expect("verify");
}
#[test]
fn invalid_secret_key_is_rejected() {
let error = SecretKey::from_bytes([0u8; 32]).expect_err("must reject zero secret key");
assert!(matches!(error, SecpError::InvalidSecretKey));
}
#[test]
fn xonly_public_key_roundtrip() {
let secret = SecretKey::generate().expect("secret key");
let public = secret.xonly_public_key().expect("public key");
let restored = XOnlyPublicKey::from_bytes(public.to_bytes()).expect("restored public key");
assert_eq!(public.to_bytes(), restored.to_bytes());
}
#[test]
fn public_key_sec1_roundtrip() {
let secret = SecretKey::generate().expect("secret key");
let public = secret.public_key().expect("public key");
let restored =
PublicKey::from_sec1_bytes(&public.to_sec1_bytes()).expect("restored public key");
assert_eq!(public.to_sec1_bytes(), restored.to_sec1_bytes());
}
#[test]
fn secret_key_hex_roundtrip() {
let key = SecretKey::generate().expect("generate");
let hex = key.to_hex();
assert_eq!(hex.len(), 64);
let decoded = SecretKey::from_hex(&hex).expect("from_hex");
assert_eq!(key, decoded);
}
#[test]
fn xonly_hex_roundtrip() {
let key = SecretKey::generate().expect("generate");
let xonly = key.xonly_public_key().expect("xonly");
let hex = xonly.to_hex();
assert_eq!(hex.len(), 64);
let decoded = XOnlyPublicKey::from_hex(&hex).expect("from_hex");
assert_eq!(xonly, decoded);
}
#[test]
fn public_key_hex_roundtrip() {
let key = SecretKey::generate().expect("generate");
let pubkey = key.public_key().expect("pubkey");
let hex = pubkey.to_hex();
assert_eq!(hex.len(), 66);
let decoded = PublicKey::from_hex(&hex).expect("from_hex");
assert_eq!(pubkey, decoded);
}
#[test]
fn event_id_hex_roundtrip() {
let id = EventId::from_bytes([0xab; 32]);
let hex = id.to_hex();
assert_eq!(hex, "ab".repeat(32));
let decoded = EventId::from_hex(&hex).expect("from_hex");
assert_eq!(id, decoded);
}
#[test]
fn hex_decode_case_insensitive() {
let upper = "ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789";
let lower = upper.to_lowercase();
let id_upper = EventId::from_hex(upper).expect("upper");
let id_lower = EventId::from_hex(&lower).expect("lower");
assert_eq!(id_upper, id_lower);
}
#[test]
fn hex_decode_odd_length_fails() {
assert!(matches!(
EventId::from_hex("abc"),
Err(SecpError::InvalidHex(_))
));
}
#[test]
fn hex_decode_invalid_char_fails() {
assert!(matches!(
EventId::from_hex("zz00000000000000000000000000000000000000000000000000000000000000"),
Err(SecpError::InvalidHex(_))
));
}
#[test]
fn hex_decode_wrong_length_fails() {
assert!(matches!(
SecretKey::from_hex("aabb"),
Err(SecpError::InvalidHex(_))
));
}
#[test]
fn hex_encode_is_lowercase() {
let id = EventId::from_bytes([0xAB; 32]);
let hex = id.to_hex();
assert_eq!(hex, hex.to_lowercase());
}
#[test]
fn keybundle_generate() {
let bundle = KeyBundle::generate().expect("key bundle");
let derived = bundle
.secret()
.xonly_public_key()
.expect("derived public key");
assert_eq!(*bundle.xonly_public_key(), derived);
}
#[cfg(feature = "batch")]
#[test]
fn keybundle_generate_batch() {
let bundles = KeyBundle::generate_batch(4).expect("batch");
assert_eq!(bundles.len(), 4);
for bundle in bundles {
let derived = bundle
.secret()
.xonly_public_key()
.expect("derived public key");
assert_eq!(*bundle.xonly_public_key(), derived);
}
}
#[cfg(feature = "batch")]
#[test]
fn keybundle_generate_batch_zero() {
let bundles = KeyBundle::generate_batch(0).expect("batch");
assert!(bundles.is_empty());
}
#[cfg(all(feature = "batch", feature = "nip19"))]
fn run_vanity_stress_tests() -> bool {
std::env::var("AINE_RUN_VANITY_TESTS")
.map(|value| value == "1")
.unwrap_or(false)
}
#[cfg(feature = "batch")]
fn run_pow_stress_tests() -> bool {
std::env::var("AINE_RUN_POW_TESTS")
.map(|value| value == "1")
.unwrap_or(false)
}
#[cfg(all(feature = "batch", feature = "nip19"))]
#[test]
fn mine_vanity_npub_finds_match() {
if !run_vanity_stress_tests() {
return;
}
let bundle = mine_vanity_npub("q", 100_000).expect("vanity match");
assert!(bundle.npub().expect("npub")[5..].starts_with("q"));
}
#[cfg(all(feature = "batch", feature = "nip19"))]
#[test]
fn mine_vanity_npub_exhausted() {
let error = mine_vanity_npub("zzzzzzzzzz", 10).expect_err("must exhaust attempts");
assert!(matches!(error, SecpError::ExhaustedAttempts));
}
#[test]
#[cfg(all(feature = "batch", feature = "nip19"))]
fn vanity_candidates_returns_top_k() {
if !run_vanity_stress_tests() {
return;
}
let candidates = mine_vanity_npub_candidates("q", 10_000, 3).expect("candidates");
assert!(candidates.len() <= 3);
for c in &candidates {
assert!(c.matched_len() >= 1);
}
for w in candidates.windows(2) {
assert!(w[0].matched_len() >= w[1].matched_len());
}
}
#[test]
#[cfg(all(feature = "batch", feature = "nip19"))]
fn vanity_candidates_zero_top_k() {
let candidates = mine_vanity_npub_candidates("q", 100, 0).expect("candidates");
assert!(candidates.is_empty());
}
#[test]
#[cfg(all(feature = "batch", feature = "nip19"))]
fn vanity_candidates_exact_match_included() {
if !run_vanity_stress_tests() {
return;
}
let candidates = mine_vanity_npub_candidates("q", 100_000, 5).expect("candidates");
let has_exact = candidates.iter().any(|c| c.matched_len() == 1);
assert!(
has_exact,
"should find at least one exact single-char match"
);
}
#[test]
#[cfg(all(feature = "batch", feature = "nip19"))]
fn vanity_prefix_match_counter_matches_encoded_npub() {
let secret = SecretKey::from_bytes([0x31; 32]).expect("secret");
let xonly = secret.xonly_public_key().expect("pubkey");
let npub = nip19::encode_npub(&xonly).expect("npub");
let npub_data = &npub[5..];
for len in [0usize, 1, 2, 3, 5, 8, 12] {
let prefix = &npub_data[..len];
let matched = count_npub_prefix_matches(&xonly.to_bytes(), prefix).expect("matched");
assert_eq!(matched, len);
}
let mismatched = format!("{}x", &npub_data[..4]);
let matched = count_npub_prefix_matches(&xonly.to_bytes(), &mismatched).expect("matched");
assert_eq!(matched, 4);
}
#[test]
#[cfg(all(feature = "batch", feature = "nip19"))]
fn vanity_prefix_rejects_invalid_bech32_chars() {
let err = mine_vanity_npub("!", 1).expect_err("invalid prefix");
assert!(matches!(
err,
SecpError::InvalidNip19("invalid npub vanity prefix")
));
let err = mine_vanity_npub_candidates("I", 1, 1).expect_err("invalid prefix");
assert!(matches!(
err,
SecpError::InvalidNip19("invalid npub vanity prefix")
));
}
#[cfg(feature = "batch")]
#[test]
fn mine_pow_finds_match() {
if !run_pow_stress_tests() {
return;
}
let bundle = mine_pow(1, 100_000).expect("pow match");
assert!(count_leading_zero_nibbles(&bundle.xonly_public_key().to_bytes()) >= 1);
}
#[cfg(feature = "batch")]
#[test]
fn mine_pow_exhausted() {
let error = mine_pow(64, 10).expect_err("must exhaust attempts");
assert!(matches!(error, SecpError::ExhaustedAttempts));
}
#[test]
#[cfg(feature = "batch")]
fn mine_pow_best_returns_best() {
if !run_pow_stress_tests() {
return;
}
let (bundle, diff) = mine_pow_best(1, 100_000).expect("pow best");
assert!(diff >= 1);
let actual = count_leading_zero_nibbles(&bundle.xonly_public_key().to_bytes());
assert_eq!(diff, actual);
}
#[test]
#[cfg(feature = "batch")]
fn mine_pow_best_exhausted() {
let err = mine_pow_best(64, 10).expect_err("should exhaust");
assert!(matches!(err, SecpError::ExhaustedAttempts));
}
#[cfg(feature = "batch")]
#[test]
fn count_leading_zero_nibbles_cases() {
assert_eq!(count_leading_zero_nibbles(&[0x00, 0x0a, 0xff]), 3);
assert_eq!(count_leading_zero_nibbles(&[0x00, 0x00]), 4);
assert_eq!(count_leading_zero_nibbles(&[0xab]), 0);
assert_eq!(count_leading_zero_nibbles(&[0x0f]), 1);
}
#[test]
fn schnorr_verify_rejects_wrong_digest() {
let secret = SecretKey::generate().expect("secret key");
let pubkey = secret.xonly_public_key().expect("pubkey");
let sig = secret
.sign_schnorr_prehash([1u8; 32])
.expect("signature for digest");
let error = pubkey
.verify_schnorr_prehash([2u8; 32], &sig)
.expect_err("must reject wrong digest");
assert!(matches!(error, SecpError::InvalidSignature));
}
#[test]
fn schnorr_verify_rejects_wrong_public_key() {
let secret_a = SecretKey::generate().expect("secret key a");
let secret_b = SecretKey::generate().expect("secret key b");
let pubkey_b = secret_b.xonly_public_key().expect("pubkey b");
let sig = secret_a
.sign_schnorr_prehash([3u8; 32])
.expect("signature for digest");
let error = pubkey_b
.verify_schnorr_prehash([3u8; 32], &sig)
.expect_err("must reject wrong public key");
assert!(matches!(error, SecpError::InvalidSignature));
}
#[cfg(feature = "nostr")]
#[test]
fn finalize_and_verify_event() {
let secret = SecretKey::generate().expect("secret key");
let event = UnsignedEvent {
created_at: 1_700_000_000,
kind: 1,
tags: vec![vec!["t".to_string(), "rust".to_string()]],
content: "hello".to_string(),
};
let signed = nostr::finalize_event(event, &secret).expect("finalize");
nostr::verify_event(&signed).expect("verify");
}
#[cfg(feature = "nostr")]
#[test]
fn nostr_matches_known_nostr_tools_fixture() {
let secret = SecretKey::from_hex(
"d217c1ff2f8a65c3e3a1740db3b9f58b\
8c848bb45e26d00ed4714e4a0f4ceecf",
)
.expect("secret");
let pubkey = secret.xonly_public_key().expect("pubkey");
let unsigned = UnsignedEvent {
created_at: 1_617_932_115,
kind: 1,
tags: vec![],
content: "Hello, world!".to_string(),
};
let serialized = nostr::serialize_event(&pubkey, &unsigned).expect("serialize");
let hash = nostr::compute_event_id(&pubkey, &unsigned).expect("hash");
let signed = nostr::finalize_event(unsigned, &secret).expect("finalize");
assert_eq!(
pubkey.to_hex(),
"6af0f9de588f2c53cedcba26c5e2402e0d0aa64ec7b47c9f8d97b5bc562bab5f"
);
assert_eq!(
serialized,
r#"[0,"6af0f9de588f2c53cedcba26c5e2402e0d0aa64ec7b47c9f8d97b5bc562bab5f",1617932115,1,[],"Hello, world!"]"#
);
assert_eq!(
hash.to_hex(),
"b2a44af84ca99b14820ae91c44e1ef0908f8aadc4e10620a6e6caa344507f03c"
);
assert_eq!(signed.id.to_hex(), hash.to_hex());
assert_eq!(
signed
.sig
.to_bytes()
.iter()
.map(|byte| format!("{byte:02x}"))
.collect::<String>()
.len(),
128
);
nostr::verify_event(&signed).expect("verify");
}
#[cfg(feature = "nostr")]
#[test]
fn serialize_event_matches_expected_shape() {
let secret = SecretKey::generate().expect("secret key");
let public = secret.xonly_public_key().expect("public key");
let event = UnsignedEvent {
created_at: 1_700_000_123,
kind: 7,
tags: vec![vec!["p".to_string(), "abcd".to_string()]],
content: "hello\nworld".to_string(),
};
let serialized = nostr::serialize_event(&public, &event).expect("serialize");
let value: serde_json::Value = serde_json::from_str(&serialized).expect("json");
let array = value.as_array().expect("array payload");
assert_eq!(array.len(), 6);
assert_eq!(array[0], serde_json::json!(0));
assert_eq!(array[2], serde_json::json!(event.created_at));
assert_eq!(array[3], serde_json::json!(event.kind));
assert_eq!(array[4], serde_json::json!(event.tags));
assert_eq!(array[5], serde_json::json!(event.content));
}
#[cfg(feature = "nostr")]
#[test]
fn compute_event_id_is_reproducible() {
let secret = SecretKey::generate().expect("secret key");
let public = secret.xonly_public_key().expect("public key");
let event = UnsignedEvent {
created_at: 10,
kind: 1,
tags: vec![vec!["e".to_string(), "1".to_string()]],
content: "same".to_string(),
};
let id_a = nostr::compute_event_id(&public, &event).expect("id a");
let id_b = nostr::compute_event_id(&public, &event).expect("id b");
assert_eq!(id_a, id_b);
}
#[cfg(feature = "nostr")]
#[test]
fn verify_rejects_tampered_content() {
let secret = SecretKey::generate().expect("secret key");
let event = UnsignedEvent {
created_at: 1,
kind: 1,
tags: Vec::new(),
content: "hello".to_string(),
};
let mut signed = nostr::finalize_event(event, &secret).expect("finalize");
signed.content = "tampered".to_string();
let error = nostr::verify_event(&signed).expect_err("must fail");
assert!(matches!(error, SecpError::InvalidEvent(_)));
}
#[cfg(feature = "nostr")]
#[test]
fn verify_rejects_tampered_tags() {
let secret = SecretKey::generate().expect("secret key");
let event = UnsignedEvent {
created_at: 2,
kind: 1,
tags: vec![vec!["t".to_string(), "rust".to_string()]],
content: "hello".to_string(),
};
let mut signed = nostr::finalize_event(event, &secret).expect("finalize");
signed.tags.push(vec!["p".to_string(), "peer".to_string()]);
let error = nostr::verify_event(&signed).expect_err("must fail");
assert!(matches!(error, SecpError::InvalidEvent(_)));
}
#[cfg(feature = "nostr")]
#[test]
fn verify_rejects_tampered_created_at() {
let secret = SecretKey::generate().expect("secret key");
let event = UnsignedEvent {
created_at: 3,
kind: 1,
tags: Vec::new(),
content: "hello".to_string(),
};
let mut signed = nostr::finalize_event(event, &secret).expect("finalize");
signed.created_at += 1;
let error = nostr::verify_event(&signed).expect_err("must fail");
assert!(matches!(error, SecpError::InvalidEvent(_)));
}
#[cfg(feature = "nostr")]
#[test]
fn verify_rejects_tampered_kind() {
let secret = SecretKey::generate().expect("secret key");
let event = UnsignedEvent {
created_at: 4,
kind: 1,
tags: Vec::new(),
content: "hello".to_string(),
};
let mut signed = nostr::finalize_event(event, &secret).expect("finalize");
signed.kind = 42;
let error = nostr::verify_event(&signed).expect_err("must fail");
assert!(matches!(error, SecpError::InvalidEvent(_)));
}
#[cfg(feature = "nostr")]
#[test]
fn verify_rejects_tampered_id() {
let secret = SecretKey::generate().expect("secret key");
let event = UnsignedEvent {
created_at: 5,
kind: 1,
tags: Vec::new(),
content: "hello".to_string(),
};
let mut signed = nostr::finalize_event(event, &secret).expect("finalize");
signed.id = EventId { bytes: [9u8; 32] };
let error = nostr::verify_event(&signed).expect_err("must fail");
assert!(matches!(error, SecpError::InvalidEvent(_)));
}
#[cfg(feature = "nostr")]
#[test]
fn verify_rejects_tampered_signature() {
let secret = SecretKey::generate().expect("secret key");
let event = UnsignedEvent {
created_at: 6,
kind: 1,
tags: Vec::new(),
content: "hello".to_string(),
};
let mut signed = nostr::finalize_event(event, &secret).expect("finalize");
let mut sig = signed.sig.to_bytes();
sig[0] ^= 0x01;
signed.sig = SchnorrSignature { bytes: sig };
let error = nostr::verify_event(&signed).expect_err("must fail");
assert!(matches!(error, SecpError::InvalidSignature));
}
#[cfg(feature = "nostr")]
#[test]
fn signed_event_serialize_parse_roundtrip() {
let secret = SecretKey::generate().expect("secret key");
let signed = nostr::finalize_event(
UnsignedEvent {
created_at: 1_700_000_222,
kind: 14,
tags: vec![vec!["p".to_string(), "peer".to_string()]],
content: "sealed".to_string(),
},
&secret,
)
.expect("finalize");
let json = nostr::serialize_signed_event(&signed).expect("serialize signed");
let parsed = nostr::parse_signed_event(&json).expect("parse signed");
assert_eq!(parsed, signed);
}
#[cfg(feature = "nostr")]
#[test]
fn nip42_create_and_validate() {
let secret = SecretKey::generate().expect("secret key");
let expected = secret.xonly_public_key().expect("pubkey");
let signed = nip42::create_auth_event(
"challenge-123",
"wss://relay.example.com",
&secret,
1_700_000_666,
)
.expect("create auth event");
assert_eq!(signed.kind, 22_242);
assert_eq!(signed.content, "");
assert_eq!(
signed.tags,
vec![
vec!["relay".to_string(), "wss://relay.example.com".to_string()],
vec!["challenge".to_string(), "challenge-123".to_string()],
]
);
assert_eq!(
nip42::validate_auth_event(&signed, "challenge-123", "wss://relay.example.com")
.expect("validate auth event"),
expected
);
}
#[cfg(feature = "nostr")]
#[test]
fn nip42_wrong_challenge_fails() {
let secret = SecretKey::generate().expect("secret key");
let signed = nip42::create_auth_event(
"challenge-123",
"wss://relay.example.com",
&secret,
1_700_000_777,
)
.expect("create auth event");
let error =
nip42::validate_auth_event(&signed, "wrong-challenge", "wss://relay.example.com")
.expect_err("wrong challenge must fail");
assert!(matches!(
error,
SecpError::InvalidEvent("auth event challenge tag mismatch")
));
}
#[cfg(feature = "nostr")]
#[test]
fn nip42_wrong_relay_fails() {
let secret = SecretKey::generate().expect("secret key");
let signed = nip42::create_auth_event(
"challenge-123",
"wss://relay.example.com",
&secret,
1_700_000_888,
)
.expect("create auth event");
let error = nip42::validate_auth_event(&signed, "challenge-123", "wss://other.example.com")
.expect_err("wrong relay must fail");
assert!(matches!(
error,
SecpError::InvalidEvent("auth event relay tag mismatch")
));
}
#[cfg(all(feature = "nostr", feature = "nip44"))]
#[test]
fn nip17_seal_roundtrip() {
let sender = SecretKey::generate().expect("sender");
let recipient = SecretKey::generate().expect("recipient");
let recipient_pubkey = recipient.xonly_public_key().expect("recipient pubkey");
let inner = UnsignedEvent {
created_at: 1_700_000_333,
kind: 14,
tags: vec![vec!["p".to_string(), recipient_pubkey.to_hex()]],
content: "hello seal".to_string(),
};
let seal = nip17::create_seal(inner.clone(), &sender, &recipient_pubkey).expect("seal");
let opened = nip17::open_seal(&seal, &recipient).expect("open seal");
assert_eq!(seal.kind, 13);
assert!(seal.tags.is_empty());
assert_eq!(
seal.pubkey,
sender.xonly_public_key().expect("sender pubkey")
);
assert_eq!(opened.created_at, inner.created_at);
assert_eq!(opened.kind, inner.kind);
assert_eq!(opened.tags, inner.tags);
assert_eq!(opened.content, inner.content);
}
#[cfg(all(feature = "nostr", feature = "nip44"))]
#[test]
fn nip17_gift_wrap_roundtrip() {
let sender = SecretKey::generate().expect("sender");
let recipient = SecretKey::generate().expect("recipient");
let recipient_pubkey = recipient.xonly_public_key().expect("recipient pubkey");
let inner = UnsignedEvent {
created_at: 1_700_000_444,
kind: 14,
tags: vec![vec!["p".to_string(), recipient_pubkey.to_hex()]],
content: "hello wrap".to_string(),
};
let seal = nip17::create_seal(inner.clone(), &sender, &recipient_pubkey).expect("seal");
let gift_wrap = nip17::create_gift_wrap(&seal, &recipient_pubkey).expect("gift wrap");
let opened = nip17::open_gift_wrap(&gift_wrap, &recipient).expect("open gift wrap");
assert_eq!(gift_wrap.kind, 1059);
assert_eq!(
gift_wrap.tags,
vec![vec!["p".to_string(), recipient_pubkey.to_hex()]]
);
assert_ne!(
gift_wrap.pubkey,
sender.xonly_public_key().expect("sender pubkey")
);
assert_eq!(opened.created_at, inner.created_at);
assert_eq!(opened.kind, inner.kind);
assert_eq!(opened.tags, inner.tags);
assert_eq!(opened.content, inner.content);
}
#[cfg(all(feature = "nostr", feature = "nip44"))]
#[test]
fn nip17_wrong_recipient_fails() {
let sender = SecretKey::generate().expect("sender");
let recipient = SecretKey::generate().expect("recipient");
let wrong_recipient = SecretKey::generate().expect("wrong recipient");
let recipient_pubkey = recipient.xonly_public_key().expect("recipient pubkey");
let inner = UnsignedEvent {
created_at: 1_700_000_555,
kind: 14,
tags: vec![vec!["p".to_string(), recipient_pubkey.to_hex()]],
content: "secret".to_string(),
};
let seal = nip17::create_seal(inner, &sender, &recipient_pubkey).expect("seal");
let gift_wrap = nip17::create_gift_wrap(&seal, &recipient_pubkey).expect("gift wrap");
let error =
nip17::open_gift_wrap(&gift_wrap, &wrong_recipient).expect_err("wrong recipient");
assert!(matches!(
error,
SecpError::InvalidNip44(_) | SecpError::InvalidEvent(_)
));
}
#[cfg(feature = "nip19")]
#[test]
fn nip19_nsec_roundtrip() {
let secret = SecretKey::generate().expect("secret key");
let encoded = nip19::encode_nsec(&secret).expect("encode");
let decoded = nip19::decode(&encoded).expect("decode");
assert_eq!(decoded, Nip19::Nsec(secret));
}
#[cfg(feature = "nip19")]
#[test]
fn nip19_npub_roundtrip() {
let secret = SecretKey::generate().expect("secret key");
let public = secret.xonly_public_key().expect("public key");
let encoded = nip19::encode_npub(&public).expect("encode");
let decoded = nip19::decode(&encoded).expect("decode");
assert_eq!(decoded, Nip19::Npub(public));
}
#[cfg(feature = "nip19")]
#[test]
fn keybundle_nip19() {
let bundle = KeyBundle::generate().expect("key bundle");
let npub = bundle.npub().expect("npub");
let nsec = bundle.nsec().expect("nsec");
assert_eq!(
nip19::decode(&npub).expect("decode npub"),
Nip19::Npub(*bundle.xonly_public_key())
);
assert_eq!(
nip19::decode(&nsec).expect("decode nsec"),
Nip19::Nsec(*bundle.secret())
);
}
#[cfg(feature = "nip19")]
#[test]
fn nip19_note_roundtrip() {
let id = EventId::from_bytes([0x42; 32]);
let encoded = nip19::encode_note(&id).expect("encode");
let decoded = nip19::decode(&encoded).expect("decode");
assert_eq!(decoded, Nip19::Note(id));
}
#[cfg(feature = "nip19")]
#[test]
fn nip19_decodes_known_valid_npub_fixture() {
let public = XOnlyPublicKey::from_bytes([
0x6e, 0x46, 0x84, 0x22, 0xdf, 0xb7, 0x4a, 0x57, 0x38, 0x70, 0x2a, 0x88, 0x23, 0xb9,
0xb2, 0x81, 0x68, 0xab, 0xab, 0x86, 0x55, 0xfa, 0xac, 0xb6, 0x85, 0x3c, 0xd0, 0xee,
0x15, 0xde, 0xee, 0x93,
])
.expect("fixture pubkey");
let decoded =
nip19::decode("npub1dergggklka99wwrs92yz8wdjs952h2ux2ha2ed598ngwu9w7a6fsh9xzpc")
.expect("decode fixture");
assert_eq!(decoded, Nip19::Npub(public));
}
#[cfg(feature = "nip19")]
#[test]
fn nip19_matches_known_nostr_tools_fixture() {
let pubkey = XOnlyPublicKey::from_hex(
"6af0f9de588f2c53cedcba26c5e2402e0d0aa64ec7b47c9f8d97b5bc562bab5f",
)
.expect("pubkey");
let event_id =
EventId::from_hex("b2a44af84ca99b14820ae91c44e1ef0908f8aadc4e10620a6e6caa344507f03c")
.expect("event id");
let profile = NProfile {
pubkey,
relays: vec![
"wss://relay.damus.io".to_string(),
"wss://nostr.example.com".to_string(),
],
};
let event = NEvent {
id: event_id,
relays: vec![
"wss://relay.example.com".to_string(),
"wss://relay2.example.com".to_string(),
],
author: Some(pubkey),
kind: Some(1),
};
let addr = NAddr {
identifier: "article-1".to_string(),
relays: vec![
"wss://relay.example.com".to_string(),
"wss://relay2.example.com".to_string(),
],
author: pubkey,
kind: 30_023,
};
assert_eq!(
nip19::encode_npub(&pubkey).expect("npub"),
"npub1dtc0nhjc3uk98nkuhgnvtcjq9cxs4fjwc768e8udj76mc43t4d0sw73h32"
);
assert_eq!(
nip19::encode_nprofile(&profile).expect("nprofile"),
"nprofile1qy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9mhwden5te0dehhxarj9ejhsctdwpkx2tnrdaksqgr27ruauky093fuah96ymz7yspwp592vnk8k37flrvhkk79v2attuwkrkwx"
);
assert_eq!(
nip19::encode_nevent(&event).expect("nevent"),
"nevent1qvzqqqqqqypzq6hsl8093rev208dew3xch3yqtsdp2nya3a50j0cm9a4h3tzh26lqythwumn8ghj7un9d3shjtn90psk6urvv5hxxmmdqyv8wumn8ghj7un9d3shjv3wv4uxzmtsd3jjucm0d5qzpv4yftuye2vmzjpq46gugns77zgglz4dcnssvg9xum92x3zs0upuv94f3z"
);
assert_eq!(
nip19::encode_naddr(&addr).expect("naddr"),
"naddr1qvzqqqr4gupzq6hsl8093rev208dew3xch3yqtsdp2nya3a50j0cm9a4h3tzh26lqythwumn8ghj7un9d3shjtn90psk6urvv5hxxmmdqyv8wumn8ghj7un9d3shjv3wv4uxzmtsd3jjucm0d5qqjctjw35kxmr995csn0d4es"
);
}
#[cfg(feature = "nip19")]
#[test]
fn nip19_nprofile_roundtrip_with_multiple_relays() {
let pubkey = XOnlyPublicKey::from_bytes([
0x6e, 0x46, 0x84, 0x22, 0xdf, 0xb7, 0x4a, 0x57, 0x38, 0x70, 0x2a, 0x88, 0x23, 0xb9,
0xb2, 0x81, 0x68, 0xab, 0xab, 0x86, 0x55, 0xfa, 0xac, 0xb6, 0x85, 0x3c, 0xd0, 0xee,
0x15, 0xde, 0xee, 0x93,
])
.expect("pubkey");
let profile = NProfile {
pubkey,
relays: vec![
"wss://relay.example".to_string(),
"wss://relay2.example".to_string(),
],
};
let encoded = nip19::encode_nprofile(&profile).expect("encode");
let decoded = nip19::decode(&encoded).expect("decode");
assert_eq!(decoded, Nip19::NProfile(profile));
}
#[cfg(feature = "nip19")]
#[test]
fn nip19_nevent_roundtrip_with_author_and_kind() {
let event = NEvent {
id: EventId::from_bytes([0x34; 32]),
relays: vec!["wss://relay.example".to_string()],
author: Some(
XOnlyPublicKey::from_bytes([
0x4f, 0x35, 0x5b, 0xdc, 0xb7, 0xcc, 0x0a, 0xf7, 0x28, 0xef, 0x3c, 0xce, 0xb9,
0x61, 0x5d, 0x90, 0x68, 0x4b, 0xb5, 0xb2, 0xca, 0x5f, 0x85, 0x9a, 0xb0, 0xf0,
0xb7, 0x04, 0x07, 0x58, 0x71, 0xaa,
])
.expect("author"),
),
kind: Some(30_023),
};
let encoded = nip19::encode_nevent(&event).expect("encode");
let decoded = nip19::decode(&encoded).expect("decode");
assert_eq!(decoded, Nip19::NEvent(event));
}
#[cfg(feature = "nip19")]
#[test]
fn nip19_naddr_roundtrip_allows_empty_identifier() {
let addr = NAddr {
identifier: String::new(),
relays: vec!["wss://relay.example".to_string()],
author: XOnlyPublicKey::from_bytes([
0x6e, 0x46, 0x84, 0x22, 0xdf, 0xb7, 0x4a, 0x57, 0x38, 0x70, 0x2a, 0x88, 0x23, 0xb9,
0xb2, 0x81, 0x68, 0xab, 0xab, 0x86, 0x55, 0xfa, 0xac, 0xb6, 0x85, 0x3c, 0xd0, 0xee,
0x15, 0xde, 0xee, 0x93,
])
.expect("author"),
kind: 30_023,
};
let encoded = nip19::encode_naddr(&addr).expect("encode");
let decoded = nip19::decode(&encoded).expect("decode");
assert_eq!(decoded, Nip19::NAddr(addr));
}
#[cfg(feature = "nip19")]
#[test]
fn nip19_nrelay_roundtrip() {
let relay = NRelay {
relay: "wss://relay.example".to_string(),
};
let encoded = nip19::encode_nrelay(&relay).expect("encode");
let decoded = nip19::decode(&encoded).expect("decode");
assert_eq!(decoded, Nip19::NRelay(relay));
}
#[cfg(feature = "nip19")]
#[test]
fn nip19_decode_ignores_unknown_tlv_entries() {
use bech32::ToBase32;
let mut payload = vec![9, 3, b'x', b'y', b'z', 0, 32];
payload.extend_from_slice(&[
0x6e, 0x46, 0x84, 0x22, 0xdf, 0xb7, 0x4a, 0x57, 0x38, 0x70, 0x2a, 0x88, 0x23, 0xb9,
0xb2, 0x81, 0x68, 0xab, 0xab, 0x86, 0x55, 0xfa, 0xac, 0xb6, 0x85, 0x3c, 0xd0, 0xee,
0x15, 0xde, 0xee, 0x93,
]);
let encoded = bech32::encode("nprofile", payload.to_base32(), bech32::Variant::Bech32)
.expect("bech32");
let decoded = nip19::decode(&encoded).expect("decode");
assert_eq!(
decoded,
Nip19::NProfile(NProfile {
pubkey: XOnlyPublicKey::from_bytes([
0x6e, 0x46, 0x84, 0x22, 0xdf, 0xb7, 0x4a, 0x57, 0x38, 0x70, 0x2a, 0x88, 0x23,
0xb9, 0xb2, 0x81, 0x68, 0xab, 0xab, 0x86, 0x55, 0xfa, 0xac, 0xb6, 0x85, 0x3c,
0xd0, 0xee, 0x15, 0xde, 0xee, 0x93,
])
.expect("pubkey"),
relays: Vec::new(),
})
);
}
#[cfg(feature = "nip19")]
#[test]
fn nip19_decode_rejects_missing_required_nprofile_pubkey() {
use bech32::ToBase32;
let payload = vec![1, 5, b'r', b'e', b'l', b'a', b'y'];
let encoded = bech32::encode("nprofile", payload.to_base32(), bech32::Variant::Bech32)
.expect("bech32");
let error = nip19::decode(&encoded).expect_err("missing pubkey must fail");
assert!(matches!(
error,
SecpError::InvalidNip19("missing TLV 0 for nprofile")
));
}
#[cfg(feature = "nip19")]
#[test]
fn nip19_decode_rejects_missing_required_naddr_fields() {
use bech32::ToBase32;
let mut payload = vec![0, 0];
payload.extend_from_slice(&[3, 4, 0, 0, 0x75, 0x37]);
let encoded =
bech32::encode("naddr", payload.to_base32(), bech32::Variant::Bech32).expect("bech32");
let error = nip19::decode(&encoded).expect_err("missing author must fail");
assert!(matches!(
error,
SecpError::InvalidNip19("missing TLV 2 for naddr")
));
}
#[cfg(feature = "nip19")]
#[test]
fn nip19_decode_rejects_invalid_nevent_kind_length() {
use bech32::ToBase32;
let mut payload = vec![0, 32];
payload.extend_from_slice(&[0x34; 32]);
payload.extend_from_slice(&[3, 3, 0, 0x75, 0x37]);
let encoded =
bech32::encode("nevent", payload.to_base32(), bech32::Variant::Bech32).expect("bech32");
let error = nip19::decode(&encoded).expect_err("invalid kind length must fail");
assert!(matches!(
error,
SecpError::InvalidNip19("TLV 3 should be 4 bytes")
));
}
#[cfg(feature = "nip19")]
#[test]
fn nip19_nevent_kind_is_big_endian() {
let event = NEvent {
id: EventId::from_bytes([0x12; 32]),
relays: Vec::new(),
author: None,
kind: Some(30_023),
};
let encoded = nip19::encode_nevent(&event).expect("encode");
let decoded = nip19::decode(&encoded).expect("decode");
assert_eq!(decoded, Nip19::NEvent(event));
}
#[cfg(feature = "nip19")]
#[test]
fn nip19_decode_rejects_invalid_checksum() {
let error =
nip19::decode("npub1de5gss7lkafc0pe2s2sz8wjsx6v4hvxxg8l6e60v8uuguvs7m5fsq4qwxn")
.expect_err("checksum must fail");
assert!(matches!(error, SecpError::InvalidNip19(_)));
}
#[cfg(feature = "nip19")]
#[test]
fn nip19_decode_rejects_invalid_length() {
use bech32::ToBase32;
let short = bech32::encode("npub", vec![1u8; 31].to_base32(), bech32::Variant::Bech32)
.expect("fixture encode");
let error = nip19::decode(&short).expect_err("short payload must fail");
assert!(matches!(error, SecpError::InvalidNip19(_)));
}
#[cfg(feature = "nip44")]
#[test]
fn nip44_conversation_key_matches_from_both_sides() {
let secret_a = SecretKey::from_bytes([0x11; 32]).expect("secret a");
let secret_b = SecretKey::from_bytes([0x22; 32]).expect("secret b");
let pubkey_a = secret_a.xonly_public_key().expect("pubkey a");
let pubkey_b = secret_b.xonly_public_key().expect("pubkey b");
let key_ab = nip44::get_conversation_key(&secret_a, &pubkey_b).expect("key ab");
let key_ba = nip44::get_conversation_key(&secret_b, &pubkey_a).expect("key ba");
assert_eq!(key_ab, key_ba);
}
#[cfg(feature = "nip44")]
#[test]
fn nip44_encrypt_and_decrypt_roundtrip() {
let secret = SecretKey::from_bytes([0x11; 32]).expect("secret");
let peer = SecretKey::from_bytes([0x22; 32]).expect("peer");
let peer_pubkey = peer.xonly_public_key().expect("peer pubkey");
let conversation_key =
nip44::get_conversation_key(&secret, &peer_pubkey).expect("conversation key");
let nonce = [0x33; 32];
let payload = nip44::encrypt("hello from neco-secp", &conversation_key, Some(nonce))
.expect("encrypt");
let plaintext = nip44::decrypt(&payload, &conversation_key).expect("decrypt");
assert_eq!(plaintext, "hello from neco-secp");
}
#[cfg(feature = "nip44")]
#[test]
fn nip44_matches_known_oracle_fixture() {
let secret = SecretKey::from_bytes([0x11; 32]).expect("secret");
let peer_pubkey = XOnlyPublicKey::from_bytes([
0x46, 0x6d, 0x7f, 0xca, 0xe5, 0x63, 0xe5, 0xcb, 0x09, 0xa0, 0xd1, 0x87, 0x0b, 0xb5,
0x80, 0x34, 0x48, 0x04, 0x61, 0x78, 0x79, 0xa1, 0x49, 0x49, 0xcf, 0x22, 0x28, 0x5f,
0x1b, 0xae, 0x3f, 0x27,
])
.expect("peer pubkey");
let conversation_key =
nip44::get_conversation_key(&secret, &peer_pubkey).expect("conversation key");
assert_eq!(
conversation_key,
[
0x2c, 0xbd, 0xf0, 0x74, 0xf6, 0x01, 0x17, 0x8c, 0x24, 0xda, 0x3f, 0x82, 0x9b, 0x50,
0x45, 0x07, 0xa1, 0xf5, 0x50, 0xf9, 0x7d, 0x47, 0x2a, 0xf0, 0xf3, 0xf2, 0xcc, 0x59,
0xab, 0x77, 0x57, 0xd1,
]
);
let payload = nip44::encrypt(ORACLE_NIP44_PLAINTEXT, &conversation_key, Some([0x33; 32]))
.expect("encrypt");
assert_eq!(
payload,
"AjMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzJDZzpLQFdobg13n/RufVeG0ps8acSBfghr22oozB/q91IVexzbaA/lxkSa0R+6Dly9F1gKsZLCy1tzW4LPplhuWg"
);
let plaintext = nip44::decrypt(&payload, &conversation_key).expect("decrypt");
assert_eq!(plaintext, ORACLE_NIP44_PLAINTEXT);
}
#[cfg(feature = "nip44")]
#[test]
fn nip44_calc_padded_len_contract() {
assert_eq!(nip44::calc_padded_len(1).expect("len"), 32);
assert_eq!(nip44::calc_padded_len(32).expect("len"), 32);
assert_eq!(nip44::calc_padded_len(33).expect("len"), 64);
assert_eq!(nip44::calc_padded_len(300).expect("len"), 320);
}
#[cfg(feature = "nip44")]
#[test]
fn nip44_rejects_invalid_mac() {
use base64::Engine;
let secret = SecretKey::from_bytes([0x11; 32]).expect("secret");
let peer = SecretKey::from_bytes([0x22; 32]).expect("peer");
let peer_pubkey = peer.xonly_public_key().expect("peer pubkey");
let conversation_key =
nip44::get_conversation_key(&secret, &peer_pubkey).expect("conversation key");
let nonce = [0x33; 32];
let payload = nip44::encrypt("hello", &conversation_key, Some(nonce)).expect("encrypt");
let mut raw = base64::engine::general_purpose::STANDARD
.decode(payload)
.expect("decode");
let last = raw.len() - 1;
raw[last] ^= 0x01;
let tampered = base64::engine::general_purpose::STANDARD.encode(raw);
let error = nip44::decrypt(&tampered, &conversation_key).expect_err("invalid mac");
assert!(matches!(error, SecpError::InvalidNip44("invalid MAC")));
}
#[cfg(feature = "nip44")]
#[test]
fn nip44_rejects_invalid_version() {
use base64::Engine;
let secret = SecretKey::from_bytes([0x11; 32]).expect("secret");
let peer = SecretKey::from_bytes([0x22; 32]).expect("peer");
let peer_pubkey = peer.xonly_public_key().expect("peer pubkey");
let conversation_key =
nip44::get_conversation_key(&secret, &peer_pubkey).expect("conversation key");
let nonce = [0x33; 32];
let payload = nip44::encrypt("hello", &conversation_key, Some(nonce)).expect("encrypt");
let mut raw = base64::engine::general_purpose::STANDARD
.decode(payload)
.expect("decode");
raw[0] = 3;
let tampered = base64::engine::general_purpose::STANDARD.encode(raw);
let error = nip44::decrypt(&tampered, &conversation_key).expect_err("invalid version");
assert!(matches!(
error,
SecpError::InvalidNip44("unknown encryption version")
));
}
#[cfg(feature = "nip44")]
#[test]
fn nip44_rejects_invalid_payload_length() {
let error = nip44::decrypt("short", &[0u8; 32]).expect_err("invalid payload");
assert!(matches!(
error,
SecpError::InvalidNip44("invalid payload length")
));
}
#[cfg(feature = "nip04")]
#[test]
fn nip04_encrypt_and_decrypt_roundtrip() {
let secret = SecretKey::from_bytes([0x11; 32]).expect("secret");
let peer = SecretKey::from_bytes([0x22; 32]).expect("peer");
let peer_pubkey = peer.xonly_public_key().expect("peer pubkey");
let payload = nip04::encrypt(
&secret,
&peer_pubkey,
"hello from neco-secp",
Some([0x44; 16]),
)
.expect("encrypt");
let plaintext =
nip04::decrypt(&peer, &secret.xonly_public_key().expect("pubkey"), &payload)
.expect("decrypt");
assert_eq!(plaintext, "hello from neco-secp");
}
#[cfg(feature = "nip04")]
#[test]
fn nip04_matches_known_oracle_fixture() {
let secret = SecretKey::from_bytes([0x11; 32]).expect("secret");
let peer_pubkey = XOnlyPublicKey::from_bytes([
0x46, 0x6d, 0x7f, 0xca, 0xe5, 0x63, 0xe5, 0xcb, 0x09, 0xa0, 0xd1, 0x87, 0x0b, 0xb5,
0x80, 0x34, 0x48, 0x04, 0x61, 0x78, 0x79, 0xa1, 0x49, 0x49, 0xcf, 0x22, 0x28, 0x5f,
0x1b, 0xae, 0x3f, 0x27,
])
.expect("peer pubkey");
let payload = nip04::encrypt(
&secret,
&peer_pubkey,
ORACLE_NIP04_PLAINTEXT,
Some([0x44; 16]),
)
.expect("encrypt");
assert_eq!(
payload,
"xftPpDirMJGDoq3ktNutZsG6W+lmUsILU9XMp06pYmM=?iv=RERERERERERERERERERERA=="
);
}
#[cfg(feature = "nip04")]
#[test]
fn nip04_rejects_invalid_payload() {
let secret = SecretKey::from_bytes([0x11; 32]).expect("secret");
let peer_pubkey = SecretKey::from_bytes([0x22; 32])
.expect("peer")
.xonly_public_key()
.expect("pubkey");
let error = nip04::decrypt(&secret, &peer_pubkey, "invalid").expect_err("invalid payload");
assert!(matches!(error, SecpError::InvalidNip04("invalid payload")));
}
#[cfg(feature = "nip04")]
#[test]
fn nip04_rejects_invalid_iv() {
let secret = SecretKey::from_bytes([0x11; 32]).expect("secret");
let peer_pubkey = SecretKey::from_bytes([0x22; 32])
.expect("peer")
.xonly_public_key()
.expect("pubkey");
let error = nip04::decrypt(&secret, &peer_pubkey, "abcd?iv=bad!").expect_err("invalid iv");
assert!(matches!(error, SecpError::InvalidNip04("invalid iv")));
}
}