[ENG-256] Crypto Crate Refinements (#410)

* add experimental preview media support

* clippy+formatting

* add support for mk/pvm nonces

* fix cursor positioning+size deserialization bug

* clippy+formatting

* cleanup code and add `decrypt_preview_media()`

* update comment

* remove `Mode` from `Keyslot`

* use little-endian `usize` for media length

* remove memory objects

* remove mode enum

* add `.zeroize()` to `Protected<>`

* remove `Mode` ser/de

* refactor, remove `Mode`, add helper functions

* formatting

* remove unused error

* update comments/docs

* comments, fix large files, insane performance

* doc updates and fix decrypt zeroize

* revert to 1048576 byte `BLOCK_SIZE`

* `Keyslot` and `PreviewMedia` constructors now handle encryption

* add metadata item with associated functions

* update comments, clean up useless items

* add metadata support within the header

* remove stray 128 in length calcs

* include metadata in header ser/de

* fmt+clippy

* rework keyslot decryption

* formatting

* api changes, code cleanup

* docs for `stream.rs`

* massive documentation re-write

* mark expensive doc test as ignore

* minor api tweaks

* add examples

* formatting+linting

* finalise documentation and add more examples

* formatting
This commit is contained in:
jake 2022-10-13 23:25:42 +01:00 committed by GitHub
parent c0b51bcd4a
commit b5c571541e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
23 changed files with 1650 additions and 569 deletions

2
Cargo.lock generated
View file

@ -5199,6 +5199,8 @@ dependencies = [
"chacha20poly1305",
"rand 0.8.5",
"rand_chacha 0.3.1",
"serde",
"serde_json",
"thiserror",
"zeroize",
]

View file

@ -24,4 +24,8 @@ aead = { version = "0.5.1", features = ["stream"] }
zeroize = "1.5.7"
# error handling
thiserror = "1.0.37"
thiserror = "1.0.37"
# metadata de/serialization
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"

View file

@ -0,0 +1,82 @@
use std::fs::File;
use sd_crypto::{
crypto::stream::{Algorithm, StreamDecryption, StreamEncryption},
header::{
file::{FileHeader, FileHeaderVersion},
keyslot::{Keyslot, KeyslotVersion},
},
keys::hashing::{HashingAlgorithm, Params},
primitives::generate_master_key,
Protected,
};
const ALGORITHM: Algorithm = Algorithm::XChaCha20Poly1305;
const HASHING_ALGORITHM: HashingAlgorithm = HashingAlgorithm::Argon2id(Params::Standard);
pub fn encrypt() {
let password = Protected::new(b"password".to_vec());
// Open both the source and the output file
let mut reader = File::open("test").unwrap();
let mut writer = File::create("test.encrypted").unwrap();
// This needs to be generated here, otherwise we won't have access to it for encryption
let master_key = generate_master_key();
// Create a keyslot to be added to the header
let mut keyslots: Vec<Keyslot> = Vec::new();
keyslots.push(
Keyslot::new(
KeyslotVersion::V1,
ALGORITHM,
HASHING_ALGORITHM,
password,
&master_key,
)
.unwrap(),
);
// Create the header for the encrypted file
let header = FileHeader::new(FileHeaderVersion::V1, ALGORITHM, keyslots, None, None);
// Write the header to the file
header.write(&mut writer).unwrap();
// Use the nonce created by the header to initialize a stream encryption object
let encryptor = StreamEncryption::new(master_key, &header.nonce, header.algorithm).unwrap();
// Encrypt the data from the reader, and write it to the writer
// Use AAD so the header can be authenticated against every block of data
encryptor
.encrypt_streams(&mut reader, &mut writer, &header.generate_aad())
.unwrap();
}
pub fn decrypt() {
let password = Protected::new(b"password".to_vec());
// Open both the encrypted file and the output file
let mut reader = File::open("test.encrypted").unwrap();
let mut writer = File::create("test.original").unwrap();
// Deserialize the header, keyslots, etc from the encrypted file
let (header, aad) = FileHeader::deserialize(&mut reader).unwrap();
// Decrypt the master key with the user's password
let master_key = header.decrypt_master_key(password).unwrap();
// Initialize a stream decryption object using data provided by the header
let decryptor = StreamDecryption::new(master_key, &header.nonce, header.algorithm).unwrap();
// Decrypt data the from the writer, and write it to the writer
decryptor
.decrypt_streams(&mut reader, &mut writer, &aad)
.unwrap();
}
fn main() {
encrypt();
decrypt();
}

View file

@ -0,0 +1,110 @@
use std::fs::File;
use sd_crypto::{
crypto::stream::{Algorithm, StreamEncryption},
header::{
file::{FileHeader, FileHeaderVersion},
keyslot::{Keyslot, KeyslotVersion},
metadata::{Metadata, MetadataVersion},
},
keys::hashing::{HashingAlgorithm, Params},
primitives::{generate_master_key, generate_salt},
Protected,
};
use serde::{Deserialize, Serialize};
const ALGORITHM: Algorithm = Algorithm::XChaCha20Poly1305;
const HASHING_ALGORITHM: HashingAlgorithm = HashingAlgorithm::Argon2id(Params::Standard);
#[derive(Serialize, Deserialize)]
pub struct FileInformation {
pub file_name: String,
}
fn encrypt() {
let embedded_metadata = FileInformation {
file_name: "filename.txt".to_string(),
};
let password = Protected::new(b"password".to_vec());
// Open both the source and the output file
let mut reader = File::open("test").unwrap();
let mut writer = File::create("test.encrypted").unwrap();
// This needs to be generated here, otherwise we won't have access to it for encryption
let master_key = generate_master_key();
// Create a keyslot to be added to the header
// The password is cloned as we also need to provide this for the metadata
let mut keyslots: Vec<Keyslot> = Vec::new();
keyslots.push(
Keyslot::new(
KeyslotVersion::V1,
ALGORITHM,
HASHING_ALGORITHM,
password.clone(),
&master_key,
)
.unwrap(),
);
// Ideally this will be generated via the key management system
let md_salt = generate_salt();
let md = Metadata::new(
MetadataVersion::V1,
ALGORITHM,
HASHING_ALGORITHM,
password,
&md_salt,
&embedded_metadata,
)
.unwrap();
// Create the header for the encrypted file (and include our metadata)
let header = FileHeader::new(FileHeaderVersion::V1, ALGORITHM, keyslots, Some(md), None);
// Write the header to the file
header.write(&mut writer).unwrap();
// Use the nonce created by the header to initialise a stream encryption object
let encryptor = StreamEncryption::new(master_key, &header.nonce, header.algorithm).unwrap();
// Encrypt the data from the reader, and write it to the writer
// Use AAD so the header can be authenticated against every block of data
encryptor
.encrypt_streams(&mut reader, &mut writer, &header.generate_aad())
.unwrap();
}
pub fn decrypt_metadata() {
let password = Protected::new(b"password".to_vec());
// Open the encrypted file
let mut reader = File::open("test.encrypted").unwrap();
// Deserialize the header, keyslots, etc from the encrypted file
let (header, _) = FileHeader::deserialize(&mut reader).unwrap();
// Checks should be made to ensure the file actually contains any metadata
let metadata = header.metadata.unwrap();
// Hash the user's password with the metadata salt
// This should be done by a key management system
let hashed_key = metadata
.hashing_algorithm
.hash(password, metadata.salt)
.unwrap();
// Decrypt the metadata
let file_info: FileInformation = metadata.decrypt_metadata(hashed_key).unwrap();
println!("file name: {}", file_info.file_name);
}
fn main() {
encrypt();
decrypt_metadata();
}

View file

@ -0,0 +1,99 @@
use std::fs::File;
use sd_crypto::{
crypto::stream::{Algorithm, StreamEncryption},
header::{
file::{FileHeader, FileHeaderVersion},
keyslot::{Keyslot, KeyslotVersion},
preview_media::{PreviewMedia, PreviewMediaVersion},
},
keys::hashing::{HashingAlgorithm, Params},
primitives::{generate_master_key, generate_salt},
Protected,
};
const ALGORITHM: Algorithm = Algorithm::XChaCha20Poly1305;
const HASHING_ALGORITHM: HashingAlgorithm = HashingAlgorithm::Argon2id(Params::Standard);
fn encrypt() {
let password = Protected::new(b"password".to_vec());
// Open both the source and the output file
let mut reader = File::open("test").unwrap();
let mut writer = File::create("test.encrypted").unwrap();
// This needs to be generated here, otherwise we won't have access to it for encryption
let master_key = generate_master_key();
// Create a keyslot to be added to the header
// The password is cloned as we also need to provide this for the preview media
let mut keyslots: Vec<Keyslot> = Vec::new();
keyslots.push(
Keyslot::new(
KeyslotVersion::V1,
ALGORITHM,
HASHING_ALGORITHM,
password.clone(),
&master_key,
)
.unwrap(),
);
// Ideally this will be generated via the key management system
let pvm_salt = generate_salt();
let pvm_media = b"a nice mountain".to_vec();
let pvm = PreviewMedia::new(
PreviewMediaVersion::V1,
ALGORITHM,
HASHING_ALGORITHM,
password,
&pvm_salt,
&pvm_media,
)
.unwrap();
// Create the header for the encrypted file (and include our preview media)
let header = FileHeader::new(FileHeaderVersion::V1, ALGORITHM, keyslots, None, Some(pvm));
// Write the header to the file
header.write(&mut writer).unwrap();
// Use the nonce created by the header to initialise a stream encryption object
let encryptor = StreamEncryption::new(master_key, &header.nonce, header.algorithm).unwrap();
// Encrypt the data from the reader, and write it to the writer
// Use AAD so the header can be authenticated against every block of data
encryptor
.encrypt_streams(&mut reader, &mut writer, &header.generate_aad())
.unwrap();
}
pub fn decrypt_preview_media() {
let password = Protected::new(b"password".to_vec());
// Open the encrypted file
let mut reader = File::open("test.encrypted").unwrap();
// Deserialize the header, keyslots, etc from the encrypted file
let (header, _) = FileHeader::deserialize(&mut reader).unwrap();
// Checks should be made to ensure the file actually contains any preview media
let pvm = header.preview_media.unwrap();
// Hash the user's password with the preview media salt
// This should be done by a key management system
let hashed_key = pvm.hashing_algorithm.hash(password, pvm.salt).unwrap();
// Decrypt the preview media
let media = pvm.decrypt_preview_media(hashed_key).unwrap();
println!("{:?}", media.expose());
}
fn main() {
encrypt();
decrypt_preview_media();
}

View file

@ -0,0 +1,2 @@
//! This module contains all encryption and decryption items. These are used throughout the crate for all encryption/decryption needs.
pub mod stream;

View file

@ -0,0 +1,343 @@
//! This module contains the crate's STREAM implementation, and wrappers that allow us to support multiple AEADs.
use std::io::{Cursor, 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::BLOCK_SIZE, Protected};
/// These are all possible algorithms that can be used for encryption and decryption
#[derive(Clone, Copy, Eq, PartialEq)]
pub enum Algorithm {
XChaCha20Poly1305,
Aes256Gcm,
}
impl Algorithm {
/// This function allows us to calculate the nonce length for a given algorithm
#[must_use]
pub const fn nonce_len(&self) -> usize {
match self {
Self::XChaCha20Poly1305 => 20,
Self::Aes256Gcm => 8,
}
}
}
pub enum StreamEncryption {
XChaCha20Poly1305(Box<EncryptorLE31<XChaCha20Poly1305>>),
Aes256Gcm(Box<EncryptorLE31<Aes256Gcm>>),
}
pub enum StreamDecryption {
Aes256Gcm(Box<DecryptorLE31<Aes256Gcm>>),
XChaCha20Poly1305(Box<DecryptorLE31<XChaCha20Poly1305>>),
}
impl StreamEncryption {
/// This should be used to initialize a stream encryption object.
///
/// The master key, a suitable nonce, and a specific algorithm should be provided.
#[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() {
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)
}
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),
}
}
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),
}
}
/// This function should be used for encrypting large amounts of data.
///
/// The streaming implementation reads blocks of data in `BLOCK_SIZE`, encrypts, and writes to the writer.
///
/// Measures are in place to zeroize any buffers that may contain sensitive information.
///
/// It requires a reader, a writer, and any AAD to go with it.
///
/// The AAD will be authenticated with each block of data.
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].into_boxed_slice();
loop {
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);
}
break;
}
}
writer.flush().map_err(Error::Io)?;
Ok(())
}
/// This should ideally only be used for small amounts of data
///
/// It is just a thin wrapper around `encrypt_streams()`, but reduces the amount of code needed elsewhere.
#[allow(unused_mut)]
pub fn encrypt_bytes(
key: Protected<[u8; 32]>,
nonce: &[u8],
algorithm: Algorithm,
bytes: &[u8],
aad: &[u8],
) -> Result<Vec<u8>, Error> {
let mut reader = Cursor::new(bytes);
let mut writer = Cursor::new(Vec::<u8>::new());
let encryptor = Self::new(key, nonce, algorithm)?;
match encryptor.encrypt_streams(&mut reader, &mut writer, aad) {
Ok(_) => Ok(writer.into_inner()),
Err(e) => Err(e),
}
}
}
impl StreamDecryption {
/// This should be used to initialize a stream decryption object.
///
/// The master key, nonce and algorithm that were used for encryption should be provided.
#[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() {
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)
}
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),
}
}
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),
}
}
/// This function should be used for decrypting large amounts of data.
///
/// The streaming implementation reads blocks of data in `BLOCK_SIZE`, decrypts, and writes to the writer.
///
/// Measures are in place to zeroize any buffers that may contain sensitive information.
///
/// It requires a reader, a writer, and any AAD that was used.
///
/// The AAD will be authenticated with each block of data - if the AAD doesn't match what was used during encryption, an error will be returned.
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 + 16].into_boxed_slice();
loop {
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(|_| Error::Decrypt)?;
// Using `write` instead of `write_all` so we can check the amount of bytes written
// Zeroize buffer on write error
let write_count = writer.write(&decrypted_data).map_err(|e| {
decrypted_data.zeroize();
Error::Io(e)
})?;
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(|_| Error::Decrypt)?;
// Using `write` instead of `write_all` so we can check the amount of bytes written
// Zeroize buffer on write error
let write_count = writer.write(&decrypted_data).map_err(|e| {
decrypted_data.zeroize();
Error::Io(e)
})?;
decrypted_data.zeroize();
if read_count - 16 != write_count {
// -16 to account for the AEAD tag
return Err(Error::WriteMismatch);
}
break;
}
}
writer.flush().map_err(Error::Io)?;
Ok(())
}
/// This should ideally only be used for small amounts of data
///
/// It is just a thin wrapper around `decrypt_streams()`, but reduces the amount of code needed elsewhere.
#[allow(unused_mut)]
pub fn decrypt_bytes(
key: Protected<[u8; 32]>,
nonce: &[u8],
algorithm: Algorithm,
bytes: &[u8],
aad: &[u8],
) -> Result<Protected<Vec<u8>>, Error> {
let mut reader = Cursor::new(bytes);
let mut writer = Cursor::new(Vec::<u8>::new());
let decryptor = Self::new(key, nonce, algorithm)?;
match decryptor.decrypt_streams(&mut reader, &mut writer, aad) {
Ok(_) => Ok(Protected::new(writer.into_inner())),
Err(e) => Err(e),
}
}
}

