use async_trait::async_trait;
use thiserror::Error;
use crate::event::{sign_event, NostrEvent, PubkeyMismatch, UnsignedEvent};
use crate::keys::Keypair;
use crate::nip04::{nip04_decrypt as nip04_dec, nip04_encrypt as nip04_enc};
use crate::nip44::{decrypt as nip44_dec, encrypt as nip44_enc};
#[derive(Debug, Error)]
pub enum SignerError {
#[error("signing failed: {0}")]
SigningFailed(String),
#[error("pubkey mismatch: expected {expected}, got {actual}")]
PubkeyMismatch {
expected: String,
actual: String,
},
#[error("key unavailable: {0}")]
KeyUnavailable(String),
#[error("backend error: {0}")]
Backend(String),
#[error("encryption failed: {0}")]
EncryptionFailed(String),
#[error("decryption failed: {0}")]
DecryptionFailed(String),
#[error("signer not available")]
Unavailable,
}
impl From<PubkeyMismatch> for SignerError {
fn from(e: PubkeyMismatch) -> Self {
SignerError::PubkeyMismatch {
expected: e.derived_pubkey,
actual: e.event_pubkey,
}
}
}
#[async_trait(?Send)]
pub trait Signer {
fn public_key(&self) -> &str;
async fn sign_event(&self, unsigned: UnsignedEvent) -> Result<NostrEvent, SignerError>;
async fn nip44_encrypt(
&self,
recipient_pubkey_hex: &str,
plaintext: &str,
) -> Result<String, SignerError>;
async fn nip44_decrypt(
&self,
sender_pubkey_hex: &str,
ciphertext: &str,
) -> Result<String, SignerError>;
async fn nip04_encrypt(
&self,
recipient_pubkey_hex: &str,
plaintext: &str,
) -> Result<String, SignerError>;
async fn nip04_decrypt(
&self,
sender_pubkey_hex: &str,
ciphertext: &str,
) -> Result<String, SignerError>;
}
pub struct PrfSigner {
keypair: Keypair,
pubkey_hex: String,
}
impl PrfSigner {
pub fn new(keypair: Keypair) -> Self {
let pubkey_hex = keypair.public.to_hex();
Self {
keypair,
pubkey_hex,
}
}
pub fn keypair(&self) -> &Keypair {
&self.keypair
}
}
#[async_trait(?Send)]
impl Signer for PrfSigner {
fn public_key(&self) -> &str {
&self.pubkey_hex
}
async fn sign_event(&self, unsigned: UnsignedEvent) -> Result<NostrEvent, SignerError> {
let sk_bytes = self.keypair.secret.as_bytes();
let signing_key = k256::schnorr::SigningKey::from_bytes(sk_bytes)
.map_err(|e| SignerError::SigningFailed(e.to_string()))?;
sign_event(unsigned, &signing_key).map_err(SignerError::from)
}
async fn nip44_encrypt(
&self,
recipient_pubkey_hex: &str,
plaintext: &str,
) -> Result<String, SignerError> {
let pk = parse_pk32(recipient_pubkey_hex)?;
nip44_enc(self.keypair.secret.as_bytes(), &pk, plaintext)
.map_err(|e| SignerError::EncryptionFailed(e.to_string()))
}
async fn nip44_decrypt(
&self,
sender_pubkey_hex: &str,
ciphertext: &str,
) -> Result<String, SignerError> {
let pk = parse_pk32(sender_pubkey_hex)?;
nip44_dec(self.keypair.secret.as_bytes(), &pk, ciphertext)
.map_err(|e| SignerError::DecryptionFailed(e.to_string()))
}
async fn nip04_encrypt(
&self,
recipient_pubkey_hex: &str,
plaintext: &str,
) -> Result<String, SignerError> {
nip04_enc(
self.keypair.secret.as_bytes(),
recipient_pubkey_hex,
plaintext,
)
.map_err(|e| SignerError::EncryptionFailed(e.to_string()))
}
async fn nip04_decrypt(
&self,
sender_pubkey_hex: &str,
ciphertext: &str,
) -> Result<String, SignerError> {
nip04_dec(
self.keypair.secret.as_bytes(),
sender_pubkey_hex,
ciphertext,
)
.map_err(|e| SignerError::DecryptionFailed(e.to_string()))
}
}
fn parse_pk32(hex_str: &str) -> Result<[u8; 32], SignerError> {
let bytes = hex::decode(hex_str)
.map_err(|e| SignerError::Backend(format!("pubkey hex decode: {e}")))?;
if bytes.len() != 32 {
return Err(SignerError::Backend(format!(
"expected 32-byte pubkey, got {}",
bytes.len()
)));
}
let mut arr = [0u8; 32];
arr.copy_from_slice(&bytes);
Ok(arr)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::keys::generate_keypair;
#[allow(dead_code)]
fn make_prf_signer() -> (PrfSigner, String) {
let kp = generate_keypair().unwrap();
let pk = kp.public.to_hex();
(PrfSigner::new(kp), pk)
}
#[test]
fn prf_signer_public_key_matches_keypair() {
let kp = generate_keypair().unwrap();
let expected_pk = kp.public.to_hex();
let signer = PrfSigner::new(kp);
assert_eq!(signer.public_key(), expected_pk);
}
#[test]
fn signer_error_from_pubkey_mismatch() {
let mismatch = PubkeyMismatch {
derived_pubkey: "aa".repeat(32),
event_pubkey: "bb".repeat(32),
};
let err = SignerError::from(mismatch);
assert!(matches!(err, SignerError::PubkeyMismatch { .. }));
}
#[test]
fn nip44_roundtrip_via_signer() {
let kp_a = generate_keypair().unwrap();
let kp_b = generate_keypair().unwrap();
let pk_a = kp_a.public.to_hex();
let pk_b = kp_b.public.to_hex();
let signer_a = PrfSigner::new(kp_a);
let signer_b = PrfSigner::new(kp_b);
let ct = block_on(signer_a.nip44_encrypt(&pk_b, "nip44 test")).unwrap();
let pt = block_on(signer_b.nip44_decrypt(&pk_a, &ct)).unwrap();
assert_eq!(pt, "nip44 test");
}
#[test]
fn nip04_roundtrip_via_signer() {
let kp_a = generate_keypair().unwrap();
let kp_b = generate_keypair().unwrap();
let pk_a = kp_a.public.to_hex();
let pk_b = kp_b.public.to_hex();
let signer_a = PrfSigner::new(kp_a);
let signer_b = PrfSigner::new(kp_b);
let ct = block_on(signer_a.nip04_encrypt(&pk_b, "nip04 test")).unwrap();
assert!(ct.contains("?iv="), "NIP-04 must have ?iv= separator");
let pt = block_on(signer_b.nip04_decrypt(&pk_a, &ct)).unwrap();
assert_eq!(pt, "nip04 test");
}
fn block_on<F>(f: F) -> F::Output
where
F: std::future::Future,
F::Output: BlockOnSentinel,
{
use std::task::{Context, Poll, RawWaker, RawWakerVTable, Waker};
fn noop_clone(p: *const ()) -> RawWaker {
RawWaker::new(p, &VTAB)
}
fn noop(_: *const ()) {}
static VTAB: RawWakerVTable = RawWakerVTable::new(noop_clone, noop, noop, noop);
let waker = unsafe { Waker::from_raw(RawWaker::new(std::ptr::null(), &VTAB)) };
let mut cx = Context::from_waker(&waker);
let mut pinned = Box::pin(f);
const MAX_SPINS: usize = 64;
for _ in 0..MAX_SPINS {
match pinned.as_mut().poll(&mut cx) {
Poll::Ready(v) => return v,
Poll::Pending => continue,
}
}
F::Output::from_pending_timeout()
}
trait BlockOnSentinel {
fn from_pending_timeout() -> Self;
}
impl<T, E> BlockOnSentinel for Result<T, E>
where
E: From<SignerError>,
{
fn from_pending_timeout() -> Self {
Err(
SignerError::Backend("block_on: future did not resolve within spin budget".into())
.into(),
)
}
}
}