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")) }