feat: add NIP-04 and NIP-44 encrypt/decrypt RPC endpoints for iframe apps
Backend: identity.nostr-encrypt-nip04, identity.nostr-decrypt-nip04, identity.nostr-encrypt-nip44, identity.nostr-decrypt-nip44 endpoints with auto-resolve to default identity. Frontend: appLauncher routes nip04.* and nip44.* postMessage calls to backend RPC. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
1d3a8e2050
commit
83c0092f1b
@@ -63,7 +63,7 @@ serde_yaml = "0.9"
|
||||
reqwest = { version = "0.11", default-features = false, features = ["json", "socks", "rustls-tls"] }
|
||||
|
||||
# Nostr (node discovery + NIP-44 encrypted peer handshake)
|
||||
nostr-sdk = { version = "0.44", features = ["nip44"] }
|
||||
nostr-sdk = { version = "0.44", features = ["nip04", "nip44"] }
|
||||
|
||||
# Backup encryption (DID identity export) + TOTP 2FA encryption
|
||||
argon2 = "0.5"
|
||||
|
||||
@@ -340,6 +340,97 @@ impl RpcHandler {
|
||||
}))
|
||||
}
|
||||
|
||||
/// Resolve the identity ID from params, falling back to the default identity.
|
||||
async fn resolve_identity_id(&self, params: &serde_json::Value) -> Result<String> {
|
||||
if let Some(id) = params.get("id").and_then(|v| v.as_str()) {
|
||||
return Ok(id.to_string());
|
||||
}
|
||||
let manager = IdentityManager::new(&self.config.data_dir).await?;
|
||||
let (records, default_id) = manager.list().await?;
|
||||
// Prefer the default identity
|
||||
if let Some(default_id) = default_id {
|
||||
return Ok(default_id);
|
||||
}
|
||||
// Fall back to first identity with a Nostr key, or just the first identity
|
||||
records.iter()
|
||||
.find(|i| i.nostr_pubkey.is_some())
|
||||
.or(records.first())
|
||||
.map(|i| i.id.clone())
|
||||
.ok_or_else(|| anyhow::anyhow!("No identity found"))
|
||||
}
|
||||
|
||||
/// NIP-04 encrypt plaintext for a peer.
|
||||
pub(super) async fn handle_identity_nostr_encrypt_nip04(
|
||||
&self,
|
||||
params: Option<serde_json::Value>,
|
||||
) -> Result<serde_json::Value> {
|
||||
let params = params.unwrap_or_default();
|
||||
let id = self.resolve_identity_id(¶ms).await?;
|
||||
let pubkey = params.get("pubkey").and_then(|v| v.as_str())
|
||||
.ok_or_else(|| anyhow::anyhow!("Missing required parameter: pubkey"))?;
|
||||
let plaintext = params.get("plaintext").and_then(|v| v.as_str())
|
||||
.ok_or_else(|| anyhow::anyhow!("Missing required parameter: plaintext"))?;
|
||||
|
||||
let manager = IdentityManager::new(&self.config.data_dir).await?;
|
||||
let ciphertext = manager.nostr_encrypt_nip04(&id, pubkey, plaintext).await?;
|
||||
|
||||
Ok(serde_json::json!({ "ciphertext": ciphertext }))
|
||||
}
|
||||
|
||||
/// NIP-04 decrypt ciphertext from a peer.
|
||||
pub(super) async fn handle_identity_nostr_decrypt_nip04(
|
||||
&self,
|
||||
params: Option<serde_json::Value>,
|
||||
) -> Result<serde_json::Value> {
|
||||
let params = params.unwrap_or_default();
|
||||
let id = self.resolve_identity_id(¶ms).await?;
|
||||
let pubkey = params.get("pubkey").and_then(|v| v.as_str())
|
||||
.ok_or_else(|| anyhow::anyhow!("Missing required parameter: pubkey"))?;
|
||||
let ciphertext = params.get("ciphertext").and_then(|v| v.as_str())
|
||||
.ok_or_else(|| anyhow::anyhow!("Missing required parameter: ciphertext"))?;
|
||||
|
||||
let manager = IdentityManager::new(&self.config.data_dir).await?;
|
||||
let plaintext = manager.nostr_decrypt_nip04(&id, pubkey, ciphertext).await?;
|
||||
|
||||
Ok(serde_json::json!({ "plaintext": plaintext }))
|
||||
}
|
||||
|
||||
/// NIP-44 encrypt plaintext for a peer.
|
||||
pub(super) async fn handle_identity_nostr_encrypt_nip44(
|
||||
&self,
|
||||
params: Option<serde_json::Value>,
|
||||
) -> Result<serde_json::Value> {
|
||||
let params = params.unwrap_or_default();
|
||||
let id = self.resolve_identity_id(¶ms).await?;
|
||||
let pubkey = params.get("pubkey").and_then(|v| v.as_str())
|
||||
.ok_or_else(|| anyhow::anyhow!("Missing required parameter: pubkey"))?;
|
||||
let plaintext = params.get("plaintext").and_then(|v| v.as_str())
|
||||
.ok_or_else(|| anyhow::anyhow!("Missing required parameter: plaintext"))?;
|
||||
|
||||
let manager = IdentityManager::new(&self.config.data_dir).await?;
|
||||
let ciphertext = manager.nostr_encrypt_nip44(&id, pubkey, plaintext).await?;
|
||||
|
||||
Ok(serde_json::json!({ "ciphertext": ciphertext }))
|
||||
}
|
||||
|
||||
/// NIP-44 decrypt ciphertext from a peer.
|
||||
pub(super) async fn handle_identity_nostr_decrypt_nip44(
|
||||
&self,
|
||||
params: Option<serde_json::Value>,
|
||||
) -> Result<serde_json::Value> {
|
||||
let params = params.unwrap_or_default();
|
||||
let id = self.resolve_identity_id(¶ms).await?;
|
||||
let pubkey = params.get("pubkey").and_then(|v| v.as_str())
|
||||
.ok_or_else(|| anyhow::anyhow!("Missing required parameter: pubkey"))?;
|
||||
let ciphertext = params.get("ciphertext").and_then(|v| v.as_str())
|
||||
.ok_or_else(|| anyhow::anyhow!("Missing required parameter: ciphertext"))?;
|
||||
|
||||
let manager = IdentityManager::new(&self.config.data_dir).await?;
|
||||
let plaintext = manager.nostr_decrypt_nip44(&id, pubkey, ciphertext).await?;
|
||||
|
||||
Ok(serde_json::json!({ "plaintext": plaintext }))
|
||||
}
|
||||
|
||||
/// Resolve a remote peer's DID Document over Tor.
|
||||
/// Queries the peer's /rpc/ endpoint for identity.resolve-did.
|
||||
pub(super) async fn handle_identity_resolve_remote_did(
|
||||
|
||||
@@ -337,6 +337,10 @@ impl RpcHandler {
|
||||
"identity.verify-did-document" => self.handle_identity_verify_did_document(params).await,
|
||||
"identity.create-nostr-key" => self.handle_identity_create_nostr_key(params).await,
|
||||
"identity.nostr-sign" => self.handle_identity_nostr_sign(params).await,
|
||||
"identity.nostr-encrypt-nip04" => self.handle_identity_nostr_encrypt_nip04(params).await,
|
||||
"identity.nostr-decrypt-nip04" => self.handle_identity_nostr_decrypt_nip04(params).await,
|
||||
"identity.nostr-encrypt-nip44" => self.handle_identity_nostr_encrypt_nip44(params).await,
|
||||
"identity.nostr-decrypt-nip44" => self.handle_identity_nostr_decrypt_nip44(params).await,
|
||||
|
||||
// Bitcoin domain names (NIP-05)
|
||||
"identity.register-name" => self.handle_identity_register_name(params).await,
|
||||
|
||||
@@ -288,6 +288,55 @@ impl IdentityManager {
|
||||
Ok(sig.to_string())
|
||||
}
|
||||
|
||||
/// Load the Nostr secret key for an identity, returning the parsed Keys.
|
||||
async fn load_nostr_keys(&self, id: &str) -> Result<nostr_sdk::Keys> {
|
||||
let file_path = self.identities_dir.join(format!("{}.json", id));
|
||||
if !file_path.exists() {
|
||||
return Err(anyhow::anyhow!("Identity not found: {}", id));
|
||||
}
|
||||
let data = fs::read(&file_path).await.context("Failed to read identity file")?;
|
||||
let file: IdentityFile = serde_json::from_slice(&data).context("Failed to parse identity file")?;
|
||||
let secret_hex = file.nostr_secret_hex
|
||||
.ok_or_else(|| anyhow::anyhow!("No Nostr key for this identity"))?;
|
||||
nostr_sdk::Keys::parse(&secret_hex).context("Invalid Nostr secret key")
|
||||
}
|
||||
|
||||
/// NIP-04 encrypt plaintext for a peer pubkey.
|
||||
pub async fn nostr_encrypt_nip04(&self, id: &str, peer_pubkey_hex: &str, plaintext: &str) -> Result<String> {
|
||||
let keys = self.load_nostr_keys(id).await?;
|
||||
let peer_pk = nostr_sdk::PublicKey::from_hex(peer_pubkey_hex)
|
||||
.context("Invalid peer pubkey hex")?;
|
||||
nostr_sdk::nips::nip04::encrypt(keys.secret_key(), &peer_pk, plaintext)
|
||||
.context("NIP-04 encryption failed")
|
||||
}
|
||||
|
||||
/// NIP-04 decrypt ciphertext from a peer pubkey.
|
||||
pub async fn nostr_decrypt_nip04(&self, id: &str, peer_pubkey_hex: &str, ciphertext: &str) -> Result<String> {
|
||||
let keys = self.load_nostr_keys(id).await?;
|
||||
let peer_pk = nostr_sdk::PublicKey::from_hex(peer_pubkey_hex)
|
||||
.context("Invalid peer pubkey hex")?;
|
||||
nostr_sdk::nips::nip04::decrypt(keys.secret_key(), &peer_pk, ciphertext)
|
||||
.context("NIP-04 decryption failed")
|
||||
}
|
||||
|
||||
/// NIP-44 encrypt plaintext for a peer pubkey.
|
||||
pub async fn nostr_encrypt_nip44(&self, id: &str, peer_pubkey_hex: &str, plaintext: &str) -> Result<String> {
|
||||
let keys = self.load_nostr_keys(id).await?;
|
||||
let peer_pk = nostr_sdk::PublicKey::from_hex(peer_pubkey_hex)
|
||||
.context("Invalid peer pubkey hex")?;
|
||||
nostr_sdk::nips::nip44::encrypt(keys.secret_key(), &peer_pk, plaintext, nostr_sdk::nips::nip44::Version::V2)
|
||||
.context("NIP-44 encryption failed")
|
||||
}
|
||||
|
||||
/// NIP-44 decrypt ciphertext from a peer pubkey.
|
||||
pub async fn nostr_decrypt_nip44(&self, id: &str, peer_pubkey_hex: &str, ciphertext: &str) -> Result<String> {
|
||||
let keys = self.load_nostr_keys(id).await?;
|
||||
let peer_pk = nostr_sdk::PublicKey::from_hex(peer_pubkey_hex)
|
||||
.context("Invalid peer pubkey hex")?;
|
||||
nostr_sdk::nips::nip44::decrypt(keys.secret_key(), &peer_pk, ciphertext)
|
||||
.context("NIP-44 decryption failed")
|
||||
}
|
||||
|
||||
// --- internal helpers ---
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user