[ENG-258] Key Manager and Crypto Crate Improvements (#423)

* add base keymanager structs/functions

* change md/pvm `new()` to prevent useless re-hashing

* update lockfile

* update keymanager

* cleanup code, make things easier to understand

* move md and pvm construction to the header

* update pvm ser/de

* update metadata ser/de

* additional API changes, update example

* formatting and clippy

* update examples

* move `impl` to associated files

* formatting+clippy

* add more keymanager functionality

* add docs

* wrap `master_password` as an option

* add `sd-crypto` crate as a dependency to `core`

* add key manager to `LibraryContext`

* rename `id` -> `uuid`

* add more keymanager functions

* add `set_master_password()` function

* function to see if keymanager has master password

* update schema

* add default bool to schema

* populate keystore on SD startup

* clippy+formatting

* implement requested changes

* remove unwrap

* fmt, clippy, remove ser/de derives

Co-authored-by: Brendan Allan <brendonovich@outlook.com>
This commit is contained in:
jake 2022-10-23 11:47:41 +01:00 committed by GitHub
parent 8ac42c0353
commit 0c7aed5f86
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
18 changed files with 690 additions and 272 deletions

2
Cargo.lock generated
View file

@ -5186,6 +5186,7 @@ dependencies = [
"rmp",
"rmp-serde",
"rspc",
"sd-crypto",
"sd-ffmpeg",
"sd-file-ext",
"serde",
@ -5235,6 +5236,7 @@ dependencies = [
"serde",
"serde_json",
"thiserror",
"uuid 1.2.1",
"zeroize",
]

View file

@ -54,7 +54,8 @@ image = "0.24.4"
webp = "0.2.2"
ffmpeg-next = { version = "5.1.1", optional = true, features = [] }
sd-ffmpeg = { path = "../crates/ffmpeg", optional = true }
sd-file-ext = { path = "../crates/file-ext" }
sd-crypto = { path = "../crates/crypto" }
sd-file-ext = { path = "../crates/file-ext"}
fs_extra = "1.2.0"
tracing = "0.1.36"
tracing-subscriber = { version = "0.3.15", features = ["env-filter"] }

View file

@ -189,13 +189,30 @@ model FileConflict {
// they can be "mounted" to a client, and then used to decrypt files automatically
model Key {
id Int @id @default(autoincrement())
// used to identify the key when it is entered by user
checksum String @unique
// uuid to identify the key
uuid String @unique
// the name that the user sets
name String?
// is this key the default for encryption?
default Boolean
// nullable if concealed for security
date_created DateTime? @default(now())
// so we know which algorithm to use, can be null if user must select
algorithm Int? @default(0)
// encryption algorithm used to encrypt the key
algorithm Bytes
// hashing algorithm used for hashing the master password
hashing_algorithm Bytes
// salt to hash the master password with
salt Bytes
// salt used for encrypting data with this key
content_salt Bytes
// the *encrypted* master key (48 bytes)
master_key Bytes
// the nonce used for encrypting the master key
master_key_nonce Bytes
// the nonce used for encrypting the key
key_nonce Bytes
// the *encrypted* key
key Bytes
objects Object[]
file_paths FilePath[]

View file

@ -1,5 +1,7 @@
use crate::job::DynJob;
use sd_crypto::keys::keymanager::KeyManager;
use std::sync::Arc;
use tokio::sync::Mutex;
use tracing::warn;
use uuid::Uuid;
@ -16,6 +18,8 @@ pub struct LibraryContext {
pub config: LibraryConfig,
/// db holds the database client for the current library.
pub db: Arc<PrismaClient>,
/// key manager that provides encryption keys to functions that require them
pub key_manager: Arc<Mutex<KeyManager>>,
/// node_local_id holds the local ID of the node which is running the library.
pub node_local_id: i32,
/// node_context holds the node context for the node which this library is running on.

View file

@ -1,7 +1,7 @@
use crate::{
invalidate_query,
node::Platform,
prisma::node,
prisma::{node, PrismaClient},
util::{
db::load_and_migrate,
seeder::{indexer_rules_seeder, SeederError},
@ -9,6 +9,14 @@ use crate::{
NodeContext,
};
use sd_crypto::{
crypto::stream::Algorithm,
keys::{
hashing::HashingAlgorithm,
keymanager::{KeyManager, StoredKey},
},
primitives::to_array,
};
use std::{
env, fs, io,
path::{Path, PathBuf},
@ -16,7 +24,7 @@ use std::{
sync::Arc,
};
use thiserror::Error;
use tokio::sync::RwLock;
use tokio::sync::{Mutex, RwLock};
use uuid::Uuid;
use super::{LibraryConfig, LibraryConfigWrapped, LibraryContext};
@ -31,6 +39,53 @@ pub struct LibraryManager {
node_context: NodeContext,
}
pub async fn create_keymanager(client: &PrismaClient) -> Result<KeyManager, SeederError> {
// retrieve all stored keys from the DB
let mut key_manager = KeyManager::new(vec![], None);
let db_stored_keys = client.key().find_many(vec![]).exec().await?;
let mut default = Uuid::default();
// collect and serialize the stored keys
let stored_keys: Vec<StoredKey> = db_stored_keys
.iter()
.map(|d| {
let d = d.clone();
let uuid = uuid::Uuid::from_str(&d.uuid).unwrap();
if d.default {
default = uuid;
}
StoredKey {
uuid,
salt: to_array(d.salt).unwrap(),
algorithm: Algorithm::deserialize(to_array(d.algorithm).unwrap()).unwrap(),
content_salt: to_array(d.content_salt).unwrap(),
master_key: to_array(d.master_key).unwrap(),
master_key_nonce: d.master_key_nonce,
key_nonce: d.key_nonce,
key: d.key,
hashing_algorithm: HashingAlgorithm::deserialize(
to_array(d.hashing_algorithm).unwrap(),
)
.unwrap(),
}
})
.collect();
// insert all keys from the DB into the keymanager's keystore
key_manager.populate_keystore(stored_keys).unwrap();
// if any key had an associated default tag
if !default.is_nil() {
key_manager.set_default(default).unwrap();
}
Ok(key_manager)
}
#[derive(Error, Debug)]
pub enum LibraryManagerError {
#[error("error saving or loading the config from the filesystem")]
@ -263,10 +318,13 @@ impl LibraryManager {
// Run seeders
indexer_rules_seeder(&db).await?;
let key_manager = Arc::new(Mutex::new(create_keymanager(&db).await?));
Ok(LibraryContext {
id,
config,
db,
key_manager,
node_local_id: node_data.id,
node_context,
})

View file

@ -28,4 +28,7 @@ thiserror = "1.0.37"
# metadata de/serialization
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
serde_json = "1.0"
uuid = { version = "1.1.2", features = ["v4", "serde"] }

View file

@ -7,7 +7,7 @@ use sd_crypto::{
keyslot::{Keyslot, KeyslotVersion},
},
keys::hashing::{HashingAlgorithm, Params},
primitives::generate_master_key,
primitives::{generate_master_key, generate_salt},
Protected,
};
@ -24,6 +24,9 @@ pub fn encrypt() {
// This needs to be generated here, otherwise we won't have access to it for encryption
let master_key = generate_master_key();
// This ideally should be done by the KMS
let salt = generate_salt();
// Create a keyslot to be added to the header
let mut keyslots: Vec<Keyslot> = Vec::new();
keyslots.push(
@ -31,6 +34,7 @@ pub fn encrypt() {
KeyslotVersion::V1,
ALGORITHM,
HASHING_ALGORITHM,
salt,
password,
&master_key,
)
@ -38,7 +42,7 @@ pub fn encrypt() {
);
// Create the header for the encrypted file
let header = FileHeader::new(FileHeaderVersion::V1, ALGORITHM, keyslots, None, None);
let header = FileHeader::new(FileHeaderVersion::V1, ALGORITHM, keyslots);
// Write the header to the file
header.write(&mut writer).unwrap();

View file

@ -5,7 +5,7 @@ use sd_crypto::{
header::{
file::{FileHeader, FileHeaderVersion},
keyslot::{Keyslot, KeyslotVersion},
metadata::{Metadata, MetadataVersion},
metadata::MetadataVersion,
},
keys::hashing::{HashingAlgorithm, Params},
primitives::{generate_master_key, generate_salt},
@ -28,6 +28,9 @@ fn encrypt() {
let password = Protected::new(b"password".to_vec());
// This ideally should be done by the KMS
let salt = generate_salt();
// Open both the source and the output file
let mut reader = File::open("test").unwrap();
let mut writer = File::create("test.encrypted").unwrap();
@ -43,27 +46,24 @@ fn encrypt() {
KeyslotVersion::V1,
ALGORITHM,
HASHING_ALGORITHM,
salt,
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);
let mut header = FileHeader::new(FileHeaderVersion::V1, ALGORITHM, keyslots);
header
.add_metadata(
MetadataVersion::V1,
ALGORITHM,
&master_key,
&embedded_metadata,
)
.unwrap();
// Write the header to the file
header.write(&mut writer).unwrap();
@ -87,18 +87,8 @@ pub fn decrypt_metadata() {
// 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();
let file_info: FileInformation = header.decrypt_metadata(password).unwrap();
println!("file name: {}", file_info.file_name);
}

View file

@ -5,7 +5,7 @@ use sd_crypto::{
header::{
file::{FileHeader, FileHeaderVersion},
keyslot::{Keyslot, KeyslotVersion},
preview_media::{PreviewMedia, PreviewMediaVersion},
preview_media::PreviewMediaVersion,
},
keys::hashing::{HashingAlgorithm, Params},
primitives::{generate_master_key, generate_salt},
@ -25,6 +25,9 @@ fn encrypt() {
// This needs to be generated here, otherwise we won't have access to it for encryption
let master_key = generate_master_key();
// This ideally should be done by the KMS
let salt = generate_salt();
// 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();
@ -33,29 +36,21 @@ fn encrypt() {
KeyslotVersion::V1,
ALGORITHM,
HASHING_ALGORITHM,
salt,
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));
let mut header = FileHeader::new(FileHeaderVersion::V1, ALGORITHM, keyslots);
header
.add_preview_media(PreviewMediaVersion::V1, ALGORITHM, &master_key, &pvm_media)
.unwrap();
// Write the header to the file
header.write(&mut writer).unwrap();
@ -79,15 +74,8 @@ pub fn decrypt_preview_media() {
// 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();
let media = header.decrypt_preview_media(password).unwrap();
println!("{:?}", media.expose());
}

View file

@ -13,6 +13,7 @@ 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)]
#[allow(clippy::use_self)]
pub enum Algorithm {
XChaCha20Poly1305,
Aes256Gcm,
@ -127,6 +128,8 @@ impl StreamEncryption {
})?;
// zeroize before writing, so any potential errors won't result in a potential data leak
// this specific zeroize technically isn't needed due to the boxed slice, but performance impact is
// negligible and it's good practice either way
read_buffer.zeroize();
// Using `write` instead of `write_all` so we can check the amount of bytes written

View file

@ -36,4 +36,12 @@ pub enum Error {
NoMetadata,
#[error("tried adding too many keyslots to a header")]
TooManyKeyslots,
#[error("requested key wasn't found in the key manager")]
KeyNotFound,
#[error("no default key has been set")]
NoDefaultKeySet,
#[error("no master password has been provided to the keymanager")]
NoMasterPassword,
#[error("mismatch between supplied keys and the keystore")]
KeystoreMismatch,
}

View file

@ -82,8 +82,8 @@ impl FileHeader {
version: FileHeaderVersion,
algorithm: Algorithm,
keyslots: Vec<Keyslot>,
metadata: Option<Metadata>,
preview_media: Option<PreviewMedia>,
//metadata: Option<Metadata>,
//preview_media: Option<PreviewMedia>,
) -> Self {
let nonce = generate_nonce(algorithm);
@ -92,12 +92,12 @@ impl FileHeader {
algorithm,
nonce,
keyslots,
metadata,
preview_media,
metadata: None,
preview_media: None,
}
}
/// This is a helper function to decrypt a master key from keyslots that are attached to a header.
/// This is a helper function to decrypt a master key from keyslots that are attached to a header, from a user-supplied password.
///
/// You receive an error if the password doesn't match or if there are no keyslots.
#[allow(clippy::needless_pass_by_value)]
@ -124,6 +124,7 @@ impl FileHeader {
}
}
/// This is a helper function to serialize and write a header to a file.
pub fn write<W>(&self, writer: &mut W) -> Result<(), Error>
where
W: Write + Seek,
@ -132,6 +133,39 @@ impl FileHeader {
Ok(())
}
/// This is a helper function to decrypt a master key from keyslots that are attached to a header.
///
/// It takes in a Vec of pre-hashed keys, which is what the key manager returns
///
/// 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_from_prehashed(
&self,
hashed_keys: Vec<Protected<[u8; 32]>>,
) -> Result<Protected<[u8; MASTER_KEY_LEN]>, Error> {
let mut master_key = [0u8; MASTER_KEY_LEN];
if self.keyslots.is_empty() {
return Err(Error::NoKeyslots);
}
for key in hashed_keys {
for keyslot in &self.keyslots {
if let Ok(decrypted_master_key) =
keyslot.decrypt_master_key_from_prehashed(key.clone())
{
master_key.copy_from_slice(&decrypted_master_key);
}
}
}
if master_key == [0u8; MASTER_KEY_LEN] {
Err(Error::IncorrectPassword)
} else {
Ok(Protected::new(master_key))
}
}
/// This function should be used for generating AAD before encryption
///
/// Use the return value from `FileHeader::deserialize()` for decryption

View file

@ -27,9 +27,7 @@ use crate::{
crypto::stream::{Algorithm, StreamDecryption, StreamEncryption},
error::Error,
keys::hashing::HashingAlgorithm,
primitives::{
generate_nonce, generate_salt, to_array, ENCRYPTED_MASTER_KEY_LEN, MASTER_KEY_LEN, SALT_LEN,
},
primitives::{generate_nonce, to_array, ENCRYPTED_MASTER_KEY_LEN, MASTER_KEY_LEN, SALT_LEN},
Protected,
};
@ -64,13 +62,13 @@ impl Keyslot {
version: KeyslotVersion,
algorithm: Algorithm,
hashing_algorithm: HashingAlgorithm,
salt: [u8; SALT_LEN],
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 hashed_password = hashing_algorithm.hash(password, salt)?;
let encrypted_master_key: [u8; 48] = to_array(StreamEncryption::encrypt_bytes(
hashed_password,
@ -107,6 +105,20 @@ impl Keyslot {
StreamDecryption::decrypt_bytes(key, &self.nonce, self.algorithm, &self.master_key, &[])
}
/// This function should not be used directly, use `header.decrypt_master_key()` instead
///
/// This attempts to decrypt the master key for a single keyslot, using a pre-hashed key
///
/// No hashing is done internally.
///
/// An error will be returned on failure.
pub fn decrypt_master_key_from_prehashed(
&self,
key: Protected<[u8; 32]>,
) -> Result<Protected<Vec<u8>>, Error> {
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> {

View file

@ -32,14 +32,12 @@ 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,
},
primitives::{generate_nonce, MASTER_KEY_LEN},
Protected,
};
use super::file::FileHeader;
/// 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.
@ -48,11 +46,7 @@ use crate::{
#[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 algorithm: Algorithm, // encryption algorithm
pub metadata_nonce: Vec<u8>,
pub metadata: Vec<u8>,
}
@ -62,7 +56,7 @@ pub enum MetadataVersion {
V1,
}
impl Metadata {
impl FileHeader {
/// This should be used for creating a header metadata item.
///
/// It handles encrypting the master key and metadata.
@ -70,55 +64,101 @@ impl 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>(
pub fn add_metadata<T>(
&mut self,
version: MetadataVersion,
algorithm: Algorithm,
hashing_algorithm: HashingAlgorithm,
password: Protected<Vec<u8>>,
salt: &[u8; SALT_LEN],
media: &T,
) -> Result<Self, Error>
master_key: &Protected<[u8; MASTER_KEY_LEN]>,
metadata: &T,
) -> Result<(), 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,
master_key.clone(),
&metadata_nonce,
algorithm,
&serde_json::to_vec(media).map_err(|_| Error::MetadataDeSerialization)?,
&serde_json::to_vec(metadata).map_err(|_| Error::MetadataDeSerialization)?,
&[],
)?;
Ok(Self {
let metadata = Metadata {
version,
algorithm,
hashing_algorithm,
salt: *salt,
master_key: encrypted_master_key,
master_key_nonce,
metadata_nonce,
metadata: encrypted_metadata,
})
};
self.metadata = Some(metadata);
Ok(())
}
/// This function should be used to retrieve the metadata for a file
///
/// All it requires is pre-hashed keys returned from the key manager
///
/// A deserialized data type will be returned from this function
pub fn decrypt_metadata_from_prehashed<T>(
&self,
hashed_keys: Vec<Protected<[u8; 32]>>,
) -> Result<T, Error>
where
T: serde::de::DeserializeOwned,
{
let master_key = self.decrypt_master_key_from_prehashed(hashed_keys)?;
// could be an expensive clone (a few MiB at most)
if let Some(metadata) = self.metadata.clone() {
let metadata = StreamDecryption::decrypt_bytes(
master_key,
&metadata.metadata_nonce,
metadata.algorithm,
&metadata.metadata,
&[],
)?;
serde_json::from_slice::<T>(&metadata).map_err(|_| Error::MetadataDeSerialization)
} else {
Err(Error::NoMetadata)
}
}
/// This function should be used to retrieve the metadata for a file
///
/// All it requires is a password. Hashing is handled for you.
///
/// A deserialized data type will be returned from this function
pub fn decrypt_metadata<T>(&self, password: Protected<Vec<u8>>) -> Result<T, Error>
where
T: serde::de::DeserializeOwned,
{
let master_key = self.decrypt_master_key(password)?;
// could be an expensive clone (a few MiB at most)
if let Some(metadata) = self.metadata.clone() {
let metadata = StreamDecryption::decrypt_bytes(
master_key,
&metadata.metadata_nonce,
metadata.algorithm,
&metadata.metadata,
&[],
)?;
serde_json::from_slice::<T>(&metadata).map_err(|_| Error::MetadataDeSerialization)
} else {
Err(Error::NoMetadata)
}
}
}
impl Metadata {
#[must_use]
pub fn get_length(&self) -> usize {
match self.version {
MetadataVersion::V1 => 128 + self.metadata.len(),
MetadataVersion::V1 => 36 + self.metadata.len(),
}
}
@ -132,55 +172,15 @@ impl Metadata {
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_nonce); // 24 max
metadata.extend_from_slice(&vec![0u8; 24 - self.metadata_nonce.len()]); // 28
metadata.extend_from_slice(&self.metadata.len().to_le_bytes()); // 36 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
@ -200,23 +200,6 @@ impl Metadata {
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)?;
@ -235,10 +218,6 @@ impl Metadata {
let metadata = Self {
version,
algorithm,
hashing_algorithm,
salt,
master_key,
master_key_nonce,
metadata_nonce,
metadata,
};

View file

@ -25,14 +25,12 @@ 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,
},
primitives::{generate_nonce, MASTER_KEY_LEN},
Protected,
};
use super::file::FileHeader;
/// 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.
@ -41,11 +39,7 @@ use crate::{
#[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 algorithm: Algorithm, // encryption algorithm
pub media_nonce: Vec<u8>,
pub media: Vec<u8>,
}
@ -55,7 +49,7 @@ pub enum PreviewMediaVersion {
V1,
}
impl PreviewMedia {
impl FileHeader {
/// This should be used for creating a header preview media item.
///
/// This handles encrypting the master key and preview media.
@ -63,47 +57,95 @@ impl PreviewMedia {
/// 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(
pub fn add_preview_media(
&mut self,
version: PreviewMediaVersion,
algorithm: Algorithm,
hashing_algorithm: HashingAlgorithm,
password: Protected<Vec<u8>>,
salt: &[u8; SALT_LEN],
master_key: &Protected<[u8; MASTER_KEY_LEN]>,
media: &[u8],
) -> Result<Self, Error> {
) -> Result<(), 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,
let encrypted_media = StreamEncryption::encrypt_bytes(
master_key.clone(),
&media_nonce,
algorithm,
master_key.expose(),
media,
&[],
)?)?;
)?;
let encrypted_media =
StreamEncryption::encrypt_bytes(master_key, &media_nonce, algorithm, media, &[])?;
Ok(Self {
let pvm = PreviewMedia {
version,
algorithm,
hashing_algorithm,
salt: *salt,
master_key: encrypted_master_key,
master_key_nonce,
media_nonce,
media: encrypted_media,
})
};
self.preview_media = Some(pvm);
Ok(())
}
/// This function is what you'll want to use to get the preview media for a file
///
/// All it requires is pre-hashed keys returned from the key manager
///
/// Once provided, a `Vec<u8>` is returned that contains the preview media
pub fn decrypt_preview_media_from_prehashed(
&self,
hashed_keys: Vec<Protected<[u8; 32]>>,
) -> Result<Protected<Vec<u8>>, Error> {
let master_key = self.decrypt_master_key_from_prehashed(hashed_keys)?;
// could be an expensive clone (a few MiB at most)
if let Some(pvm) = self.preview_media.clone() {
let media = StreamDecryption::decrypt_bytes(
master_key,
&pvm.media_nonce,
pvm.algorithm,
&pvm.media,
&[],
)?;
Ok(media)
} else {
Err(Error::NoPreviewMedia)
}
}
/// This function is what you'll want to use to get the preview media for a file
///
/// All it requires is the user's password. Hashing is handled for you.
///
/// Once provided, a `Vec<u8>` is returned that contains the preview media
pub fn decrypt_preview_media(
&self,
password: Protected<Vec<u8>>,
) -> Result<Protected<Vec<u8>>, Error> {
let master_key = self.decrypt_master_key(password)?;
// could be an expensive clone (a few MiB at most)
if let Some(pvm) = self.preview_media.clone() {
let media = StreamDecryption::decrypt_bytes(
master_key,
&pvm.media_nonce,
pvm.algorithm,
&pvm.media,
&[],
)?;
Ok(media)
} else {
Err(Error::NoPreviewMedia)
}
}
}
impl PreviewMedia {
#[must_use]
pub fn get_length(&self) -> usize {
match self.version {
PreviewMediaVersion::V1 => 128 + self.media.len(),
PreviewMediaVersion::V1 => 36 + self.media.len(),
}
}
@ -117,55 +159,15 @@ impl PreviewMedia {
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_nonce); // 24 max
preview_media.extend_from_slice(&vec![0u8; 24 - self.media_nonce.len()]); // 28 total bytes
preview_media.extend_from_slice(&self.media.len().to_le_bytes()); // 36 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
@ -186,23 +188,6 @@ impl PreviewMedia {
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)?;
@ -221,10 +206,6 @@ impl PreviewMedia {
let preview_media = Self {
version,
algorithm,
hashing_algorithm,
salt,
master_key,
master_key_nonce,
media_nonce,
media,
};

View file

@ -17,7 +17,8 @@ use argon2::Argon2;
/// These parameters define the password-hashing level.
///
/// The harder the parameter, the longer the password will take to hash.
#[derive(Clone, Copy)]
#[derive(Clone, Copy, PartialEq, Eq)]
#[allow(clippy::use_self)]
pub enum Params {
Standard,
Hardened,
@ -25,11 +26,20 @@ pub enum Params {
}
/// This defines all available password hashing algorithms.
#[derive(Clone, Copy)]
#[derive(Clone, Copy, PartialEq, Eq)]
pub enum HashingAlgorithm {
Argon2id(Params),
}
/// This is so we can iterate over all hashing algorithms and parameters.
///
/// The main usage is for pre-hashing a key during mounting.
pub const HASHING_ALGORITHM_LIST: [HashingAlgorithm; 3] = [
HashingAlgorithm::Argon2id(Params::Standard),
HashingAlgorithm::Argon2id(Params::Hardened),
HashingAlgorithm::Argon2id(Params::Paranoid),
];
impl HashingAlgorithm {
/// This function should be used to hash passwords
///

View file

@ -0,0 +1,323 @@
use std::collections::HashMap;
use crate::crypto::stream::{StreamDecryption, StreamEncryption};
use crate::error::Error;
use crate::primitives::{
generate_master_key, generate_nonce, generate_salt, to_array, MASTER_KEY_LEN,
};
use crate::{
crypto::stream::Algorithm,
primitives::{ENCRYPTED_MASTER_KEY_LEN, SALT_LEN},
Protected,
};
use uuid::Uuid;
use super::hashing::{HashingAlgorithm, HASHING_ALGORITHM_LIST};
// The terminology in this file is very confusing.
// The `master_key` is specific to the `StoredKey`, and is just used internally for encryption.
// The `key` is what the user added/generated within their Spacedrive key manager.
// The `password` in this sense is the user's "master password", similar to a password manager's main password
// The `hashed_key` refers to the value you'd pass to PVM/MD decryption functions. It has been pre-hashed with the content salt.
// The content salt refers to the semi-universal salt that's used for metadata/preview media (unique to each key in the manager)
#[derive(Clone, PartialEq, Eq)]
pub struct StoredKey {
pub uuid: uuid::Uuid, // uuid for identification. shared with mounted keys
pub algorithm: Algorithm, // encryption algorithm for encrypting the master key. can be changed (requires a re-encryption though)
pub hashing_algorithm: HashingAlgorithm, // hashing algorithm to use for hashing everything related to this key. can't be changed once set.
pub salt: [u8; SALT_LEN], // salt to hash the master password with
pub content_salt: [u8; SALT_LEN], // salt used for file data
pub master_key: [u8; ENCRYPTED_MASTER_KEY_LEN], // this is for encrypting the `key`
pub master_key_nonce: Vec<u8>, // nonce for encrypting the master key
pub key_nonce: Vec<u8>, // nonce used for encrypting the main key
pub key: Vec<u8>, // encrypted. the key stored in spacedrive (e.g. generated 64 char key)
}
pub struct KeyManager {
master_password: Option<Protected<Vec<u8>>>, // the user's. we take ownership here to prevent other functions attempting to manage/pass it to us
keystore: HashMap<Uuid, StoredKey>,
keymount: HashMap<Uuid, MountedKey>,
default: Option<Uuid>,
}
/// The `KeyManager` functions should be used for all key-related management.
impl KeyManager {
/// Initialize the Key Manager with the user's master password, and `StoredKeys` retrieved from Prisma
#[must_use]
pub fn new(stored_keys: Vec<StoredKey>, master_password: Option<Protected<Vec<u8>>>) -> Self {
let mut keystore = HashMap::new();
for key in stored_keys {
keystore.insert(key.uuid, key);
}
let keymount: HashMap<Uuid, MountedKey> = HashMap::new();
Self {
master_password,
keystore,
keymount,
default: None,
}
}
/// This function should be used to populate the keystore with multiple stored keys at a time.
///
/// It's suitable for when you created the key manager without populating it.
pub fn populate_keystore(&mut self, stored_keys: Vec<StoredKey>) -> Result<(), Error> {
for key in stored_keys {
self.keystore.insert(key.uuid, key);
}
Ok(())
}
/// This allows you to set the default key
pub fn set_default(&mut self, uuid: Uuid) -> Result<(), Error> {
if self.keystore.contains_key(&uuid) {
self.default = Some(uuid);
Ok(())
} else {
Err(Error::KeyNotFound)
}
}
/// This allows you to get the default key's ID
pub const fn get_default(&self) -> Result<Uuid, Error> {
if let Some(default) = self.default {
Ok(default)
} else {
Err(Error::NoDefaultKeySet)
}
}
/// This should ONLY be used within the key manager
fn get_master_password(&self) -> Result<Protected<Vec<u8>>, Error> {
match &self.master_password {
Some(k) => Ok(k.clone()),
None => Err(Error::NoMasterPassword),
}
}
pub fn set_master_password(
&mut self,
master_password: Protected<Vec<u8>>,
) -> Result<(), Error> {
// this returns a result, so we can potentially implement password checking functionality
self.master_password = Some(master_password);
Ok(())
}
#[must_use]
pub const fn has_master_password(&self) -> bool {
self.master_password.is_some()
}
/// This function is used for emptying the entire keystore.
pub fn empty_keystore(&mut self) {
self.keystore.clear();
}
/// This function is used for unmounting all keys at once.
pub fn empty_keymount(&mut self) {
// i'm unsure whether or not `.clear()` also calls drop
// if it doesn't, we're going to need to find another way to call drop on these values
// that way they will be zeroized and removed from memory fully
self.keymount.clear();
}
/// This function can be used for comparing an array of `StoredKeys` to the currently loaded keystore.
pub fn compare_keystore(&self, supplied_keys: &[StoredKey]) -> Result<(), Error> {
if supplied_keys.len() != self.keystore.len() {
return Err(Error::KeystoreMismatch);
}
for key in supplied_keys {
let keystore_key = match self.keystore.get(&key.uuid) {
Some(key) => key,
None => return Err(Error::KeystoreMismatch),
};
if key != keystore_key {
return Err(Error::KeystoreMismatch);
}
}
Ok(())
}
pub fn unmount(&mut self, uuid: Uuid) -> Result<(), Error> {
if self.keymount.contains_key(&uuid) {
self.keymount.remove(&uuid);
Ok(())
} else {
Err(Error::KeyNotFound)
}
}
/// This function returns a Vec of `StoredKey`s, so you can write them somewhere/update the database with them/etc
///
/// The database and keystore should be in sync at ALL times
pub fn dump_keystore(&self) -> Result<Vec<StoredKey>, Error> {
let mut keys = Vec::<StoredKey>::new();
for key in self.keystore.values() {
keys.push(key.clone());
}
Ok(keys)
}
/// This function does not return a value by design.
/// Once a key is mounted, access it with `KeyManager::access()`
/// This is to ensure that only functions which require access to the mounted key receive it.
/// We could add a log to this, so that the user can view mounts
pub fn mount(&mut self, uuid: Uuid) -> Result<(), Error> {
match self.keystore.get(&uuid) {
Some(stored_key) => {
let master_password = self.get_master_password()?;
let hashed_password = stored_key
.hashing_algorithm
.hash(master_password, stored_key.salt)?;
let mut master_key = [0u8; MASTER_KEY_LEN];
// Decrypt the StoredKey's master key using the user's hashed password
let master_key = if let Ok(decrypted_master_key) = StreamDecryption::decrypt_bytes(
hashed_password,
&stored_key.master_key_nonce,
stored_key.algorithm,
&stored_key.master_key,
&[],
) {
master_key.copy_from_slice(&decrypted_master_key);
Ok(Protected::new(master_key))
} else {
Err(Error::IncorrectPassword)
}?;
// Decrypt the StoredKey using the decrypted master key
let key = StreamDecryption::decrypt_bytes(
master_key,
&stored_key.key_nonce,
stored_key.algorithm,
&stored_key.key,
&[],
)?;
let mut hashed_keys = Vec::<Protected<[u8; 32]>>::new();
// Hash the StoredKey using each available password hashing parameter, so all content is accessible no matter the settings.
// It makes key mounting more expensive, but it allows for greater UX and customizability.
for hashing_algorithm in HASHING_ALGORITHM_LIST {
hashed_keys.push(hashing_algorithm.hash(key.clone(), stored_key.content_salt)?);
}
// Construct the MountedKey and insert it into the Keymount
let mounted_key = MountedKey {
uuid: stored_key.uuid,
key,
content_salt: stored_key.content_salt,
hashed_keys,
};
self.keymount.insert(uuid, mounted_key);
Ok(())
}
None => Err(Error::KeyNotFound),
}
}
/// This function is for accessing the internal keymount.
///
/// We could add a log to this, so that the user can view accesses
pub fn access_keymount(&self, uuid: Uuid) -> Result<MountedKey, Error> {
match self.keymount.get(&uuid) {
Some(key) => Ok(key.clone()),
None => Err(Error::KeyNotFound),
}
}
/// This function is for accessing a `StoredKey` from an ID.
pub fn access_keystore(&self, uuid: Uuid) -> Result<StoredKey, Error> {
match self.keystore.get(&uuid) {
Some(key) => Ok(key.clone()),
None => Err(Error::KeyNotFound),
}
}
/// This function is used to add a new key/password to the keystore.
///
/// You should use this when a new key is added, as it will generate salts/nonces/etc.
///
/// It does not mount the key, it just registers it.
///
/// Once added, you will need to use `KeyManager::access_keystore()` to retrieve it and add it to Prisma.
///
/// You may use the returned ID to identify this key.
#[allow(clippy::needless_pass_by_value)]
pub fn add_to_keystore(
&mut self,
key: Protected<Vec<u8>>,
algorithm: Algorithm,
hashing_algorithm: HashingAlgorithm,
) -> Result<Uuid, Error> {
let master_password = self.get_master_password()?;
let uuid = uuid::Uuid::new_v4();
// Generate items we'll need for encryption
let key_nonce = generate_nonce(algorithm);
let master_key = generate_master_key();
let master_key_nonce = generate_nonce(algorithm);
let salt = generate_salt();
let content_salt = generate_salt(); // for PVM/MD
// Hash the user's master password
let hashed_password = hashing_algorithm.hash(master_password, salt)?;
// Encrypted the master key with the user's hashed password
let encrypted_master_key: [u8; 48] = to_array(StreamEncryption::encrypt_bytes(
hashed_password,
&master_key_nonce,
algorithm,
master_key.expose(),
&[],
)?)?;
// Encrypt the actual key (e.g. user-added/autogenerated, text-encodable)
let encrypted_key =
StreamEncryption::encrypt_bytes(master_key, &key_nonce, algorithm, &key, &[])?;
// Construct the StoredKey
let stored_key = StoredKey {
uuid,
algorithm,
hashing_algorithm,
salt,
content_salt,
master_key: encrypted_master_key,
master_key_nonce,
key_nonce,
key: encrypted_key,
};
// Insert it into the Keystore
self.keystore.insert(stored_key.uuid, stored_key);
// Return the ID so it can be identified
Ok(uuid)
}
}
// derive explicit CLONES only
#[derive(Clone)]
pub struct MountedKey {
pub uuid: Uuid, // used for identification. shared with stored keys
pub key: Protected<Vec<u8>>, // the actual key itself, text format encodable (so it can be viewed with an UI)
pub content_salt: [u8; SALT_LEN], // the salt used for file data
pub hashed_keys: Vec<Protected<[u8; 32]>>, // this is hashed with the content salt, for instant access
}

View file

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