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:
parent
398e94b5d3
commit
3383b43a75
@ -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 ---
|
||||
|
||||
}
|
||||
|
||||
@ -488,7 +488,7 @@
|
||||
|
||||
- [x] **NIP07-03** — Test NIP-07 with a real Nostr web app. Install `nostr-rs-relay` container if not already running (it's in the app catalog). Deploy a Nostr web client that supports NIP-07 — add Nostrudel (https://nostrudel.ninja) as a web-only app entry in `Marketplace.vue` `getCuratedAppList()` (category: "Social", opens in iframe). Open Nostrudel, verify it detects `window.nostr`, can fetch the pubkey, and can sign events (post a note). **Acceptance**: Can post a signed Nostr note from within the Archipelago iframe using the node's Nostr identity. Verify the note appears on a public Nostr client.
|
||||
|
||||
- [ ] **NIP07-04** — Support NIP-04 and NIP-44 encryption in iframe provider. The `nostr-provider.js` already has stubs for `nip04.encrypt`, `nip04.decrypt`, `nip44.encrypt`, `nip44.decrypt`. Add backend RPC endpoints: `identity.nostr-encrypt-nip04`, `identity.nostr-decrypt-nip04`, `identity.nostr-encrypt-nip44`, `identity.nostr-decrypt-nip44`. Each takes the identity ID, peer pubkey, and plaintext/ciphertext. Use `nostr_sdk` for the actual crypto. Register in RPC router. Wire the appLauncher `handleNostrRequest` to route `nip04.*` and `nip44.*` calls to these endpoints. **Acceptance**: From an iframe app, call `window.nostr.nip44.encrypt(peerPubkey, "hello")` — returns ciphertext. Call `nip44.decrypt` with same ciphertext — returns "hello". Deploy and verify.
|
||||
- [x] **NIP07-04** — Support NIP-04 and NIP-44 encryption in iframe provider. The `nostr-provider.js` already has stubs for `nip04.encrypt`, `nip04.decrypt`, `nip44.encrypt`, `nip44.decrypt`. Add backend RPC endpoints: `identity.nostr-encrypt-nip04`, `identity.nostr-decrypt-nip04`, `identity.nostr-encrypt-nip44`, `identity.nostr-decrypt-nip44`. Each takes the identity ID, peer pubkey, and plaintext/ciphertext. Use `nostr_sdk` for the actual crypto. Register in RPC router. Wire the appLauncher `handleNostrRequest` to route `nip04.*` and `nip44.*` calls to these endpoints. **Acceptance**: From an iframe app, call `window.nostr.nip44.encrypt(peerPubkey, "hello")` — returns ciphertext. Call `nip44.decrypt` with same ciphertext — returns "hello". Deploy and verify.
|
||||
|
||||
### Sprint 42: Tor Address Rotation & Per-App Toggle (May 2026 Week 1-2)
|
||||
|
||||
|
||||
@ -218,6 +218,30 @@ export const useAppLauncherStore = defineStore('appLauncher', () => {
|
||||
result = res
|
||||
} else if (method === 'getRelays') {
|
||||
result = {}
|
||||
} else if (method === 'nip04.encrypt') {
|
||||
const res = await rpcClient.call<{ ciphertext: string }>({
|
||||
method: 'identity.nostr-encrypt-nip04',
|
||||
params: { pubkey: params.pubkey, plaintext: params.plaintext }
|
||||
})
|
||||
result = res.ciphertext
|
||||
} else if (method === 'nip04.decrypt') {
|
||||
const res = await rpcClient.call<{ plaintext: string }>({
|
||||
method: 'identity.nostr-decrypt-nip04',
|
||||
params: { pubkey: params.pubkey, ciphertext: params.ciphertext }
|
||||
})
|
||||
result = res.plaintext
|
||||
} else if (method === 'nip44.encrypt') {
|
||||
const res = await rpcClient.call<{ ciphertext: string }>({
|
||||
method: 'identity.nostr-encrypt-nip44',
|
||||
params: { pubkey: params.pubkey, plaintext: params.plaintext }
|
||||
})
|
||||
result = res.ciphertext
|
||||
} else if (method === 'nip44.decrypt') {
|
||||
const res = await rpcClient.call<{ plaintext: string }>({
|
||||
method: 'identity.nostr-decrypt-nip44',
|
||||
params: { pubkey: params.pubkey, ciphertext: params.ciphertext }
|
||||
})
|
||||
result = res.plaintext
|
||||
} else {
|
||||
throw new Error(`Unsupported NIP-07 method: ${method}`)
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user