feat: add support for encrypted attachment #45

Merged
reya merged 12 commits from encrypt-media into master 2026-09-16 01:25:15 +00:00
4 changed files with 48 additions and 32 deletions
Showing only changes of commit c8ee4f63bb - Show all commits
Generated
+1 -1
View File
@@ -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",
+1 -1
View File
@@ -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"
+1 -1
View File
@@ -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
+45 -29
View File
@@ -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<EncryptedFile, Error> {
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<Vec<u8>, 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::<U12>::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::<U12>::from_slice(&nonce), data)
.map_err(|_| anyhow!("Failed to decrypt file")),
16 => AesGcm::<Aes256, U16>::new_from_slice(&key)
.map_err(|_| anyhow!("Invalid decryption key"))?
.decrypt(Nonce::<U16>::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::<Url, Error>(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<Vec<u8>, 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<Vec<u8>, Error> {
HEXLOWER
.decode(value.to_ascii_lowercase().as_bytes())
.map_err(|_| anyhow!("Invalid {label} encoding"))
}