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.
This commit is contained in:
2026-09-15 20:45:32 +07:00
parent 06449d226f
commit c8ee4f63bb
4 changed files with 48 additions and 32 deletions
Generated
+1 -1
View File
@@ -6679,9 +6679,9 @@ version = "1.0.1"
dependencies = [ dependencies = [
"aes-gcm", "aes-gcm",
"anyhow", "anyhow",
"base64 0.22.1",
"browser-signer-proxy", "browser-signer-proxy",
"common", "common",
"data-encoding",
"flume 0.11.1", "flume 0.11.1",
"futures", "futures",
"gpui", "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) # Crypto (NIP-17 encrypted file messages)
aes-gcm = "0.10" aes-gcm = "0.10"
sha2 = "0.10" sha2 = "0.10"
base64 = "0.22" data-encoding = "2"
# Others # Others
anyhow = "1.0.44" anyhow = "1.0.44"
+1 -1
View File
@@ -26,7 +26,7 @@ mime_guess = "2.0.4"
aes-gcm.workspace = true aes-gcm.workspace = true
sha2.workspace = true sha2.workspace = true
base64.workspace = true data-encoding.workspace = true
[target.'cfg(target_arch = "wasm32")'.dependencies] [target.'cfg(target_arch = "wasm32")'.dependencies]
nostr-memory.workspace = true nostr-memory.workspace = true
+45 -29
View File
@@ -1,11 +1,11 @@
use std::path::PathBuf; 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::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 anyhow::{Error, anyhow, bail};
use base64::Engine as _; use data_encoding::HEXLOWER;
use base64::engine::general_purpose::{STANDARD, STANDARD_NO_PAD, URL_SAFE, URL_SAFE_NO_PAD};
use futures::AsyncReadExt; use futures::AsyncReadExt;
use gpui::http_client::AsyncBody; use gpui::http_client::AsyncBody;
use gpui::{AsyncApp, SharedString}; use gpui::{AsyncApp, SharedString};
@@ -130,20 +130,34 @@ pub fn encrypt(data: &[u8]) -> Result<EncryptedFile, Error> {
Ok(EncryptedFile { Ok(EncryptedFile {
data, data,
key: STANDARD.encode(key.as_slice()), key: HEXLOWER.encode(key.as_slice()),
nonce: STANDARD.encode(nonce.as_slice()), nonce: HEXLOWER.encode(nonce.as_slice()),
}) })
} }
pub fn decrypt(data: &[u8], key: &str, nonce: &str) -> Result<Vec<u8>, Error> { pub fn decrypt(data: &[u8], key: &str, nonce: &str) -> Result<Vec<u8>, Error> {
let key = decode(key, 32, "decryption key")?; let key = decode(key, "decryption key")?;
let nonce = decode(nonce, 12, "decryption nonce")?; 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 // Amethyst uses 16 bytes nonces, everything else uses the standard 12
.decrypt(Nonce::<U12>::from_slice(&nonce), data) match nonce.len() {
.map_err(|_| anyhow!("Failed to decrypt file")) 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 { pub fn sha256_hex(data: &[u8]) -> String {
@@ -168,6 +182,7 @@ pub async fn upload_encrypted(
let sha256 = Some(sha256_hex(&encrypted.data)); let sha256 = Some(sha256_hex(&encrypted.data));
let original_sha256 = Some(sha256_hex(&data)); let original_sha256 = Some(sha256_hex(&data));
let size = Some(encrypted.data.len() as u64); let size = Some(encrypted.data.len() as u64);
let base_url = server.to_string();
let keys = Keys::generate(); let keys = Keys::generate();
let client = BlossomClient::new(server); let client = BlossomClient::new(server);
@@ -179,7 +194,20 @@ pub async fn upload_encrypted(
None, None,
Some(&keys), 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) Ok::<Url, Error>(blob.url)
}) })
@@ -311,20 +339,8 @@ fn parse_dim(value: &str) -> Option<(u32, u32)> {
Some((width.parse().ok()?, height.parse().ok()?)) Some((width.parse().ok()?, height.parse().ok()?))
} }
fn decode(value: &str, expected_len: usize, label: &str) -> Result<Vec<u8>, Error> { fn decode(value: &str, label: &str) -> Result<Vec<u8>, Error> {
let decoded = STANDARD HEXLOWER
.decode(value) .decode(value.to_ascii_lowercase().as_bytes())
.or_else(|_| STANDARD_NO_PAD.decode(value)) .map_err(|_| anyhow!("Invalid {label} encoding"))
.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)
} }