use crate::{compress, entry::write::derive_key_material, error::UnknownValueError};
use password_hash::Output;
pub(crate) use private::*;
use std::{io, str::FromStr};
mod private {
use super::*;
#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)]
pub enum Compress {
No,
Deflate(compress::deflate::DeflateCompressionLevel),
ZStandard(compress::zstandard::ZstdCompressionLevel),
XZ(compress::xz::XZCompressionLevel),
}
#[derive(Clone, Debug)]
pub struct Cipher {
pub(crate) password: Password,
pub(crate) derived: DerivedKeyMaterial,
pub(crate) hash_algorithm: HashAlgorithm,
pub(crate) cipher_algorithm: CipherAlgorithm,
pub(crate) mode: CipherMode,
}
impl Cipher {
#[inline]
pub(crate) const fn new(
password: Password,
derived: DerivedKeyMaterial,
hash_algorithm: HashAlgorithm,
cipher_algorithm: CipherAlgorithm,
mode: CipherMode,
) -> Self {
Self {
password,
derived,
hash_algorithm,
cipher_algorithm,
mode,
}
}
}
#[derive(Clone, Debug)]
pub struct DerivedKeyMaterial {
pub(crate) phsf: String,
pub(crate) key: Output,
}
pub trait WriteOption {
fn compress(&self) -> Compress;
fn cipher(&self) -> Option<&Cipher>;
#[inline]
fn compression(&self) -> Compression {
match self.compress() {
Compress::No => Compression::No,
Compress::Deflate(_) => Compression::Deflate,
Compress::ZStandard(_) => Compression::ZStandard,
Compress::XZ(_) => Compression::XZ,
}
}
#[inline]
fn encryption(&self) -> Encryption {
self.cipher()
.map_or(Encryption::No, |it| match it.cipher_algorithm {
CipherAlgorithm::Aes => Encryption::Aes,
CipherAlgorithm::Camellia => Encryption::Camellia,
})
}
#[inline]
fn cipher_mode(&self) -> CipherMode {
self.cipher().map_or(CipherMode::CTR, |it| it.mode)
}
#[inline]
fn hash_algorithm(&self) -> HashAlgorithm {
self.cipher()
.map_or_else(HashAlgorithm::argon2id, |it| it.hash_algorithm)
}
#[inline]
fn password(&self) -> Option<&[u8]> {
self.cipher().map(|it| it.password.as_bytes())
}
}
impl WriteOption for WriteOptions {
#[inline]
fn compress(&self) -> Compress {
self.compress
}
#[inline]
fn cipher(&self) -> Option<&Cipher> {
self.cipher.as_ref()
}
}
impl<T> WriteOption for &T
where
T: WriteOption,
{
#[inline]
fn compress(&self) -> Compress {
T::compress(self)
}
#[inline]
fn cipher(&self) -> Option<&Cipher> {
T::cipher(self)
}
}
pub trait ReadOption {
fn password(&self) -> Option<&[u8]>;
}
impl<T: ReadOption> ReadOption for &T {
#[inline]
fn password(&self) -> Option<&[u8]> {
T::password(self)
}
}
impl ReadOption for ReadOptions {
#[inline]
fn password(&self) -> Option<&[u8]> {
self.password.as_deref()
}
}
}
#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)]
pub enum Compression {
No,
Deflate,
ZStandard,
XZ,
Reserved(u8),
Private(u8),
}
impl Compression {
#[inline]
pub const fn to_byte(self) -> u8 {
match self {
Self::No => 0,
Self::Deflate => 1,
Self::ZStandard => 2,
Self::XZ => 4,
Self::Reserved(v) | Self::Private(v) => v,
}
}
#[inline]
pub const fn is_reserved(self) -> bool {
matches!(self, Self::Reserved(_))
}
#[inline]
pub const fn is_private(self) -> bool {
matches!(self, Self::Private(_))
}
}
impl TryFrom<u8> for Compression {
type Error = UnknownValueError;
#[inline]
fn try_from(value: u8) -> Result<Self, Self::Error> {
match value {
0 => Ok(Self::No),
1 => Ok(Self::Deflate),
2 => Ok(Self::ZStandard),
4 => Ok(Self::XZ),
v if v < 128 => Ok(Self::Reserved(v)),
v => Ok(Self::Private(v)),
}
}
}
#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)]
pub(crate) enum CompressionLevelImpl {
Min,
Max,
Default,
Custom(i64),
}
impl FromStr for CompressionLevelImpl {
type Err = core::num::ParseIntError;
#[inline]
fn from_str(s: &str) -> Result<Self, Self::Err> {
if s.eq_ignore_ascii_case("min") {
Ok(Self::Min)
} else if s.eq_ignore_ascii_case("max") {
Ok(Self::Max)
} else if s.eq_ignore_ascii_case("default") {
Ok(Self::Default)
} else {
Ok(Self::Custom(i64::from_str(s)?))
}
}
}
#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)]
pub struct CompressionLevel(pub(crate) CompressionLevelImpl);
impl CompressionLevel {
pub(crate) const DEFAULT: Self = Self(CompressionLevelImpl::Default);
#[inline]
pub const fn min() -> Self {
Self(CompressionLevelImpl::Min)
}
#[inline]
pub const fn max() -> Self {
Self(CompressionLevelImpl::Max)
}
}
impl Default for CompressionLevel {
#[inline]
fn default() -> Self {
Self::DEFAULT
}
}
impl<T: Into<i64>> From<T> for CompressionLevel {
#[inline]
fn from(value: T) -> Self {
Self(CompressionLevelImpl::Custom(value.into()))
}
}
impl FromStr for CompressionLevel {
type Err = core::num::ParseIntError;
#[inline]
fn from_str(s: &str) -> Result<Self, Self::Err> {
Ok(Self(CompressionLevelImpl::from_str(s)?))
}
}
#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)]
pub enum CipherAlgorithm {
Aes,
Camellia,
}
#[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)]
pub(crate) struct Password(Vec<u8>);
impl Password {
#[inline]
pub(crate) const fn as_bytes(&self) -> &[u8] {
self.0.as_slice()
}
}
impl<T: AsRef<[u8]>> From<T> for Password {
#[inline]
fn from(value: T) -> Self {
Self(value.as_ref().to_vec())
}
}
#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)]
pub enum Encryption {
No,
Aes,
Camellia,
Reserved(u8),
Private(u8),
}
impl Encryption {
#[inline]
pub const fn to_byte(self) -> u8 {
match self {
Self::No => 0,
Self::Aes => 1,
Self::Camellia => 2,
Self::Reserved(v) | Self::Private(v) => v,
}
}
#[inline]
pub const fn is_reserved(self) -> bool {
matches!(self, Self::Reserved(_))
}
#[inline]
pub const fn is_private(self) -> bool {
matches!(self, Self::Private(_))
}
}
impl TryFrom<u8> for Encryption {
type Error = UnknownValueError;
#[inline]
fn try_from(value: u8) -> Result<Self, Self::Error> {
match value {
0 => Ok(Self::No),
1 => Ok(Self::Aes),
2 => Ok(Self::Camellia),
v if v < 128 => Ok(Self::Reserved(v)),
v => Ok(Self::Private(v)),
}
}
}
#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)]
pub enum CipherMode {
CBC,
CTR,
Reserved(u8),
Private(u8),
}
impl CipherMode {
#[inline]
pub const fn to_byte(self) -> u8 {
match self {
Self::CBC => 0,
Self::CTR => 1,
Self::Reserved(v) | Self::Private(v) => v,
}
}
#[inline]
pub const fn is_reserved(self) -> bool {
matches!(self, Self::Reserved(_))
}
#[inline]
pub const fn is_private(self) -> bool {
matches!(self, Self::Private(_))
}
}
impl TryFrom<u8> for CipherMode {
type Error = UnknownValueError;
#[inline]
fn try_from(value: u8) -> Result<Self, Self::Error> {
match value {
0 => Ok(Self::CBC),
1 => Ok(Self::CTR),
v if v < 128 => Ok(Self::Reserved(v)),
v => Ok(Self::Private(v)),
}
}
}
#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)]
pub(crate) enum HashAlgorithmParams {
Pbkdf2Sha256 {
rounds: Option<u32>,
},
Argon2Id {
time_cost: Option<u32>,
memory_cost: Option<u32>,
parallelism_cost: Option<u32>,
},
}
#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)]
pub struct HashAlgorithm(pub(crate) HashAlgorithmParams);
impl HashAlgorithm {
#[inline]
pub const fn pbkdf2_sha256() -> Self {
Self::pbkdf2_sha256_with(None)
}
#[inline]
pub const fn pbkdf2_sha256_with(rounds: Option<u32>) -> Self {
Self(HashAlgorithmParams::Pbkdf2Sha256 { rounds })
}
#[inline]
pub const fn argon2id() -> Self {
Self::argon2id_with(None, None, None)
}
#[inline]
pub const fn argon2id_with(
time_cost: Option<u32>,
memory_cost: Option<u32>,
parallelism_cost: Option<u32>,
) -> Self {
Self(HashAlgorithmParams::Argon2Id {
time_cost,
memory_cost,
parallelism_cost,
})
}
}
#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)]
pub enum DataKind {
File,
Directory,
SymbolicLink,
HardLink,
Reserved(u8),
Private(u8),
}
impl DataKind {
#[inline]
pub const fn to_byte(self) -> u8 {
match self {
Self::File => 0,
Self::Directory => 1,
Self::SymbolicLink => 2,
Self::HardLink => 3,
Self::Reserved(v) | Self::Private(v) => v,
}
}
#[inline]
pub const fn is_reserved(self) -> bool {
matches!(self, Self::Reserved(_))
}
#[inline]
pub const fn is_private(self) -> bool {
matches!(self, Self::Private(_))
}
}
impl TryFrom<u8> for DataKind {
type Error = UnknownValueError;
#[inline]
fn try_from(value: u8) -> Result<Self, Self::Error> {
match value {
0 => Ok(Self::File),
1 => Ok(Self::Directory),
2 => Ok(Self::SymbolicLink),
3 => Ok(Self::HardLink),
v if v < 128 => Ok(Self::Reserved(v)),
v => Ok(Self::Private(v)),
}
}
}
#[derive(Clone, Debug)]
pub struct WriteOptions {
compress: Compress,
cipher: Option<Cipher>,
}
impl WriteOptions {
#[inline]
pub const fn store() -> Self {
Self {
compress: Compress::No,
cipher: None,
}
}
#[inline]
pub const fn builder() -> WriteOptionsBuilder {
WriteOptionsBuilder::new()
}
#[inline]
pub fn into_builder(self) -> WriteOptionsBuilder {
self.into()
}
}
#[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)]
pub struct WriteOptionsBuilder {
compression: Compression,
compression_level: CompressionLevel,
encryption: Encryption,
cipher_mode: CipherMode,
hash_algorithm: HashAlgorithm,
password: Option<Vec<u8>>,
}
impl Default for WriteOptionsBuilder {
#[inline]
fn default() -> Self {
Self::new()
}
}
impl From<WriteOptions> for WriteOptionsBuilder {
#[inline]
fn from(value: WriteOptions) -> Self {
let (compression, compression_level) = match value.compress {
Compress::No => (Compression::No, CompressionLevel::DEFAULT),
Compress::Deflate(level) => (Compression::Deflate, level.into()),
Compress::ZStandard(level) => (Compression::ZStandard, level.into()),
Compress::XZ(level) => (Compression::XZ, level.into()),
};
Self {
compression,
compression_level,
encryption: value.encryption(),
cipher_mode: value.cipher_mode(),
hash_algorithm: value.hash_algorithm(),
password: value.password().map(|p| p.to_vec()),
}
}
}
impl WriteOptionsBuilder {
const fn new() -> Self {
Self {
compression: Compression::No,
compression_level: CompressionLevel::DEFAULT,
encryption: Encryption::No,
cipher_mode: CipherMode::CTR,
hash_algorithm: HashAlgorithm::argon2id(),
password: None,
}
}
#[inline]
pub fn compression(&mut self, compression: Compression) -> &mut Self {
self.compression = compression;
self
}
#[inline]
pub fn compression_level(&mut self, compression_level: CompressionLevel) -> &mut Self {
self.compression_level = compression_level;
self
}
#[inline]
pub fn encryption(&mut self, encryption: Encryption) -> &mut Self {
self.encryption = encryption;
self
}
#[inline]
pub fn cipher_mode(&mut self, cipher_mode: CipherMode) -> &mut Self {
self.cipher_mode = cipher_mode;
self
}
#[inline]
pub fn hash_algorithm(&mut self, algorithm: HashAlgorithm) -> &mut Self {
self.hash_algorithm = algorithm;
self
}
#[inline]
pub fn password<B: AsRef<[u8]>>(&mut self, password: Option<B>) -> &mut Self {
self.password = password.map(|it| it.as_ref().to_vec());
self
}
#[inline]
#[must_use = "building options without using them is wasteful"]
pub fn try_build(&self) -> io::Result<WriteOptions> {
let cipher = if self.encryption != Encryption::No {
let cipher_algorithm = match self.encryption {
Encryption::Aes => CipherAlgorithm::Aes,
Encryption::Camellia => CipherAlgorithm::Camellia,
other => {
return Err(io::Error::new(
io::ErrorKind::Unsupported,
format!(
"unsupported encryption method for writing: byte={}",
other.to_byte()
),
));
}
};
let password = self.password.as_deref().ok_or_else(|| {
io::Error::new(io::ErrorKind::InvalidInput, "Password was not provided.")
})?;
let derived = derive_key_material(cipher_algorithm, self.hash_algorithm, password)?;
Some(Cipher::new(
password.into(),
derived,
self.hash_algorithm,
cipher_algorithm,
self.cipher_mode,
))
} else {
None
};
Ok(WriteOptions {
compress: match self.compression {
Compression::No => Compress::No,
Compression::Deflate => Compress::Deflate(self.compression_level.into()),
Compression::ZStandard => Compress::ZStandard(self.compression_level.into()),
Compression::XZ => Compress::XZ(self.compression_level.into()),
other => {
return Err(io::Error::new(
io::ErrorKind::Unsupported,
format!(
"unsupported compression method for writing: byte={}",
other.to_byte()
),
));
}
},
cipher,
})
}
#[inline]
#[must_use = "building options without using them is wasteful"]
pub fn build(&self) -> WriteOptions {
match self.try_build() {
Ok(options) => options,
Err(e) => panic!("{e}"),
}
}
}
#[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)]
pub struct ReadOptions {
password: Option<Vec<u8>>,
}
impl ReadOptions {
#[inline]
pub fn with_password<B: AsRef<[u8]>>(password: Option<B>) -> Self {
Self {
password: password.map(|p| p.as_ref().to_vec()),
}
}
#[inline]
pub const fn builder() -> ReadOptionsBuilder {
ReadOptionsBuilder::new()
}
#[inline]
pub fn into_builder(self) -> ReadOptionsBuilder {
self.into()
}
}
#[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug, Default)]
pub struct ReadOptionsBuilder {
password: Option<Vec<u8>>,
}
impl From<ReadOptions> for ReadOptionsBuilder {
#[inline]
fn from(value: ReadOptions) -> Self {
Self {
password: value.password,
}
}
}
impl ReadOptionsBuilder {
#[inline]
const fn new() -> Self {
Self { password: None }
}
#[inline]
#[must_use = "building options without using them is wasteful"]
pub fn build(&self) -> ReadOptions {
ReadOptions {
password: self.password.clone(),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[cfg(all(target_family = "wasm", target_os = "unknown"))]
use wasm_bindgen_test::wasm_bindgen_test as test;
#[test]
fn try_build_derives_key_at_build() {
let options = WriteOptions::builder()
.encryption(Encryption::Aes)
.password(Some("password"))
.try_build()
.unwrap();
let cipher = options.cipher.unwrap();
assert!(!cipher.derived.phsf.is_empty());
assert_eq!(cipher.derived.key.len(), 32);
}
#[test]
fn each_build_generates_fresh_salt() {
let mut builder = WriteOptions::builder();
builder
.encryption(Encryption::Aes)
.password(Some("password"));
let first = builder.try_build().unwrap();
let second = builder.try_build().unwrap();
assert_ne!(
first.cipher.unwrap().derived.phsf,
second.cipher.unwrap().derived.phsf,
);
}
#[test]
fn try_build_without_password_returns_error() {
let err = WriteOptions::builder()
.encryption(Encryption::Aes)
.try_build()
.unwrap_err();
assert_eq!(err.kind(), io::ErrorKind::InvalidInput);
}
#[test]
fn try_build_with_invalid_kdf_params_returns_error() {
let result = WriteOptions::builder()
.encryption(Encryption::Aes)
.hash_algorithm(HashAlgorithm::argon2id_with(Some(0), None, None))
.password(Some("password"))
.try_build();
assert!(result.is_err());
}
#[test]
#[should_panic(expected = "Password was not provided.")]
fn build_without_password_panics() {
let _ = WriteOptions::builder().encryption(Encryption::Aes).build();
}
}