feat: add support for encrypted attachment #45
Generated
+1
-1
@@ -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
@@ -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"
|
||||||
|
|||||||
@@ -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
@@ -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)
|
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user