View file

@ -1,3 +1,4 @@
//! This module contains all possible errors that this crate can return.
use thiserror::Error;
/// This enum defines all possible errors that this crate can give
@ -19,12 +20,20 @@ pub enum Error {
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,
#[error("error while parsing preview media length")]
MediaLengthParse,
#[error("no preview media found")]
NoPreviewMedia,
#[error("error while serializing/deserializing the metadata")]
MetadataDeSerialization,
#[error("no metadata found")]
NoMetadata,
#[error("tried adding too many keyslots to a header")]
TooManyKeyslots,
}

View file

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

View file

@ -1,78 +1,119 @@
use std::io::{Read, Seek, Write};
use zeroize::Zeroize;
//! This module contains a standard file header, and the functions needed to serialize/deserialize it.
//!
//! # Examples
//!
//! ```rust,ignore
//! let password = Protected::new(b"password".to_vec());
//!
//! let mut writer = File::create("test.encrypted").unwrap();
//!
//! // This needs to be generated here, otherwise we won't have access to it for encryption
//! let master_key = generate_master_key();
//!
//! // Create a keyslot to be added to the header
//! let mut keyslots: Vec<Keyslot> = Vec::new();
//! keyslots.push(
//! Keyslot::new(
//! KeyslotVersion::V1,
//! ALGORITHM,
//! HASHING_ALGORITHM,
//! password,
//! &master_key,
//! )
//! .unwrap(),
//! );
//!
//! // Create the header for the encrypted file
//! let header = FileHeader::new(FileHeaderVersion::V1, ALGORITHM, keyslots, None, None);
//!
//! // Write the header to the file
//! header.write(&mut writer).unwrap();
//! ```
use std::io::{Cursor, Read, Seek, SeekFrom, Write};
use crate::{
crypto::stream::Algorithm,
error::Error,
objects::memory::MemoryDecryption,
primitives::{Algorithm, Mode, MASTER_KEY_LEN},
protected::Protected,
primitives::{generate_nonce, MASTER_KEY_LEN},
Protected,
};
use super::keyslot::Keyslot;
use super::{keyslot::Keyslot, metadata::Metadata, preview_media::PreviewMedia};
/// These are used to quickly and easily identify Spacedrive-encrypted files
/// Random values - can be changed (up until 0.1.0)
/// These currently are set as "ballapp"
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 alleviate 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
/// This header is primarily used for encrypting/decrypting single files.
///
/// It has support for 2 keyslots (maximum).
///
/// You may optionally attach `Metadata` and `PreviewMedia` structs to this header, and they will be accessible on deserialization.
///
/// This contains everything necessary for decryption, and the entire header can be flaunted with no worries (provided a suitable password was selected by the user).
#[derive(Clone)]
pub struct FileHeader {
pub version: FileHeaderVersion,
pub algorithm: Algorithm,
pub mode: Mode,
pub nonce: Vec<u8>,
pub keyslots: Vec<Keyslot>,
pub metadata: Option<Metadata>,
pub preview_media: Option<PreviewMedia>,
}
/// This defines the main file header version
/// This defines the main file header version.
#[derive(Clone, Copy)]
pub enum FileHeaderVersion {
V1,
}
/// This includes the magic bytes at the start of the file, and remainder of the header itself (excluding keyslots, metadata, and preview media as these can all change)
#[must_use]
pub const fn aad_length(version: FileHeaderVersion) -> usize {
match version {
FileHeaderVersion::V1 => 36,
}
}
impl FileHeader {
/// This function is used for creating a file header.
#[must_use]
pub fn new(
version: FileHeaderVersion,
algorithm: Algorithm,
nonce: Vec<u8>,
keyslots: Vec<Keyslot>,
metadata: Option<Metadata>,
preview_media: Option<PreviewMedia>,
) -> Self {
let nonce = generate_nonce(algorithm);
Self {
version,
algorithm,
mode: Mode::Stream,
nonce,
keyslots,
metadata,
preview_media,
}
}
/// 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
/// This is a helper function to decrypt a master key from keyslots that are attached to a header.
///
/// You receive an error if the password doesn't match or if there are no keyslots.
#[allow(clippy::needless_pass_by_value)]
pub fn decrypt_master_key(
&self,
password: Protected<Vec<u8>>,
) -> Result<Protected<[u8; 32]>, Error> {
) -> Result<Protected<[u8; MASTER_KEY_LEN]>, 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)?;
if self.keyslots.is_empty() {
return Err(Error::NoKeyslots);
}
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)
{
for keyslot in &self.keyslots {
if let Ok(decrypted_master_key) = keyslot.decrypt_master_key(&password) {
master_key.copy_from_slice(&decrypted_master_key);
decrypted_master_key.zeroize();
}
}
@ -87,37 +128,48 @@ impl FileHeader {
where
W: Write + Seek,
{
writer.write(&self.serialize()).map_err(Error::Io)?;
writer.write(&self.serialize()?).map_err(Error::Io)?;
Ok(())
}
/// This function should be used for generating AAD before encryption
///
/// Use the return value from `FileHeader::deserialize()` for decryption
#[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.extend_from_slice(&MAGIC_BYTES); // 7
aad.extend_from_slice(&self.version.serialize()); // 9
aad.extend_from_slice(&self.algorithm.serialize()); // 11
aad.extend_from_slice(&self.nonce); // 19 OR 31
aad.extend_from_slice(&vec![0u8; 25 - self.nonce.len()]); // padded until 36 bytes
aad
}
}
}
#[must_use]
pub fn serialize(&self) -> Vec<u8> {
/// This function serializes a full header.
///
/// This will include keyslots, metadata and preview media (if provided)
///
/// An error will be returned if there are no keyslots/more than two keyslots attached.
pub fn serialize(&self) -> Result<Vec<u8>, Error> {
match self.version {
FileHeaderVersion::V1 => {
if self.keyslots.len() > 2 {
return Err(Error::TooManyKeyslots);
} else if self.keyslots.is_empty() {
return Err(Error::NoKeyslots);
}
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
header.extend_from_slice(&MAGIC_BYTES); // 7
header.extend_from_slice(&self.version.serialize()); // 9
header.extend_from_slice(&self.algorithm.serialize()); // 11
header.extend_from_slice(&self.nonce); // 19 OR 31
header.extend_from_slice(&vec![0u8; 25 - self.nonce.len()]); // padded until 36 bytes
for keyslot in &self.keyslots {
header.extend_from_slice(&keyslot.serialize());
@ -127,30 +179,24 @@ impl FileHeader {
header.extend_from_slice(&[0u8; 96]);
}
header
if let Some(metadata) = self.metadata.clone() {
header.extend_from_slice(&metadata.serialize());
}
if let Some(preview_media) = self.preview_media.clone() {
header.extend_from_slice(&preview_media.serialize());
}
Ok(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
/// This deserializes a header directly from a reader, and leaves the reader at the start of the encrypted data.
///
/// On error, the cursor will not be rewound.
///
/// It returns both the header, and the AAD that should be used for decryption.
pub fn deserialize<R>(reader: &mut R) -> Result<(Self, Vec<u8>), Error>
where
R: Read + Seek,
@ -167,54 +213,225 @@ impl FileHeader {
reader.read(&mut version).map_err(Error::Io)?;
let version = FileHeaderVersion::deserialize(version)?;
// Rewind so we can get the AAD
reader.rewind().map_err(Error::Io)?;
let mut aad = vec![0u8; aad_length(version)];
reader.read(&mut aad).map_err(Error::Io)?;
reader
.seek(SeekFrom::Start(MAGIC_BYTES.len() as u64 + 2))
.map_err(Error::Io)?;
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)];
let mut nonce = vec![0u8; algorithm.nonce_len()];
reader.read(&mut nonce).map_err(Error::Io)?;
// read and discard the padding
reader
.read(&mut vec![0u8; 24 - nonce.len()])
.read(&mut vec![0u8; 25 - nonce.len()])
.map_err(Error::Io)?;
let mut keyslot_bytes = [0u8; 192]; // length of 2x keyslots
let mut keyslots: Vec<Keyslot> = Vec::new();
reader.read(&mut keyslot_bytes).map_err(Error::Io)?;
let mut keyslot_reader = Cursor::new(keyslot_bytes);
for _ in 0..2 {
if let Ok(keyslot) = Keyslot::deserialize(reader) {
if let Ok(keyslot) = Keyslot::deserialize(&mut keyslot_reader) {
keyslots.push(keyslot);
}
}
let metadata = if let Ok(metadata) = Metadata::deserialize(reader) {
Some(metadata)
} else {
// header/aad area, keyslot area
reader.seek(SeekFrom::Start(36 + 192)).map_err(Error::Io)?;
None
};
let preview_media = if let Ok(preview_media) = PreviewMedia::deserialize(reader) {
Some(preview_media)
} else {
// header/aad area, keyslot area, full metadata length
if metadata.is_some() {
reader
.seek(SeekFrom::Start(
36 + 192 + metadata.clone().unwrap().get_length() as u64,
))
.map_err(Error::Io)?;
} else {
// header/aad area, keyslot area
reader.seek(SeekFrom::Start(36 + 192)).map_err(Error::Io)?;
}
None
};
Self {
version,
algorithm,
mode,
nonce,
keyslots,
metadata,
preview_media,
}
}
};
// 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))
}
}
#[cfg(test)]
mod test {
use crate::{
crypto::stream::Algorithm,
header::keyslot::{Keyslot, KeyslotVersion},
keys::hashing::{HashingAlgorithm, Params},
};
use std::io::Cursor;
use super::{FileHeader, FileHeaderVersion};
const HEADER_BYTES_NO_ADDITIONAL_OBJECTS: [u8; 228] = [
98, 97, 108, 108, 97, 112, 112, 10, 1, 11, 1, 230, 47, 48, 63, 225, 227, 15, 211, 115, 69,
169, 184, 184, 18, 110, 189, 167, 0, 144, 26, 0, 0, 0, 0, 0, 13, 1, 11, 1, 15, 1, 104, 176,
135, 146, 133, 75, 34, 155, 165, 148, 179, 133, 114, 245, 235, 117, 160, 55, 36, 93, 100,
83, 164, 171, 19, 57, 66, 65, 253, 42, 160, 239, 74, 205, 239, 253, 48, 239, 249, 203, 121,
126, 231, 52, 38, 49, 154, 254, 234, 41, 113, 169, 25, 195, 84, 78, 180, 212, 54, 4, 198,
109, 33, 216, 163, 148, 79, 207, 121, 142, 102, 39, 169, 31, 55, 41, 231, 248, 65, 131,
184, 216, 175, 202, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
];
#[test]
fn deserialize_header() {
let mut reader = Cursor::new(HEADER_BYTES_NO_ADDITIONAL_OBJECTS);
FileHeader::deserialize(&mut reader).unwrap();
}
#[test]
fn serialize_header() {
let header: FileHeader = FileHeader {
version: FileHeaderVersion::V1,
algorithm: Algorithm::XChaCha20Poly1305,
nonce: [
230, 47, 48, 63, 225, 227, 15, 211, 115, 69, 169, 184, 184, 18, 110, 189, 167, 0,
144, 26,
]
.to_vec(),
keyslots: [Keyslot {
version: KeyslotVersion::V1,
algorithm: Algorithm::XChaCha20Poly1305,
hashing_algorithm: HashingAlgorithm::Argon2id(Params::Standard),
salt: [
104, 176, 135, 146, 133, 75, 34, 155, 165, 148, 179, 133, 114, 245, 235, 117,
],
master_key: [
160, 55, 36, 93, 100, 83, 164, 171, 19, 57, 66, 65, 253, 42, 160, 239, 74, 205,
239, 253, 48, 239, 249, 203, 121, 126, 231, 52, 38, 49, 154, 254, 234, 41, 113,
169, 25, 195, 84, 78, 180, 212, 54, 4, 198, 109, 33, 216,
],
nonce: [
163, 148, 79, 207, 121, 142, 102, 39, 169, 31, 55, 41, 231, 248, 65, 131, 184,
216, 175, 202,
]
.to_vec(),
}]
.to_vec(),
metadata: None,
preview_media: None,
};
let header_bytes = header.serialize().unwrap();
assert_eq!(HEADER_BYTES_NO_ADDITIONAL_OBJECTS.to_vec(), header_bytes)
}
#[test]
#[should_panic]
fn serialize_header_with_too_many_keyslots() {
let header: FileHeader = FileHeader {
version: FileHeaderVersion::V1,
algorithm: Algorithm::XChaCha20Poly1305,
nonce: [
230, 47, 48, 63, 225, 227, 15, 211, 115, 69, 169, 184, 184, 18, 110, 189, 167, 0,
144, 26,
]
.to_vec(),
keyslots: [
Keyslot {
version: KeyslotVersion::V1,
algorithm: Algorithm::XChaCha20Poly1305,
hashing_algorithm: HashingAlgorithm::Argon2id(Params::Standard),
salt: [
104, 176, 135, 146, 133, 75, 34, 155, 165, 148, 179, 133, 114, 245, 235,
117,
],
master_key: [
160, 55, 36, 93, 100, 83, 164, 171, 19, 57, 66, 65, 253, 42, 160, 239, 74,
205, 239, 253, 48, 239, 249, 203, 121, 126, 231, 52, 38, 49, 154, 254, 234,
41, 113, 169, 25, 195, 84, 78, 180, 212, 54, 4, 198, 109, 33, 216,
],
nonce: [
163, 148, 79, 207, 121, 142, 102, 39, 169, 31, 55, 41, 231, 248, 65, 131,
184, 216, 175, 202,
]
.to_vec(),
},
Keyslot {
version: KeyslotVersion::V1,
algorithm: Algorithm::XChaCha20Poly1305,
hashing_algorithm: HashingAlgorithm::Argon2id(Params::Standard),
salt: [
104, 176, 135, 146, 133, 75, 34, 155, 165, 148, 179, 133, 114, 245, 235,
117,
],
master_key: [
160, 55, 36, 93, 100, 83, 164, 171, 19, 57, 66, 65, 253, 42, 160, 239, 74,
205, 239, 253, 48, 239, 249, 203, 121, 126, 231, 52, 38, 49, 154, 254, 234,
41, 113, 169, 25, 195, 84, 78, 180, 212, 54, 4, 198, 109, 33, 216,
],
nonce: [
163, 148, 79, 207, 121, 142, 102, 39, 169, 31, 55, 41, 231, 248, 65, 131,
184, 216, 175, 202,
]
.to_vec(),
},
Keyslot {
version: KeyslotVersion::V1,
algorithm: Algorithm::XChaCha20Poly1305,
hashing_algorithm: HashingAlgorithm::Argon2id(Params::Standard),
salt: [
104, 176, 135, 146, 133, 75, 34, 155, 165, 148, 179, 133, 114, 245, 235,
117,
],
master_key: [
160, 55, 36, 93, 100, 83, 164, 171, 19, 57, 66, 65, 253, 42, 160, 239, 74,
205, 239, 253, 48, 239, 249, 203, 121, 126, 231, 52, 38, 49, 154, 254, 234,
41, 113, 169, 25, 195, 84, 78, 180, 212, 54, 4, 198, 109, 33, 216,
],
nonce: [
163, 148, 79, 207, 121, 142, 102, 39, 169, 31, 55, 41, 231, 248, 65, 131,
184, 216, 175, 202,
]
.to_vec(),
},
]
.to_vec(),
metadata: None,
preview_media: None,
};
header.serialize().unwrap();
}
}

View file

@ -1,50 +1,112 @@
//! This module contains the keyslot header item.
//!
//! At least one keyslot needs to be attached to a main header.
//!
//! Headers have limitations on the maximum amount of keyslots, and you should double check before usage.
//!
//! The `Keyslot::new()` function should always be used to create a keyslot, as it handles encrypting the master key.
//!
//! # Examples
//!
//! ```rust,ignore
//! use sd_crypto::header::keyslot::{Keyslot, KeyslotVersion};
//! use sd_crypto::Protected;
//! use sd_crypto::keys::hashing::{HashingAlgorithm, Params};
//! use sd_crypto::crypto::stream::Algorithm;
//! use sd_crypto::primitives::generate_master_key;
//!
//!
//! let user_password = Protected::new(b"password".to_vec());
//! let master_key = generate_master_key();
//!
//! let keyslot = Keyslot::new(KeyslotVersion::V1, Algorithm::XChaCha20Poly1305, HashingAlgorithm::Argon2id(Params::Standard), user_password, &master_key).unwrap();
//! ```
use std::io::{Read, Seek};
use crate::{
crypto::stream::{Algorithm, StreamDecryption, StreamEncryption},
error::Error,
primitives::{Algorithm, HashingAlgorithm, Mode, ENCRYPTED_MASTER_KEY_LEN, SALT_LEN},
keys::hashing::HashingAlgorithm,
primitives::{
generate_nonce, generate_salt, to_array, ENCRYPTED_MASTER_KEY_LEN, MASTER_KEY_LEN, SALT_LEN,
},
Protected,
};
/// A keyslot. 96 bytes, and contains all the information for future-proofing while keeping the size reasonable
/// A keyslot - 96 bytes (as of V1), 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
/// The algorithm (should) be inherited from the parent (the header, in this case), but that's not a guarantee so we include it here too
#[derive(Clone)]
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
///
/// The goal is to not increment this much, but it's here in case we need to make breaking changes
#[derive(Clone, Copy)]
pub enum KeyslotVersion {
V1,
}
impl Keyslot {
#[must_use]
/// This should be used for creating a keyslot.
///
/// This handles generating the nonce/salt, and encrypting the master key.
///
/// You will need to provide the password, and a generated master key (this can't generate it, otherwise it can't be used elsewhere)
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 {
password: Protected<Vec<u8>>,
master_key: &Protected<[u8; MASTER_KEY_LEN]>,
) -> Result<Self, Error> {
let salt = generate_salt();
let nonce = generate_nonce(algorithm);
let hashed_password = hashing_algorithm.hash(password, salt).unwrap();
let encrypted_master_key: [u8; 48] = to_array(StreamEncryption::encrypt_bytes(
hashed_password,
&nonce,
algorithm,
master_key.expose(),
&[],
)?)?;
Ok(Self {
version,
algorithm,
hashing_algorithm,
mode: Mode::Memory,
salt,
master_key: encrypted_master_key,
nonce,
}
})
}
/// This function should not be used directly, use `header.decrypt_master_key()` instead
///
/// This attempts to decrypt the master key for a single keyslot
///
/// An error will be returned on failure.
pub fn decrypt_master_key(
&self,
password: &Protected<Vec<u8>>,
) -> Result<Protected<Vec<u8>>, Error> {
let key = self
.hashing_algorithm
.hash(password.clone(), self.salt)
.map_err(|_| Error::PasswordHash)?;
StreamDecryption::decrypt_bytes(key, &self.nonce, self.algorithm, &self.master_key, &[])
}
/// This function is used to serialize a keyslot into bytes
#[must_use]
pub fn serialize(&self) -> Vec<u8> {
@ -54,17 +116,20 @@ impl Keyslot {
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.extend_from_slice(&self.salt); // 22
keyslot.extend_from_slice(&self.master_key); // 70
keyslot.extend_from_slice(&self.nonce); // 78 or 90
keyslot.extend_from_slice(&vec![0u8; 26 - self.nonce.len()]); // 96 total bytes
keyslot
}
}
}
/// This function reads a keyslot from a reader, and attempts to serialize a keyslot
/// This function reads a keyslot from a reader
///
/// It will leave the cursor at the end of the keyslot on success
///
/// The cursor will not be rewound on error.
pub fn deserialize<R>(reader: &mut R) -> Result<Self, Error>
where
R: Read + Seek,
@ -83,17 +148,13 @@ impl Keyslot {
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)];
let mut nonce = vec![0u8; algorithm.nonce_len()];
reader.read(&mut nonce).map_err(Error::Io)?;
reader
@ -104,7 +165,6 @@ impl Keyslot {
version,
algorithm,
hashing_algorithm,
mode,
salt,
master_key,
nonce,

View file

@ -0,0 +1,250 @@
//! This module contains the metadata header item.
//!
//! This is an optional item, and anything that may be serialized with `serde` can be used here.
//!
//! # Examples
//!
//! ```rust,ignore
//! #[derive(Serialize, Deserialize)]
//! pub struct FileInformation {
//! pub file_name: String,
//! }
//!
//! let embedded_metadata = FileInformation {
//! file_name: "filename.txt".to_string(),
//! };
//!
//! // Ideally this will be generated via a key management system
//! let md_salt = generate_salt();
//!
//! let md = Metadata::new(
//! MetadataVersion::V1,
//! ALGORITHM,
//! HASHING_ALGORITHM,
//! password,
//! &md_salt,
//! &embedded_metadata,
//! )
//! .unwrap();
//! ```
use std::io::{Read, Seek};
use crate::{
crypto::stream::{Algorithm, StreamDecryption, StreamEncryption},
error::Error,
keys::hashing::HashingAlgorithm,
primitives::{
generate_master_key, generate_nonce, to_array, ENCRYPTED_MASTER_KEY_LEN, MASTER_KEY_LEN,
SALT_LEN,
},
Protected,
};
/// This is a metadata header item. You may add it to a header, and this will be stored with the file.
///
/// The `Metadata::new()` function handles master key and metadata encryption.
///
/// The salt should be generated elsewhere (e.g. a key management system).
#[derive(Clone)]
pub struct Metadata {
pub version: MetadataVersion,
pub algorithm: Algorithm, // encryption algorithm
pub hashing_algorithm: HashingAlgorithm, // password hashing algorithm
pub salt: [u8; SALT_LEN],
pub master_key: [u8; ENCRYPTED_MASTER_KEY_LEN],
pub master_key_nonce: Vec<u8>,
pub metadata_nonce: Vec<u8>,
pub metadata: Vec<u8>,
}
#[derive(Clone, Copy)]
pub enum MetadataVersion {
V1,
}
impl Metadata {
/// This should be used for creating a header metadata item.
///
/// It handles encrypting the master key and metadata.
///
/// You will need to provide the user's password, and a semi-universal salt for hashing the user's password. This allows for extremely fast decryption.
///
/// Metadata needs to be accessed switfly, so a key management system should handle the salt generation.
pub fn new<T>(
version: MetadataVersion,
algorithm: Algorithm,
hashing_algorithm: HashingAlgorithm,
password: Protected<Vec<u8>>,
salt: &[u8; SALT_LEN],
media: &T,
) -> Result<Self, Error>
where
T: ?Sized + serde::Serialize,
{
let metadata_nonce = generate_nonce(algorithm);
let master_key_nonce = generate_nonce(algorithm);
let master_key = generate_master_key();
let hashed_password = hashing_algorithm.hash(password, *salt)?;
let encrypted_master_key: [u8; 48] = to_array(StreamEncryption::encrypt_bytes(
hashed_password,
&master_key_nonce,
algorithm,
master_key.expose(),
&[],
)?)?;
let encrypted_metadata = StreamEncryption::encrypt_bytes(
master_key,
&metadata_nonce,
algorithm,
&serde_json::to_vec(media).map_err(|_| Error::MetadataDeSerialization)?,
&[],
)?;
Ok(Self {
version,
algorithm,
hashing_algorithm,
salt: *salt,
master_key: encrypted_master_key,
master_key_nonce,
metadata_nonce,
metadata: encrypted_metadata,
})
}
#[must_use]
pub fn get_length(&self) -> usize {
match self.version {
MetadataVersion::V1 => 128 + self.metadata.len(),
}
}
/// This function is used to serialize a metadata item into bytes
///
/// This also includes the encrypted metadata itself, so this may be sizeable
#[must_use]
pub fn serialize(&self) -> Vec<u8> {
match self.version {
MetadataVersion::V1 => {
let mut metadata: Vec<u8> = Vec::new();
metadata.extend_from_slice(&self.version.serialize()); // 2
metadata.extend_from_slice(&self.algorithm.serialize()); // 4
metadata.extend_from_slice(&self.hashing_algorithm.serialize()); // 6
metadata.extend_from_slice(&self.salt); // 22
metadata.extend_from_slice(&self.master_key); // 70
metadata.extend_from_slice(&self.master_key_nonce); // 82 or 94
metadata.extend_from_slice(&vec![0u8; 26 - self.master_key_nonce.len()]); // 96
metadata.extend_from_slice(&self.metadata_nonce); // 108 or 120
metadata.extend_from_slice(&vec![0u8; 24 - self.metadata_nonce.len()]); // 120
metadata.extend_from_slice(&self.metadata.len().to_le_bytes()); // 128 total bytes
metadata.extend_from_slice(&self.metadata); // this can vary in length
metadata
}
}
}
/// This function should be used to retrieve the metadata for a file
///
/// All it requires is a pre-hashed key (hashed with the salt provided on creation)
///
/// A deserialized data type will be returned from this function
pub fn decrypt_metadata<T>(&self, hashed_key: Protected<[u8; 32]>) -> Result<T, Error>
where
T: serde::de::DeserializeOwned,
{
let mut master_key = [0u8; MASTER_KEY_LEN];
let master_key = if let Ok(decrypted_master_key) = StreamDecryption::decrypt_bytes(
hashed_key,
&self.master_key_nonce,
self.algorithm,
&self.master_key,
&[],
) {
master_key.copy_from_slice(&decrypted_master_key);
Ok(Protected::new(master_key))
} else {
Err(Error::IncorrectPassword)
}?;
let metadata = StreamDecryption::decrypt_bytes(
master_key,
&self.metadata_nonce,
self.algorithm,
&self.metadata,
&[],
)?;
serde_json::from_slice::<T>(&metadata).map_err(|_| Error::MetadataDeSerialization)
}
/// This function reads a metadata header item from a reader
///
/// The cursor will be left at the end of the metadata item on success
///
/// The cursor will not be rewound on error.
pub fn deserialize<R>(reader: &mut R) -> Result<Self, Error>
where
R: Read + Seek,
{
let mut version = [0u8; 2];
reader.read(&mut version).map_err(Error::Io)?;
let version = MetadataVersion::deserialize(version).map_err(|_| Error::NoMetadata)?;
match version {
MetadataVersion::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 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 master_key_nonce = vec![0u8; algorithm.nonce_len()];
reader.read(&mut master_key_nonce).map_err(Error::Io)?;
reader
.read(&mut vec![0u8; 26 - master_key_nonce.len()])
.map_err(Error::Io)?;
let mut metadata_nonce = vec![0u8; algorithm.nonce_len()];
reader.read(&mut metadata_nonce).map_err(Error::Io)?;
reader
.read(&mut vec![0u8; 24 - metadata_nonce.len()])
.map_err(Error::Io)?;
let mut metadata_length = [0u8; 8];
reader.read(&mut metadata_length).map_err(Error::Io)?;
let metadata_length: usize = usize::from_le_bytes(metadata_length);
let mut metadata = vec![0u8; metadata_length];
reader.read(&mut metadata).map_err(Error::Io)?;
let metadata = Self {
version,
algorithm,
hashing_algorithm,
salt,
master_key,
master_key_nonce,
metadata_nonce,
metadata,
};
Ok(metadata)
}
}
}
}

