mirror of
https://github.com/spacedriveapp/spacedrive.git
synced 2025-12-11 20:15:30 +01:00
[ENG-250] Crypto Library (#400)
* add crypto crate with some functionality * formatting * add `argon2id` parameter levels * add descriptive comments * add stream decryption objects * add `StreamEncryptor` struct * add `StreamDecryptor` * general cleanup * add `thiserror` and error handling * add header structs * add basic serialization functionality * advance serialization * finish serialization * clean up serialization and use `impl` * finalise deserialization * add stream helper functions and remove old code * add AAD creation and retrieval * add important comment * add `ChaCha20Rng` as a CSPRNG * cleanup and crate-wide clippy lints * apply nursery lints * add in-memory encryption objects * rename `utils` to `objects` * move (de)serialization rules to separate file * add header-write helper function * add password hash helper function * add `decrypt_master_key` function * cleanup, formatting, linting * move keyslots to separate file, and rename them * add basic comments * remove `secrecy` dependency and import `protected` * add `to_array` helper function * `sd_crypto` -> `sd-crypto` * remove manual drops * add clippy allows * add `new()` for `Keyslot` and `FileHeader` * remove license * zeroize read buffer on error * magic bytes are now `ballapp` Co-authored-by: Brendan Allan <brendonovich@outlook.com> Co-authored-by: Jamie Pine <32987599+jamiepine@users.noreply.github.com>
This commit is contained in:
parent
5ff3d5fecb
commit
0db9603823
181
Cargo.lock
generated
181
Cargo.lock
generated
@ -29,6 +29,41 @@ version = "1.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "aae1277d39aeec15cb388266ecc24b11c80469deae6067e17a1a7aa9e5c1f234"
|
||||
|
||||
[[package]]
|
||||
name = "aead"
|
||||
version = "0.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5c192eb8f11fc081b0fe4259ba5af04217d4e0faddd02417310a927911abd7c8"
|
||||
dependencies = [
|
||||
"crypto-common",
|
||||
"generic-array",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "aes"
|
||||
version = "0.8.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bfe0133578c0986e1fe3dfcd4af1cc5b2dd6c3dbf534d69916ce16a2701d40ba"
|
||||
dependencies = [
|
||||
"cfg-if 1.0.0",
|
||||
"cipher",
|
||||
"cpufeatures",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "aes-gcm"
|
||||
version = "0.10.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "82e1366e0c69c9f927b1fa5ce2c7bf9eafc8f9268c0b9800729e8b267612447c"
|
||||
dependencies = [
|
||||
"aead",
|
||||
"aes",
|
||||
"cipher",
|
||||
"ctr",
|
||||
"ghash",
|
||||
"subtle",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ahash"
|
||||
version = "0.7.6"
|
||||
@ -88,6 +123,17 @@ version = "1.0.65"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "98161a4e3e2184da77bb14f02184cdd111e83bbbcc9979dfee3c44b9a85f5602"
|
||||
|
||||
[[package]]
|
||||
name = "argon2"
|
||||
version = "0.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "db4ce4441f99dbd377ca8a8f57b698c44d0d6e712d8329b5040da5a64aa1ce73"
|
||||
dependencies = [
|
||||
"base64ct",
|
||||
"blake2",
|
||||
"password-hash",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ascii"
|
||||
version = "0.9.3"
|
||||
@ -465,6 +511,15 @@ dependencies = [
|
||||
"typenum",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "blake2"
|
||||
version = "0.10.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b9cf849ee05b2ee5fba5e36f97ff8ec2533916700fc0758d40d92136a42f3388"
|
||||
dependencies = [
|
||||
"digest 0.10.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "block"
|
||||
version = "0.1.6"
|
||||
@ -662,6 +717,30 @@ version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
|
||||
|
||||
[[package]]
|
||||
name = "chacha20"
|
||||
version = "0.9.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c7fc89c7c5b9e7a02dfe45cd2367bae382f9ed31c61ca8debe5f827c420a2f08"
|
||||
dependencies = [
|
||||
"cfg-if 1.0.0",
|
||||
"cipher",
|
||||
"cpufeatures",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "chacha20poly1305"
|
||||
version = "0.10.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "10cd79432192d1c0f4e1a0fef9527696cc039165d729fb41b3f4f4f354c2dc35"
|
||||
dependencies = [
|
||||
"aead",
|
||||
"chacha20",
|
||||
"cipher",
|
||||
"poly1305",
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "chrono"
|
||||
version = "0.4.22"
|
||||
@ -678,6 +757,17 @@ dependencies = [
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cipher"
|
||||
version = "0.4.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d1873270f8f7942c191139cb8a40fd228da6c3fd2fc376d7e92d47aa14aeb59e"
|
||||
dependencies = [
|
||||
"crypto-common",
|
||||
"inout",
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clang-sys"
|
||||
version = "1.4.0"
|
||||
@ -949,6 +1039,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3"
|
||||
dependencies = [
|
||||
"generic-array",
|
||||
"rand_core 0.6.4",
|
||||
"typenum",
|
||||
]
|
||||
|
||||
@ -989,6 +1080,15 @@ dependencies = [
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ctr"
|
||||
version = "0.9.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0369ee1ad671834580515889b80f2ea915f23b8be8d0daa4bbaf2ac5c7590835"
|
||||
dependencies = [
|
||||
"cipher",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ctrlc"
|
||||
version = "3.2.3"
|
||||
@ -1750,6 +1850,16 @@ dependencies = [
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ghash"
|
||||
version = "0.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d930750de5717d2dd0b8c0d42c076c0e884c81a73e6cab859bbd2339c71e3e40"
|
||||
dependencies = [
|
||||
"opaque-debug",
|
||||
"polyval",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "gif"
|
||||
version = "0.11.4"
|
||||
@ -2355,6 +2465,15 @@ dependencies = [
|
||||
"adler32",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "inout"
|
||||
version = "0.1.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a0c10553d664a4d0bcff9f4215d0aac67a639cc68ef660840afe309b807bc9f5"
|
||||
dependencies = [
|
||||
"generic-array",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "instant"
|
||||
version = "0.1.12"
|
||||
@ -3578,6 +3697,17 @@ dependencies = [
|
||||
"schema-ast",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "password-hash"
|
||||
version = "0.4.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7676374caaee8a325c9e7a2ae557f216c5563a171d6997b0ef8a65af35147700"
|
||||
dependencies = [
|
||||
"base64ct",
|
||||
"rand_core 0.6.4",
|
||||
"subtle",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "paste"
|
||||
version = "1.0.9"
|
||||
@ -3853,6 +3983,29 @@ dependencies = [
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "poly1305"
|
||||
version = "0.8.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8159bd90725d2df49889a078b54f4f79e87f1f8a8444194cdca81d38f5393abf"
|
||||
dependencies = [
|
||||
"cpufeatures",
|
||||
"opaque-debug",
|
||||
"universal-hash",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "polyval"
|
||||
version = "0.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7ef234e08c11dfcb2e56f79fd70f6f2eb7f025c0ce2333e82f4f0518ecad30c6"
|
||||
dependencies = [
|
||||
"cfg-if 1.0.0",
|
||||
"cpufeatures",
|
||||
"opaque-debug",
|
||||
"universal-hash",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ppv-lite86"
|
||||
version = "0.2.16"
|
||||
@ -4927,14 +5080,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd"
|
||||
|
||||
[[package]]
|
||||
name = "sct"
|
||||
version = "0.7.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d53dcdb7c9f8158937a7981b48accfd39a43af418591a5d008c7b22b5e1b7ca4"
|
||||
name = "sd-crypto"
|
||||
version = "0.0.0"
|
||||
dependencies = [
|
||||
"ring 0.16.20",
|
||||
"untrusted",
|
||||
]
|
||||
"aead",
|
||||
"aes-gcm",
|
||||
"argon2",
|
||||
"chacha20poly1305",
|
||||
"rand 0.8.5",
|
||||
"rand_chacha 0.3.1",
|
||||
"thiserror",
|
||||
"zeroize",
|
||||
|
||||
|
||||
[[package]]
|
||||
name = "sd-core"
|
||||
@ -6576,6 +6733,16 @@ version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "39ec24b3121d976906ece63c9daad25b85969647682eee313cb5779fdd69e14e"
|
||||
|
||||
[[package]]
|
||||
name = "universal-hash"
|
||||
version = "0.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7d3160b73c9a19f7e2939a2fdad446c57c1bbbbf4d919d3213ff1267a580d8b5"
|
||||
dependencies = [
|
||||
"crypto-common",
|
||||
"subtle",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "unreachable"
|
||||
version = "1.0.0"
|
||||
|
||||
27
crates/crypto/Cargo.toml
Normal file
27
crates/crypto/Cargo.toml
Normal file
@ -0,0 +1,27 @@
|
||||
[package]
|
||||
name = "sd-crypto"
|
||||
version = "0.0.0"
|
||||
authors = ["Jake <brxken128@tutanota.com>"]
|
||||
readme = "README.md"
|
||||
description = "A library to handle cryptographic functions within Spacedrive"
|
||||
edition = "2021"
|
||||
rust-version = "1.64.0"
|
||||
|
||||
[dependencies]
|
||||
# rng
|
||||
rand = "0.8.5"
|
||||
rand_chacha = "0.3.1"
|
||||
|
||||
# password hashing
|
||||
argon2 = "0.4.1"
|
||||
|
||||
# aeads
|
||||
aes-gcm = "0.10.1"
|
||||
chacha20poly1305 = "0.10.1"
|
||||
aead = { version = "0.5.1", features = ["stream"] }
|
||||
|
||||
# cryptographic hygiene
|
||||
zeroize = "1.5.7"
|
||||
|
||||
# error handling
|
||||
thiserror = "1.0.37"
|
||||
30
crates/crypto/src/error.rs
Normal file
30
crates/crypto/src/error.rs
Normal file
@ -0,0 +1,30 @@
|
||||
use thiserror::Error;
|
||||
|
||||
/// This enum defines all possible errors that this crate can give
|
||||
#[derive(Error, Debug)]
|
||||
pub enum Error {
|
||||
#[error("not enough bytes were written to the output file")]
|
||||
WriteMismatch,
|
||||
#[error("there was an error hashing the password")]
|
||||
PasswordHash,
|
||||
#[error("I/O error: {0}")]
|
||||
Io(#[from] std::io::Error),
|
||||
#[error("error while encrypting")]
|
||||
Encrypt,
|
||||
#[error("error while decrypting")]
|
||||
Decrypt,
|
||||
#[error("nonce length mismatch")]
|
||||
NonceLengthMismatch,
|
||||
#[error("invalid file header")]
|
||||
FileHeader,
|
||||
#[error("error initialising stream encryption/decryption")]
|
||||
StreamModeInit,
|
||||
#[error("error initialising in-memory encryption/decryption")]
|
||||
MemoryModeInit,
|
||||
#[error("wrong password provided")]
|
||||
IncorrectPassword,
|
||||
#[error("no keyslots available")]
|
||||
NoKeyslots,
|
||||
#[error("mismatched data length while converting vec to array")]
|
||||
VecArrSizeMismatch,
|
||||
}
|
||||
1
crates/crypto/src/header/container.rs
Normal file
1
crates/crypto/src/header/container.rs
Normal file
@ -0,0 +1 @@
|
||||
/// This is a placeholder file
|
||||
220
crates/crypto/src/header/file.rs
Normal file
220
crates/crypto/src/header/file.rs
Normal file
@ -0,0 +1,220 @@
|
||||
use std::io::{Read, Seek, Write};
|
||||
|
||||
use zeroize::Zeroize;
|
||||
|
||||
use crate::{
|
||||
error::Error,
|
||||
objects::memory::MemoryDecryption,
|
||||
primitives::{Algorithm, Mode, MASTER_KEY_LEN},
|
||||
protected::Protected,
|
||||
};
|
||||
|
||||
use super::keyslot::Keyslot;
|
||||
|
||||
/// These are used to quickly and easily identify Spacedrive-encrypted files
|
||||
/// Random values - can be changed (up until 0.1.0)
|
||||
pub const MAGIC_BYTES: [u8; 7] = [0x62, 0x61, 0x6C, 0x6C, 0x61, 0x70, 0x70];
|
||||
|
||||
// Everything contained within this header can be flaunted around with minimal security risk
|
||||
// The only way this could compromise any data is if a weak password/key was used
|
||||
// Even then, `argon2id` helps alleiviate this somewhat (brute-forcing it is incredibly tough)
|
||||
// We also use high memory parameters in order to hinder attacks with ASICs
|
||||
// There should be no more than two keyslots in this header type
|
||||
pub struct FileHeader {
|
||||
pub version: FileHeaderVersion,
|
||||
pub algorithm: Algorithm,
|
||||
pub mode: Mode,
|
||||
pub nonce: Vec<u8>,
|
||||
pub keyslots: Vec<Keyslot>,
|
||||
}
|
||||
|
||||
/// This defines the main file header version
|
||||
pub enum FileHeaderVersion {
|
||||
V1,
|
||||
}
|
||||
|
||||
impl FileHeader {
|
||||
#[must_use]
|
||||
pub fn new(
|
||||
version: FileHeaderVersion,
|
||||
algorithm: Algorithm,
|
||||
nonce: Vec<u8>,
|
||||
keyslots: Vec<Keyslot>,
|
||||
) -> Self {
|
||||
Self {
|
||||
version,
|
||||
algorithm,
|
||||
mode: Mode::Stream,
|
||||
nonce,
|
||||
keyslots,
|
||||
}
|
||||
}
|
||||
|
||||
/// This is a helper function to decrypt a master key from a set of keyslots
|
||||
/// It's easier to call this on the header for now - but this may be changed in the future
|
||||
/// You receive an error if the password doesn't match
|
||||
#[allow(clippy::needless_pass_by_value)]
|
||||
pub fn decrypt_master_key(
|
||||
&self,
|
||||
password: Protected<Vec<u8>>,
|
||||
) -> Result<Protected<[u8; 32]>, Error> {
|
||||
let mut master_key = [0u8; MASTER_KEY_LEN];
|
||||
|
||||
for keyslot in &self.keyslots {
|
||||
let key = keyslot
|
||||
.hashing_algorithm
|
||||
.hash(password.clone(), keyslot.salt)
|
||||
.map_err(|_| Error::PasswordHash)?;
|
||||
|
||||
let decryptor =
|
||||
MemoryDecryption::new(key, keyslot.algorithm).map_err(|_| Error::MemoryModeInit)?;
|
||||
if let Ok(mut decrypted_master_key) =
|
||||
decryptor.decrypt(keyslot.master_key.as_ref(), &keyslot.nonce)
|
||||
{
|
||||
master_key.copy_from_slice(&decrypted_master_key);
|
||||
decrypted_master_key.zeroize();
|
||||
}
|
||||
}
|
||||
|
||||
if master_key == [0u8; MASTER_KEY_LEN] {
|
||||
Err(Error::IncorrectPassword)
|
||||
} else {
|
||||
Ok(Protected::new(master_key))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn write<W>(&self, writer: &mut W) -> Result<(), Error>
|
||||
where
|
||||
W: Write + Seek,
|
||||
{
|
||||
writer.write(&self.serialize()).map_err(Error::Io)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn generate_aad(&self) -> Vec<u8> {
|
||||
match self.version {
|
||||
FileHeaderVersion::V1 => {
|
||||
let mut aad: Vec<u8> = Vec::new();
|
||||
aad.extend_from_slice(&MAGIC_BYTES); // 6
|
||||
aad.extend_from_slice(&self.version.serialize()); // 8
|
||||
aad.extend_from_slice(&self.algorithm.serialize()); // 10
|
||||
aad.extend_from_slice(&self.mode.serialize()); // 12
|
||||
aad.extend_from_slice(&self.nonce); // 20 OR 32
|
||||
aad.extend_from_slice(&vec![0u8; 24 - self.nonce.len()]); // padded until 36 bytes
|
||||
aad
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn serialize(&self) -> Vec<u8> {
|
||||
match self.version {
|
||||
FileHeaderVersion::V1 => {
|
||||
let mut header: Vec<u8> = Vec::new();
|
||||
header.extend_from_slice(&MAGIC_BYTES); // 6
|
||||
header.extend_from_slice(&self.version.serialize()); // 8
|
||||
header.extend_from_slice(&self.algorithm.serialize()); // 10
|
||||
header.extend_from_slice(&self.mode.serialize()); // 12
|
||||
header.extend_from_slice(&self.nonce); // 20 OR 32
|
||||
header.extend_from_slice(&vec![0u8; 24 - self.nonce.len()]); // padded until 36 bytes
|
||||
|
||||
for keyslot in &self.keyslots {
|
||||
header.extend_from_slice(&keyslot.serialize());
|
||||
}
|
||||
|
||||
for _ in 0..(2 - self.keyslots.len()) {
|
||||
header.extend_from_slice(&[0u8; 96]);
|
||||
}
|
||||
|
||||
header
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// This includes the magic bytes at the start of the file
|
||||
#[must_use]
|
||||
pub const fn length(&self) -> usize {
|
||||
match self.version {
|
||||
FileHeaderVersion::V1 => 222 + MAGIC_BYTES.len(),
|
||||
}
|
||||
}
|
||||
|
||||
// This includes the magic bytes at the start of the file
|
||||
#[must_use]
|
||||
pub const fn aad_length(&self) -> usize {
|
||||
match self.version {
|
||||
FileHeaderVersion::V1 => 30 + MAGIC_BYTES.len(),
|
||||
}
|
||||
}
|
||||
|
||||
// The AAD retrieval here could be optimised - we do rewind a couple of times
|
||||
/// This deserializes a header directly from a reader, and leaves the reader at the start of the encrypted data
|
||||
/// 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>), Error>
|
||||
where
|
||||
R: Read + Seek,
|
||||
{
|
||||
let mut magic_bytes = [0u8; MAGIC_BYTES.len()];
|
||||
reader.read(&mut magic_bytes).map_err(Error::Io)?;
|
||||
|
||||
if magic_bytes != MAGIC_BYTES {
|
||||
return Err(Error::FileHeader);
|
||||
}
|
||||
|
||||
let mut version = [0u8; 2];
|
||||
|
||||
reader.read(&mut version).map_err(Error::Io)?;
|
||||
let version = FileHeaderVersion::deserialize(version)?;
|
||||
|
||||
let header = match version {
|
||||
FileHeaderVersion::V1 => {
|
||||
let mut algorithm = [0u8; 2];
|
||||
reader.read(&mut algorithm).map_err(Error::Io)?;
|
||||
let algorithm = Algorithm::deserialize(algorithm)?;
|
||||
|
||||
let mut mode = [0u8; 2];
|
||||
reader.read(&mut mode).map_err(Error::Io)?;
|
||||
let mode = Mode::deserialize(mode)?;
|
||||
|
||||
let mut nonce = vec![0u8; algorithm.nonce_len(mode)];
|
||||
reader.read(&mut nonce).map_err(Error::Io)?;
|
||||
|
||||
// read and discard the padding
|
||||
reader
|
||||
.read(&mut vec![0u8; 24 - nonce.len()])
|
||||
.map_err(Error::Io)?;
|
||||
|
||||
let mut keyslots: Vec<Keyslot> = Vec::new();
|
||||
|
||||
for _ in 0..2 {
|
||||
if let Ok(keyslot) = Keyslot::deserialize(reader) {
|
||||
keyslots.push(keyslot);
|
||||
}
|
||||
}
|
||||
|
||||
Self {
|
||||
version,
|
||||
algorithm,
|
||||
mode,
|
||||
nonce,
|
||||
keyslots,
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Rewind so we can get the AAD
|
||||
reader.rewind().map_err(Error::Io)?;
|
||||
|
||||
let mut aad = vec![0u8; header.aad_length()];
|
||||
reader.read(&mut aad).map_err(Error::Io)?;
|
||||
|
||||
// We return the cursor position to the end of the header,
|
||||
// So that the encrypted data can be read directly afterwards
|
||||
reader
|
||||
.seek(std::io::SeekFrom::Start(header.length() as u64))
|
||||
.map_err(Error::Io)?;
|
||||
|
||||
Ok((header, aad))
|
||||
}
|
||||
}
|
||||
117
crates/crypto/src/header/keyslot.rs
Normal file
117
crates/crypto/src/header/keyslot.rs
Normal file
@ -0,0 +1,117 @@
|
||||
use std::io::{Read, Seek};
|
||||
|
||||
use crate::{
|
||||
error::Error,
|
||||
primitives::{Algorithm, HashingAlgorithm, Mode, ENCRYPTED_MASTER_KEY_LEN, SALT_LEN},
|
||||
};
|
||||
|
||||
/// A keyslot. 96 bytes, and contains all the information for future-proofing while keeping the size reasonable
|
||||
///
|
||||
/// The mode was added so others can see that master keys are encrypted differently from data
|
||||
///
|
||||
/// The algorithm (should) be inherited from the parent header, but that's not a guarantee
|
||||
pub struct Keyslot {
|
||||
pub version: KeyslotVersion,
|
||||
pub algorithm: Algorithm, // encryption algorithm
|
||||
pub hashing_algorithm: HashingAlgorithm, // password hashing algorithm
|
||||
pub mode: Mode,
|
||||
pub salt: [u8; SALT_LEN],
|
||||
pub master_key: [u8; ENCRYPTED_MASTER_KEY_LEN], // this is encrypted so we can store it
|
||||
pub nonce: Vec<u8>,
|
||||
}
|
||||
|
||||
/// This defines the keyslot version
|
||||
pub enum KeyslotVersion {
|
||||
V1,
|
||||
}
|
||||
|
||||
impl Keyslot {
|
||||
#[must_use]
|
||||
pub fn new(
|
||||
version: KeyslotVersion,
|
||||
algorithm: Algorithm,
|
||||
hashing_algorithm: HashingAlgorithm,
|
||||
salt: [u8; SALT_LEN],
|
||||
encrypted_master_key: [u8; ENCRYPTED_MASTER_KEY_LEN],
|
||||
nonce: Vec<u8>,
|
||||
) -> Self {
|
||||
Self {
|
||||
version,
|
||||
algorithm,
|
||||
hashing_algorithm,
|
||||
mode: Mode::Memory,
|
||||
salt,
|
||||
master_key: encrypted_master_key,
|
||||
nonce,
|
||||
}
|
||||
}
|
||||
/// This function is used to serialize a keyslot into bytes
|
||||
#[must_use]
|
||||
pub fn serialize(&self) -> Vec<u8> {
|
||||
match self.version {
|
||||
KeyslotVersion::V1 => {
|
||||
let mut keyslot: Vec<u8> = 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.mode.serialize()); // 8
|
||||
keyslot.extend_from_slice(&self.salt); // 24
|
||||
keyslot.extend_from_slice(&self.master_key); // 72
|
||||
keyslot.extend_from_slice(&self.nonce); // 82 OR 94
|
||||
keyslot.extend_from_slice(&vec![0u8; 24 - self.nonce.len()]); // 96 total bytes
|
||||
keyslot
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// This function reads a keyslot from a reader, and attempts to serialize a keyslot
|
||||
pub fn deserialize<R>(reader: &mut R) -> Result<Self, Error>
|
||||
where
|
||||
R: Read + Seek,
|
||||
{
|
||||
let mut version = [0u8; 2];
|
||||
reader.read(&mut version).map_err(Error::Io)?;
|
||||
let version = KeyslotVersion::deserialize(version)?;
|
||||
|
||||
match version {
|
||||
KeyslotVersion::V1 => {
|
||||
let mut algorithm = [0u8; 2];
|
||||
reader.read(&mut algorithm).map_err(Error::Io)?;
|
||||
let algorithm = Algorithm::deserialize(algorithm)?;
|
||||
|
||||
let mut hashing_algorithm = [0u8; 2];
|
||||
reader.read(&mut hashing_algorithm).map_err(Error::Io)?;
|
||||
let hashing_algorithm = HashingAlgorithm::deserialize(hashing_algorithm)?;
|
||||
|
||||
let mut mode = [0u8; 2];
|
||||
reader.read(&mut mode).map_err(Error::Io)?;
|
||||
let mode = Mode::deserialize(mode)?;
|
||||
|
||||
let mut salt = [0u8; SALT_LEN];
|
||||
reader.read(&mut salt).map_err(Error::Io)?;
|
||||
|
||||
let mut master_key = [0u8; ENCRYPTED_MASTER_KEY_LEN];
|
||||
reader.read(&mut master_key).map_err(Error::Io)?;
|
||||
|
||||
let mut nonce = vec![0u8; algorithm.nonce_len(mode)];
|
||||
reader.read(&mut nonce).map_err(Error::Io)?;
|
||||
|
||||
reader
|
||||
.read(&mut vec![0u8; 26 - nonce.len()])
|
||||
.map_err(Error::Io)?;
|
||||
|
||||
let keyslot = Self {
|
||||
version,
|
||||
algorithm,
|
||||
hashing_algorithm,
|
||||
mode,
|
||||
salt,
|
||||
master_key,
|
||||
nonce,
|
||||
};
|
||||
|
||||
Ok(keyslot)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
6
crates/crypto/src/header/mod.rs
Normal file
6
crates/crypto/src/header/mod.rs
Normal file
@ -0,0 +1,6 @@
|
||||
//! This module will contain all encrypted header related functions, information, etc.
|
||||
//! It'll handle serialisation, deserialisation, AAD, keyslots and everything else
|
||||
|
||||
pub mod file;
|
||||
pub mod keyslot;
|
||||
pub mod serialization;
|
||||
101
crates/crypto/src/header/serialization.rs
Normal file
101
crates/crypto/src/header/serialization.rs
Normal file
@ -0,0 +1,101 @@
|
||||
//! This module defines all of the serialization and deserialization rules for the headers
|
||||
//!
|
||||
//! It contains byte -> enum and enum -> byte conversions for everything that could be written to a header (except headers and keyslots themselves)
|
||||
|
||||
use crate::{
|
||||
error::Error,
|
||||
keys::hashing::Params,
|
||||
primitives::{Algorithm, HashingAlgorithm, Mode},
|
||||
};
|
||||
|
||||
use super::{file::FileHeaderVersion, keyslot::KeyslotVersion};
|
||||
|
||||
impl FileHeaderVersion {
|
||||
#[must_use]
|
||||
pub const fn serialize(&self) -> [u8; 2] {
|
||||
match self {
|
||||
Self::V1 => [0x0A, 0x01],
|
||||
}
|
||||
}
|
||||
|
||||
pub const fn deserialize(bytes: [u8; 2]) -> Result<Self, Error> {
|
||||
match bytes {
|
||||
[0x0A, 0x01] => Ok(Self::V1),
|
||||
_ => Err(Error::FileHeader),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl KeyslotVersion {
|
||||
#[must_use]
|
||||
pub const fn serialize(&self) -> [u8; 2] {
|
||||
match self {
|
||||
Self::V1 => [0x0D, 0x01],
|
||||
}
|
||||
}
|
||||
|
||||
pub const fn deserialize(bytes: [u8; 2]) -> Result<Self, Error> {
|
||||
match bytes {
|
||||
[0x0D, 0x01] => Ok(Self::V1),
|
||||
_ => Err(Error::FileHeader),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl HashingAlgorithm {
|
||||
#[must_use]
|
||||
pub const fn serialize(&self) -> [u8; 2] {
|
||||
match self {
|
||||
Self::Argon2id(p) => match p {
|
||||
Params::Standard => [0x0F, 0x01],
|
||||
Params::Hardened => [0x0F, 0x02],
|
||||
Params::Paranoid => [0x0F, 0x03],
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
pub const fn deserialize(bytes: [u8; 2]) -> Result<Self, Error> {
|
||||
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),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Algorithm {
|
||||
#[must_use]
|
||||
pub const fn serialize(&self) -> [u8; 2] {
|
||||
match self {
|
||||
Self::XChaCha20Poly1305 => [0x0B, 0x01],
|
||||
Self::Aes256Gcm => [0x0B, 0x02],
|
||||
}
|
||||
}
|
||||
|
||||
pub const fn deserialize(bytes: [u8; 2]) -> Result<Self, Error> {
|
||||
match bytes {
|
||||
[0x0B, 0x01] => Ok(Self::XChaCha20Poly1305),
|
||||
[0x0B, 0x02] => Ok(Self::Aes256Gcm),
|
||||
_ => Err(Error::FileHeader),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Mode {
|
||||
#[must_use]
|
||||
pub const fn serialize(&self) -> [u8; 2] {
|
||||
match self {
|
||||
Self::Stream => [0x0C, 0x01],
|
||||
Self::Memory => [0x0C, 0x02],
|
||||
}
|
||||
}
|
||||
|
||||
pub const fn deserialize(bytes: [u8; 2]) -> Result<Self, Error> {
|
||||
match bytes {
|
||||
[0x0C, 0x01] => Ok(Self::Stream),
|
||||
[0x0C, 0x02] => Ok(Self::Memory),
|
||||
_ => Err(Error::FileHeader),
|
||||
}
|
||||
}
|
||||
}
|
||||
65
crates/crypto/src/keys/hashing.rs
Normal file
65
crates/crypto/src/keys/hashing.rs
Normal file
@ -0,0 +1,65 @@
|
||||
use crate::protected::Protected;
|
||||
use crate::{error::Error, primitives::SALT_LEN};
|
||||
use argon2::Argon2;
|
||||
|
||||
// These names are not final
|
||||
// I'm considering adding an `(i32)` to each, to allow specific versioning of each parameter version
|
||||
// These will be serializable/deserializable with regards to the header/storage of this information
|
||||
#[derive(Clone, Copy)]
|
||||
pub enum Params {
|
||||
Standard,
|
||||
Hardened,
|
||||
Paranoid,
|
||||
}
|
||||
|
||||
impl Params {
|
||||
#[must_use]
|
||||
pub fn get_argon2_params(&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 16MB 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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Shouldn't be called directly - call it on the `HashingAlgorithm` struct
|
||||
/// 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; 32]>, Error> {
|
||||
let mut key = [0u8; 32];
|
||||
|
||||
let argon2 = Argon2::new(
|
||||
argon2::Algorithm::Argon2id,
|
||||
argon2::Version::V0x13,
|
||||
params.get_argon2_params(),
|
||||
);
|
||||
|
||||
let result = argon2.hash_password_into(password.expose(), &salt, &mut key);
|
||||
|
||||
if result.is_ok() {
|
||||
Ok(Protected::new(key))
|
||||
} else {
|
||||
Err(Error::PasswordHash)
|
||||
}
|
||||
}
|
||||
1
crates/crypto/src/keys/mod.rs
Normal file
1
crates/crypto/src/keys/mod.rs
Normal file
@ -0,0 +1 @@
|
||||
pub mod hashing;
|
||||
23
crates/crypto/src/lib.rs
Normal file
23
crates/crypto/src/lib.rs
Normal file
@ -0,0 +1,23 @@
|
||||
#![forbid(unsafe_code)]
|
||||
#![warn(clippy::pedantic)]
|
||||
#![warn(clippy::correctness)]
|
||||
#![warn(clippy::perf)]
|
||||
#![warn(clippy::style)]
|
||||
#![warn(clippy::suspicious)]
|
||||
#![warn(clippy::nursery)]
|
||||
#![warn(clippy::correctness)]
|
||||
#![allow(clippy::missing_panics_doc)]
|
||||
#![allow(clippy::missing_errors_doc)]
|
||||
#![allow(clippy::module_name_repetitions)]
|
||||
#![allow(clippy::similar_names)]
|
||||
|
||||
pub mod error;
|
||||
pub mod header;
|
||||
pub mod keys;
|
||||
pub mod objects;
|
||||
pub mod primitives;
|
||||
pub mod protected;
|
||||
|
||||
// Re-export this so that payloads can be generated elsewhere
|
||||
pub use aead::Payload;
|
||||
pub use zeroize::Zeroize;
|
||||
84
crates/crypto/src/objects/memory.rs
Normal file
84
crates/crypto/src/objects/memory.rs
Normal file
@ -0,0 +1,84 @@
|
||||
use crate::protected::Protected;
|
||||
use aead::{Aead, KeyInit, Payload};
|
||||
use aes_gcm::Aes256Gcm;
|
||||
use chacha20poly1305::XChaCha20Poly1305;
|
||||
|
||||
use crate::{error::Error, primitives::Algorithm};
|
||||
|
||||
// Although these two objects are identical, I think it'll be good practice to keep their usage separate.
|
||||
// One for encryption, and one for decryption. This can easily be changed if needed.
|
||||
pub enum MemoryEncryption {
|
||||
XChaCha20Poly1305(Box<XChaCha20Poly1305>),
|
||||
Aes256Gcm(Box<Aes256Gcm>),
|
||||
}
|
||||
|
||||
pub enum MemoryDecryption {
|
||||
XChaCha20Poly1305(Box<XChaCha20Poly1305>),
|
||||
Aes256Gcm(Box<Aes256Gcm>),
|
||||
}
|
||||
|
||||
impl MemoryEncryption {
|
||||
#[allow(clippy::needless_pass_by_value)]
|
||||
pub fn new(key: Protected<[u8; 32]>, algorithm: Algorithm) -> Result<Self, Error> {
|
||||
let encryption_object = match algorithm {
|
||||
Algorithm::XChaCha20Poly1305 => {
|
||||
let cipher = XChaCha20Poly1305::new_from_slice(key.expose())
|
||||
.map_err(|_| Error::MemoryModeInit)?;
|
||||
|
||||
Self::XChaCha20Poly1305(Box::new(cipher))
|
||||
}
|
||||
Algorithm::Aes256Gcm => {
|
||||
let cipher =
|
||||
Aes256Gcm::new_from_slice(key.expose()).map_err(|_| Error::MemoryModeInit)?;
|
||||
|
||||
Self::Aes256Gcm(Box::new(cipher))
|
||||
}
|
||||
};
|
||||
|
||||
Ok(encryption_object)
|
||||
}
|
||||
|
||||
pub fn encrypt<'msg, 'aad>(
|
||||
&self,
|
||||
plaintext: impl Into<Payload<'msg, 'aad>>,
|
||||
nonce: &[u8],
|
||||
) -> aead::Result<Vec<u8>> {
|
||||
match self {
|
||||
Self::XChaCha20Poly1305(m) => m.encrypt(nonce.into(), plaintext),
|
||||
Self::Aes256Gcm(m) => m.encrypt(nonce.into(), plaintext),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl MemoryDecryption {
|
||||
#[allow(clippy::needless_pass_by_value)]
|
||||
pub fn new(key: Protected<[u8; 32]>, algorithm: Algorithm) -> Result<Self, Error> {
|
||||
let decryption_object = match algorithm {
|
||||
Algorithm::XChaCha20Poly1305 => {
|
||||
let cipher = XChaCha20Poly1305::new_from_slice(key.expose())
|
||||
.map_err(|_| Error::MemoryModeInit)?;
|
||||
|
||||
Self::XChaCha20Poly1305(Box::new(cipher))
|
||||
}
|
||||
Algorithm::Aes256Gcm => {
|
||||
let cipher =
|
||||
Aes256Gcm::new_from_slice(key.expose()).map_err(|_| Error::MemoryModeInit)?;
|
||||
|
||||
Self::Aes256Gcm(Box::new(cipher))
|
||||
}
|
||||
};
|
||||
|
||||
Ok(decryption_object)
|
||||
}
|
||||
|
||||
pub fn decrypt<'msg, 'aad>(
|
||||
&self,
|
||||
ciphertext: impl Into<Payload<'msg, 'aad>>,
|
||||
nonce: &[u8],
|
||||
) -> aead::Result<Vec<u8>> {
|
||||
match self {
|
||||
Self::XChaCha20Poly1305(m) => m.decrypt(nonce.into(), ciphertext),
|
||||
Self::Aes256Gcm(m) => m.decrypt(nonce.into(), ciphertext),
|
||||
}
|
||||
}
|
||||
}
|
||||
2
crates/crypto/src/objects/mod.rs
Normal file
2
crates/crypto/src/objects/mod.rs
Normal file
@ -0,0 +1,2 @@
|
||||
pub mod memory;
|
||||
pub mod stream;
|
||||
258
crates/crypto/src/objects/stream.rs
Normal file
258
crates/crypto/src/objects/stream.rs
Normal file
@ -0,0 +1,258 @@
|
||||
use std::io::{Read, Seek, Write};
|
||||
|
||||
use aead::{
|
||||
stream::{DecryptorLE31, EncryptorLE31},
|
||||
KeyInit, Payload,
|
||||
};
|
||||
use aes_gcm::Aes256Gcm;
|
||||
use chacha20poly1305::XChaCha20Poly1305;
|
||||
use zeroize::Zeroize;
|
||||
|
||||
use crate::{
|
||||
error::Error,
|
||||
primitives::{Algorithm, Mode, BLOCK_SIZE},
|
||||
protected::Protected,
|
||||
};
|
||||
|
||||
pub enum StreamEncryption {
|
||||
XChaCha20Poly1305(Box<EncryptorLE31<XChaCha20Poly1305>>),
|
||||
Aes256Gcm(Box<EncryptorLE31<Aes256Gcm>>),
|
||||
}
|
||||
|
||||
pub enum StreamDecryption {
|
||||
Aes256Gcm(Box<DecryptorLE31<Aes256Gcm>>),
|
||||
XChaCha20Poly1305(Box<DecryptorLE31<XChaCha20Poly1305>>),
|
||||
}
|
||||
|
||||
impl StreamEncryption {
|
||||
#[allow(clippy::needless_pass_by_value)]
|
||||
pub fn new(
|
||||
key: Protected<[u8; 32]>,
|
||||
nonce: &[u8],
|
||||
algorithm: Algorithm,
|
||||
) -> Result<Self, Error> {
|
||||
if nonce.len() != algorithm.nonce_len(Mode::Stream) {
|
||||
return Err(Error::NonceLengthMismatch);
|
||||
}
|
||||
|
||||
let encryption_object = match algorithm {
|
||||
Algorithm::XChaCha20Poly1305 => {
|
||||
let cipher = XChaCha20Poly1305::new_from_slice(key.expose())
|
||||
.map_err(|_| Error::StreamModeInit)?;
|
||||
|
||||
let stream = EncryptorLE31::from_aead(cipher, nonce.into());
|
||||
Self::XChaCha20Poly1305(Box::new(stream))
|
||||
}
|
||||
Algorithm::Aes256Gcm => {
|
||||
let cipher =
|
||||
Aes256Gcm::new_from_slice(key.expose()).map_err(|_| Error::StreamModeInit)?;
|
||||
|
||||
let stream = EncryptorLE31::from_aead(cipher, nonce.into());
|
||||
Self::Aes256Gcm(Box::new(stream))
|
||||
}
|
||||
};
|
||||
|
||||
Ok(encryption_object)
|
||||
}
|
||||
|
||||
// This should be used for every block, except the final block
|
||||
pub fn encrypt_next<'msg, 'aad>(
|
||||
&mut self,
|
||||
payload: impl Into<Payload<'msg, 'aad>>,
|
||||
) -> aead::Result<Vec<u8>> {
|
||||
match self {
|
||||
Self::XChaCha20Poly1305(s) => s.encrypt_next(payload),
|
||||
Self::Aes256Gcm(s) => s.encrypt_next(payload),
|
||||
}
|
||||
}
|
||||
|
||||
// This should be used to encrypt the final block of data
|
||||
// This takes ownership of `self` to prevent usage after finalization
|
||||
pub fn encrypt_last<'msg, 'aad>(
|
||||
self,
|
||||
payload: impl Into<Payload<'msg, 'aad>>,
|
||||
) -> aead::Result<Vec<u8>> {
|
||||
match self {
|
||||
Self::XChaCha20Poly1305(s) => s.encrypt_last(payload),
|
||||
Self::Aes256Gcm(s) => s.encrypt_last(payload),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn encrypt_streams<R, W>(
|
||||
mut self,
|
||||
mut reader: R,
|
||||
mut writer: W,
|
||||
aad: &[u8],
|
||||
) -> Result<(), Error>
|
||||
where
|
||||
R: Read + Seek,
|
||||
W: Write + Seek,
|
||||
{
|
||||
let mut read_buffer = vec![0u8; BLOCK_SIZE];
|
||||
let read_count = reader.read(&mut read_buffer).map_err(Error::Io)?;
|
||||
if read_count == BLOCK_SIZE {
|
||||
let payload = Payload {
|
||||
aad,
|
||||
msg: &read_buffer,
|
||||
};
|
||||
|
||||
let encrypted_data = self.encrypt_next(payload).map_err(|_| {
|
||||
read_buffer.zeroize();
|
||||
Error::Encrypt
|
||||
})?;
|
||||
|
||||
// zeroize before writing, so any potential errors won't result in a potential data leak
|
||||
read_buffer.zeroize();
|
||||
|
||||
// Using `write` instead of `write_all` so we can check the amount of bytes written
|
||||
let write_count = writer.write(&encrypted_data).map_err(Error::Io)?;
|
||||
|
||||
if read_count != write_count - 16 {
|
||||
// -16 to account for the AEAD tag
|
||||
return Err(Error::WriteMismatch);
|
||||
}
|
||||
} else {
|
||||
// we use `..read_count` in order to only use the read data, and not zeroes also
|
||||
let payload = Payload {
|
||||
aad,
|
||||
msg: &read_buffer[..read_count],
|
||||
};
|
||||
|
||||
let encrypted_data = self.encrypt_last(payload).map_err(|_| {
|
||||
read_buffer.zeroize();
|
||||
Error::Encrypt
|
||||
})?;
|
||||
|
||||
// zeroize before writing, so any potential errors won't result in a potential data leak
|
||||
read_buffer.zeroize();
|
||||
|
||||
// Using `write` instead of `write_all` so we can check the amount of bytes written
|
||||
let write_count = writer.write(&encrypted_data).map_err(Error::Io)?;
|
||||
|
||||
if read_count != write_count - 16 {
|
||||
// -16 to account for the AEAD tag
|
||||
return Err(Error::WriteMismatch);
|
||||
}
|
||||
}
|
||||
|
||||
writer.flush().map_err(Error::Io)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl StreamDecryption {
|
||||
#[allow(clippy::needless_pass_by_value)]
|
||||
pub fn new(
|
||||
key: Protected<[u8; 32]>,
|
||||
nonce: &[u8],
|
||||
algorithm: Algorithm,
|
||||
) -> Result<Self, Error> {
|
||||
if nonce.len() != algorithm.nonce_len(Mode::Stream) {
|
||||
return Err(Error::NonceLengthMismatch);
|
||||
}
|
||||
|
||||
let decryption_object = match algorithm {
|
||||
Algorithm::XChaCha20Poly1305 => {
|
||||
let cipher = XChaCha20Poly1305::new_from_slice(key.expose())
|
||||
.map_err(|_| Error::StreamModeInit)?;
|
||||
|
||||
let stream = DecryptorLE31::from_aead(cipher, nonce.into());
|
||||
Self::XChaCha20Poly1305(Box::new(stream))
|
||||
}
|
||||
Algorithm::Aes256Gcm => {
|
||||
let cipher =
|
||||
Aes256Gcm::new_from_slice(key.expose()).map_err(|_| Error::StreamModeInit)?;
|
||||
|
||||
let stream = DecryptorLE31::from_aead(cipher, nonce.into());
|
||||
Self::Aes256Gcm(Box::new(stream))
|
||||
}
|
||||
};
|
||||
|
||||
Ok(decryption_object)
|
||||
}
|
||||
|
||||
// This should be used for every block, except the final block
|
||||
pub fn decrypt_next<'msg, 'aad>(
|
||||
&mut self,
|
||||
payload: impl Into<Payload<'msg, 'aad>>,
|
||||
) -> aead::Result<Vec<u8>> {
|
||||
match self {
|
||||
Self::XChaCha20Poly1305(s) => s.decrypt_next(payload),
|
||||
Self::Aes256Gcm(s) => s.decrypt_next(payload),
|
||||
}
|
||||
}
|
||||
|
||||
// This should be used to decrypt the final block of data
|
||||
// This takes ownership of `self` to prevent usage after finalization
|
||||
pub fn decrypt_last<'msg, 'aad>(
|
||||
self,
|
||||
payload: impl Into<Payload<'msg, 'aad>>,
|
||||
) -> aead::Result<Vec<u8>> {
|
||||
match self {
|
||||
Self::XChaCha20Poly1305(s) => s.decrypt_last(payload),
|
||||
Self::Aes256Gcm(s) => s.decrypt_last(payload),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn decrypt_streams<R, W>(
|
||||
mut self,
|
||||
mut reader: R,
|
||||
mut writer: W,
|
||||
aad: &[u8],
|
||||
) -> Result<(), Error>
|
||||
where
|
||||
R: Read + Seek,
|
||||
W: Write + Seek,
|
||||
{
|
||||
let mut read_buffer = vec![0u8; BLOCK_SIZE];
|
||||
let read_count = reader.read(&mut read_buffer).map_err(Error::Io)?;
|
||||
if read_count == (BLOCK_SIZE + 16) {
|
||||
let payload = Payload {
|
||||
aad,
|
||||
msg: &read_buffer,
|
||||
};
|
||||
|
||||
let mut decrypted_data = self.decrypt_next(payload).map_err(|_| {
|
||||
read_buffer.zeroize();
|
||||
Error::Decrypt
|
||||
})?;
|
||||
|
||||
// Using `write` instead of `write_all` so we can check the amount of bytes written
|
||||
let write_count = writer.write(&decrypted_data).map_err(Error::Io)?;
|
||||
|
||||
// zeroize before writing, so any potential errors won't result in a potential data leak
|
||||
decrypted_data.zeroize();
|
||||
|
||||
if read_count - 16 != write_count {
|
||||
// -16 to account for the AEAD tag
|
||||
return Err(Error::WriteMismatch);
|
||||
}
|
||||
} else {
|
||||
let payload = Payload {
|
||||
aad,
|
||||
msg: &read_buffer[..read_count],
|
||||
};
|
||||
|
||||
let mut decrypted_data = self.decrypt_last(payload).map_err(|_| {
|
||||
read_buffer.zeroize();
|
||||
Error::Decrypt
|
||||
})?;
|
||||
|
||||
// Using `write` instead of `write_all` so we can check the amount of bytes written
|
||||
let write_count = writer.write(&decrypted_data).map_err(Error::Io)?;
|
||||
|
||||
// zeroize before writing, so any potential errors won't result in a potential data leak
|
||||
decrypted_data.zeroize();
|
||||
|
||||
if read_count - 16 != write_count {
|
||||
// -16 to account for the AEAD tag
|
||||
return Err(Error::WriteMismatch);
|
||||
}
|
||||
}
|
||||
|
||||
writer.flush().map_err(Error::Io)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
123
crates/crypto/src/primitives.rs
Normal file
123
crates/crypto/src/primitives.rs
Normal file
@ -0,0 +1,123 @@
|
||||
use rand::{RngCore, SeedableRng};
|
||||
use zeroize::Zeroize;
|
||||
|
||||
use crate::{
|
||||
error::Error,
|
||||
keys::hashing::{password_hash_argon2id, Params},
|
||||
protected::Protected,
|
||||
};
|
||||
|
||||
// This is the default salt size, and the recommended size for argon2id.
|
||||
pub const SALT_LEN: usize = 16;
|
||||
|
||||
/// The size used for streaming blocks. This size seems to offer the best performance compared to alternatives.
|
||||
/// The file size gain is 16 bytes per 1MiB (due to the AEAD tag)
|
||||
pub const BLOCK_SIZE: usize = 1_048_576;
|
||||
|
||||
/// The length of the encrypted master key
|
||||
pub const ENCRYPTED_MASTER_KEY_LEN: usize = 48;
|
||||
|
||||
/// The length of the (unencrypted) master key
|
||||
pub const MASTER_KEY_LEN: usize = 32;
|
||||
|
||||
/// These are all possible algorithms that can be used for encryption
|
||||
#[derive(Clone, Copy, Eq, PartialEq)]
|
||||
pub enum Algorithm {
|
||||
XChaCha20Poly1305,
|
||||
Aes256Gcm,
|
||||
}
|
||||
|
||||
/// These are the different "modes" for encryption
|
||||
/// Stream works in "blocks", incrementing the nonce on each block (so the same nonce isn't used twice)
|
||||
///
|
||||
/// Memory loads all data into memory before encryption, and encrypts it in one pass
|
||||
///
|
||||
/// Stream mode is going to be the default for files, containers, etc. as memory usage is roughly equal to the `BLOCK_SIZE`
|
||||
///
|
||||
/// Memory mode is only going to be used for small amounts of data (such as a master key) - streaming modes aren't viable here
|
||||
#[derive(Clone, Copy, Eq, PartialEq)]
|
||||
pub enum Mode {
|
||||
Stream,
|
||||
Memory,
|
||||
}
|
||||
|
||||
// (Password)HashingAlgorithm
|
||||
pub enum HashingAlgorithm {
|
||||
Argon2id(Params),
|
||||
}
|
||||
|
||||
impl HashingAlgorithm {
|
||||
/// This function should be used to hash passwords
|
||||
///
|
||||
/// It handles all of the security "levels" and paramaters
|
||||
pub fn hash(
|
||||
&self,
|
||||
password: Protected<Vec<u8>>,
|
||||
salt: [u8; SALT_LEN],
|
||||
) -> Result<Protected<[u8; 32]>, Error> {
|
||||
match self {
|
||||
Self::Argon2id(params) => password_hash_argon2id(password, salt, *params),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Algorithm {
|
||||
// This function calculates the expected nonce length for a given algorithm
|
||||
// 4 bytes are deducted for streaming mode, due to the LE31 counter being the last 4 bytes of the nonce
|
||||
#[must_use]
|
||||
pub const fn nonce_len(&self, mode: Mode) -> usize {
|
||||
let base = match self {
|
||||
Self::XChaCha20Poly1305 => 24,
|
||||
Self::Aes256Gcm => 12,
|
||||
};
|
||||
|
||||
match mode {
|
||||
Mode::Stream => base - 4,
|
||||
Mode::Memory => base,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The length can easily be obtained via `algorithm.nonce_len(mode)`
|
||||
///
|
||||
/// This function uses `ChaCha20Rng` for cryptographically-securely generating random data
|
||||
#[must_use]
|
||||
pub fn generate_nonce(len: usize) -> Vec<u8> {
|
||||
let mut nonce = vec![0u8; len];
|
||||
rand_chacha::ChaCha20Rng::from_entropy().fill_bytes(&mut nonce);
|
||||
nonce
|
||||
}
|
||||
|
||||
/// This function uses `ChaCha20Rng` for cryptographically-securely generating random data
|
||||
#[must_use]
|
||||
pub fn generate_salt() -> [u8; SALT_LEN] {
|
||||
let mut salt = [0u8; SALT_LEN];
|
||||
rand_chacha::ChaCha20Rng::from_entropy().fill_bytes(&mut salt);
|
||||
salt
|
||||
}
|
||||
|
||||
/// This generates a master key, which should be used for encrypting the data
|
||||
///
|
||||
/// This is then stored encrypted in the header
|
||||
///
|
||||
/// This function uses `ChaCha20Rng` for cryptographically-securely generating random data
|
||||
#[must_use]
|
||||
pub fn generate_master_key() -> Protected<[u8; MASTER_KEY_LEN]> {
|
||||
let mut master_key = [0u8; MASTER_KEY_LEN];
|
||||
rand_chacha::ChaCha20Rng::from_entropy().fill_bytes(&mut master_key);
|
||||
Protected::new(master_key)
|
||||
}
|
||||
|
||||
/// This is used for converting a `Vec<u8>` to an array of bytes
|
||||
///
|
||||
/// It's main usage is for converting an encrypted master key from a `Vec<u8>` to `[u8; ENCRYPTED_MASTER_KEY_LEN]`
|
||||
///
|
||||
/// As the master key is encrypted at this point, it does not need to be `Protected<>`
|
||||
///
|
||||
/// This function still `zeroize`s any data it can
|
||||
pub fn to_array<const I: usize>(bytes: Vec<u8>) -> Result<[u8; I], Error> {
|
||||
bytes.try_into().map_err(|mut b: Vec<u8>| {
|
||||
b.zeroize();
|
||||
Error::VecArrSizeMismatch
|
||||
})
|
||||
}
|
||||
81
crates/crypto/src/protected.rs
Normal file
81
crates/crypto/src/protected.rs
Normal file
@ -0,0 +1,81 @@
|
||||
//! This is a basic wrapper for secret/hidden values
|
||||
//!
|
||||
//! It's worth noting that this wrapper does not provide additional security that you can't get manually, it just makes it a LOT easier.
|
||||
//!
|
||||
//! It implements zeroize-on-drop, meaning the data is securely erased from memory once it goes out of scope.
|
||||
//! You may call `drop()` prematurely if you wish to erase it sooner.
|
||||
//!
|
||||
//! `Protected` values are also hidden from `fmt::Debug`, and will display `[REDACTED]` instead.
|
||||
//!
|
||||
//! The only way to access the data within a `Protected` value is to call `.expose()` - this is to prevent accidental leakage.
|
||||
//! This also makes any `Protected` value easier to audit, as you are able to quickly view wherever the data is accessed.
|
||||
//!
|
||||
//! `Protected` values are not able to be copied within memory, to prevent accidental leakage. They are able to be `cloned` however - but this is always explicit and you will be aware of it.
|
||||
//!
|
||||
//! I'd like to give a huge thank you to the authors of the [secrecy crate](https://crates.io/crates/secrecy),
|
||||
//! as that crate's functionality inspired this implementation.
|
||||
//!
|
||||
//! # Examples
|
||||
//!
|
||||
//! ```rust,ignore
|
||||
//! let secret_data = "this is classified information".to_string();
|
||||
//! let protected_data = Protected::new(secret_data);
|
||||
//!
|
||||
//! // the only way to access the data within the `Protected` wrapper
|
||||
//! // is by calling `.expose()`
|
||||
//! let value = protected_data.expose();
|
||||
//! ```
|
||||
//!
|
||||
|
||||
use std::fmt::Debug;
|
||||
use zeroize::Zeroize;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Protected<T>
|
||||
where
|
||||
T: Zeroize,
|
||||
{
|
||||
data: T,
|
||||
}
|
||||
|
||||
impl<T> std::ops::Deref for Protected<T>
|
||||
where
|
||||
T: Zeroize,
|
||||
{
|
||||
type Target = T;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.data
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Protected<T>
|
||||
where
|
||||
T: Zeroize,
|
||||
{
|
||||
pub const fn new(value: T) -> Self {
|
||||
Self { data: value }
|
||||
}
|
||||
|
||||
pub const fn expose(&self) -> &T {
|
||||
&self.data
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Drop for Protected<T>
|
||||
where
|
||||
T: Zeroize,
|
||||
{
|
||||
fn drop(&mut self) {
|
||||
self.data.zeroize();
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Debug for Protected<T>
|
||||
where
|
||||
T: Zeroize,
|
||||
{
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.write_str("[REDACTED]")
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user