mirror of
https://github.com/spacedriveapp/spacedrive.git
synced 2026-05-06 06:13:22 -04:00
[ENG-319] Balloon hashing (#489)
* add key attribute to `Key` in `ListOfKeys` * add balloon hashing function with untailored parameters * add ser/de rules for blake3-balloon (and change argon2id's) * fix benchmark * use `to_bytes`, `from_bytes` and `from_reader` * cleanup code * add blake3-balloon options to the UI and fix library sync/automount enable bug * cleanup some serialization code * fix hashing algorithm deserialization * clean up header serialization + more idiomatic master key decryption * clippy * add generic ser/de error to crypto crate * fix `Display` and crypto cli * move crypto cli to cli app Co-authored-by: Brendan Allan <brendonovich@outlook.com>
This commit is contained in:
BIN
Cargo.lock
generated
BIN
Cargo.lock
generated
Binary file not shown.
@@ -6,6 +6,7 @@ members = [
|
||||
# "crates/p2p/tunnel",
|
||||
# "crates/p2p/tunnel/utils",
|
||||
"crates/sync/example/api",
|
||||
"apps/cli",
|
||||
"apps/desktop/src-tauri",
|
||||
"apps/mobile/rust",
|
||||
"apps/server",
|
||||
|
||||
13
apps/cli/Cargo.toml
Normal file
13
apps/cli/Cargo.toml
Normal file
@@ -0,0 +1,13 @@
|
||||
[package]
|
||||
name = "cli"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
indoc = "1.0.8"
|
||||
clap = { version = "4.0.32", features = ["derive"] }
|
||||
anyhow = "1.0.68"
|
||||
hex = "0.4.3"
|
||||
sd-crypto = { path = "../../crates/crypto" }
|
||||
4
apps/cli/README.md
Normal file
4
apps/cli/README.md
Normal file
@@ -0,0 +1,4 @@
|
||||
# CLI
|
||||
|
||||
Basic CLI for interacting with encrypted files.
|
||||
Will be expanded to a general Spacedrive CLI in the future.
|
||||
83
apps/cli/src/main.rs
Normal file
83
apps/cli/src/main.rs
Normal file
@@ -0,0 +1,83 @@
|
||||
use anyhow::{Context, Result};
|
||||
use clap::Parser;
|
||||
use indoc::printdoc;
|
||||
use sd_crypto::header::file::FileHeader;
|
||||
use std::{fs::File, path::PathBuf};
|
||||
|
||||
#[derive(Parser)]
|
||||
struct Args {
|
||||
#[arg(help = "the file path to get details for")]
|
||||
path: PathBuf,
|
||||
}
|
||||
|
||||
fn main() -> Result<()> {
|
||||
let args = Args::parse();
|
||||
|
||||
let mut reader = File::open(args.path).context("unable to open file")?;
|
||||
let (header, aad) = FileHeader::from_reader(&mut reader)?;
|
||||
print_details(&header, &aad);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn print_details(header: &FileHeader, aad: &[u8]) {
|
||||
printdoc! {"
|
||||
Header version: {version}
|
||||
Encryption algorithm: {algorithm}
|
||||
AAD (hex): {hex}
|
||||
",
|
||||
version = header.version,
|
||||
algorithm = header.algorithm,
|
||||
hex = hex::encode(aad)
|
||||
};
|
||||
|
||||
header.keyslots.iter().enumerate().for_each(|(i, k)| {
|
||||
printdoc! {"
|
||||
Keyslot {index}:
|
||||
Version: {version}
|
||||
Algorithm: {algorithm}
|
||||
Hashing algorithm: {hashing_algorithm}
|
||||
Salt (hex): {salt}
|
||||
Master Key (hex, encrypted): {master}
|
||||
Master key nonce (hex): {nonce}
|
||||
",
|
||||
index = i + i,
|
||||
version = k.version,
|
||||
algorithm = k.algorithm,
|
||||
hashing_algorithm = k.hashing_algorithm,
|
||||
salt = hex::encode(k.salt),
|
||||
master = hex::encode(k.master_key),
|
||||
nonce = hex::encode(k.nonce.clone())
|
||||
};
|
||||
});
|
||||
|
||||
header.metadata.iter().for_each(|m| {
|
||||
printdoc! {"
|
||||
Metadata:
|
||||
Version: {version}
|
||||
Algorithm: {algorithm}
|
||||
Encrypted size: {size}
|
||||
Nonce (hex): {nonce}
|
||||
",
|
||||
version = m.version,
|
||||
algorithm = m.algorithm,
|
||||
size = m.metadata.len(),
|
||||
nonce = hex::encode(m.metadata_nonce.clone())
|
||||
}
|
||||
});
|
||||
|
||||
header.preview_media.iter().for_each(|p| {
|
||||
printdoc! {"
|
||||
Preview Media:
|
||||
Version: {version}
|
||||
Algorithm: {algorithm}
|
||||
Encrypted size: {size}
|
||||
Nonce (hex): {nonce}
|
||||
",
|
||||
version = p.version,
|
||||
algorithm = p.algorithm,
|
||||
size = p.media.len(),
|
||||
nonce = hex::encode(p.media_nonce.clone())
|
||||
};
|
||||
});
|
||||
}
|
||||
@@ -76,6 +76,7 @@ pub async fn create_keymanager(
|
||||
client: &PrismaClient,
|
||||
) -> Result<Arc<KeyManager>, LibraryManagerError> {
|
||||
let key_manager = KeyManager::new(vec![])?;
|
||||
|
||||
let mut default: Option<Uuid> = None;
|
||||
|
||||
// collect and serialize the stored keys
|
||||
@@ -97,13 +98,13 @@ pub async fn create_keymanager(
|
||||
|
||||
let stored_key = StoredKey {
|
||||
uuid,
|
||||
algorithm: Algorithm::deserialize(to_array(key.algorithm)?)?,
|
||||
algorithm: Algorithm::from_bytes(to_array(key.algorithm)?)?,
|
||||
content_salt: to_array(key.content_salt)?,
|
||||
master_key: to_array(key.master_key)?,
|
||||
master_key_nonce: key.master_key_nonce,
|
||||
key_nonce: key.key_nonce,
|
||||
key: key.key,
|
||||
hashing_algorithm: HashingAlgorithm::deserialize(to_array(key.hashing_algorithm)?)?,
|
||||
hashing_algorithm: HashingAlgorithm::from_bytes(to_array(key.hashing_algorithm)?)?,
|
||||
salt: to_array(key.salt)?,
|
||||
memory_only: false,
|
||||
automount: key.automount,
|
||||
|
||||
@@ -118,7 +118,7 @@ impl StatefulJob for FileDecryptorJob {
|
||||
let mut reader = std::fs::File::open(step.obj_path.clone())?;
|
||||
let mut writer = std::fs::File::create(output_path)?;
|
||||
|
||||
let (header, aad) = FileHeader::deserialize(&mut reader)?;
|
||||
let (header, aad) = FileHeader::from_reader(&mut reader)?;
|
||||
|
||||
let master_key = if let Some(password) = state.init.password.clone() {
|
||||
if let Some(save_to_library) = state.init.save_to_library {
|
||||
|
||||
@@ -171,7 +171,7 @@ impl StatefulJob for FileEncryptorJob {
|
||||
user_key_details.hashing_algorithm,
|
||||
user_key_details.content_salt,
|
||||
user_key,
|
||||
&master_key,
|
||||
master_key.clone(),
|
||||
)?];
|
||||
|
||||
let mut header =
|
||||
@@ -203,7 +203,7 @@ impl StatefulJob for FileEncryptorJob {
|
||||
header.add_metadata(
|
||||
LATEST_METADATA,
|
||||
state.init.algorithm,
|
||||
&master_key,
|
||||
master_key.clone(),
|
||||
&metadata,
|
||||
)?;
|
||||
}
|
||||
|
||||
@@ -50,8 +50,8 @@ pub async fn write_storedkey_to_db(db: &PrismaClient, key: &StoredKey) -> Result
|
||||
db.key()
|
||||
.create(
|
||||
key.uuid.to_string(),
|
||||
key.algorithm.serialize().to_vec(),
|
||||
key.hashing_algorithm.serialize().to_vec(),
|
||||
key.algorithm.to_bytes().to_vec(),
|
||||
key.hashing_algorithm.to_bytes().to_vec(),
|
||||
key.content_salt.to_vec(),
|
||||
key.master_key.to_vec(),
|
||||
key.master_key_nonce.to_vec(),
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
[package]
|
||||
name = "crypto-cli"
|
||||
version = "0.0.0"
|
||||
edition = "2021"
|
||||
authors = ["Jake Robinson <jake@spacedrive.com>"]
|
||||
description = "A CLI tool to view encrypted file details (for files encrypted with Spacedrive)"
|
||||
rust-version = "1.64.0"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
clap = { version = "4.0.32", features = ["derive"] }
|
||||
sd-crypto = { path = "../crypto", features = ["serde"] }
|
||||
anyhow = "1.0.68"
|
||||
hex = "0.4.3"
|
||||
@@ -1,53 +0,0 @@
|
||||
use anyhow::{Context, Result};
|
||||
use clap::Parser;
|
||||
use sd_crypto::header::file::FileHeader;
|
||||
use std::{fs::File, path::PathBuf};
|
||||
|
||||
#[derive(Parser)]
|
||||
struct Args {
|
||||
#[arg(help = "the file path to get details for")]
|
||||
path: PathBuf,
|
||||
}
|
||||
|
||||
fn main() -> Result<()> {
|
||||
let args = Args::parse();
|
||||
|
||||
let mut reader = File::open(args.path).context("unable to open file")?;
|
||||
let (header, aad) = FileHeader::deserialize(&mut reader)?;
|
||||
print_details(&header, &aad)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn print_details(header: &FileHeader, aad: &[u8]) -> Result<()> {
|
||||
println!("Header version: {}", header.version);
|
||||
println!("Encryption algorithm: {}", header.algorithm);
|
||||
println!("AAD (hex): {}", hex::encode(aad));
|
||||
header.keyslots.iter().enumerate().for_each(|(i, k)| {
|
||||
println!("Keyslot {}:", i + 1);
|
||||
println!(" Version: {}", k.version);
|
||||
println!(" Algorithm: {}", k.algorithm);
|
||||
println!(" Hashing algorithm: {}", k.hashing_algorithm);
|
||||
println!(" Salt (hex): {}", hex::encode(k.salt));
|
||||
println!(" Master Key (hex, encrypted): {}", hex::encode(k.master_key));
|
||||
println!(" Master key nonce (hex): {}", hex::encode(k.nonce.clone()));
|
||||
});
|
||||
|
||||
header.metadata.clone().iter().for_each(|m| {
|
||||
println!("Metadata:");
|
||||
println!(" Version: {}", m.version);
|
||||
println!(" Algorithm: {}", m.algorithm);
|
||||
println!(" Encrypted size: {}", m.metadata.len());
|
||||
println!(" Nonce (hex): {}", hex::encode(m.metadata_nonce.clone()));
|
||||
});
|
||||
|
||||
header.preview_media.clone().iter().for_each(|p| {
|
||||
println!("Preview Media:");
|
||||
println!(" Version: {}", p.version);
|
||||
println!(" Algorithm: {}", p.algorithm);
|
||||
println!(" Encrypted size: {}", p.media.len());
|
||||
println!(" Nonce (hex): {}", hex::encode(p.media_nonce.clone()))
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -14,7 +14,8 @@ rand_chacha = "0.3.1"
|
||||
|
||||
# hashing
|
||||
argon2 = "0.4.1"
|
||||
blake3 = "1.3.3"
|
||||
balloon-hash = "0.3.0"
|
||||
blake3 = { version = "1.3.3", features = ["traits-preview"] }
|
||||
|
||||
# aeads
|
||||
aes-gcm = "0.10.1"
|
||||
|
||||
@@ -15,16 +15,13 @@ fn bench(c: &mut Criterion) {
|
||||
let salt = generate_salt();
|
||||
let hashing_algorithm = HashingAlgorithm::Argon2id(param);
|
||||
|
||||
group.bench_function(
|
||||
BenchmarkId::new("hash", param.get_argon2_params().m_cost()),
|
||||
|b| {
|
||||
b.iter_batched(
|
||||
|| (key.clone(), salt.clone()),
|
||||
|(key, salt)| hashing_algorithm.hash(key, salt),
|
||||
BatchSize::SmallInput,
|
||||
)
|
||||
},
|
||||
);
|
||||
group.bench_function(BenchmarkId::new("hash", param.argon2id().m_cost()), |b| {
|
||||
b.iter_batched(
|
||||
|| (key.clone(), salt.clone()),
|
||||
|(key, salt)| hashing_algorithm.hash(key, salt),
|
||||
BatchSize::SmallInput,
|
||||
)
|
||||
});
|
||||
}
|
||||
|
||||
group.finish();
|
||||
|
||||
@@ -32,7 +32,7 @@ pub fn encrypt() {
|
||||
HASHING_ALGORITHM,
|
||||
content_salt,
|
||||
hashed_password,
|
||||
&master_key,
|
||||
master_key.clone(),
|
||||
)
|
||||
.unwrap()];
|
||||
|
||||
@@ -60,7 +60,7 @@ pub fn decrypt() {
|
||||
let mut writer = File::create("test.original").unwrap();
|
||||
|
||||
// Deserialize the header, keyslots, etc from the encrypted file
|
||||
let (header, aad) = FileHeader::deserialize(&mut reader).unwrap();
|
||||
let (header, aad) = FileHeader::from_reader(&mut reader).unwrap();
|
||||
|
||||
// Decrypt the master key with the user's password
|
||||
let master_key = header.decrypt_master_key(password).unwrap();
|
||||
|
||||
@@ -41,7 +41,7 @@ fn encrypt() {
|
||||
HASHING_ALGORITHM,
|
||||
content_salt,
|
||||
hashed_password,
|
||||
&master_key,
|
||||
master_key.clone(),
|
||||
)
|
||||
.unwrap()];
|
||||
|
||||
@@ -52,7 +52,7 @@ fn encrypt() {
|
||||
.add_metadata(
|
||||
MetadataVersion::V1,
|
||||
ALGORITHM,
|
||||
&master_key,
|
||||
master_key.clone(),
|
||||
&embedded_metadata,
|
||||
)
|
||||
.unwrap();
|
||||
@@ -77,7 +77,7 @@ pub fn decrypt_metadata() {
|
||||
let mut reader = File::open("test.encrypted").unwrap();
|
||||
|
||||
// Deserialize the header, keyslots, etc from the encrypted file
|
||||
let (header, _) = FileHeader::deserialize(&mut reader).unwrap();
|
||||
let (header, _) = FileHeader::from_reader(&mut reader).unwrap();
|
||||
|
||||
// Decrypt the metadata
|
||||
let file_info: FileInformation = header.decrypt_metadata(password).unwrap();
|
||||
|
||||
@@ -32,7 +32,7 @@ fn encrypt() {
|
||||
HASHING_ALGORITHM,
|
||||
content_salt,
|
||||
hashed_password,
|
||||
&master_key,
|
||||
master_key.clone(),
|
||||
)
|
||||
.unwrap()];
|
||||
|
||||
@@ -42,7 +42,12 @@ fn encrypt() {
|
||||
let mut header = FileHeader::new(LATEST_FILE_HEADER, ALGORITHM, keyslots);
|
||||
|
||||
header
|
||||
.add_preview_media(PreviewMediaVersion::V1, ALGORITHM, &master_key, &pvm_media)
|
||||
.add_preview_media(
|
||||
PreviewMediaVersion::V1,
|
||||
ALGORITHM,
|
||||
master_key.clone(),
|
||||
&pvm_media,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
// Write the header to the file
|
||||
@@ -65,7 +70,7 @@ pub fn decrypt_preview_media() {
|
||||
let mut reader = File::open("test.encrypted").unwrap();
|
||||
|
||||
// Deserialize the header, keyslots, etc from the encrypted file
|
||||
let (header, _) = FileHeader::deserialize(&mut reader).unwrap();
|
||||
let (header, _) = FileHeader::from_reader(&mut reader).unwrap();
|
||||
|
||||
// Decrypt the preview media
|
||||
let media = header.decrypt_preview_media(password).unwrap();
|
||||
|
||||
@@ -18,7 +18,7 @@ pub type Result<T> = std::result::Result<T, Error>;
|
||||
pub enum Error {
|
||||
#[error("not enough bytes were written to the output file")]
|
||||
WriteMismatch,
|
||||
#[error("there was an error hashing the password")]
|
||||
#[error("there was an error while password hashing")]
|
||||
PasswordHash,
|
||||
#[error("I/O error: {0}")]
|
||||
Io(#[from] std::io::Error),
|
||||
@@ -42,8 +42,8 @@ pub enum Error {
|
||||
MediaLengthParse,
|
||||
#[error("no preview media found")]
|
||||
NoPreviewMedia,
|
||||
#[error("error while serializing/deserializing the metadata")]
|
||||
MetadataDeSerialization,
|
||||
#[error("error while serializing/deserializing an item")]
|
||||
Serialization,
|
||||
#[error("no metadata found")]
|
||||
NoMetadata,
|
||||
#[error("tried adding too many keyslots to a header")]
|
||||
|
||||
@@ -29,7 +29,7 @@
|
||||
//! // Write the header to the file
|
||||
//! header.write(&mut writer).unwrap();
|
||||
//! ```
|
||||
use std::io::{Cursor, Read, Seek, SeekFrom, Write};
|
||||
use std::io::{Read, Seek, SeekFrom, Write};
|
||||
|
||||
use crate::{
|
||||
crypto::stream::Algorithm,
|
||||
@@ -110,22 +110,15 @@ impl FileHeader {
|
||||
&self,
|
||||
password: Protected<Vec<u8>>,
|
||||
) -> Result<Protected<[u8; KEY_LEN]>> {
|
||||
let mut master_key: Option<Protected<[u8; KEY_LEN]>> = None;
|
||||
|
||||
if self.keyslots.is_empty() {
|
||||
return Err(Error::NoKeyslots);
|
||||
}
|
||||
|
||||
for keyslot in &self.keyslots {
|
||||
if let Ok(decrypted_master_key) = keyslot.decrypt_master_key(&password) {
|
||||
master_key = Some(Protected::new(to_array(
|
||||
decrypted_master_key.expose().clone(),
|
||||
)?));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
master_key.ok_or(Error::IncorrectPassword)
|
||||
self.keyslots
|
||||
.iter()
|
||||
.find_map(|v| v.decrypt_master_key(password.clone()).ok())
|
||||
.map(|v| Protected::new(to_array::<KEY_LEN>(v.expose().clone()).unwrap()))
|
||||
.ok_or(Error::IncorrectPassword)
|
||||
}
|
||||
|
||||
/// This is a helper function to find which keyslot a key belongs to.
|
||||
@@ -137,21 +130,19 @@ impl FileHeader {
|
||||
return Err(Error::NoKeyslots);
|
||||
}
|
||||
|
||||
for (i, keyslot) in self.keyslots.clone().iter().enumerate() {
|
||||
if keyslot.decrypt_master_key(&password).is_ok() {
|
||||
return Ok(i);
|
||||
}
|
||||
}
|
||||
|
||||
Err(Error::IncorrectPassword)
|
||||
self.keyslots
|
||||
.iter()
|
||||
.enumerate()
|
||||
.find_map(|(i, v)| v.decrypt_master_key(password.clone()).ok().map(|_| i))
|
||||
.ok_or(Error::IncorrectPassword)
|
||||
}
|
||||
|
||||
/// This is a helper function to serialize and write a header to a file.
|
||||
pub fn write<W>(&self, writer: &mut W) -> Result<()>
|
||||
where
|
||||
W: Write + Seek,
|
||||
W: Write,
|
||||
{
|
||||
writer.write_all(&self.serialize()?)?;
|
||||
writer.write_all(&self.to_bytes()?)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -165,26 +156,20 @@ impl FileHeader {
|
||||
&self,
|
||||
hashed_keys: Vec<Protected<[u8; KEY_LEN]>>,
|
||||
) -> Result<Protected<[u8; KEY_LEN]>> {
|
||||
let mut master_key: Option<Protected<[u8; KEY_LEN]>> = None;
|
||||
|
||||
if self.keyslots.is_empty() {
|
||||
return Err(Error::NoKeyslots);
|
||||
}
|
||||
|
||||
'full: for key in hashed_keys {
|
||||
for keyslot in &self.keyslots {
|
||||
if let Ok(decrypted_master_key) =
|
||||
keyslot.decrypt_master_key_from_prehashed(key.clone())
|
||||
{
|
||||
master_key = Some(Protected::new(to_array(
|
||||
decrypted_master_key.expose().clone(),
|
||||
)?));
|
||||
break 'full;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
master_key.ok_or(Error::IncorrectPassword)
|
||||
hashed_keys
|
||||
.iter()
|
||||
.find_map(|v| {
|
||||
self.keyslots.iter().find_map(|z| {
|
||||
z.decrypt_master_key_from_prehashed(v.clone())
|
||||
.ok()
|
||||
.map(|x| Protected::new(to_array::<KEY_LEN>(x.expose().clone()).unwrap()))
|
||||
})
|
||||
})
|
||||
.ok_or(Error::IncorrectPassword)
|
||||
}
|
||||
|
||||
/// This function should be used for generating AAD before encryption
|
||||
@@ -193,15 +178,17 @@ impl FileHeader {
|
||||
#[must_use]
|
||||
pub fn generate_aad(&self) -> Vec<u8> {
|
||||
match self.version {
|
||||
FileHeaderVersion::V1 => {
|
||||
let mut aad = Vec::new();
|
||||
aad.extend_from_slice(&MAGIC_BYTES); // 7
|
||||
aad.extend_from_slice(&self.version.serialize()); // 9
|
||||
aad.extend_from_slice(&self.algorithm.serialize()); // 11
|
||||
aad.extend_from_slice(&self.nonce); // 19 OR 31
|
||||
aad.extend_from_slice(&vec![0u8; 25 - self.nonce.len()]); // padded until 36 bytes
|
||||
aad
|
||||
}
|
||||
FileHeaderVersion::V1 => vec![
|
||||
MAGIC_BYTES.as_ref(),
|
||||
self.version.to_bytes().as_ref(),
|
||||
self.algorithm.to_bytes().as_ref(),
|
||||
self.nonce.as_ref(),
|
||||
&vec![0u8; 25 - self.nonce.len()],
|
||||
]
|
||||
.iter()
|
||||
.flat_map(|&v| v)
|
||||
.copied()
|
||||
.collect(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -210,7 +197,7 @@ impl FileHeader {
|
||||
/// This will include keyslots, metadata and preview media (if provided)
|
||||
///
|
||||
/// An error will be returned if there are no keyslots/more than two keyslots attached.
|
||||
pub fn serialize(&self) -> Result<Vec<u8>> {
|
||||
pub fn to_bytes(&self) -> Result<Vec<u8>> {
|
||||
match self.version {
|
||||
FileHeaderVersion::V1 => {
|
||||
if self.keyslots.len() > 2 {
|
||||
@@ -219,28 +206,35 @@ impl FileHeader {
|
||||
return Err(Error::NoKeyslots);
|
||||
}
|
||||
|
||||
let mut header = Vec::new();
|
||||
header.extend_from_slice(&MAGIC_BYTES); // 7
|
||||
header.extend_from_slice(&self.version.serialize()); // 9
|
||||
header.extend_from_slice(&self.algorithm.serialize()); // 11
|
||||
header.extend_from_slice(&self.nonce); // 19 OR 31
|
||||
header.extend_from_slice(&vec![0u8; 25 - self.nonce.len()]); // padded until 36 bytes
|
||||
let mut keyslots: Vec<Vec<u8>> =
|
||||
self.keyslots.iter().map(Keyslot::to_bytes).collect();
|
||||
|
||||
for keyslot in &self.keyslots {
|
||||
header.extend_from_slice(&keyslot.serialize());
|
||||
if keyslots.len() == 1 {
|
||||
keyslots.push(vec![0u8; KEYSLOT_SIZE]);
|
||||
}
|
||||
|
||||
for _ in 0..(2 - self.keyslots.len()) {
|
||||
header.extend_from_slice(&[0u8; KEYSLOT_SIZE]);
|
||||
}
|
||||
let metadata = self.metadata.clone().map_or(Vec::new(), |v| v.to_bytes());
|
||||
|
||||
if let Some(metadata) = self.metadata.clone() {
|
||||
header.extend_from_slice(&metadata.serialize());
|
||||
}
|
||||
let preview_media = self
|
||||
.preview_media
|
||||
.clone()
|
||||
.map_or(Vec::new(), |v| v.to_bytes());
|
||||
|
||||
if let Some(preview_media) = self.preview_media.clone() {
|
||||
header.extend_from_slice(&preview_media.serialize());
|
||||
}
|
||||
let header = vec![
|
||||
MAGIC_BYTES.as_ref(),
|
||||
&self.version.to_bytes(),
|
||||
&self.algorithm.to_bytes(),
|
||||
&self.nonce,
|
||||
&vec![0u8; 25 - self.nonce.len()],
|
||||
&keyslots[0],
|
||||
&keyslots[1],
|
||||
&metadata,
|
||||
&preview_media,
|
||||
]
|
||||
.iter()
|
||||
.flat_map(|&v| v)
|
||||
.copied()
|
||||
.collect();
|
||||
|
||||
Ok(header)
|
||||
}
|
||||
@@ -252,7 +246,7 @@ impl FileHeader {
|
||||
/// On error, the cursor will not be rewound.
|
||||
///
|
||||
/// It returns both the header, and the AAD that should be used for decryption.
|
||||
pub fn deserialize<R>(reader: &mut R) -> Result<(Self, Vec<u8>)>
|
||||
pub fn from_reader<R>(reader: &mut R) -> Result<(Self, Vec<u8>)>
|
||||
where
|
||||
R: Read + Seek,
|
||||
{
|
||||
@@ -266,7 +260,7 @@ impl FileHeader {
|
||||
let mut version = [0u8; 2];
|
||||
|
||||
reader.read_exact(&mut version)?;
|
||||
let version = FileHeaderVersion::deserialize(version)?;
|
||||
let version = FileHeaderVersion::from_bytes(version)?;
|
||||
|
||||
// Rewind so we can get the AAD
|
||||
reader.rewind()?;
|
||||
@@ -283,7 +277,7 @@ impl FileHeader {
|
||||
FileHeaderVersion::V1 => {
|
||||
let mut algorithm = [0u8; 2];
|
||||
reader.read_exact(&mut algorithm)?;
|
||||
let algorithm = Algorithm::deserialize(algorithm)?;
|
||||
let algorithm = Algorithm::from_bytes(algorithm)?;
|
||||
|
||||
let mut nonce = vec![0u8; algorithm.nonce_len()];
|
||||
reader.read_exact(&mut nonce)?;
|
||||
@@ -295,15 +289,14 @@ impl FileHeader {
|
||||
let mut keyslots: Vec<Keyslot> = Vec::new();
|
||||
|
||||
reader.read_exact(&mut keyslot_bytes)?;
|
||||
let mut keyslot_reader = Cursor::new(keyslot_bytes);
|
||||
|
||||
for _ in 0..2 {
|
||||
if let Ok(keyslot) = Keyslot::deserialize(&mut keyslot_reader) {
|
||||
if let Ok(keyslot) = Keyslot::from_reader(&mut keyslot_bytes.as_ref()) {
|
||||
keyslots.push(keyslot);
|
||||
}
|
||||
}
|
||||
|
||||
let metadata = if let Ok(metadata) = Metadata::deserialize(reader) {
|
||||
let metadata = if let Ok(metadata) = Metadata::from_reader(reader) {
|
||||
Some(metadata)
|
||||
} else {
|
||||
// header/aad area, keyslot area
|
||||
@@ -313,7 +306,7 @@ impl FileHeader {
|
||||
None
|
||||
};
|
||||
|
||||
let preview_media = if let Ok(preview_media) = PreviewMedia::deserialize(reader) {
|
||||
let preview_media = if let Ok(preview_media) = PreviewMedia::from_reader(reader) {
|
||||
Some(preview_media)
|
||||
} else if let Some(metadata) = metadata.clone() {
|
||||
reader.seek(SeekFrom::Start(
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
//!
|
||||
//! let keyslot = Keyslot::new(KeyslotVersion::V1, Algorithm::XChaCha20Poly1305, HashingAlgorithm::Argon2id(Params::Standard), user_password, &master_key).unwrap();
|
||||
//! ```
|
||||
use std::io::{Read, Seek};
|
||||
use std::io::Read;
|
||||
|
||||
use crate::{
|
||||
crypto::stream::{Algorithm, StreamDecryption, StreamEncryption},
|
||||
@@ -63,13 +63,14 @@ impl Keyslot {
|
||||
/// This handles generating the nonce and encrypting the master key.
|
||||
///
|
||||
/// You will need to provide the password, and a generated master key (this can't generate it, otherwise it can't be used elsewhere)
|
||||
#[allow(clippy::needless_pass_by_value)]
|
||||
pub fn new(
|
||||
version: KeyslotVersion,
|
||||
algorithm: Algorithm,
|
||||
hashing_algorithm: HashingAlgorithm,
|
||||
content_salt: [u8; SALT_LEN],
|
||||
hashed_key: Protected<[u8; KEY_LEN]>,
|
||||
master_key: &Protected<[u8; KEY_LEN]>,
|
||||
master_key: Protected<[u8; KEY_LEN]>,
|
||||
) -> Result<Self> {
|
||||
let nonce = generate_nonce(algorithm);
|
||||
|
||||
@@ -100,10 +101,11 @@ impl Keyslot {
|
||||
/// This attempts to decrypt the master key for a single keyslot
|
||||
///
|
||||
/// An error will be returned on failure.
|
||||
pub fn decrypt_master_key(&self, password: &Protected<Vec<u8>>) -> Result<Protected<Vec<u8>>> {
|
||||
#[allow(clippy::needless_pass_by_value)]
|
||||
pub fn decrypt_master_key(&self, password: Protected<Vec<u8>>) -> Result<Protected<Vec<u8>>> {
|
||||
let key = self
|
||||
.hashing_algorithm
|
||||
.hash(password.clone(), self.content_salt)
|
||||
.hash(password, self.content_salt)
|
||||
.map_err(|_| Error::PasswordHash)?;
|
||||
|
||||
let derived_key = derive_key(key, self.salt, FILE_KEY_CONTEXT);
|
||||
@@ -141,20 +143,22 @@ impl Keyslot {
|
||||
|
||||
/// This function is used to serialize a keyslot into bytes
|
||||
#[must_use]
|
||||
pub fn serialize(&self) -> Vec<u8> {
|
||||
pub fn to_bytes(&self) -> Vec<u8> {
|
||||
match self.version {
|
||||
KeyslotVersion::V1 => {
|
||||
let mut keyslot = Vec::new();
|
||||
keyslot.extend_from_slice(&self.version.serialize()); // 2
|
||||
keyslot.extend_from_slice(&self.algorithm.serialize()); // 4
|
||||
keyslot.extend_from_slice(&self.hashing_algorithm.serialize()); // 6
|
||||
keyslot.extend_from_slice(&self.salt); // 22
|
||||
keyslot.extend_from_slice(&self.content_salt); // 38
|
||||
keyslot.extend_from_slice(&self.master_key); // 86
|
||||
keyslot.extend_from_slice(&self.nonce); // 94 or 106
|
||||
keyslot.extend_from_slice(&vec![0u8; 26 - self.nonce.len()]); // 112 total bytes
|
||||
keyslot
|
||||
}
|
||||
KeyslotVersion::V1 => vec![
|
||||
self.version.to_bytes().as_ref(),
|
||||
self.algorithm.to_bytes().as_ref(),
|
||||
self.hashing_algorithm.to_bytes().as_ref(),
|
||||
&self.salt,
|
||||
&self.content_salt,
|
||||
&self.master_key,
|
||||
&self.nonce,
|
||||
&vec![0u8; 26 - self.nonce.len()],
|
||||
]
|
||||
.iter()
|
||||
.flat_map(|&v| v)
|
||||
.copied()
|
||||
.collect(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -163,23 +167,23 @@ impl Keyslot {
|
||||
/// It will leave the cursor at the end of the keyslot on success
|
||||
///
|
||||
/// The cursor will not be rewound on error.
|
||||
pub fn deserialize<R>(reader: &mut R) -> Result<Self>
|
||||
pub fn from_reader<R>(reader: &mut R) -> Result<Self>
|
||||
where
|
||||
R: Read + Seek,
|
||||
R: Read,
|
||||
{
|
||||
let mut version = [0u8; 2];
|
||||
reader.read_exact(&mut version)?;
|
||||
let version = KeyslotVersion::deserialize(version)?;
|
||||
let version = KeyslotVersion::from_bytes(version)?;
|
||||
|
||||
match version {
|
||||
KeyslotVersion::V1 => {
|
||||
let mut algorithm = [0u8; 2];
|
||||
reader.read_exact(&mut algorithm)?;
|
||||
let algorithm = Algorithm::deserialize(algorithm)?;
|
||||
let algorithm = Algorithm::from_bytes(algorithm)?;
|
||||
|
||||
let mut hashing_algorithm = [0u8; 2];
|
||||
reader.read_exact(&mut hashing_algorithm)?;
|
||||
let hashing_algorithm = HashingAlgorithm::deserialize(hashing_algorithm)?;
|
||||
let hashing_algorithm = HashingAlgorithm::from_bytes(hashing_algorithm)?;
|
||||
|
||||
let mut salt = [0u8; SALT_LEN];
|
||||
reader.read_exact(&mut salt)?;
|
||||
|
||||
@@ -27,7 +27,7 @@
|
||||
//! )
|
||||
//! .unwrap();
|
||||
//! ```
|
||||
use std::io::{Read, Seek};
|
||||
use std::io::Read;
|
||||
|
||||
#[cfg(feature = "serde")]
|
||||
use crate::{
|
||||
@@ -67,11 +67,12 @@ impl FileHeader {
|
||||
///
|
||||
/// Metadata needs to be accessed switfly, so a key management system should handle the salt generation.
|
||||
#[cfg(feature = "serde")]
|
||||
#[allow(clippy::needless_pass_by_value)]
|
||||
pub fn add_metadata<T>(
|
||||
&mut self,
|
||||
version: MetadataVersion,
|
||||
algorithm: Algorithm,
|
||||
master_key: &Protected<[u8; KEY_LEN]>,
|
||||
master_key: Protected<[u8; KEY_LEN]>,
|
||||
metadata: &T,
|
||||
) -> Result<()>
|
||||
where
|
||||
@@ -80,10 +81,10 @@ impl FileHeader {
|
||||
let metadata_nonce = generate_nonce(algorithm);
|
||||
|
||||
let encrypted_metadata = StreamEncryption::encrypt_bytes(
|
||||
master_key.clone(),
|
||||
master_key,
|
||||
&metadata_nonce,
|
||||
algorithm,
|
||||
&serde_json::to_vec(metadata).map_err(|_| Error::MetadataDeSerialization)?,
|
||||
&serde_json::to_vec(metadata).map_err(|_| Error::Serialization)?,
|
||||
&[],
|
||||
)?;
|
||||
|
||||
@@ -124,7 +125,7 @@ impl FileHeader {
|
||||
&[],
|
||||
)?;
|
||||
|
||||
serde_json::from_slice::<T>(&metadata).map_err(|_| Error::MetadataDeSerialization)
|
||||
serde_json::from_slice::<T>(&metadata).map_err(|_| Error::Serialization)
|
||||
} else {
|
||||
Err(Error::NoMetadata)
|
||||
}
|
||||
@@ -152,7 +153,7 @@ impl FileHeader {
|
||||
&[],
|
||||
)?;
|
||||
|
||||
serde_json::from_slice::<T>(&metadata).map_err(|_| Error::MetadataDeSerialization)
|
||||
serde_json::from_slice::<T>(&metadata).map_err(|_| Error::Serialization)
|
||||
} else {
|
||||
Err(Error::NoMetadata)
|
||||
}
|
||||
@@ -162,28 +163,27 @@ impl FileHeader {
|
||||
impl Metadata {
|
||||
#[must_use]
|
||||
pub fn size(&self) -> usize {
|
||||
self.serialize().len()
|
||||
self.to_bytes().len()
|
||||
}
|
||||
|
||||
/// This function is used to serialize a metadata item into bytes
|
||||
///
|
||||
/// This also includes the encrypted metadata itself, so this may be sizeable
|
||||
#[must_use]
|
||||
pub fn serialize(&self) -> Vec<u8> {
|
||||
pub fn to_bytes(&self) -> Vec<u8> {
|
||||
match self.version {
|
||||
MetadataVersion::V1 => {
|
||||
let mut metadata = Vec::new();
|
||||
metadata.extend_from_slice(&self.version.serialize()); // 2
|
||||
metadata.extend_from_slice(&self.algorithm.serialize()); // 4
|
||||
metadata.extend_from_slice(&self.metadata_nonce); // 24 max
|
||||
metadata.extend_from_slice(&vec![0u8; 24 - self.metadata_nonce.len()]); // 28
|
||||
|
||||
let metadata_len = self.metadata.len() as u64;
|
||||
|
||||
metadata.extend_from_slice(&metadata_len.to_le_bytes()); // 36 total bytes
|
||||
metadata.extend_from_slice(&self.metadata); // this can vary in length
|
||||
metadata
|
||||
}
|
||||
MetadataVersion::V1 => vec![
|
||||
self.version.to_bytes().as_ref(),
|
||||
self.algorithm.to_bytes().as_ref(),
|
||||
&self.metadata_nonce,
|
||||
&vec![0u8; 24 - self.metadata_nonce.len()],
|
||||
&(self.metadata.len() as u64).to_le_bytes(),
|
||||
&self.metadata,
|
||||
]
|
||||
.iter()
|
||||
.flat_map(|&v| v)
|
||||
.copied()
|
||||
.collect(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -192,19 +192,19 @@ impl Metadata {
|
||||
/// The cursor will be left at the end of the metadata item on success
|
||||
///
|
||||
/// The cursor will not be rewound on error.
|
||||
pub fn deserialize<R>(reader: &mut R) -> Result<Self>
|
||||
pub fn from_reader<R>(reader: &mut R) -> Result<Self>
|
||||
where
|
||||
R: Read + Seek,
|
||||
R: Read,
|
||||
{
|
||||
let mut version = [0u8; 2];
|
||||
reader.read_exact(&mut version)?;
|
||||
let version = MetadataVersion::deserialize(version).map_err(|_| Error::NoMetadata)?;
|
||||
let version = MetadataVersion::from_bytes(version).map_err(|_| Error::NoMetadata)?;
|
||||
|
||||
match version {
|
||||
MetadataVersion::V1 => {
|
||||
let mut algorithm = [0u8; 2];
|
||||
reader.read_exact(&mut algorithm)?;
|
||||
let algorithm = Algorithm::deserialize(algorithm)?;
|
||||
let algorithm = Algorithm::from_bytes(algorithm)?;
|
||||
|
||||
let mut metadata_nonce = vec![0u8; algorithm.nonce_len()];
|
||||
reader.read_exact(&mut metadata_nonce)?;
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
//! )
|
||||
//! .unwrap();
|
||||
//! ```
|
||||
use std::io::{Read, Seek};
|
||||
use std::io::Read;
|
||||
|
||||
use crate::{
|
||||
crypto::stream::{Algorithm, StreamDecryption, StreamEncryption},
|
||||
@@ -56,22 +56,18 @@ impl FileHeader {
|
||||
/// You will need to provide the user's password, and a semi-universal salt for hashing the user's password. This allows for extremely fast decryption.
|
||||
///
|
||||
/// Preview media needs to be accessed switfly, so a key management system should handle the salt generation.
|
||||
#[allow(clippy::needless_pass_by_value)]
|
||||
pub fn add_preview_media(
|
||||
&mut self,
|
||||
version: PreviewMediaVersion,
|
||||
algorithm: Algorithm,
|
||||
master_key: &Protected<[u8; KEY_LEN]>,
|
||||
master_key: Protected<[u8; KEY_LEN]>,
|
||||
media: &[u8],
|
||||
) -> Result<()> {
|
||||
let media_nonce = generate_nonce(algorithm);
|
||||
|
||||
let encrypted_media = StreamEncryption::encrypt_bytes(
|
||||
master_key.clone(),
|
||||
&media_nonce,
|
||||
algorithm,
|
||||
media,
|
||||
&[],
|
||||
)?;
|
||||
let encrypted_media =
|
||||
StreamEncryption::encrypt_bytes(master_key, &media_nonce, algorithm, media, &[])?;
|
||||
|
||||
let pvm = PreviewMedia {
|
||||
version,
|
||||
@@ -143,28 +139,27 @@ impl FileHeader {
|
||||
impl PreviewMedia {
|
||||
#[must_use]
|
||||
pub fn size(&self) -> usize {
|
||||
self.serialize().len()
|
||||
self.to_bytes().len()
|
||||
}
|
||||
|
||||
/// This function is used to serialize a preview media header item into bytes
|
||||
///
|
||||
/// This also includes the encrypted preview media itself, so this may be sizeable
|
||||
#[must_use]
|
||||
pub fn serialize(&self) -> Vec<u8> {
|
||||
pub fn to_bytes(&self) -> Vec<u8> {
|
||||
match self.version {
|
||||
PreviewMediaVersion::V1 => {
|
||||
let mut preview_media = Vec::new();
|
||||
preview_media.extend_from_slice(&self.version.serialize()); // 2
|
||||
preview_media.extend_from_slice(&self.algorithm.serialize()); // 4
|
||||
preview_media.extend_from_slice(&self.media_nonce); // 24 max
|
||||
preview_media.extend_from_slice(&vec![0u8; 24 - self.media_nonce.len()]); // 28 total bytes
|
||||
|
||||
let media_len = self.media.len() as u64;
|
||||
|
||||
preview_media.extend_from_slice(&media_len.to_le_bytes()); // 36 total bytes
|
||||
preview_media.extend_from_slice(&self.media); // this can vary in length
|
||||
preview_media
|
||||
}
|
||||
PreviewMediaVersion::V1 => vec![
|
||||
self.version.to_bytes().as_ref(),
|
||||
self.algorithm.to_bytes().as_ref(),
|
||||
&self.media_nonce,
|
||||
&vec![0u8; 24 - self.media_nonce.len()],
|
||||
&(self.media.len() as u64).to_le_bytes(),
|
||||
&self.media,
|
||||
]
|
||||
.iter()
|
||||
.flat_map(|&v| v)
|
||||
.copied()
|
||||
.collect(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -173,20 +168,20 @@ impl PreviewMedia {
|
||||
/// The cursor will be left at the end of the preview media item on success
|
||||
///
|
||||
/// The cursor will not be rewound on error.
|
||||
pub fn deserialize<R>(reader: &mut R) -> Result<Self>
|
||||
pub fn from_reader<R>(reader: &mut R) -> Result<Self>
|
||||
where
|
||||
R: Read + Seek,
|
||||
R: Read,
|
||||
{
|
||||
let mut version = [0u8; 2];
|
||||
reader.read_exact(&mut version)?;
|
||||
let version =
|
||||
PreviewMediaVersion::deserialize(version).map_err(|_| Error::NoPreviewMedia)?;
|
||||
PreviewMediaVersion::from_bytes(version).map_err(|_| Error::NoPreviewMedia)?;
|
||||
|
||||
match version {
|
||||
PreviewMediaVersion::V1 => {
|
||||
let mut algorithm = [0u8; 2];
|
||||
reader.read_exact(&mut algorithm)?;
|
||||
let algorithm = Algorithm::deserialize(algorithm)?;
|
||||
let algorithm = Algorithm::from_bytes(algorithm)?;
|
||||
|
||||
let mut media_nonce = vec![0u8; algorithm.nonce_len()];
|
||||
reader.read_exact(&mut media_nonce)?;
|
||||
|
||||
@@ -16,16 +16,16 @@ use super::{
|
||||
|
||||
impl FileHeaderVersion {
|
||||
#[must_use]
|
||||
pub const fn serialize(&self) -> [u8; 2] {
|
||||
pub const fn to_bytes(&self) -> [u8; 2] {
|
||||
match self {
|
||||
Self::V1 => [0x0A, 0x01],
|
||||
}
|
||||
}
|
||||
|
||||
pub const fn deserialize(bytes: [u8; 2]) -> Result<Self> {
|
||||
pub const fn from_bytes(bytes: [u8; 2]) -> Result<Self> {
|
||||
match bytes {
|
||||
[0x0A, 0x01] => Ok(Self::V1),
|
||||
_ => Err(Error::FileHeader),
|
||||
_ => Err(Error::Serialization),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -40,16 +40,16 @@ impl Display for FileHeaderVersion {
|
||||
|
||||
impl KeyslotVersion {
|
||||
#[must_use]
|
||||
pub const fn serialize(&self) -> [u8; 2] {
|
||||
pub const fn to_bytes(&self) -> [u8; 2] {
|
||||
match self {
|
||||
Self::V1 => [0x0D, 0x01],
|
||||
}
|
||||
}
|
||||
|
||||
pub const fn deserialize(bytes: [u8; 2]) -> Result<Self> {
|
||||
pub const fn from_bytes(bytes: [u8; 2]) -> Result<Self> {
|
||||
match bytes {
|
||||
[0x0D, 0x01] => Ok(Self::V1),
|
||||
_ => Err(Error::FileHeader),
|
||||
_ => Err(Error::Serialization),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -64,16 +64,16 @@ impl Display for KeyslotVersion {
|
||||
|
||||
impl PreviewMediaVersion {
|
||||
#[must_use]
|
||||
pub const fn serialize(&self) -> [u8; 2] {
|
||||
pub const fn to_bytes(&self) -> [u8; 2] {
|
||||
match self {
|
||||
Self::V1 => [0x0E, 0x01],
|
||||
}
|
||||
}
|
||||
|
||||
pub const fn deserialize(bytes: [u8; 2]) -> Result<Self> {
|
||||
pub const fn from_bytes(bytes: [u8; 2]) -> Result<Self> {
|
||||
match bytes {
|
||||
[0x0E, 0x01] => Ok(Self::V1),
|
||||
_ => Err(Error::FileHeader),
|
||||
_ => Err(Error::Serialization),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -88,16 +88,16 @@ impl Display for PreviewMediaVersion {
|
||||
|
||||
impl MetadataVersion {
|
||||
#[must_use]
|
||||
pub const fn serialize(&self) -> [u8; 2] {
|
||||
pub const fn to_bytes(&self) -> [u8; 2] {
|
||||
match self {
|
||||
Self::V1 => [0x1F, 0x01],
|
||||
}
|
||||
}
|
||||
|
||||
pub const fn deserialize(bytes: [u8; 2]) -> Result<Self> {
|
||||
pub const fn from_bytes(bytes: [u8; 2]) -> Result<Self> {
|
||||
match bytes {
|
||||
[0x1F, 0x01] => Ok(Self::V1),
|
||||
_ => Err(Error::FileHeader),
|
||||
_ => Err(Error::Serialization),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -112,22 +112,30 @@ impl Display for MetadataVersion {
|
||||
|
||||
impl HashingAlgorithm {
|
||||
#[must_use]
|
||||
pub const fn serialize(&self) -> [u8; 2] {
|
||||
pub const fn to_bytes(&self) -> [u8; 2] {
|
||||
match self {
|
||||
Self::Argon2id(p) => match p {
|
||||
Params::Standard => [0x0F, 0x01],
|
||||
Params::Hardened => [0x0F, 0x02],
|
||||
Params::Paranoid => [0x0F, 0x03],
|
||||
Params::Standard => [0xA2, 0x01],
|
||||
Params::Hardened => [0xA2, 0x02],
|
||||
Params::Paranoid => [0xA2, 0x03],
|
||||
},
|
||||
Self::BalloonBlake3(p) => match p {
|
||||
Params::Standard => [0xB3, 0x01],
|
||||
Params::Hardened => [0xB3, 0x02],
|
||||
Params::Paranoid => [0xB3, 0x03],
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
pub const fn deserialize(bytes: [u8; 2]) -> Result<Self> {
|
||||
pub const fn from_bytes(bytes: [u8; 2]) -> Result<Self> {
|
||||
match bytes {
|
||||
[0x0F, 0x01] => Ok(Self::Argon2id(Params::Standard)),
|
||||
[0x0F, 0x02] => Ok(Self::Argon2id(Params::Hardened)),
|
||||
[0x0F, 0x03] => Ok(Self::Argon2id(Params::Paranoid)),
|
||||
_ => Err(Error::FileHeader),
|
||||
[0xA2, 0x01] => Ok(Self::Argon2id(Params::Standard)),
|
||||
[0xA2, 0x02] => Ok(Self::Argon2id(Params::Hardened)),
|
||||
[0xA2, 0x03] => Ok(Self::Argon2id(Params::Paranoid)),
|
||||
[0xB3, 0x01] => Ok(Self::BalloonBlake3(Params::Standard)),
|
||||
[0xB3, 0x02] => Ok(Self::BalloonBlake3(Params::Hardened)),
|
||||
[0xB3, 0x03] => Ok(Self::BalloonBlake3(Params::Paranoid)),
|
||||
_ => Err(Error::Serialization),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -136,6 +144,7 @@ impl Display for HashingAlgorithm {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match *self {
|
||||
Self::Argon2id(p) => write!(f, "Argon2id ({})", p),
|
||||
Self::BalloonBlake3(p) => write!(f, "BLAKE3-Balloon ({})", p),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -152,18 +161,18 @@ impl Display for Params {
|
||||
|
||||
impl Algorithm {
|
||||
#[must_use]
|
||||
pub const fn serialize(&self) -> [u8; 2] {
|
||||
pub const fn to_bytes(&self) -> [u8; 2] {
|
||||
match self {
|
||||
Self::XChaCha20Poly1305 => [0x0B, 0x01],
|
||||
Self::Aes256Gcm => [0x0B, 0x02],
|
||||
}
|
||||
}
|
||||
|
||||
pub const fn deserialize(bytes: [u8; 2]) -> Result<Self> {
|
||||
pub const fn from_bytes(bytes: [u8; 2]) -> Result<Self> {
|
||||
match bytes {
|
||||
[0x0B, 0x01] => Ok(Self::XChaCha20Poly1305),
|
||||
[0x0B, 0x02] => Ok(Self::Aes256Gcm),
|
||||
_ => Err(Error::FileHeader),
|
||||
_ => Err(Error::Serialization),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ use crate::primitives::KEY_LEN;
|
||||
use crate::Protected;
|
||||
use crate::{primitives::SALT_LEN, Error, Result};
|
||||
use argon2::Argon2;
|
||||
use balloon_hash::Balloon;
|
||||
|
||||
/// These parameters define the password-hashing level.
|
||||
///
|
||||
@@ -42,6 +43,7 @@ pub enum Params {
|
||||
#[cfg_attr(feature = "rspc", derive(specta::Type))]
|
||||
pub enum HashingAlgorithm {
|
||||
Argon2id(Params),
|
||||
BalloonBlake3(Params),
|
||||
}
|
||||
|
||||
impl HashingAlgorithm {
|
||||
@@ -54,7 +56,8 @@ impl HashingAlgorithm {
|
||||
salt: [u8; SALT_LEN],
|
||||
) -> Result<Protected<[u8; KEY_LEN]>> {
|
||||
match self {
|
||||
Self::Argon2id(params) => password_hash_argon2id(password, salt, *params),
|
||||
Self::Argon2id(params) => PasswordHasher::argon2id(password, salt, *params),
|
||||
Self::BalloonBlake3(params) => PasswordHasher::balloon_blake3(password, salt, *params),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -64,51 +67,75 @@ impl Params {
|
||||
///
|
||||
/// This should not be called directly. Call it via the `HashingAlgorithm` struct (e.g. `HashingAlgorithm::Argon2id(Params::Standard).hash()`)
|
||||
#[must_use]
|
||||
pub fn get_argon2_params(&self) -> argon2::Params {
|
||||
pub fn argon2id(&self) -> argon2::Params {
|
||||
match self {
|
||||
// We can use `.unwrap()` here as the values are hardcoded, and this shouldn't error
|
||||
// The values are NOT final, as we need to find a good average.
|
||||
// It's very hardware dependant but we should aim for at least 64MB of RAM usage on standard
|
||||
// Provided they all take one (ish) second or longer, and less than 3/4 seconds (for paranoid), they will be fine
|
||||
// It's not so much the parameters themselves that matter, it's the duration (and ensuring that they use enough RAM to hinder ASIC brute-force attacks)
|
||||
Self::Standard => {
|
||||
argon2::Params::new(131_072, 8, 4, Some(argon2::Params::DEFAULT_OUTPUT_LEN))
|
||||
.unwrap()
|
||||
}
|
||||
Self::Paranoid => {
|
||||
argon2::Params::new(262_144, 8, 4, Some(argon2::Params::DEFAULT_OUTPUT_LEN))
|
||||
.unwrap()
|
||||
}
|
||||
Self::Hardened => {
|
||||
argon2::Params::new(524_288, 8, 4, Some(argon2::Params::DEFAULT_OUTPUT_LEN))
|
||||
.unwrap()
|
||||
}
|
||||
Self::Standard => argon2::Params::new(131_072, 8, 4, None).unwrap(),
|
||||
Self::Paranoid => argon2::Params::new(262_144, 8, 4, None).unwrap(),
|
||||
Self::Hardened => argon2::Params::new(524_288, 8, 4, None).unwrap(),
|
||||
}
|
||||
}
|
||||
|
||||
/// This function is used to generate parameters for password hashing.
|
||||
///
|
||||
/// This should not be called directly. Call it via the `HashingAlgorithm` struct (e.g. `HashingAlgorithm::Argon2id(Params::Standard).hash()`)
|
||||
#[must_use]
|
||||
pub fn balloon_blake3(&self) -> balloon_hash::Params {
|
||||
match self {
|
||||
// We can use `.unwrap()` here as the values are hardcoded, and this shouldn't error
|
||||
// The values are NOT final, as we need to find a good average.
|
||||
// It's very hardware dependant but we should aim for at least 64MB of RAM usage on standard
|
||||
// Provided they all take one (ish) second or longer, and less than 3/4 seconds (for paranoid), they will be fine
|
||||
// It's not so much the parameters themselves that matter, it's the duration (and ensuring that they use enough RAM to hinder ASIC brute-force attacks)
|
||||
Self::Standard => balloon_hash::Params::new(131_072, 1, 1).unwrap(),
|
||||
Self::Paranoid => balloon_hash::Params::new(262_144, 1, 1).unwrap(),
|
||||
Self::Hardened => balloon_hash::Params::new(524_288, 1, 1).unwrap(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// This function should NOT be called directly!
|
||||
///
|
||||
/// Call it via the `HashingAlgorithm` struct (e.g. `HashingAlgorithm::Argon2id(Params::Standard).hash()`)
|
||||
#[allow(clippy::needless_pass_by_value)]
|
||||
pub fn password_hash_argon2id(
|
||||
password: Protected<Vec<u8>>,
|
||||
salt: [u8; SALT_LEN],
|
||||
params: Params,
|
||||
) -> Result<Protected<[u8; KEY_LEN]>> {
|
||||
let mut key = [0u8; KEY_LEN];
|
||||
struct PasswordHasher;
|
||||
|
||||
let argon2 = Argon2::new(
|
||||
argon2::Algorithm::Argon2id,
|
||||
argon2::Version::V0x13,
|
||||
params.get_argon2_params(),
|
||||
);
|
||||
impl PasswordHasher {
|
||||
#[allow(clippy::needless_pass_by_value)]
|
||||
fn argon2id(
|
||||
password: Protected<Vec<u8>>,
|
||||
salt: [u8; SALT_LEN],
|
||||
params: Params,
|
||||
) -> Result<Protected<[u8; KEY_LEN]>> {
|
||||
let mut key = [0u8; KEY_LEN];
|
||||
|
||||
let result = argon2.hash_password_into(password.expose(), &salt, &mut key);
|
||||
let argon2 = Argon2::new(
|
||||
argon2::Algorithm::Argon2id,
|
||||
argon2::Version::V0x13,
|
||||
params.argon2id(),
|
||||
);
|
||||
|
||||
if result.is_ok() {
|
||||
Ok(Protected::new(key))
|
||||
} else {
|
||||
Err(Error::PasswordHash)
|
||||
argon2
|
||||
.hash_password_into(password.expose(), &salt, &mut key)
|
||||
.map_or(Err(Error::PasswordHash), |_| Ok(Protected::new(key)))
|
||||
}
|
||||
|
||||
#[allow(clippy::needless_pass_by_value)]
|
||||
fn balloon_blake3(
|
||||
password: Protected<Vec<u8>>,
|
||||
salt: [u8; SALT_LEN],
|
||||
params: Params,
|
||||
) -> Result<Protected<[u8; KEY_LEN]>> {
|
||||
let mut key = [0u8; KEY_LEN];
|
||||
|
||||
let balloon = Balloon::<blake3::Hasher>::new(
|
||||
balloon_hash::Algorithm::Balloon,
|
||||
params.balloon_blake3(),
|
||||
None,
|
||||
);
|
||||
|
||||
balloon
|
||||
.hash_into(password.expose(), &salt, &mut key)
|
||||
.map_or(Err(Error::PasswordHash), |_| Ok(Protected::new(key)))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -105,7 +105,7 @@ export interface GenerateThumbsForLocationArgs { id: number, path: string }
|
||||
|
||||
export interface GetArgs { id: number }
|
||||
|
||||
export type HashingAlgorithm = { Argon2id: Params }
|
||||
export type HashingAlgorithm = { Argon2id: Params } | { BalloonBlake3: Params }
|
||||
|
||||
export interface IdentifyUniqueFilesArgs { id: number, path: string }
|
||||
|
||||
|
||||
@@ -159,13 +159,16 @@ export const EncryptFileDialog = (props: EncryptDialogProps) => {
|
||||
<span className="text-xs font-bold">Hashing</span>
|
||||
<Select
|
||||
className="mt-2 text-gray-400/80"
|
||||
onChange={() => {}}
|
||||
disabled
|
||||
value={hashingAlgo}
|
||||
onChange={(e) => setHashingAlgo(e)}
|
||||
>
|
||||
<SelectOption value="Argon2id-s">Argon2id (standard)</SelectOption>
|
||||
<SelectOption value="Argon2id-h">Argon2id (hardened)</SelectOption>
|
||||
<SelectOption value="Argon2id-p">Argon2id (paranoid)</SelectOption>
|
||||
<SelectOption value="BalloonBlake3-s">Blake3-Balloon (standard)</SelectOption>
|
||||
<SelectOption value="BalloonBlake3-h">Blake3-Balloon (hardened)</SelectOption>
|
||||
<SelectOption value="BalloonBlake3-p">Blake3-Balloon (paranoid)</SelectOption>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -105,6 +105,9 @@ export const KeyViewerDialog = (props: KeyViewerDialogProps) => {
|
||||
<SelectOption value="Argon2id-s">Argon2id (standard)</SelectOption>
|
||||
<SelectOption value="Argon2id-h">Argon2id (hardened)</SelectOption>
|
||||
<SelectOption value="Argon2id-p">Argon2id (paranoid)</SelectOption>
|
||||
<SelectOption value="BalloonBlake3-s">Blake3-Balloon (standard)</SelectOption>
|
||||
<SelectOption value="BalloonBlake3-h">Blake3-Balloon (hardened)</SelectOption>
|
||||
<SelectOption value="BalloonBlake3-p">Blake3-Balloon (paranoid)</SelectOption>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -42,6 +42,7 @@ export const ListOfKeys = () => {
|
||||
return (
|
||||
<Key
|
||||
index={index}
|
||||
key={key.uuid}
|
||||
data={{
|
||||
id: key.uuid,
|
||||
name: `Key ${key.uuid.substring(0, 8).toUpperCase()}`,
|
||||
|
||||
@@ -90,7 +90,7 @@ export function KeyMounter() {
|
||||
size="sm"
|
||||
checked={librarySync}
|
||||
onCheckedChange={(e) => {
|
||||
if (autoMount && e) setAutoMount(false);
|
||||
if (autoMount && !e) setAutoMount(false);
|
||||
setLibrarySync(e);
|
||||
}}
|
||||
/>
|
||||
@@ -106,7 +106,7 @@ export function KeyMounter() {
|
||||
size="sm"
|
||||
checked={autoMount}
|
||||
onCheckedChange={(e) => {
|
||||
if (librarySync && e) setLibrarySync(false);
|
||||
if (!librarySync && e) setLibrarySync(true);
|
||||
setAutoMount(e);
|
||||
}}
|
||||
/>
|
||||
@@ -131,6 +131,9 @@ export function KeyMounter() {
|
||||
<SelectOption value="Argon2id-s">Argon2id (standard)</SelectOption>
|
||||
<SelectOption value="Argon2id-h">Argon2id (hardened)</SelectOption>
|
||||
<SelectOption value="Argon2id-p">Argon2id (paranoid)</SelectOption>
|
||||
<SelectOption value="BalloonBlake3-s">Blake3-Balloon (standard)</SelectOption>
|
||||
<SelectOption value="BalloonBlake3-h">Blake3-Balloon (hardened)</SelectOption>
|
||||
<SelectOption value="BalloonBlake3-p">Blake3-Balloon (paranoid)</SelectOption>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -304,6 +304,15 @@ export const getCryptoSettings = (
|
||||
case 'Argon2id-p':
|
||||
hashing_algorithm = { Argon2id: 'Paranoid' as Params };
|
||||
break;
|
||||
case 'BalloonBlake3-s':
|
||||
hashing_algorithm = { BalloonBlake3: 'Standard' as Params };
|
||||
break;
|
||||
case 'BalloonBlake3-h':
|
||||
hashing_algorithm = { BalloonBlake3: 'Hardened' as Params };
|
||||
break;
|
||||
case 'BalloonBlake3-p':
|
||||
hashing_algorithm = { BalloonBlake3: 'Paranoid' as Params };
|
||||
break;
|
||||
}
|
||||
|
||||
return [algorithm, hashing_algorithm];
|
||||
@@ -313,16 +322,25 @@ export const getCryptoSettings = (
|
||||
export const getHashingAlgorithmString = (hashingAlgorithm: HashingAlgorithm): string => {
|
||||
let hashing_algorithm = '';
|
||||
|
||||
switch (hashingAlgorithm.Argon2id) {
|
||||
case 'Standard':
|
||||
switch (hashingAlgorithm) {
|
||||
case { Argon2id: 'Standard' }:
|
||||
hashing_algorithm = 'Argon2id-s';
|
||||
break;
|
||||
case 'Hardened':
|
||||
case { Argon2id: 'Hardened' }:
|
||||
hashing_algorithm = 'Argon2id-h';
|
||||
break;
|
||||
case 'Paranoid':
|
||||
case { Argon2id: 'Paranoid' }:
|
||||
hashing_algorithm = 'Argon2id-p';
|
||||
break;
|
||||
case { BalloonBlake3: 'Standard' }:
|
||||
hashing_algorithm = 'BalloonBlake3-s';
|
||||
break;
|
||||
case { BalloonBlake3: 'Hardened' }:
|
||||
hashing_algorithm = 'BalloonBlake3-h';
|
||||
break;
|
||||
case { BalloonBlake3: 'Paranoid' }:
|
||||
hashing_algorithm = 'BalloonBlake3-p';
|
||||
break;
|
||||
}
|
||||
|
||||
return hashing_algorithm;
|
||||
|
||||
Reference in New Issue
Block a user