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 = [
|
||||
"aes-gcm",
|
||||
"anyhow",
|
||||
"base64 0.22.1",
|
||||
"browser-signer-proxy",
|
||||
"common",
|
||||
"data-encoding",
|
||||
"flume 0.11.1",
|
||||
"futures",
|
||||
"gpui",
|
||||
|
||||
+1
-1
@@ -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"
|
||||
|
||||
+127
-72
@@ -33,6 +33,7 @@ use ui::input::{Input, InputEvent, InputState};
|
||||
use ui::menu::DropdownMenu;
|
||||
use ui::notification::Notification;
|
||||
use ui::scroll::Scrollbar;
|
||||
use ui::tooltip::Tooltip;
|
||||
use ui::{
|
||||
Disableable, Icon, IconName, InteractiveElementExt, Sizable, StyledExt, WindowExtension,
|
||||
h_flex, v_flex,
|
||||
@@ -714,17 +715,34 @@ impl ChatPanel {
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
this.update(cx, |this, cx| {
|
||||
this.set_uploading(true, cx);
|
||||
this.update_in(cx, |this, window, 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 {
|
||||
upload_encrypted(server, path.clone(), cx)
|
||||
upload_encrypted(server.clone(), path.clone(), cx)
|
||||
.await
|
||||
.map(|file| Uploaded::File(file, path))
|
||||
.map(|file| Uploaded::File(file, path.clone()))
|
||||
} 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| {
|
||||
@@ -733,6 +751,9 @@ impl ChatPanel {
|
||||
match result {
|
||||
Ok(Uploaded::Url(url)) => this.add_attachment(url, 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) => {
|
||||
window.push_notification(
|
||||
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>) {
|
||||
self.uploading = uploading;
|
||||
cx.notify();
|
||||
@@ -1599,7 +1652,7 @@ impl ChatPanel {
|
||||
.size_16()
|
||||
.when(cx.theme().shadow, |this| this.shadow_lg())
|
||||
.rounded(cx.theme().radius)
|
||||
.object_fit(ObjectFit::ScaleDown),
|
||||
.object_fit(ObjectFit::Cover),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
@@ -1682,33 +1735,23 @@ impl ChatPanel {
|
||||
label: SharedString,
|
||||
cx: &Context<Self>,
|
||||
) -> AnyElement {
|
||||
div()
|
||||
h_flex()
|
||||
.id(SharedString::from(format!("file-{id}")))
|
||||
.flex()
|
||||
.items_center()
|
||||
.self_start()
|
||||
.items_start()
|
||||
.min_w_0()
|
||||
.gap_2()
|
||||
.py_1()
|
||||
.px_2()
|
||||
.p_2()
|
||||
.border_1()
|
||||
.border_color(cx.theme().border_variant)
|
||||
.rounded(cx.theme().radius)
|
||||
.hover(|this| this.bg(cx.theme().surface_background))
|
||||
.child(
|
||||
Icon::new(IconName::Lock)
|
||||
.small()
|
||||
.text_color(cx.theme().text_placeholder),
|
||||
)
|
||||
.child(Icon::new(IconName::Lock).text_color(cx.theme().icon_accent))
|
||||
.child(
|
||||
v_flex()
|
||||
.flex_1()
|
||||
.min_w_0()
|
||||
.overflow_hidden()
|
||||
.text_sm()
|
||||
.child(
|
||||
div()
|
||||
.text_ellipsis()
|
||||
.line_clamp(1)
|
||||
.child(file.display_name()),
|
||||
)
|
||||
.child(div().line_height(relative(1.2)).child(file.display_name()))
|
||||
.child(
|
||||
div()
|
||||
.text_xs()
|
||||
@@ -1730,58 +1773,59 @@ impl ChatPanel {
|
||||
/// Render an uploaded, encrypted file which is not sent yet
|
||||
fn render_pending_file(&self, pending: &PendingFile, cx: &Context<Self>) -> impl IntoElement {
|
||||
let file = &pending.file;
|
||||
let label = file.display_name();
|
||||
|
||||
div()
|
||||
.id(SharedString::from(file.url.to_string()))
|
||||
.relative()
|
||||
.flex()
|
||||
.items_center()
|
||||
.gap_2()
|
||||
.p_1()
|
||||
.pr_2()
|
||||
.border_1()
|
||||
.border_color(cx.theme().border_variant)
|
||||
.rounded(cx.theme().radius)
|
||||
.when(file.is_image(), |this| {
|
||||
this.child(
|
||||
img(pending.path.clone())
|
||||
.size_8()
|
||||
.rounded(cx.theme().radius)
|
||||
.object_fit(ObjectFit::Cover),
|
||||
)
|
||||
.w_16()
|
||||
.tooltip(move |window, cx| Tooltip::new(label.clone(), window, cx).into())
|
||||
.map(|this| {
|
||||
if file.is_image() {
|
||||
this.child(
|
||||
img(pending.path.clone())
|
||||
.size_16()
|
||||
.when(cx.theme().shadow, |this| this.shadow_sm())
|
||||
.rounded(cx.theme().radius)
|
||||
.object_fit(ObjectFit::Cover),
|
||||
)
|
||||
} else {
|
||||
this.child(
|
||||
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(
|
||||
v_flex()
|
||||
.text_sm()
|
||||
.child(
|
||||
div()
|
||||
.max_w(px(160.))
|
||||
.text_ellipsis()
|
||||
.line_clamp(1)
|
||||
.child(file.display_name()),
|
||||
)
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_1()
|
||||
.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);
|
||||
})
|
||||
}),
|
||||
.absolute()
|
||||
.top_neg_1()
|
||||
.right_neg_1()
|
||||
.size_4()
|
||||
.items_center()
|
||||
.justify_center()
|
||||
.rounded_full()
|
||||
.border_1()
|
||||
.border_color(cx.theme().border_variant)
|
||||
.bg(gpui::green())
|
||||
.child(Icon::new(IconName::Lock).size_2().text_color(gpui::white())),
|
||||
)
|
||||
.on_click({
|
||||
let url = file.url.clone();
|
||||
cx.listener(move |this, _, _, cx| {
|
||||
this.remove_pending_file(&url, cx);
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
fn render_pending_file_list(
|
||||
@@ -1974,6 +2018,10 @@ impl Focusable for ChatPanel {
|
||||
|
||||
impl Render for ChatPanel {
|
||||
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()
|
||||
.image_cache(coop_cache(self.id.clone(), 100))
|
||||
.on_action(cx.listener(Self::on_command))
|
||||
@@ -2007,10 +2055,8 @@ impl Render for ChatPanel {
|
||||
.map(|this| {
|
||||
if self.messages.is_empty() {
|
||||
this.child(
|
||||
div()
|
||||
h_flex()
|
||||
.size_full()
|
||||
.flex()
|
||||
.items_center()
|
||||
.justify_end()
|
||||
.child(self.render_announcement(cx)),
|
||||
)
|
||||
@@ -2037,6 +2083,15 @@ impl Render for ChatPanel {
|
||||
.children(self.render_attachment_list(window, cx))
|
||||
.children(self.render_pending_file_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(
|
||||
h_flex()
|
||||
.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)
|
||||
}
|
||||
|
||||
const DEFAULT_FILE_SERVER: &str = "https://nostr.download/";
|
||||
const LEGACY_FILE_SERVER: &str = "blossom.band";
|
||||
|
||||
macro_rules! setting_accessors {
|
||||
($(pub $field:ident: $type:ty),* $(,)?) => {
|
||||
impl AppSettings {
|
||||
@@ -138,7 +141,7 @@ impl Default for Settings {
|
||||
screening: true,
|
||||
nip4e: false,
|
||||
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| {
|
||||
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
|
||||
this.update_in(cx, |this, window, cx| {
|
||||
|
||||
@@ -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
|
||||
|
||||
+32
-10
@@ -4,30 +4,52 @@ use anyhow::{Error, anyhow};
|
||||
use gpui::AsyncApp;
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
use gpui_tokio::Tokio;
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
use mime_guess::from_path;
|
||||
use nostr_blossom::prelude::*;
|
||||
use nostr_sdk::prelude::*;
|
||||
|
||||
#[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 keys = Keys::generate();
|
||||
use crate::file::sha256_hex;
|
||||
|
||||
// Construct the blossom client
|
||||
let client = BlossomClient::new(server);
|
||||
/// Upload a blob to a blossom server and return its URL
|
||||
#[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 {
|
||||
let blob = client
|
||||
match client
|
||||
.upload_blob(data, Some(content_type), None, Some(&keys))
|
||||
.await?;
|
||||
|
||||
Ok(blob.url)
|
||||
.await
|
||||
{
|
||||
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
|
||||
.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")]
|
||||
pub async fn upload(_server: Url, _path: PathBuf, _cx: &AsyncApp) -> Result<Url, Error> {
|
||||
Err(anyhow!("File upload not supported on web"))
|
||||
|
||||
+62
-55
@@ -1,21 +1,17 @@
|
||||
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::{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};
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
use gpui_tokio::Tokio;
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
use mime_guess::from_path;
|
||||
use nostr::nips::nip94::Sha256Hash;
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
use nostr_blossom::prelude::*;
|
||||
use nostr_sdk::prelude::*;
|
||||
use sha2::{Digest, Sha256};
|
||||
|
||||
@@ -121,8 +117,8 @@ impl FileAttachment {
|
||||
|
||||
pub fn encrypt(data: &[u8]) -> Result<EncryptedFile, Error> {
|
||||
let key = Aes256Gcm::generate_key(OsRng);
|
||||
let nonce = Aes256Gcm::generate_nonce(OsRng);
|
||||
let cipher = Aes256Gcm::new(&key);
|
||||
let nonce = AesGcm::<Aes256, U16>::generate_nonce(OsRng);
|
||||
let cipher = AesGcm::<Aes256, U16>::new(&key);
|
||||
|
||||
let data = cipher
|
||||
.encrypt(&nonce, data)
|
||||
@@ -130,20 +126,37 @@ 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"))
|
||||
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")),
|
||||
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 {
|
||||
@@ -165,35 +178,41 @@ pub async fn upload_encrypted(
|
||||
let data = smol::fs::read(&path).await?;
|
||||
|
||||
let encrypted = encrypt(&data)?;
|
||||
let sha256 = Some(sha256_hex(&encrypted.data));
|
||||
let original_sha256 = Some(sha256_hex(&data));
|
||||
let size = Some(encrypted.data.len() as u64);
|
||||
let keys = Keys::generate();
|
||||
let client = BlossomClient::new(server);
|
||||
let sha256 = sha256_hex(&encrypted.data);
|
||||
let original_sha256 = sha256_hex(&data);
|
||||
let size = encrypted.data.len() as u64;
|
||||
let base_url = server.to_string();
|
||||
|
||||
let url = Tokio::spawn(cx, async move {
|
||||
let blob = client
|
||||
.upload_blob(
|
||||
encrypted.data,
|
||||
Some("application/octet-stream".to_string()),
|
||||
None,
|
||||
Some(&keys),
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok::<Url, Error>(blob.url)
|
||||
})
|
||||
let url = crate::blossom::upload_blob(
|
||||
&server,
|
||||
encrypted.data,
|
||||
"application/octet-stream",
|
||||
&sha256,
|
||||
cx,
|
||||
)
|
||||
.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 {
|
||||
url,
|
||||
mime,
|
||||
key: encrypted.key,
|
||||
nonce: encrypted.nonce,
|
||||
sha256,
|
||||
original_sha256,
|
||||
size,
|
||||
sha256: Some(sha256),
|
||||
original_sha256: Some(original_sha256),
|
||||
size: Some(size),
|
||||
dim: None,
|
||||
name,
|
||||
})
|
||||
@@ -311,20 +330,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"))
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user