[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:
jake 2022-10-07 15:31:40 +01:00 committed by GitHub
parent 5ff3d5fecb
commit 0db9603823
16 changed files with 1313 additions and 7 deletions

181
Cargo.lock generated
View File

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

View 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,
}

View File

@ -0,0 +1 @@
/// This is a placeholder file

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

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

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

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

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

View File

@ -0,0 +1 @@
pub mod hashing;

23
crates/crypto/src/lib.rs Normal file
View 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;

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

View File

@ -0,0 +1,2 @@
pub mod memory;
pub mod stream;

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

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

View 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]")
}
}