mirror of
https://github.com/spacedriveapp/spacedrive
synced 2024-07-04 11:03:27 +00:00
[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:
parent
c0b51bcd4a
commit
b5c571541e
2
Cargo.lock
generated
2
Cargo.lock
generated
|
@ -5199,6 +5199,8 @@ dependencies = [
|
|||
"chacha20poly1305",
|
||||
"rand 0.8.5",
|
||||
"rand_chacha 0.3.1",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"thiserror",
|
||||
"zeroize",
|
||||
]
|
||||
|
|
|
@ -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"
|
82
crates/crypto/examples/single_file.rs
Normal file
82
crates/crypto/examples/single_file.rs
Normal 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();
|
||||
}
|
110
crates/crypto/examples/single_file_with_metadata.rs
Normal file
110
crates/crypto/examples/single_file_with_metadata.rs
Normal 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();
|
||||
}
|
99
crates/crypto/examples/single_file_with_preview_media.rs
Normal file
99
crates/crypto/examples/single_file_with_preview_media.rs
Normal 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();
|
||||
}
|
2
crates/crypto/src/crypto/mod.rs
Normal file
2
crates/crypto/src/crypto/mod.rs
Normal 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;
|
343
crates/crypto/src/crypto/stream.rs
Normal file
343
crates/crypto/src/crypto/stream.rs
Normal 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),
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
}
|
||||
|
|
|
@ -1 +1 @@
|
|||
/// This is a placeholder file
|
||||
// This is a placeholder 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();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
250
crates/crypto/src/header/metadata.rs
Normal file
250
crates/crypto/src/header/metadata.rs
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
|
|
236
crates/crypto/src/header/preview_media.rs
Normal file
236
crates/crypto/src/header/preview_media.rs
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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()`)
|
||||
|
|
|
@ -1 +1,2 @@
|
|||
//! This module contains all key and hashing related functions.
|
||||
pub mod hashing;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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),
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,2 +0,0 @@
|
|||
pub mod memory;
|
||||
pub mod stream;
|
|
@ -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(())
|
||||
}
|
||||
}
|
|
@ -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];
|
||||
|
|
|
@ -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>
|
||||
|
|
Loading…
Reference in a new issue