View file

@ -1,6 +1,8 @@
//! This module will contain all encrypted header related functions, information, etc.
//! It'll handle serialisation, deserialisation, AAD, keyslots and everything else
//! This module will contains all header related functions.
//!
//! It handles serialisation, deserialisation, AAD, keyslots and metadata, preview media.
pub mod file;
pub mod keyslot;
pub mod metadata;
pub mod preview_media;
pub mod serialization;

View file

@ -0,0 +1,236 @@
//! This module contains the preview media header item.
//!
//! It is an optional extension to a header, and is intended for video/image thumbnails.
//!
//! # Examples
//!
//! ```rust,ignore
//! // Ideally this will be generated via a key management system
//! let pvm_salt = generate_salt();
//!
//! let pvm_media = b"a nice mountain".to_vec();
//!
//! let pvm = PreviewMedia::new(
//! PreviewMediaVersion::V1,
//! ALGORITHM,
//! HASHING_ALGORITHM,
//! password,
//! &pvm_salt,
//! &pvm_media,
//! )
//! .unwrap();
//! ```
use std::io::{Read, Seek};
use crate::{
crypto::stream::{Algorithm, StreamDecryption, StreamEncryption},
error::Error,
keys::hashing::HashingAlgorithm,
primitives::{
generate_master_key, generate_nonce, to_array, ENCRYPTED_MASTER_KEY_LEN, MASTER_KEY_LEN,
SALT_LEN,
},
Protected,
};
/// This is a preview media header item. You may add it to a header, and this will be stored with the file.
///
/// The `Metadata::new()` function handles master key and metadata encryption.
///
/// The salt should be generated elsewhere (e.g. a key management system).
#[derive(Clone)]
pub struct PreviewMedia {
pub version: PreviewMediaVersion,
pub algorithm: Algorithm, // encryption algorithm
pub hashing_algorithm: HashingAlgorithm, // password hashing algorithm
pub salt: [u8; SALT_LEN],
pub master_key: [u8; ENCRYPTED_MASTER_KEY_LEN],
pub master_key_nonce: Vec<u8>,
pub media_nonce: Vec<u8>,
pub media: Vec<u8>,
}
#[derive(Clone, Copy)]
pub enum PreviewMediaVersion {
V1,
}
impl PreviewMedia {
/// This should be used for creating a header preview media item.
///
/// This handles encrypting the master key and preview media.
///
/// You will need to provide the user's password, and a semi-universal salt for hashing the user's password. This allows for extremely fast decryption.
///
/// Preview media needs to be accessed switfly, so a key management system should handle the salt generation.
pub fn new(
version: PreviewMediaVersion,
algorithm: Algorithm,
hashing_algorithm: HashingAlgorithm,
password: Protected<Vec<u8>>,
salt: &[u8; SALT_LEN],
media: &[u8],
) -> Result<Self, Error> {
let media_nonce = generate_nonce(algorithm);
let master_key_nonce = generate_nonce(algorithm);
let master_key = generate_master_key();
let hashed_password = hashing_algorithm.hash(password, *salt)?;
let encrypted_master_key: [u8; 48] = to_array(StreamEncryption::encrypt_bytes(
hashed_password,
&master_key_nonce,
algorithm,
master_key.expose(),
&[],
)?)?;
let encrypted_media =
StreamEncryption::encrypt_bytes(master_key, &media_nonce, algorithm, media, &[])?;
Ok(Self {
version,
algorithm,
hashing_algorithm,
salt: *salt,
master_key: encrypted_master_key,
master_key_nonce,
media_nonce,
media: encrypted_media,
})
}
#[must_use]
pub fn get_length(&self) -> usize {
match self.version {
PreviewMediaVersion::V1 => 128 + self.media.len(),
}
}
/// This function is used to serialize a preview media header item into bytes
///
/// This also includes the encrypted preview media itself, so this may be sizeable
#[must_use]
pub fn serialize(&self) -> Vec<u8> {
match self.version {
PreviewMediaVersion::V1 => {
let mut preview_media: Vec<u8> = Vec::new();
preview_media.extend_from_slice(&self.version.serialize()); // 2
preview_media.extend_from_slice(&self.algorithm.serialize()); // 4
preview_media.extend_from_slice(&self.hashing_algorithm.serialize()); // 6
preview_media.extend_from_slice(&self.salt); // 22
preview_media.extend_from_slice(&self.master_key); // 70
preview_media.extend_from_slice(&self.master_key_nonce); // 82 or 94
preview_media.extend_from_slice(&vec![0u8; 26 - self.master_key_nonce.len()]); // 96
preview_media.extend_from_slice(&self.media_nonce); // 108 or 120
preview_media.extend_from_slice(&vec![0u8; 24 - self.media_nonce.len()]); // 120
preview_media.extend_from_slice(&self.media.len().to_le_bytes()); // 128 total bytes
preview_media.extend_from_slice(&self.media); // this can vary in length
preview_media
}
}
}
/// This function is what you'll want to use to get the preview media for a file
///
/// All it requires is a pre-hashed key (hashed with the salt provided on creation)
///
/// Once provided, a `Vec<u8>` is returned that contains the preview media
pub fn decrypt_preview_media(
&self,
hashed_key: Protected<[u8; 32]>,
) -> Result<Protected<Vec<u8>>, Error> {
let mut master_key = [0u8; MASTER_KEY_LEN];
let master_key = if let Ok(decrypted_master_key) = StreamDecryption::decrypt_bytes(
hashed_key,
&self.master_key_nonce,
self.algorithm,
&self.master_key,
&[],
) {
master_key.copy_from_slice(&decrypted_master_key);
Ok(Protected::new(master_key))
} else {
Err(Error::IncorrectPassword)
}?;
let media = StreamDecryption::decrypt_bytes(
master_key,
&self.media_nonce,
self.algorithm,
&self.media,
&[],
)?;
Ok(media)
}
/// This function reads a preview media header item from a reader
///
/// The cursor will be left at the end of the preview media item on success
///
/// The cursor will not be rewound on error.
pub fn deserialize<R>(reader: &mut R) -> Result<Self, Error>
where
R: Read + Seek,
{
let mut version = [0u8; 2];
reader.read(&mut version).map_err(Error::Io)?;
let version =
PreviewMediaVersion::deserialize(version).map_err(|_| Error::NoPreviewMedia)?;
match version {
PreviewMediaVersion::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 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 master_key_nonce = vec![0u8; algorithm.nonce_len()];
reader.read(&mut master_key_nonce).map_err(Error::Io)?;
reader
.read(&mut vec![0u8; 26 - master_key_nonce.len()])
.map_err(Error::Io)?;
let mut media_nonce = vec![0u8; algorithm.nonce_len()];
reader.read(&mut media_nonce).map_err(Error::Io)?;
reader
.read(&mut vec![0u8; 24 - media_nonce.len()])
.map_err(Error::Io)?;
let mut media_length = [0u8; 8];
reader.read(&mut media_length).map_err(Error::Io)?;
let media_length: usize = usize::from_le_bytes(media_length);
let mut media = vec![0u8; media_length];
reader.read(&mut media).map_err(Error::Io)?;
let preview_media = Self {
version,
algorithm,
hashing_algorithm,
salt,
master_key,
master_key_nonce,
media_nonce,
media,
};
Ok(preview_media)
}
}
}
}

