use crate::event_ext::FinalizeUnsignedWithId;
use nostr_sdk::prelude::{FinalizeUnsignedEvent};
use std::sync::atomic::{AtomicU8, Ordering};
use std::sync::{LazyLock, OnceLock};
use nostr_sdk::prelude::*;
use crate::signer::SignerError;
#[derive(Debug, Clone)]
pub enum Nip55Error {
NotAuthorized,
Missing,
Ipc(String),
}
impl std::fmt::Display for Nip55Error {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Nip55Error::NotAuthorized => {
write!(f, "external signer is not authorized for this operation")
}
Nip55Error::Missing => write!(f, "no external signer available"),
Nip55Error::Ipc(msg) => write!(f, "external signer IPC error: {msg}"),
}
}
}
impl std::error::Error for Nip55Error {}
pub trait Nip55Backend: Send + Sync + 'static {
fn is_installed(&self) -> Result<bool, Nip55Error>;
fn get_public_key_pairing(&self, perms_json: &str) -> Result<(String, String), Nip55Error>;
fn resolver_op(
&self,
method: &str,
data: &str,
counterparty: &str,
current_user: &str,
) -> Nip55ResolverOutcome;
fn intent_op(
&self,
intent_type: &str,
data: &str,
counterparty: &str,
current_user: &str,
) -> Result<(Option<String>, Option<String>), Nip55Error>;
fn is_foreground(&self) -> bool;
}
pub enum Nip55ResolverOutcome {
Value {
result: Option<String>,
event: Option<String>,
},
RequiresApproval,
Rejected,
Error(String),
}
static NIP55_BACKEND: OnceLock<Box<dyn Nip55Backend>> = OnceLock::new();
pub fn set_nip55_backend(backend: Box<dyn Nip55Backend>) {
let _ = NIP55_BACKEND.set(backend);
}
#[inline]
pub fn nip55_backend() -> Option<&'static dyn Nip55Backend> {
NIP55_BACKEND.get().map(|b| b.as_ref())
}
const NIP55_MAX_CONCURRENT_OPS: usize = 4;
static NIP55_SEMAPHORE: LazyLock<tokio::sync::Semaphore> =
LazyLock::new(|| tokio::sync::Semaphore::new(NIP55_MAX_CONCURRENT_OPS));
static NIP55_INTENT_SEMAPHORE: LazyLock<tokio::sync::Semaphore> =
LazyLock::new(|| tokio::sync::Semaphore::new(1));
pub const VECTOR_NIP55_ENCRYPT_TYPES: &[&str] = &[
"nip44_encrypt",
"nip44_decrypt",
"nip04_encrypt",
"nip04_decrypt",
];
pub const VECTOR_NIP55_SIGN_KINDS: &[u16] = &[
0, 3, 5, 7, 13, 14, 1059, 8, 30008, 30009, 10030, 10050, 10063, 22242, 24242, 30078, 3300, 3301, 3302, 3303, 3304, 3305, 3306,
3307, 3308, 3309, 3310, 3311, 3312, 3313,
];
pub fn nip55_perms_json() -> String {
let mut arr: Vec<serde_json::Value> = VECTOR_NIP55_ENCRYPT_TYPES
.iter()
.map(|t| serde_json::json!({ "type": t }))
.collect();
for &kind in VECTOR_NIP55_SIGN_KINDS {
arr.push(serde_json::json!({ "type": "sign_event", "kind": kind }));
}
serde_json::Value::Array(arr).to_string()
}
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
#[repr(u8)]
pub enum Nip55State {
Idle = 0,
Ready = 1,
NeedsAuth = 2,
Missing = 3,
}
impl Nip55State {
pub fn as_label(self) -> &'static str {
match self {
Nip55State::Idle => "idle",
Nip55State::Ready => "ready",
Nip55State::NeedsAuth => "needs_auth",
Nip55State::Missing => "missing",
}
}
}
static NIP55_STATE: AtomicU8 = AtomicU8::new(Nip55State::Idle as u8);
#[inline]
pub fn nip55_state() -> Nip55State {
match NIP55_STATE.load(Ordering::Acquire) {
1 => Nip55State::Ready,
2 => Nip55State::NeedsAuth,
3 => Nip55State::Missing,
_ => Nip55State::Idle,
}
}
pub fn set_nip55_state(new_state: Nip55State) {
let prev = NIP55_STATE.swap(new_state as u8, Ordering::AcqRel);
if prev == new_state as u8 {
return;
}
crate::traits::emit_event_json(
"nip55_state",
serde_json::json!({ "state": new_state.as_label() }),
);
}
pub fn drain_nip55_state() {
set_nip55_state(Nip55State::Idle);
}
#[derive(Debug, Clone)]
pub struct Nip55Signer {
user_pubkey: PublicKey,
session: crate::state::SessionGuard,
}
impl Nip55Signer {
pub fn new(user_pubkey: PublicKey) -> Self {
Self {
user_pubkey,
session: crate::state::SessionGuard::capture(),
}
}
#[inline]
pub fn user_pubkey(&self) -> PublicKey {
self.user_pubkey
}
#[inline]
fn flip(&self, state: Nip55State) {
if self.session.is_valid() {
set_nip55_state(state);
}
}
async fn run(
&self,
method: &'static str,
intent_type: &'static str,
data: String,
counterparty: String,
current_user: String,
) -> Result<(Option<String>, Option<String>), SignerError> {
let backend = match nip55_backend() {
Some(b) => b,
None => {
self.flip(Nip55State::Missing);
return Err(SignerError::backend(Nip55Error::Missing));
}
};
let outcome = {
let _permit = NIP55_SEMAPHORE.acquire().await.map_err(|_| {
SignerError::backend(Nip55Error::Ipc("nip55 semaphore closed".to_string()))
})?;
let (d, cp, cu) = (data.clone(), counterparty.clone(), current_user.clone());
match tokio::task::spawn_blocking(move || backend.resolver_op(method, &d, &cp, &cu)).await {
Ok(o) => o,
Err(e) => {
return Err(SignerError::backend(Nip55Error::Ipc(format!(
"nip55 worker join error: {e}"
))))
}
}
};
match outcome {
Nip55ResolverOutcome::Value { result, event } => {
self.flip(Nip55State::Ready);
Ok((result, event))
}
Nip55ResolverOutcome::Rejected => {
self.flip(Nip55State::NeedsAuth);
Err(SignerError::backend(Nip55Error::NotAuthorized))
}
Nip55ResolverOutcome::Error(e) => Err(SignerError::backend(Nip55Error::Ipc(e))),
Nip55ResolverOutcome::RequiresApproval => {
if !backend.is_foreground() {
self.flip(Nip55State::NeedsAuth);
return Err(SignerError::backend(Nip55Error::NotAuthorized));
}
let _intent_permit = NIP55_INTENT_SEMAPHORE.acquire().await.map_err(|_| {
SignerError::backend(Nip55Error::Ipc("nip55 intent semaphore closed".to_string()))
})?;
let res = match tokio::task::spawn_blocking(move || {
backend.intent_op(intent_type, &data, &counterparty, ¤t_user)
})
.await
{
Ok(r) => r,
Err(e) => {
return Err(SignerError::backend(Nip55Error::Ipc(format!(
"nip55 worker join error: {e}"
))))
}
};
match res {
Ok((result, event)) => {
self.flip(Nip55State::Ready);
Ok((result, event))
}
Err(e @ Nip55Error::NotAuthorized) => {
self.flip(Nip55State::NeedsAuth);
Err(SignerError::backend(e))
}
Err(e @ Nip55Error::Missing) => {
self.flip(Nip55State::Missing);
Err(SignerError::backend(e))
}
Err(e) => Err(SignerError::backend(e)),
}
}
}
}
#[cfg(test)]
pub(crate) fn session_generation_for_test(&self) -> u64 {
self.session.generation()
}
}
impl AsyncGetPublicKey for Nip55Signer {
type Error = SignerError;
fn get_public_key_async(&self) -> BoxedFuture<'_, Result<PublicKey, Self::Error>> {
Box::pin(async move { Ok(self.user_pubkey) })
}
}
impl AsyncSignEvent for Nip55Signer {
type Error = SignerError;
fn sign_event_async(&self, unsigned: UnsignedEvent) -> BoxedFuture<'_, Result<Event, Self::Error>> {
Box::pin(async move {
let event_json = unsigned.as_json();
let user_hex = self.user_pubkey.to_hex();
let (_result, signed) = self
.run("SIGN_EVENT", "sign_event", event_json, String::new(), user_hex)
.await?;
let signed_json = signed.ok_or_else(|| {
SignerError::backend(Nip55Error::Ipc("signer returned no signed event".to_string()))
})?;
let event = Event::from_json(&signed_json).map_err(SignerError::backend)?;
if event.pubkey != self.user_pubkey {
return Err(SignerError::backend(Nip55Error::Ipc(format!(
"signer returned event authored by {} (expected {})",
event.pubkey.to_hex(),
self.user_pubkey.to_hex()
))));
}
event.verify().map_err(SignerError::backend)?;
Ok(event)
})
}
}
impl AsyncNip04 for Nip55Signer {
type Error = SignerError;
fn nip04_encrypt_async<'a>(
&'a self,
public_key: &'a PublicKey,
content: &'a str,
) -> BoxedFuture<'a, Result<String, Self::Error>> {
Box::pin(async move {
let (result, _event) = self
.run("NIP04_ENCRYPT", "nip04_encrypt", content.to_string(), public_key.to_hex(), self.user_pubkey.to_hex())
.await?;
result.ok_or_else(|| SignerError::backend(Nip55Error::Ipc("signer returned no result".to_string())))
})
}
fn nip04_decrypt_async<'a>(
&'a self,
public_key: &'a PublicKey,
content: &'a str,
) -> BoxedFuture<'a, Result<String, Self::Error>> {
Box::pin(async move {
let (result, _event) = self
.run("NIP04_DECRYPT", "nip04_decrypt", content.to_string(), public_key.to_hex(), self.user_pubkey.to_hex())
.await?;
result.ok_or_else(|| SignerError::backend(Nip55Error::Ipc("signer returned no result".to_string())))
})
}
}
impl AsyncNip44 for Nip55Signer {
type Error = SignerError;
fn nip44_encrypt_async<'a>(
&'a self,
public_key: &'a PublicKey,
content: &'a str,
) -> BoxedFuture<'a, Result<String, Self::Error>> {
Box::pin(async move {
let (result, _event) = self
.run("NIP44_ENCRYPT", "nip44_encrypt", content.to_string(), public_key.to_hex(), self.user_pubkey.to_hex())
.await?;
result.ok_or_else(|| SignerError::backend(Nip55Error::Ipc("signer returned no result".to_string())))
})
}
fn nip44_decrypt_async<'a>(
&'a self,
public_key: &'a PublicKey,
content: &'a str,
) -> BoxedFuture<'a, Result<String, Self::Error>> {
Box::pin(async move {
let (result, _event) = self
.run("NIP44_DECRYPT", "nip44_decrypt", content.to_string(), public_key.to_hex(), self.user_pubkey.to_hex())
.await?;
result.ok_or_else(|| SignerError::backend(Nip55Error::Ipc("signer returned no result".to_string())))
})
}
}
pub fn nip55_is_installed() -> Result<bool, String> {
match nip55_backend() {
Some(b) => b.is_installed().map_err(|e| e.to_string()),
None => Ok(false),
}
}
pub async fn nip55_pair() -> Result<(PublicKey, String), String> {
let perms = nip55_perms_json();
let backend = nip55_backend().ok_or("no external signer available on this platform")?;
let (pk_str, package) =
tokio::task::spawn_blocking(move || backend.get_public_key_pairing(&perms))
.await
.map_err(|e| format!("pairing worker join error: {e}"))?
.map_err(|e| e.to_string())?;
let pk = PublicKey::parse(&pk_str)
.map_err(|e| format!("external signer returned an invalid pubkey: {e}"))?;
Ok((pk, package))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn perms_exclude_private_key_and_enumerate_sign_kinds() {
let json = nip55_perms_json();
assert!(!json.contains("private_key"), "perms leaked private-key access: {json}");
for t in VECTOR_NIP55_ENCRYPT_TYPES {
assert!(json.contains(t), "perms JSON missing '{t}': {json}");
}
assert!(json.contains("\"kind\":13"), "seal kind 13 must be pre-granted: {json}");
let v: serde_json::Value = serde_json::from_str(&json).expect("perms json parses");
let mut saw_sign = false;
for entry in v.as_array().expect("perms is an array") {
if entry.get("type").and_then(|t| t.as_str()) == Some("sign_event") {
saw_sign = true;
assert!(
entry.get("kind").and_then(|k| k.as_u64()).is_some(),
"bare sign_event perm would be dropped by Amber: {entry}"
);
}
}
assert!(saw_sign, "perms must request sign_event for at least one kind: {json}");
}
#[test]
fn state_label_covers_all_variants() {
assert_eq!(Nip55State::Idle.as_label(), "idle");
assert_eq!(Nip55State::Ready.as_label(), "ready");
assert_eq!(Nip55State::NeedsAuth.as_label(), "needs_auth");
assert_eq!(Nip55State::Missing.as_label(), "missing");
}
#[tokio::test]
async fn get_public_key_is_cached_and_needs_no_backend() {
let keys = Keys::generate();
let signer = Nip55Signer::new(keys.public_key());
let pk = signer.get_public_key_async().await.expect("cached pubkey resolves");
assert_eq!(pk, keys.public_key());
}
#[tokio::test]
async fn global_state_session_gate_and_missing_backend() {
use crate::state::{bump_session_generation, current_session_generation};
set_nip55_state(Nip55State::Idle);
set_nip55_state(Nip55State::Ready);
assert_eq!(nip55_state(), Nip55State::Ready);
set_nip55_state(Nip55State::NeedsAuth);
assert_eq!(nip55_state(), Nip55State::NeedsAuth);
set_nip55_state(Nip55State::Missing);
assert_eq!(nip55_state(), Nip55State::Missing);
drain_nip55_state();
assert_eq!(nip55_state(), Nip55State::Idle);
let keys = Keys::generate();
let gen_before = current_session_generation();
let signer = Nip55Signer::new(keys.public_key());
assert_eq!(
signer.session_generation_for_test(),
gen_before,
"signer must capture the live session generation at construction"
);
if signer.session_generation_for_test() == current_session_generation() {
signer.flip(Nip55State::Ready);
assert_eq!(nip55_state(), Nip55State::Ready, "valid-session flip must apply");
}
set_nip55_state(Nip55State::Ready);
bump_session_generation();
signer.flip(Nip55State::Missing);
assert_eq!(nip55_state(), Nip55State::Ready, "stale-session flip must be a no-op");
set_nip55_state(Nip55State::Idle);
let fresh = Nip55Signer::new(keys.public_key());
let unsigned = EventBuilder::text_note("hi")
.finalize_unsigned_with_id(keys.public_key());
let err = fresh.sign_event_async(unsigned).await;
assert!(err.is_err(), "no backend registered => sign must fail");
assert_eq!(
nip55_state(),
Nip55State::Missing,
"missing-backend op must surface Missing state"
);
assert_eq!(nip55_is_installed().unwrap(), false);
drain_nip55_state();
}
}