Make crypto even harder to fuck up

This commit is contained in:
Edward Shen 2021-10-30 18:38:55 -07:00
parent 8a08e8e100
commit bb35f710b2
Signed by: edward
GPG key ID: 19182661E818369F
8 changed files with 307 additions and 225 deletions

66
Cargo.lock generated
View file

@ -41,6 +41,17 @@ version = "1.0.44"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "61604a8f862e1d5c3229fdd78f8b02c68dcf73a4c4b05fd636d12240aaa242c1"
[[package]]
name = "argon2"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "34f8cda1a0ecf6f19d2bf64b9349d86900fa9bf98c979e655347a9e9dbe588c1"
dependencies = [
"base64ct",
"blake2",
"password-hash",
]
[[package]]
name = "async-trait"
version = "0.1.51"
@ -103,6 +114,12 @@ version = "0.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "904dfeac50f3cdaba28fc6f57fdcddb75f49ed61346676a78c4ffe55877802fd"
[[package]]
name = "base64ct"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8a32fd6af2b5827bce66c29053ba0e7c42b9dcab01835835058558c10851a46b"
[[package]]
name = "bincode"
version = "1.3.3"
@ -149,6 +166,17 @@ dependencies = [
"wyz",
]
[[package]]
name = "blake2"
version = "0.9.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0a4e37d16930f5459780f5621038b6382b9bb37c19016f39fb6b5808d831f174"
dependencies = [
"crypto-mac",
"digest",
"opaque-debug",
]
[[package]]
name = "block-buffer"
version = "0.9.0"
@ -336,6 +364,16 @@ dependencies = [
"cfg-if",
]
[[package]]
name = "crypto-mac"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b584a330336237c1eecd3e94266efb216c56ed91225d634cb2991c5f3fd1aeab"
dependencies = [
"generic-array",
"subtle",
]
[[package]]
name = "digest"
version = "0.9.0"
@ -922,17 +960,19 @@ dependencies = [
name = "omegaupload-common"
version = "0.1.0"
dependencies = [
"argon2",
"base64",
"bytes",
"chacha20poly1305",
"chrono",
"gloo-console",
"headers",
"http",
"lazy_static",
"rand",
"serde",
"sha2",
"thiserror",
"typenum",
"url",
"web-sys",
]
@ -1001,6 +1041,17 @@ dependencies = [
"memchr",
]
[[package]]
name = "password-hash"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1d791538a6dcc1e7cb7fe6f6b58aca40e7f79403c45b2bc274008b5e647af1d8"
dependencies = [
"base64ct",
"rand_core",
"subtle",
]
[[package]]
name = "peeking_take_while"
version = "0.1.2"
@ -1377,19 +1428,6 @@ dependencies = [
"opaque-debug",
]
[[package]]
name = "sha2"
version = "0.9.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b69f9a4c9740d74c5baa3fd2e547f9525fa8088a8a958e0ca2409a514e33f5fa"
dependencies = [
"block-buffer",
"cfg-if",
"cpufeatures",
"digest",
"opaque-debug",
]
[[package]]
name = "sharded-slab"
version = "0.1.4"

View file

@ -6,9 +6,9 @@ use std::io::{Read, Write};
use anyhow::{anyhow, bail, Context, Result};
use atty::Stream;
use clap::Parser;
use omegaupload_common::crypto::{gen_key_nonce, open_in_place, seal_in_place, Key};
use omegaupload_common::crypto::{open_in_place, seal_in_place};
use omegaupload_common::{
base64, hash, Expiration, ParsedUrl, Url, API_ENDPOINT, EXPIRATION_HEADER_NAME,
base64, Expiration, ParsedUrl, Url, API_ENDPOINT, EXPIRATION_HEADER_NAME,
};
use reqwest::blocking::Client;
use reqwest::header::EXPIRES;
@ -65,27 +65,13 @@ fn handle_upload(
bail!("This tool requires non interactive CLI. Pipe something in!");
}
let (data, nonce, key, pw_used) = {
let (enc_key, nonce) = gen_key_nonce();
let (data, key) = {
let mut container = Vec::new();
std::io::stdin().read_to_end(&mut container)?;
seal_in_place(&mut container, &nonce, &enc_key)
.map_err(|_| anyhow!("Failed to encrypt data"))?;
let pw_used = if let Some(password) = password {
let pw_hash = hash(password.expose_secret().as_bytes());
let pw_key = Key::from_slice(pw_hash.as_ref());
seal_in_place(&mut container, &nonce.increment(), pw_key)
.map_err(|_| anyhow!("Failed to encrypt data"))?;
true
} else {
false
};
let password = password.as_ref().map(|v| v.expose_secret().as_ref());
let enc_key = seal_in_place(&mut container, password)?;
let key = base64::encode(&enc_key);
let nonce = base64::encode(&nonce);
(container, nonce, key, pw_used)
(container, key)
};
let mut res = Client::new().post(url.as_ref());
@ -104,9 +90,9 @@ fn handle_upload(
.map_err(|_| anyhow!("Failed to get base URL"))?
.extend(std::iter::once(res.text()?));
let mut fragment = format!("key:{}!nonce:{}", key, nonce);
let mut fragment = format!("key:{}", key);
if pw_used {
if password.is_some() {
fragment.push_str("!pw");
}
@ -141,6 +127,7 @@ fn handle_download(mut url: ParsedUrl) -> Result<()> {
let mut data = res.bytes()?.as_ref().to_vec();
let mut password = None;
if url.needs_password {
// Only print prompt on interactive, else it messes with output
if atty::is(Stream::Stdout) {
@ -150,16 +137,10 @@ fn handle_download(mut url: ParsedUrl) -> Result<()> {
let mut input = String::new();
std::io::stdin().read_line(&mut input)?;
input.pop(); // last character is \n, we need to drop it.
let pw_hash = hash(input.as_bytes());
let pw_key = Key::from_slice(pw_hash.as_ref());
open_in_place(&mut data, &url.nonce.increment(), pw_key)
.map_err(|_| anyhow!("Failed to decrypt data. Incorrect password?"))?;
password = Some(input);
}
open_in_place(&mut data, &url.nonce, &url.decryption_key)
.map_err(|_| anyhow!("Failed to decrypt data. Incorrect decryption key?"))?;
open_in_place(&mut data, &url.decryption_key, &password)?;
if atty::is(Stream::Stdout) {
if let Ok(data) = String::from_utf8(data) {

View file

@ -8,18 +8,21 @@ edition = "2021"
[dependencies]
base64 = "0.13"
bytes = { version = "*", features = ["serde"] }
chacha20poly1305 = "0.9"
chacha20poly1305 = { version = "0.9", features = ["stream"] }
chrono = { version = "0.4", features = ["serde"] }
headers = "*"
lazy_static = "1"
rand = "0.8"
serde = { version = "1", features = ["derive"] }
sha2 = "0.9"
thiserror = "1"
typenum = "1"
url = "2"
argon2 = "0.3.1"
web-sys = { version = "0.3", features = ["Headers"], optional = true }
# Wasm features
gloo-console = { version = "0.1", optional = true }
http = { version = "0.2", optional = true }
web-sys = { version = "0.3", features = ["Headers"], optional = true }
[features]
wasm = ["web-sys", "http"]
wasm = ["gloo-console", "http", "web-sys"]

11
common/src/base64.rs Normal file
View file

@ -0,0 +1,11 @@
use base64::{DecodeError, URL_SAFE};
/// URL-safe Base64 encoding.
pub fn encode(input: impl AsRef<[u8]>) -> String {
base64::encode_config(input, URL_SAFE)
}
/// URL-safe Base64 decoding.
pub fn decode(input: impl AsRef<[u8]>) -> Result<Vec<u8>, DecodeError> {
base64::decode_config(input, URL_SAFE)
}

196
common/src/crypto.rs Normal file
View file

@ -0,0 +1,196 @@
use std::fmt::Display;
use std::ops::{Deref, DerefMut};
use argon2::Argon2;
use chacha20poly1305::aead::generic_array::sequence::GenericSequence;
use chacha20poly1305::aead::generic_array::GenericArray;
use chacha20poly1305::aead::{AeadInPlace, NewAead};
use chacha20poly1305::XChaCha20Poly1305;
use chacha20poly1305::XNonce;
use rand::{thread_rng, Rng};
use typenum::Unsigned;
pub use chacha20poly1305::Key;
#[derive(Debug)]
pub enum Error {
ChaCha20Poly1305(chacha20poly1305::aead::Error),
Argon2(argon2::Error),
}
impl From<chacha20poly1305::aead::Error> for Error {
fn from(err: chacha20poly1305::aead::Error) -> Self {
Error::ChaCha20Poly1305(err)
}
}
impl From<argon2::Error> for Error {
fn from(err: argon2::Error) -> Self {
Error::Argon2(err)
}
}
impl std::error::Error for Error {}
impl Display for Error {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Error::ChaCha20Poly1305(_) => write!(f, "Decryption failed"),
Error::Argon2(_) => write!(f, "KDF failed"),
}
}
}
/// Seals the provided message with an optional message. The resulting sealed
/// message has the nonce used to encrypt the message appended to it as well as
/// a salt string used to derive the key. In other words, the modified buffer is
/// one of the following to possibilities, depending if there was a password
/// provided:
///
/// ```
/// modified = C(message, rng_key, nonce) || nonce
/// ```
/// or
/// ```
/// modified = C(C(message, rng_key, nonce), kdf(pw, salt), nonce + 1) || nonce || salt
/// ```
///
/// Where:
/// - `C(message, key, nonce)` represents encrypting a provided message with
/// XChaCha20Poly1305.
/// - `rng_key` represents a randomly generated key.
/// - `kdf(pw, salt)` represents a key derived from Argon2.
pub fn seal_in_place(message: &mut Vec<u8>, pw: Option<&str>) -> Result<Key, Error> {
let (key, nonce) = gen_key_nonce();
let cipher = XChaCha20Poly1305::new(&key);
cipher.encrypt_in_place(&nonce, &[], message)?;
let mut maybe_salt_string = None;
if let Some(password) = pw {
let (key, salt_string) = kdf(&password)?;
maybe_salt_string = Some(salt_string);
let cipher = XChaCha20Poly1305::new(&key);
cipher.encrypt_in_place(&nonce.increment(), &[], message)?;
}
message.extend_from_slice(nonce.as_slice());
if let Some(maybe_salted_string) = maybe_salt_string {
message.extend_from_slice(maybe_salted_string.as_ref());
}
Ok(key)
}
pub fn open_in_place(data: &mut Vec<u8>, key: &Key, password: Option<&str>) -> Result<(), Error> {
let buffer_len = data.len();
let pw_key = if let Some(password) = password {
let salt_buf = data.split_off(buffer_len - Salt::SIZE);
let argon = Argon2::default();
let mut pw_key = Key::default();
argon.hash_password_into(password.as_bytes(), &salt_buf, &mut pw_key)?;
Some(pw_key)
} else {
None
};
let nonce = Nonce::from_slice(&data.split_off(Nonce::SIZE));
// At this point we should have a buffer that's only the ciphertext.
if let Some(key) = pw_key {
let cipher = XChaCha20Poly1305::new(&key);
cipher.decrypt_in_place(&nonce.increment(), &[], data)?;
}
let cipher = XChaCha20Poly1305::new(&key);
cipher.decrypt_in_place(&nonce, &[], data)?;
Ok(())
}
/// Securely generates a random key and nonce.
#[must_use]
fn gen_key_nonce() -> (Key, Nonce) {
let mut rng = thread_rng();
let mut key: Key = GenericArray::default();
rng.fill(key.as_mut_slice());
let mut nonce = Nonce::default();
rng.fill(nonce.as_mut_slice());
(key, nonce)
}
// Type alias; to ensure that we're consistent on what the inner impl is.
type NonceImpl = XNonce;
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
struct Nonce(NonceImpl);
impl Default for Nonce {
fn default() -> Self {
Self(GenericArray::default())
}
}
impl Deref for Nonce {
type Target = NonceImpl;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl DerefMut for Nonce {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.0
}
}
impl AsRef<[u8]> for Nonce {
fn as_ref(&self) -> &[u8] {
self.0.as_ref()
}
}
impl Nonce {
const SIZE: usize = <NonceImpl as GenericSequence<_>>::Length::USIZE;
#[must_use]
pub fn increment(&self) -> Self {
let mut inner = self.0;
inner.as_mut_slice()[0] += 1;
Self(inner)
}
#[must_use]
pub fn from_slice(slice: &[u8]) -> Self {
Self(*NonceImpl::from_slice(slice))
}
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
struct Salt([u8; Salt::SIZE]);
impl Salt {
const SIZE: usize = argon2::password_hash::Salt::RECOMMENDED_LENGTH;
fn random() -> Self {
let mut salt = [0u8; Salt::SIZE];
thread_rng().fill(&mut salt);
Self(salt)
}
}
impl AsRef<[u8]> for Salt {
fn as_ref(&self) -> &[u8] {
self.0.as_ref()
}
}
/// Hashes an input to output a usable key.
fn kdf(password: &str) -> Result<(Key, Salt), argon2::Error> {
let salt = Salt::random();
let hasher = Argon2::default();
let mut key = Key::default();
hasher.hash_password_into(password.as_ref(), salt.as_ref(), &mut key)?;
Ok((*Key::from_slice(&key), salt))
}

View file

@ -10,130 +10,25 @@ use chrono::{DateTime, Duration, Utc};
use headers::{Header, HeaderName, HeaderValue};
use lazy_static::lazy_static;
use serde::{Deserialize, Serialize};
use sha2::{Digest, Sha256};
use thiserror::Error;
pub use url::Url;
use crate::crypto::{Key, Nonce};
use crate::crypto::Key;
pub mod base64;
pub mod crypto;
pub const API_ENDPOINT: &str = "/api";
pub mod base64 {
/// URL-safe Base64 encoding.
pub fn encode(input: impl AsRef<[u8]>) -> String {
base64::encode_config(input, base64::URL_SAFE)
}
/// URL-safe Base64 decoding.
pub fn decode(input: impl AsRef<[u8]>) -> Result<Vec<u8>, base64::DecodeError> {
base64::decode_config(input, base64::URL_SAFE)
}
}
/// Hashes an input to output a usable key.
pub fn hash(data: impl AsRef<[u8]>) -> crypto::Key {
let mut hasher = Sha256::new();
hasher.update(data);
hasher.finalize()
}
pub mod crypto {
use std::ops::{Deref, DerefMut};
use chacha20poly1305::aead::generic_array::GenericArray;
use chacha20poly1305::aead::{Aead, AeadInPlace, Buffer, Error, NewAead};
use chacha20poly1305::XChaCha20Poly1305;
use chacha20poly1305::XNonce;
use rand::{thread_rng, Rng};
pub use chacha20poly1305::Key;
/// Securely generates a random key and nonce.
#[must_use]
pub fn gen_key_nonce() -> (Key, Nonce) {
let mut rng = thread_rng();
let mut key: Key = GenericArray::default();
rng.fill(key.as_mut_slice());
let mut nonce = Nonce::default();
rng.fill(nonce.as_mut_slice());
(key, nonce)
}
pub fn seal(plaintext: &[u8], nonce: &Nonce, key: &Key) -> Result<Vec<u8>, Error> {
let cipher = XChaCha20Poly1305::new(key);
cipher.encrypt(nonce, plaintext)
}
pub fn seal_in_place(buffer: &mut impl Buffer, nonce: &Nonce, key: &Key) -> Result<(), Error> {
let cipher = XChaCha20Poly1305::new(key);
cipher.encrypt_in_place(nonce, &[], buffer)
}
pub fn open(encrypted: &[u8], nonce: &Nonce, key: &Key) -> Result<Vec<u8>, Error> {
let cipher = XChaCha20Poly1305::new(key);
cipher.decrypt(nonce, encrypted)
}
pub fn open_in_place(buffer: &mut impl Buffer, nonce: &Nonce, key: &Key) -> Result<(), Error> {
let cipher = XChaCha20Poly1305::new(key);
cipher.decrypt_in_place(nonce, &[], buffer)
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub struct Nonce(XNonce);
impl Default for Nonce {
fn default() -> Self {
Self(GenericArray::default())
}
}
impl Deref for Nonce {
type Target = XNonce;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl DerefMut for Nonce {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.0
}
}
impl AsRef<[u8]> for Nonce {
fn as_ref(&self) -> &[u8] {
self.0.as_ref()
}
}
impl Nonce {
#[must_use]
pub fn increment(&self) -> Self {
let mut inner = self.0;
inner.as_mut_slice()[0] += 1;
Self(inner)
}
#[must_use]
pub fn from_slice(slice: &[u8]) -> Self {
Self(*XNonce::from_slice(slice))
}
}
}
pub struct ParsedUrl {
pub sanitized_url: Url,
pub decryption_key: Key,
pub nonce: Nonce,
pub needs_password: bool,
}
#[derive(Default)]
pub struct PartialParsedUrl {
pub decryption_key: Option<Key>,
pub nonce: Option<Nonce>,
pub needs_password: bool,
}
@ -150,7 +45,6 @@ impl From<&str> for PartialParsedUrl {
let mut decryption_key = None;
let mut needs_password = false;
let mut nonce = None;
for (key, value) in args {
match (key, value) {
@ -160,16 +54,12 @@ impl From<&str> for PartialParsedUrl {
("pw", _) => {
needs_password = true;
}
("nonce", Some(value)) => {
nonce = base64::decode(value).as_deref().map(Nonce::from_slice).ok();
}
_ => (),
}
}
Self {
decryption_key,
nonce,
needs_password,
}
}
@ -200,23 +90,19 @@ impl FromStr for ParsedUrl {
let PartialParsedUrl {
decryption_key,
needs_password,
nonce,
} = PartialParsedUrl::from(fragment);
url.set_fragment(None);
let (decryption_key, nonce) = match (&decryption_key, nonce) {
(None, None) => Err(ParseUrlError::NeedKeyAndNonce),
(None, Some(_)) => Err(ParseUrlError::NeedKey),
(Some(_), None) => Err(ParseUrlError::NeedNonce),
(Some(k), Some(v)) => Ok((*k, v)),
let decryption_key = match &decryption_key {
Some(k) => Ok(*k),
None => Err(ParseUrlError::NeedKey),
}?;
Ok(Self {
sanitized_url: url,
decryption_key,
needs_password,
nonce,
})
}
}

View file

@ -4,7 +4,7 @@ use std::sync::Arc;
use gloo_console::log;
use js_sys::{Array, Uint8Array};
use omegaupload_common::crypto::{open_in_place, Key, Nonce};
use omegaupload_common::crypto::{open_in_place, Key};
use serde::Serialize;
use wasm_bindgen::JsCast;
use web_sys::{Blob, BlobPropertyBag};
@ -36,31 +36,10 @@ fn now() -> f64 {
pub fn decrypt(
mut container: Vec<u8>,
key: Key,
nonce: Nonce,
maybe_password: Option<Key>,
maybe_password: Option<&str>,
) -> Result<DecryptedData, PasteCompleteConstructionError> {
log!("Stage 1 decryption started.");
let start = now();
if let Some(password) = maybe_password {
crate::render_message("Decrypting Stage 1...".into());
open_in_place(&mut container, &nonce.increment(), &password).map_err(|_| {
crate::render_message("Unable to decrypt paste with the provided password.".into());
PasteCompleteConstructionError::StageOneFailure
})?;
}
log!(format!("Stage 1 completed in {}ms", now() - start));
log!("Stage 2 decryption started.");
let start = now();
crate::render_message("Decrypting Stage 2...".into());
open_in_place(&mut container, &nonce, &key).map_err(|_| {
crate::render_message(
"Unable to decrypt paste with the provided encryption key and nonce.".into(),
);
PasteCompleteConstructionError::StageTwoFailure
})?;
log!(format!("Stage 2 completed in {}ms", now() - start));
open_in_place(&mut container, &key, maybe_password)
.map_err(|_| PasteCompleteConstructionError::Decryption)?;
let mime_type = tree_magic_mini::from_u8(&container);
log!("Mimetype: ", mime_type);
@ -79,6 +58,7 @@ pub fn decrypt(
Blob::new_with_u8_array_sequence_and_options(blob_chunks.dyn_ref().unwrap(), &blob_props)
.unwrap(),
);
log!(format!("Blob conversion completed in {}ms", now() - start));
if mime_type.starts_with("text/") {
@ -125,8 +105,7 @@ pub fn decrypt(
#[derive(Debug)]
pub enum PasteCompleteConstructionError {
StageOneFailure,
StageTwoFailure,
Decryption,
InvalidEncoding,
}
@ -135,11 +114,8 @@ impl std::error::Error for PasteCompleteConstructionError {}
impl Display for PasteCompleteConstructionError {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self {
PasteCompleteConstructionError::StageOneFailure => {
write!(f, "Failed to decrypt stage one.")
}
PasteCompleteConstructionError::StageTwoFailure => {
write!(f, "Failed to decrypt stage two.")
PasteCompleteConstructionError::Decryption => {
write!(f, "Failed to decrypt data.")
}
PasteCompleteConstructionError::InvalidEncoding => write!(
f,

View file

@ -8,9 +8,9 @@ use decrypt::DecryptedData;
use gloo_console::{error, log};
use http::uri::PathAndQuery;
use http::{StatusCode, Uri};
use js_sys::{JsString, Object, Uint8Array, Array};
use omegaupload_common::crypto::{Key, Nonce};
use omegaupload_common::{hash, Expiration, PartialParsedUrl};
use js_sys::{Array, JsString, Object, Uint8Array};
use omegaupload_common::crypto::Key;
use omegaupload_common::{Expiration, PartialParsedUrl};
use reqwasm::http::Request;
use wasm_bindgen::prelude::{wasm_bindgen, Closure};
use wasm_bindgen::{JsCast, JsValue};
@ -69,7 +69,7 @@ fn main() {
log!(&url);
log!(&request_uri.to_string());
log!(&location().pathname().unwrap());
let (key, nonce, needs_pw) = {
let (key, needs_pw) = {
let partial_parsed_url = url
.split_once('#')
.map(|(_, fragment)| PartialParsedUrl::from(fragment))
@ -81,14 +81,7 @@ fn main() {
render_message("Invalid paste link: Missing decryption key.".into());
return;
};
let nonce = if let Some(nonce) = partial_parsed_url.nonce {
nonce
} else {
error!("Nonce is missing in url; bailing.");
render_message("Invalid paste link: Missing nonce.".into());
return;
};
(key, nonce, partial_parsed_url.needs_password)
(key, partial_parsed_url.needs_password)
};
let password = if needs_pw {
@ -97,7 +90,7 @@ fn main() {
if let Ok(Some(password)) = pw {
if !password.is_empty() {
break Some(hash(password));
break Some(password);
}
}
}
@ -108,7 +101,7 @@ fn main() {
if location().pathname().unwrap() == "/" {
} else {
spawn_local(async move {
if let Err(e) = fetch_resources(request_uri, key, nonce, password).await {
if let Err(e) = fetch_resources(request_uri, key, password.as_deref()).await {
log!(e.to_string());
}
});
@ -116,12 +109,7 @@ fn main() {
}
#[allow(clippy::future_not_send)]
async fn fetch_resources(
request_uri: Uri,
key: Key,
nonce: Nonce,
password: Option<Key>,
) -> Result<()> {
async fn fetch_resources(request_uri: Uri, key: Key, password: Option<&str>) -> Result<()> {
match Request::get(&request_uri.to_string()).send().await {
Ok(resp) if resp.status() == StatusCode::OK => {
let expires = Expiration::try_from(resp.headers()).map_or_else(
@ -154,7 +142,7 @@ async fn fetch_resources(
return Ok(());
}
let decrypted = decrypt(data, key, nonce, password)?;
let decrypted = decrypt(data, key, password)?;
let db_open_req = open_idb()?;
// On success callback
@ -197,9 +185,12 @@ async fn fetch_resources(
.data(blob)
.extra(
"entries",
JsValue::from(entries.into_iter()
JsValue::from(
entries
.into_iter()
.filter_map(|x| JsValue::from_serde(x).ok())
.collect::<Array>())
.collect::<Array>(),
),
),
};