mirror of
https://github.com/spacedriveapp/spacedrive
synced 2024-07-04 12:13:27 +00:00
[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:
parent
8ac42c0353
commit
0c7aed5f86
2
Cargo.lock
generated
2
Cargo.lock
generated
|
@ -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",
|
||||
]
|
||||
|
||||
|
|
|
@ -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"] }
|
||||
|
|
|
@ -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[]
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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,
|
||||
})
|
||||
|
|
|
@ -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"] }
|
||||
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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> {
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
|
|
|
@ -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
|
||||
///
|
||||
|
|
323
crates/crypto/src/keys/keymanager.rs
Normal file
323
crates/crypto/src/keys/keymanager.rs
Normal 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
|
||||
}
|
|
@ -1,2 +1,3 @@
|
|||
//! This module contains all key and hashing related functions.
|
||||
pub mod hashing;
|
||||
pub mod keymanager;
|
||||
|
|
Loading…
Reference in a new issue