Compare commits
5
Commits
06449d226f
...
024dc90d07
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
024dc90d07 | ||
|
|
89396b8f07 | ||
|
|
ac70121bc9 | ||
|
|
ee3547fc86 | ||
|
|
c8ee4f63bb |
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"
|
||||||
|
|||||||
+127
-72
@@ -33,6 +33,7 @@ use ui::input::{Input, InputEvent, InputState};
|
|||||||
use ui::menu::DropdownMenu;
|
use ui::menu::DropdownMenu;
|
||||||
use ui::notification::Notification;
|
use ui::notification::Notification;
|
||||||
use ui::scroll::Scrollbar;
|
use ui::scroll::Scrollbar;
|
||||||
|
use ui::tooltip::Tooltip;
|
||||||
use ui::{
|
use ui::{
|
||||||
Disableable, Icon, IconName, InteractiveElementExt, Sizable, StyledExt, WindowExtension,
|
Disableable, Icon, IconName, InteractiveElementExt, Sizable, StyledExt, WindowExtension,
|
||||||
h_flex, v_flex,
|
h_flex, v_flex,
|
||||||
@@ -714,17 +715,34 @@ impl ChatPanel {
|
|||||||
return Ok(());
|
return Ok(());
|
||||||
};
|
};
|
||||||
|
|
||||||
this.update(cx, |this, cx| {
|
this.update_in(cx, |this, window, cx| {
|
||||||
this.set_uploading(true, cx);
|
this.upload_file(server, path, encrypted, window, cx);
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
// Upload the file, encrypted when it is the whole message
|
Ok(())
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Upload a file, encrypted when the attachment is the whole message
|
||||||
|
fn upload_file(
|
||||||
|
&mut self,
|
||||||
|
server: Url,
|
||||||
|
path: PathBuf,
|
||||||
|
encrypted: bool,
|
||||||
|
window: &mut Window,
|
||||||
|
cx: &mut Context<Self>,
|
||||||
|
) {
|
||||||
|
self.set_uploading(true, cx);
|
||||||
|
|
||||||
|
self.tasks.push(cx.spawn_in(window, async move |this, cx| {
|
||||||
let result = if encrypted {
|
let result = if encrypted {
|
||||||
upload_encrypted(server, path.clone(), cx)
|
upload_encrypted(server.clone(), path.clone(), cx)
|
||||||
.await
|
.await
|
||||||
.map(|file| Uploaded::File(file, path))
|
.map(|file| Uploaded::File(file, path.clone()))
|
||||||
} else {
|
} else {
|
||||||
upload(server, path, cx).await.map(Uploaded::Url)
|
upload(server.clone(), path.clone(), cx)
|
||||||
|
.await
|
||||||
|
.map(Uploaded::Url)
|
||||||
};
|
};
|
||||||
|
|
||||||
this.update_in(cx, |this, window, cx| {
|
this.update_in(cx, |this, window, cx| {
|
||||||
@@ -733,6 +751,9 @@ impl ChatPanel {
|
|||||||
match result {
|
match result {
|
||||||
Ok(Uploaded::Url(url)) => this.add_attachment(url, cx),
|
Ok(Uploaded::Url(url)) => this.add_attachment(url, cx),
|
||||||
Ok(Uploaded::File(file, path)) => this.add_pending_file(file, path, cx),
|
Ok(Uploaded::File(file, path)) => this.add_pending_file(file, path, cx),
|
||||||
|
Err(e) if encrypted => {
|
||||||
|
this.report_encrypted_upload_error(server, path, e, window, cx)
|
||||||
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
window.push_notification(
|
window.push_notification(
|
||||||
Notification::error(e.to_string()).autohide(false),
|
Notification::error(e.to_string()).autohide(false),
|
||||||
@@ -746,6 +767,38 @@ impl ChatPanel {
|
|||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Report a failed encrypted upload, offering to retry it without encryption
|
||||||
|
fn report_encrypted_upload_error(
|
||||||
|
&mut self,
|
||||||
|
server: Url,
|
||||||
|
path: PathBuf,
|
||||||
|
error: Error,
|
||||||
|
window: &mut Window,
|
||||||
|
cx: &mut Context<Self>,
|
||||||
|
) {
|
||||||
|
let view = cx.entity().downgrade();
|
||||||
|
|
||||||
|
window.push_notification(
|
||||||
|
Notification::error(error.to_string())
|
||||||
|
.title("Encrypted upload failed")
|
||||||
|
.action(move |_this, _window, _cx| {
|
||||||
|
let view = view.clone();
|
||||||
|
let server = server.clone();
|
||||||
|
let path = path.clone();
|
||||||
|
|
||||||
|
Button::new("retry-without-encryption")
|
||||||
|
.label("Upload without encryption")
|
||||||
|
.on_click(move |_ev, window, cx| {
|
||||||
|
view.update(cx, |this, cx| {
|
||||||
|
this.upload_file(server.clone(), path.clone(), false, window, cx);
|
||||||
|
})
|
||||||
|
.ok();
|
||||||
|
})
|
||||||
|
}),
|
||||||
|
cx,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
fn set_uploading(&mut self, uploading: bool, cx: &mut Context<Self>) {
|
fn set_uploading(&mut self, uploading: bool, cx: &mut Context<Self>) {
|
||||||
self.uploading = uploading;
|
self.uploading = uploading;
|
||||||
cx.notify();
|
cx.notify();
|
||||||
@@ -1599,7 +1652,7 @@ impl ChatPanel {
|
|||||||
.size_16()
|
.size_16()
|
||||||
.when(cx.theme().shadow, |this| this.shadow_lg())
|
.when(cx.theme().shadow, |this| this.shadow_lg())
|
||||||
.rounded(cx.theme().radius)
|
.rounded(cx.theme().radius)
|
||||||
.object_fit(ObjectFit::ScaleDown),
|
.object_fit(ObjectFit::Cover),
|
||||||
)
|
)
|
||||||
.child(
|
.child(
|
||||||
div()
|
div()
|
||||||
@@ -1682,33 +1735,23 @@ impl ChatPanel {
|
|||||||
label: SharedString,
|
label: SharedString,
|
||||||
cx: &Context<Self>,
|
cx: &Context<Self>,
|
||||||
) -> AnyElement {
|
) -> AnyElement {
|
||||||
div()
|
h_flex()
|
||||||
.id(SharedString::from(format!("file-{id}")))
|
.id(SharedString::from(format!("file-{id}")))
|
||||||
.flex()
|
.self_start()
|
||||||
.items_center()
|
.items_start()
|
||||||
|
.min_w_0()
|
||||||
.gap_2()
|
.gap_2()
|
||||||
.py_1()
|
.p_2()
|
||||||
.px_2()
|
|
||||||
.border_1()
|
.border_1()
|
||||||
.border_color(cx.theme().border_variant)
|
.border_color(cx.theme().border_variant)
|
||||||
.rounded(cx.theme().radius)
|
.rounded(cx.theme().radius)
|
||||||
.hover(|this| this.bg(cx.theme().surface_background))
|
.child(Icon::new(IconName::Lock).text_color(cx.theme().icon_accent))
|
||||||
.child(
|
|
||||||
Icon::new(IconName::Lock)
|
|
||||||
.small()
|
|
||||||
.text_color(cx.theme().text_placeholder),
|
|
||||||
)
|
|
||||||
.child(
|
.child(
|
||||||
v_flex()
|
v_flex()
|
||||||
.flex_1()
|
.min_w_0()
|
||||||
.overflow_hidden()
|
.overflow_hidden()
|
||||||
.text_sm()
|
.text_sm()
|
||||||
.child(
|
.child(div().line_height(relative(1.2)).child(file.display_name()))
|
||||||
div()
|
|
||||||
.text_ellipsis()
|
|
||||||
.line_clamp(1)
|
|
||||||
.child(file.display_name()),
|
|
||||||
)
|
|
||||||
.child(
|
.child(
|
||||||
div()
|
div()
|
||||||
.text_xs()
|
.text_xs()
|
||||||
@@ -1730,58 +1773,59 @@ impl ChatPanel {
|
|||||||
/// Render an uploaded, encrypted file which is not sent yet
|
/// Render an uploaded, encrypted file which is not sent yet
|
||||||
fn render_pending_file(&self, pending: &PendingFile, cx: &Context<Self>) -> impl IntoElement {
|
fn render_pending_file(&self, pending: &PendingFile, cx: &Context<Self>) -> impl IntoElement {
|
||||||
let file = &pending.file;
|
let file = &pending.file;
|
||||||
|
let label = file.display_name();
|
||||||
|
|
||||||
div()
|
div()
|
||||||
.id(SharedString::from(file.url.to_string()))
|
.id(SharedString::from(file.url.to_string()))
|
||||||
.relative()
|
.relative()
|
||||||
.flex()
|
.w_16()
|
||||||
.items_center()
|
.tooltip(move |window, cx| Tooltip::new(label.clone(), window, cx).into())
|
||||||
.gap_2()
|
.map(|this| {
|
||||||
.p_1()
|
if file.is_image() {
|
||||||
.pr_2()
|
this.child(
|
||||||
.border_1()
|
img(pending.path.clone())
|
||||||
.border_color(cx.theme().border_variant)
|
.size_16()
|
||||||
.rounded(cx.theme().radius)
|
.when(cx.theme().shadow, |this| this.shadow_sm())
|
||||||
.when(file.is_image(), |this| {
|
.rounded(cx.theme().radius)
|
||||||
this.child(
|
.object_fit(ObjectFit::Cover),
|
||||||
img(pending.path.clone())
|
)
|
||||||
.size_8()
|
} else {
|
||||||
.rounded(cx.theme().radius)
|
this.child(
|
||||||
.object_fit(ObjectFit::Cover),
|
div()
|
||||||
)
|
.size_16()
|
||||||
|
.flex()
|
||||||
|
.items_center()
|
||||||
|
.justify_center()
|
||||||
|
.rounded(cx.theme().radius)
|
||||||
|
.border_1()
|
||||||
|
.border_color(cx.theme().border_variant)
|
||||||
|
.bg(cx.theme().surface_background)
|
||||||
|
.text_xs()
|
||||||
|
.text_center()
|
||||||
|
.child("Preview not available"),
|
||||||
|
)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
.child(
|
.child(
|
||||||
v_flex()
|
v_flex()
|
||||||
.text_sm()
|
.absolute()
|
||||||
.child(
|
.top_neg_1()
|
||||||
div()
|
.right_neg_1()
|
||||||
.max_w(px(160.))
|
.size_4()
|
||||||
.text_ellipsis()
|
.items_center()
|
||||||
.line_clamp(1)
|
.justify_center()
|
||||||
.child(file.display_name()),
|
.rounded_full()
|
||||||
)
|
.border_1()
|
||||||
.child(
|
.border_color(cx.theme().border_variant)
|
||||||
h_flex()
|
.bg(gpui::green())
|
||||||
.gap_1()
|
.child(Icon::new(IconName::Lock).size_2().text_color(gpui::white())),
|
||||||
.items_center()
|
|
||||||
.text_xs()
|
|
||||||
.text_color(cx.theme().text_placeholder)
|
|
||||||
.child(Icon::new(IconName::Lock).size_2())
|
|
||||||
.child("End-to-end encrypted"),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.child(
|
|
||||||
Button::new(SharedString::from(format!("remove-{}", file.url)))
|
|
||||||
.icon(IconName::Close)
|
|
||||||
.xsmall()
|
|
||||||
.ghost()
|
|
||||||
.on_click({
|
|
||||||
let url = file.url.clone();
|
|
||||||
cx.listener(move |this, _, _, cx| {
|
|
||||||
this.remove_pending_file(&url, cx);
|
|
||||||
})
|
|
||||||
}),
|
|
||||||
)
|
)
|
||||||
|
.on_click({
|
||||||
|
let url = file.url.clone();
|
||||||
|
cx.listener(move |this, _, _, cx| {
|
||||||
|
this.remove_pending_file(&url, cx);
|
||||||
|
})
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn render_pending_file_list(
|
fn render_pending_file_list(
|
||||||
@@ -1974,6 +2018,10 @@ impl Focusable for ChatPanel {
|
|||||||
|
|
||||||
impl Render for ChatPanel {
|
impl Render for ChatPanel {
|
||||||
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||||
|
const WARNING: &str = "Attachments added while typing are uploaded without encryption";
|
||||||
|
|
||||||
|
let is_typing = !self.input.read(cx).value().trim().is_empty();
|
||||||
|
|
||||||
v_flex()
|
v_flex()
|
||||||
.image_cache(coop_cache(self.id.clone(), 100))
|
.image_cache(coop_cache(self.id.clone(), 100))
|
||||||
.on_action(cx.listener(Self::on_command))
|
.on_action(cx.listener(Self::on_command))
|
||||||
@@ -2007,10 +2055,8 @@ impl Render for ChatPanel {
|
|||||||
.map(|this| {
|
.map(|this| {
|
||||||
if self.messages.is_empty() {
|
if self.messages.is_empty() {
|
||||||
this.child(
|
this.child(
|
||||||
div()
|
h_flex()
|
||||||
.size_full()
|
.size_full()
|
||||||
.flex()
|
|
||||||
.items_center()
|
|
||||||
.justify_end()
|
.justify_end()
|
||||||
.child(self.render_announcement(cx)),
|
.child(self.render_announcement(cx)),
|
||||||
)
|
)
|
||||||
@@ -2037,6 +2083,15 @@ impl Render for ChatPanel {
|
|||||||
.children(self.render_attachment_list(window, cx))
|
.children(self.render_attachment_list(window, cx))
|
||||||
.children(self.render_pending_file_list(window, cx))
|
.children(self.render_pending_file_list(window, cx))
|
||||||
.children(self.render_reply_list(window, cx))
|
.children(self.render_reply_list(window, cx))
|
||||||
|
.when(is_typing, |this| {
|
||||||
|
this.child(
|
||||||
|
div()
|
||||||
|
.px_1()
|
||||||
|
.text_xs()
|
||||||
|
.text_color(cx.theme().text_warning)
|
||||||
|
.child(WARNING),
|
||||||
|
)
|
||||||
|
})
|
||||||
.child(
|
.child(
|
||||||
h_flex()
|
h_flex()
|
||||||
.items_end()
|
.items_end()
|
||||||
|
|||||||
@@ -12,6 +12,9 @@ pub fn init(window: &mut Window, cx: &mut App) {
|
|||||||
AppSettings::set_global(cx.new(|cx| AppSettings::new(window, cx)), cx)
|
AppSettings::set_global(cx.new(|cx| AppSettings::new(window, cx)), cx)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const DEFAULT_FILE_SERVER: &str = "https://nostr.download/";
|
||||||
|
const LEGACY_FILE_SERVER: &str = "blossom.band";
|
||||||
|
|
||||||
macro_rules! setting_accessors {
|
macro_rules! setting_accessors {
|
||||||
($(pub $field:ident: $type:ty),* $(,)?) => {
|
($(pub $field:ident: $type:ty),* $(,)?) => {
|
||||||
impl AppSettings {
|
impl AppSettings {
|
||||||
@@ -138,7 +141,7 @@ impl Default for Settings {
|
|||||||
screening: true,
|
screening: true,
|
||||||
nip4e: false,
|
nip4e: false,
|
||||||
trusted_relays: vec![],
|
trusted_relays: vec![],
|
||||||
file_server: Url::parse("https://blossom.band/").unwrap(),
|
file_server: Url::parse(DEFAULT_FILE_SERVER).unwrap(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -217,7 +220,12 @@ impl AppSettings {
|
|||||||
});
|
});
|
||||||
|
|
||||||
cx.spawn_in(window, async move |this, cx| {
|
cx.spawn_in(window, async move |this, cx| {
|
||||||
let settings = task.await.unwrap_or(Settings::default());
|
let mut settings = task.await.unwrap_or(Settings::default());
|
||||||
|
|
||||||
|
// Move settings still pointed at the old default file server over to the new one
|
||||||
|
if settings.file_server.host_str() == Some(LEGACY_FILE_SERVER) {
|
||||||
|
settings.file_server = Url::parse(DEFAULT_FILE_SERVER).unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
// Update settings
|
// Update settings
|
||||||
this.update_in(cx, |this, window, cx| {
|
this.update_in(cx, |this, window, cx| {
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
+32
-10
@@ -4,30 +4,52 @@ use anyhow::{Error, anyhow};
|
|||||||
use gpui::AsyncApp;
|
use gpui::AsyncApp;
|
||||||
#[cfg(not(target_arch = "wasm32"))]
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
use gpui_tokio::Tokio;
|
use gpui_tokio::Tokio;
|
||||||
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
use mime_guess::from_path;
|
use mime_guess::from_path;
|
||||||
use nostr_blossom::prelude::*;
|
use nostr_blossom::prelude::*;
|
||||||
use nostr_sdk::prelude::*;
|
use nostr_sdk::prelude::*;
|
||||||
|
|
||||||
#[cfg(not(target_arch = "wasm32"))]
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
pub async fn upload(server: Url, path: PathBuf, cx: &AsyncApp) -> Result<Url, Error> {
|
use crate::file::sha256_hex;
|
||||||
let content_type = from_path(&path).first_or_octet_stream().to_string();
|
|
||||||
let data = smol::fs::read(path).await?;
|
|
||||||
let keys = Keys::generate();
|
|
||||||
|
|
||||||
// Construct the blossom client
|
/// Upload a blob to a blossom server and return its URL
|
||||||
let client = BlossomClient::new(server);
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
|
pub(crate) async fn upload_blob(
|
||||||
|
server: &Url,
|
||||||
|
data: Vec<u8>,
|
||||||
|
content_type: &str,
|
||||||
|
sha256: &str,
|
||||||
|
cx: &AsyncApp,
|
||||||
|
) -> Result<Url, Error> {
|
||||||
|
let client = BlossomClient::new(server.clone());
|
||||||
|
let keys = Keys::generate();
|
||||||
|
let content_type = content_type.to_string();
|
||||||
|
let base = server.clone();
|
||||||
|
let hash = sha256.to_string();
|
||||||
|
|
||||||
Tokio::spawn(cx, async move {
|
Tokio::spawn(cx, async move {
|
||||||
let blob = client
|
match client
|
||||||
.upload_blob(data, Some(content_type), None, Some(&keys))
|
.upload_blob(data, Some(content_type), None, Some(&keys))
|
||||||
.await?;
|
.await
|
||||||
|
{
|
||||||
Ok(blob.url)
|
Ok(blob) => Ok(blob.url),
|
||||||
|
Err(e) if e.to_string().contains("201 Created") => Ok::<Url, Error>(base.join(&hash)?),
|
||||||
|
Err(e) => Err(anyhow!(e.to_string())),
|
||||||
|
}
|
||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
.map_err(|e| anyhow!("Upload error: {e}"))?
|
.map_err(|e| anyhow!("Upload error: {e}"))?
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
|
pub async fn upload(server: Url, path: PathBuf, cx: &AsyncApp) -> Result<Url, Error> {
|
||||||
|
let content_type = from_path(&path).first_or_octet_stream().to_string();
|
||||||
|
let data = smol::fs::read(&path).await?;
|
||||||
|
let sha256 = sha256_hex(&data);
|
||||||
|
|
||||||
|
upload_blob(&server, data, &content_type, &sha256, cx).await
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(target_arch = "wasm32")]
|
#[cfg(target_arch = "wasm32")]
|
||||||
pub async fn upload(_server: Url, _path: PathBuf, _cx: &AsyncApp) -> Result<Url, Error> {
|
pub async fn upload(_server: Url, _path: PathBuf, _cx: &AsyncApp) -> Result<Url, Error> {
|
||||||
Err(anyhow!("File upload not supported on web"))
|
Err(anyhow!("File upload not supported on web"))
|
||||||
|
|||||||
+62
-55
@@ -1,21 +1,17 @@
|
|||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
use aes_gcm::aead::consts::U12;
|
use aes_gcm::aead::consts::{U12, U16, U32};
|
||||||
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};
|
||||||
#[cfg(not(target_arch = "wasm32"))]
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
use gpui_tokio::Tokio;
|
|
||||||
#[cfg(not(target_arch = "wasm32"))]
|
|
||||||
use mime_guess::from_path;
|
use mime_guess::from_path;
|
||||||
use nostr::nips::nip94::Sha256Hash;
|
use nostr::nips::nip94::Sha256Hash;
|
||||||
#[cfg(not(target_arch = "wasm32"))]
|
|
||||||
use nostr_blossom::prelude::*;
|
|
||||||
use nostr_sdk::prelude::*;
|
use nostr_sdk::prelude::*;
|
||||||
use sha2::{Digest, Sha256};
|
use sha2::{Digest, Sha256};
|
||||||
|
|
||||||
@@ -121,8 +117,8 @@ impl FileAttachment {
|
|||||||
|
|
||||||
pub fn encrypt(data: &[u8]) -> Result<EncryptedFile, Error> {
|
pub fn encrypt(data: &[u8]) -> Result<EncryptedFile, Error> {
|
||||||
let key = Aes256Gcm::generate_key(OsRng);
|
let key = Aes256Gcm::generate_key(OsRng);
|
||||||
let nonce = Aes256Gcm::generate_nonce(OsRng);
|
let nonce = AesGcm::<Aes256, U16>::generate_nonce(OsRng);
|
||||||
let cipher = Aes256Gcm::new(&key);
|
let cipher = AesGcm::<Aes256, U16>::new(&key);
|
||||||
|
|
||||||
let data = cipher
|
let data = cipher
|
||||||
.encrypt(&nonce, data)
|
.encrypt(&nonce, data)
|
||||||
@@ -130,20 +126,37 @@ 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
|
match nonce.len() {
|
||||||
.decrypt(Nonce::<U12>::from_slice(&nonce), data)
|
12 => Aes256Gcm::new_from_slice(&key)
|
||||||
.map_err(|_| anyhow!("Failed to decrypt file"))
|
.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")),
|
||||||
|
32 => AesGcm::<Aes256, U32>::new_from_slice(&key)
|
||||||
|
.map_err(|_| anyhow!("Invalid decryption key"))?
|
||||||
|
.decrypt(Nonce::<U32>::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 {
|
||||||
@@ -165,35 +178,41 @@ pub async fn upload_encrypted(
|
|||||||
let data = smol::fs::read(&path).await?;
|
let data = smol::fs::read(&path).await?;
|
||||||
|
|
||||||
let encrypted = encrypt(&data)?;
|
let encrypted = encrypt(&data)?;
|
||||||
let sha256 = Some(sha256_hex(&encrypted.data));
|
let sha256 = sha256_hex(&encrypted.data);
|
||||||
let original_sha256 = Some(sha256_hex(&data));
|
let original_sha256 = sha256_hex(&data);
|
||||||
let size = Some(encrypted.data.len() as u64);
|
let size = encrypted.data.len() as u64;
|
||||||
let keys = Keys::generate();
|
let base_url = server.to_string();
|
||||||
let client = BlossomClient::new(server);
|
|
||||||
|
|
||||||
let url = Tokio::spawn(cx, async move {
|
let url = crate::blossom::upload_blob(
|
||||||
let blob = client
|
&server,
|
||||||
.upload_blob(
|
encrypted.data,
|
||||||
encrypted.data,
|
"application/octet-stream",
|
||||||
Some("application/octet-stream".to_string()),
|
&sha256,
|
||||||
None,
|
cx,
|
||||||
Some(&keys),
|
)
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
Ok::<Url, Error>(blob.url)
|
|
||||||
})
|
|
||||||
.await
|
.await
|
||||||
.map_err(|e| anyhow!("Upload error: {e}"))??;
|
.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(FileAttachment {
|
Ok(FileAttachment {
|
||||||
url,
|
url,
|
||||||
mime,
|
mime,
|
||||||
key: encrypted.key,
|
key: encrypted.key,
|
||||||
nonce: encrypted.nonce,
|
nonce: encrypted.nonce,
|
||||||
sha256,
|
sha256: Some(sha256),
|
||||||
original_sha256,
|
original_sha256: Some(original_sha256),
|
||||||
size,
|
size: Some(size),
|
||||||
dim: None,
|
dim: None,
|
||||||
name,
|
name,
|
||||||
})
|
})
|
||||||
@@ -311,20 +330,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