View file

@ -1,14 +1,16 @@
//! This module defines all of the serialization and deserialization rules for the headers
//! This module defines all of the serialization and deserialization rules for the header items
//!
//! It contains byte -> enum and enum -> byte conversions for everything that could be written to a header (except headers and keyslots themselves)
//! It contains `byte -> enum` and `enum -> byte` conversions for everything that could be written to a header (except headers, keyslots, and other header items)
use crate::{
crypto::stream::Algorithm,
error::Error,
keys::hashing::Params,
primitives::{Algorithm, HashingAlgorithm, Mode},
keys::hashing::{HashingAlgorithm, Params},
};
use super::{file::FileHeaderVersion, keyslot::KeyslotVersion};
use super::{
file::FileHeaderVersion, keyslot::KeyslotVersion, metadata::MetadataVersion,
preview_media::PreviewMediaVersion,
};
impl FileHeaderVersion {
#[must_use]
@ -42,6 +44,38 @@ impl KeyslotVersion {
}
}
impl PreviewMediaVersion {
#[must_use]
pub const fn serialize(&self) -> [u8; 2] {
match self {
Self::V1 => [0x0E, 0x01],
}
}
pub const fn deserialize(bytes: [u8; 2]) -> Result<Self, Error> {
match bytes {
[0x0E, 0x01] => Ok(Self::V1),
_ => Err(Error::FileHeader),
}
}
}
impl MetadataVersion {
#[must_use]
pub const fn serialize(&self) -> [u8; 2] {
match self {
Self::V1 => [0x1F, 0x01],
}
}
pub const fn deserialize(bytes: [u8; 2]) -> Result<Self, Error> {
match bytes {
[0x1F, 0x01] => Ok(Self::V1),
_ => Err(Error::FileHeader),
}
}
}
impl HashingAlgorithm {
#[must_use]
pub const fn serialize(&self) -> [u8; 2] {
@ -81,21 +115,3 @@ impl Algorithm {
}
}
}
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

