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