[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:
jake
2023-01-04 09:57:02 +00:00
committed by GitHub
parent 58aa3b7a0c
commit 22bc77a39e
29 changed files with 403 additions and 310 deletions

BIN
Cargo.lock generated
View File

Binary file not shown.

View File

@@ -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
View 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
View 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
View 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())
};
});
}

View File

@@ -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,

View File

@@ -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 {

View File

@@ -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,
)?;
}

View File

@@ -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(),

View File

@@ -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"

View File

@@ -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(())
}

View File

@@ -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"

View File

@@ -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();

View File

@@ -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();

View File

@@ -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();

View File

@@ -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();

View File

@@ -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")]

View File

@@ -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(

View File

@@ -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)?;

View File

@@ -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)?;

View File

@@ -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)?;

View File

@@ -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),
}
}
}

View File

@@ -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)))
}
}

View File

@@ -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 }

View File

@@ -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>

View File

@@ -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>

View File

@@ -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()}`,

View File

@@ -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>

View File

@@ -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;