@ -1,10 +1,22 @@
use crate::protected::Protected;
//! This module contains all password-hashing related functions.
//!
//! Everything contained within is used to hash a user's password into strong key material, suitable for encrypting master keys.
//!
//! # Examples
//!
//! ```rust,ignore
//! let password = Protected::new(b"password".to_vec());
//! let hashing_algorithm = HashingAlgorithm::Argon2id(Params::Standard);
//! let salt = generate_salt();
//! let hashed_password = hashing_algorithm.hash(password, salt).unwrap();
//! ```
use crate::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
/// These parameters define the password-hashing level.
///
/// The harder the parameter, the longer the password will take to hash.
#[derive(Clone, Copy)]
pub enum Params {
Standard,
@ -12,13 +24,37 @@ pub enum Params {
Paranoid,
}
/// This defines all available password hashing algorithms.
#[derive(Clone, Copy)]
pub enum HashingAlgorithm {
Argon2id(Params),
}
impl HashingAlgorithm {
/// This function should be used to hash passwords
///
/// It also handles all the password hashing parameters.
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 Params {
/// This function is used to generate parameters for password hashing.
///
/// This should not be called directly. Call it via the `HashingAlgorithm` struct (e.g. `HashingAlgorithm::Argon2id(Params::Standard).hash()`)
#[must_use]
pub fn 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
// It's very hardware dependant but we should aim for at least 64MB of RAM usage on standard
// Provided they all take one (ish) second or longer, and less than 3/4 seconds (for paranoid), they will be fine
// It's not so much the parameters themselves that matter, it's the duration (and ensuring that they use enough RAM to hinder ASIC brute-force attacks)
Self::Standard => {
@ -37,7 +73,6 @@ impl Params {
}
}
// 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()`)

View file

@ -1 +1,2 @@
//! This module contains all key and hashing related functions.
pub mod hashing;

View file

@ -11,13 +11,18 @@
#![allow(clippy::module_name_repetitions)]
#![allow(clippy::similar_names)]
pub mod crypto;
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;
// Make this easier to use (e.g. `sd_crypto::Protected`)
pub use protected::Protected;
// Re-export zeroize so it can be used elsewhere
pub use zeroize::Zeroize;

View file

@ -1,84 +0,0 @@
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

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

View file

@ -1,258 +0,0 @@
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

@ -1,17 +1,18 @@
//! This module contains constant values and functions that are used around the crate.
//!
//! This includes things such as cryptographically-secure random salt/master key/nonce generation,
//! lengths for master keys and even the streaming block size.
use rand::{RngCore, SeedableRng};
use zeroize::Zeroize;
use crate::{
error::Error,
keys::hashing::{password_hash_argon2id, Params},
protected::Protected,
};
use crate::{crypto::stream::Algorithm, error::Error, Protected};
// This is the default salt size, and the recommended size for argon2id.
/// 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)
/// The size used for streaming encryption/decryption. This size seems to offer the best performance compared to alternatives.
///
/// The file size gain is 16 bytes per 1048576 bytes (due to the AEAD tag)
pub const BLOCK_SIZE: usize = 1_048_576;
/// The length of the encrypted master key
@ -20,75 +21,21 @@ 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)
/// This should be used for generating nonces for encryption.
///
/// Memory loads all data into memory before encryption, and encrypts it in one pass
/// An algorithm is required so this function can calculate the length of the nonce.
///
/// 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
/// This function uses `ChaCha20Rng` for generating cryptographically-secure random data
#[must_use]
pub fn generate_nonce(len: usize) -> Vec<u8> {
let mut nonce = vec![0u8; len];
pub fn generate_nonce(algorithm: Algorithm) -> Vec<u8> {
let mut nonce = vec![0u8; algorithm.nonce_len()];
rand_chacha::ChaCha20Rng::from_entropy().fill_bytes(&mut nonce);
nonce
}
/// This function uses `ChaCha20Rng` for cryptographically-securely generating random data
/// This should be used for generating salts for hashing.
///
/// This function uses `ChaCha20Rng` for generating cryptographically-secure random data
#[must_use]
pub fn generate_salt() -> [u8; SALT_LEN] {
let mut salt = [0u8; SALT_LEN];
@ -98,9 +45,9 @@ pub fn generate_salt() -> [u8; SALT_LEN] {
/// This generates a master key, which should be used for encrypting the data
///
/// This is then stored encrypted in the header
/// This is then stored (encrypted) within the header.
///
/// This function uses `ChaCha20Rng` for cryptographically-securely generating random data
/// This function uses `ChaCha20Rng` for generating cryptographically-secure random data
#[must_use]
pub fn generate_master_key() -> Protected<[u8; MASTER_KEY_LEN]> {
let mut master_key = [0u8; MASTER_KEY_LEN];

View file

@ -17,7 +17,9 @@
//!
//! # Examples
//!
//! ```rust,ignore
//! ```rust
//! use sd_crypto::Protected;
//!
//! let secret_data = "this is classified information".to_string();
//! let protected_data = Protected::new(secret_data);
//!
@ -26,7 +28,6 @@
//! let value = protected_data.expose();
//! ```
//!
use std::fmt::Debug;
use zeroize::Zeroize;
@ -60,6 +61,10 @@ where
pub const fn expose(&self) -> &T {
&self.data
}
pub fn zeroize(mut self) {
self.data.zeroize();
}
}
impl<T> Drop for Protected<T>