From c8ee4f63bb81392dbf9ef3d99d84b0f9f58a142f Mon Sep 17 00:00:00 2001 From: Ren Amamiya Date: Tue, 15 Sep 2026 20:45:32 +0700 Subject: [PATCH] fix: encode NIP-17 file key and nonce as hex Amethyst parses decryption-key and decryption-nonce as hex, so base64 keys were unreadable by the only implementation of kind 15 file messages. Also accept the 16 bytes nonce Amethyst sends, while still emitting the standard 12 bytes. Replaces the now unused base64 dependency with data-encoding. --- Cargo.lock | 2 +- Cargo.toml | 2 +- crates/state/Cargo.toml | 2 +- crates/state/src/file.rs | 74 ++++++++++++++++++++++++---------------- 4 files changed, 48 insertions(+), 32 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 800a8e4d..bed8c985 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6679,9 +6679,9 @@ version = "1.0.1" dependencies = [ "aes-gcm", "anyhow", - "base64 0.22.1", "browser-signer-proxy", "common", + "data-encoding", "flume 0.11.1", "futures", "gpui", diff --git a/Cargo.toml b/Cargo.toml index 7a6bce9a..1cee8338 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -30,7 +30,7 @@ nostr = { git = "https://github.com/rust-nostr/nostr", features = [ "nip59", "ni # Crypto (NIP-17 encrypted file messages) aes-gcm = "0.10" sha2 = "0.10" -base64 = "0.22" +data-encoding = "2" # Others anyhow = "1.0.44" diff --git a/crates/state/Cargo.toml b/crates/state/Cargo.toml index 09dc9cd0..daebc7a9 100644 --- a/crates/state/Cargo.toml +++ b/crates/state/Cargo.toml @@ -26,7 +26,7 @@ mime_guess = "2.0.4" aes-gcm.workspace = true sha2.workspace = true -base64.workspace = true +data-encoding.workspace = true [target.'cfg(target_arch = "wasm32")'.dependencies] nostr-memory.workspace = true diff --git a/crates/state/src/file.rs b/crates/state/src/file.rs index b5926583..06ae44d5 100644 --- a/crates/state/src/file.rs +++ b/crates/state/src/file.rs @@ -1,11 +1,11 @@ use std::path::PathBuf; -use aes_gcm::aead::consts::U12; +use aes_gcm::aead::consts::{U12, U16}; use aes_gcm::aead::{Aead, AeadCore, KeyInit, OsRng}; -use aes_gcm::{Aes256Gcm, Nonce}; +use aes_gcm::aes::Aes256; +use aes_gcm::{Aes256Gcm, AesGcm, Nonce}; use anyhow::{Error, anyhow, bail}; -use base64::Engine as _; -use base64::engine::general_purpose::{STANDARD, STANDARD_NO_PAD, URL_SAFE, URL_SAFE_NO_PAD}; +use data_encoding::HEXLOWER; use futures::AsyncReadExt; use gpui::http_client::AsyncBody; use gpui::{AsyncApp, SharedString}; @@ -130,20 +130,34 @@ pub fn encrypt(data: &[u8]) -> Result { Ok(EncryptedFile { data, - key: STANDARD.encode(key.as_slice()), - nonce: STANDARD.encode(nonce.as_slice()), + key: HEXLOWER.encode(key.as_slice()), + nonce: HEXLOWER.encode(nonce.as_slice()), }) } pub fn decrypt(data: &[u8], key: &str, nonce: &str) -> Result, Error> { - let key = decode(key, 32, "decryption key")?; - let nonce = decode(nonce, 12, "decryption nonce")?; + let key = decode(key, "decryption key")?; + let nonce = decode(nonce, "decryption nonce")?; - let cipher = Aes256Gcm::new_from_slice(&key).map_err(|_| anyhow!("Invalid decryption key"))?; + if key.len() != 32 { + bail!( + "Invalid decryption key length: expected 32 bytes, got {}", + key.len() + ); + } - cipher - .decrypt(Nonce::::from_slice(&nonce), data) - .map_err(|_| anyhow!("Failed to decrypt file")) + // Amethyst uses 16 bytes nonces, everything else uses the standard 12 + match nonce.len() { + 12 => Aes256Gcm::new_from_slice(&key) + .map_err(|_| anyhow!("Invalid decryption key"))? + .decrypt(Nonce::::from_slice(&nonce), data) + .map_err(|_| anyhow!("Failed to decrypt file")), + 16 => AesGcm::::new_from_slice(&key) + .map_err(|_| anyhow!("Invalid decryption key"))? + .decrypt(Nonce::::from_slice(&nonce), data) + .map_err(|_| anyhow!("Failed to decrypt file")), + len => bail!("Unsupported decryption nonce length: {len} bytes"), + } } pub fn sha256_hex(data: &[u8]) -> String { @@ -168,6 +182,7 @@ pub async fn upload_encrypted( let sha256 = Some(sha256_hex(&encrypted.data)); let original_sha256 = Some(sha256_hex(&data)); let size = Some(encrypted.data.len() as u64); + let base_url = server.to_string(); let keys = Keys::generate(); let client = BlossomClient::new(server); @@ -179,7 +194,20 @@ pub async fn upload_encrypted( None, Some(&keys), ) - .await?; + .await + .map_err(|e| { + let message = e.to_string(); + + if !message.contains("415") { + return anyhow!(message); + } + + anyhow!( + "{base_url} rejected the encrypted file. Encrypted attachments are uploaded as + opaque data, which this file server does not accept. Choose a different file + server in the settings." + ) + })?; Ok::(blob.url) }) @@ -311,20 +339,8 @@ fn parse_dim(value: &str) -> Option<(u32, u32)> { Some((width.parse().ok()?, height.parse().ok()?)) } -fn decode(value: &str, expected_len: usize, label: &str) -> Result, Error> { - let decoded = STANDARD - .decode(value) - .or_else(|_| STANDARD_NO_PAD.decode(value)) - .or_else(|_| URL_SAFE.decode(value)) - .or_else(|_| URL_SAFE_NO_PAD.decode(value)) - .map_err(|_| anyhow!("Invalid {label} encoding"))?; - - if decoded.len() != expected_len { - bail!( - "Invalid {label} length: expected {expected_len} bytes, got {}", - decoded.len() - ); - } - - Ok(decoded) +fn decode(value: &str, label: &str) -> Result, Error> { + HEXLOWER + .decode(value.to_ascii_lowercase().as_bytes()) + .map_err(|_| anyhow!("Invalid {label} encoding")) }