diff --git a/cli/src/main.rs b/cli/src/main.rs index 249b4d17..b49a0e56 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -14,14 +14,10 @@ struct Cli { #[derive(Debug, Subcommand)] enum Commands { - /// Generate a random key of given length - #[command(arg_required_else_help = true)] - Generate { - /// Length of the key to generate - length: Option, - }, + /// Generate a random secret key + Generate, - /// Generate a random keypair" + /// Generate a random keypair GenerateKeypair, /// Generate secret key with parts shared accross multiple parties. @@ -73,10 +69,6 @@ enum Commands { /// The number of iteration for the derivation algorithm #[arg(short, long)] iterations: Option, - - /// The desired length of the key - #[arg(short, long)] - length: Option, }, /// Encrypt data @@ -177,7 +169,7 @@ fn main() { let cli = Cli::parse(); match cli.command { - Commands::Generate { length } => generate_key(length), + Commands::Generate => generate_key(), Commands::GenerateKeypair => generate_keypair(), Commands::GenerateArgon2Parameters { memory, @@ -194,8 +186,7 @@ fn main() { data, salt, iterations, - length, - } => derive_key(data, salt, iterations, length), + } => derive_key(data, salt, iterations), Commands::Encrypt { data, key, version } => encrypt(data, key, version), Commands::EncryptAsymmetric { data, key, version } => { encrypt_asymmetric(data, key, version) @@ -213,11 +204,11 @@ fn main() { } } -fn generate_key(length: Option) { - let length = length.unwrap_or(32); +fn generate_key() { + use devolutions_crypto::key::{generate_secret_key, KeyVersion}; - let key = base64::encode(&devolutions_crypto::utils::generate_key(length).unwrap()); - println!("{}", key); + let key: Vec = generate_secret_key(KeyVersion::Latest).into(); + println!("{}", base64::encode(&key)); } fn generate_argon2parameters( @@ -248,43 +239,101 @@ fn generate_argon2parameters( println!("{}", base64::encode(¶meters)); } -fn derive_key(data: String, salt: Option, iterations: Option, length: Option) { +fn derive_key(data: String, salt: Option, iterations: Option) { + use devolutions_crypto::key_derivation::Pbkdf2; + let data = data.as_bytes(); - let salt = match salt { - Some(s) => base64::decode(&s).unwrap(), - None => vec![0u8; 0], + let iterations = iterations.unwrap_or(DEFAULT_PBKDF2_ITERATIONS); + let pbkdf2 = Pbkdf2::with_params(iterations); + + let (secret_key, params) = match salt { + Some(s) => { + let salt = decode_base64_arg("--salt", &s); + pbkdf2.derive_with_salt(data, &salt).unwrap() + } + None => pbkdf2.derive(data).unwrap(), }; - let iterations = iterations.unwrap_or(DEFAULT_PBKDF2_ITERATIONS); + let params_bytes: Vec = params.into(); + let key_bytes: Vec = secret_key.into(); + println!("Key: {}", base64::encode(&key_bytes)); + println!("DerivationParameters: {}", base64::encode(¶ms_bytes)); +} - let length = length.unwrap_or(32); +/// Returns a human-readable description of a managed type by inspecting its header. +/// Returns "unknown" if the bytes are too short or contain an unrecognized type. +fn detect_dc_type(bytes: &[u8]) -> String { + use devolutions_crypto::{DataType, KeySubtype}; - let key = devolutions_crypto::utils::derive_key_pbkdf2(data, &salt, iterations, length); - println!("{}", base64::encode(&key)); + if bytes.len() < 8 { + return "unknown".to_string(); + } + + let data_type_raw = u16::from_le_bytes([bytes[2], bytes[3]]); + let subtype_raw = u16::from_le_bytes([bytes[4], bytes[5]]); + + match DataType::try_from(data_type_raw) { + Ok(DataType::Key) => match KeySubtype::try_from(subtype_raw) { + Ok(KeySubtype::Secret) => "SecretKey".to_string(), + Ok(KeySubtype::Public) => "PublicKey".to_string(), + Ok(KeySubtype::Private) => "PrivateKey".to_string(), + Ok(KeySubtype::Pair) => "KeyPair".to_string(), + _ => "Key (unknown subtype)".to_string(), + }, + Ok(DataType::Ciphertext) => "Ciphertext".to_string(), + Ok(DataType::PasswordHash) => "PasswordHash".to_string(), + Ok(DataType::Share) => "Share".to_string(), + Ok(DataType::SigningKey) => "SigningKey".to_string(), + Ok(DataType::Signature) => "Signature".to_string(), + Ok(DataType::KeyDerivation) => "DerivationParameters".to_string(), + _ => "unknown".to_string(), + } +} + +fn decode_base64_arg(arg_name: &str, value: &str) -> Vec { + base64::decode(value).unwrap_or_else(|_| { + eprintln!("Error: '{}' is not valid base64.", arg_name); + std::process::exit(1); + }) } fn encrypt(data: String, key: String, version: Option) { - let key = base64::decode(&key).unwrap(); + use devolutions_crypto::ciphertext::encrypt_with_secret_key; + use devolutions_crypto::key::SecretKey; + + let key_bytes = decode_base64_arg("key", &key); + let key = SecretKey::try_from(key_bytes.as_slice()).unwrap_or_else(|_| { + eprintln!( + "Error: 'key' - expected SecretKey, received {}.", + detect_dc_type(&key_bytes) + ); + std::process::exit(1); + }); let version = version.unwrap_or(0); - let version = devolutions_crypto::CiphertextVersion::try_from(version).unwrap(); - let data: Vec = devolutions_crypto::ciphertext::encrypt(data.as_bytes(), &key, version) + let data: Vec = encrypt_with_secret_key(data.as_bytes(), &key, version) .unwrap() .into(); println!("{}", base64::encode(&data)); } fn encrypt_asymmetric(data: String, key: String, version: Option) { - let key = base64::decode(&key).unwrap(); + use devolutions_crypto::key::PublicKey; - let version = version.unwrap_or(0); + let key_bytes = decode_base64_arg("key", &key); + let key = PublicKey::try_from(key_bytes.as_slice()).unwrap_or_else(|_| { + eprintln!( + "Error: 'key' - expected PublicKey, received {}.", + detect_dc_type(&key_bytes) + ); + std::process::exit(1); + }); + let version = version.unwrap_or(0); let version = devolutions_crypto::CiphertextVersion::try_from(version).unwrap(); - let key = devolutions_crypto::key::PublicKey::try_from(key.as_slice()).unwrap(); - let data: Vec = devolutions_crypto::ciphertext::encrypt_asymmetric(data.as_bytes(), &key, version) .unwrap() @@ -293,22 +342,63 @@ fn encrypt_asymmetric(data: String, key: String, version: Option) { } fn decrypt(data: String, key: String) { - let data = base64::decode(&data).unwrap(); - let data = devolutions_crypto::ciphertext::Ciphertext::try_from(data.as_slice()).unwrap(); - let key = base64::decode(&key).unwrap(); - - let data: Vec = data.decrypt(&key).unwrap(); - println!("{}", String::from_utf8_lossy(&data)); + use devolutions_crypto::ciphertext::Ciphertext; + use devolutions_crypto::key::SecretKey; + + let data_bytes = decode_base64_arg("data", &data); + let ciphertext = Ciphertext::try_from(data_bytes.as_slice()).unwrap_or_else(|_| { + eprintln!( + "Error: 'data' - expected Ciphertext, received {}.", + detect_dc_type(&data_bytes) + ); + std::process::exit(1); + }); + + let key_bytes = decode_base64_arg("key", &key); + let key = SecretKey::try_from(key_bytes.as_slice()).unwrap_or_else(|_| { + eprintln!( + "Error: 'key' - expected SecretKey, received {}.", + detect_dc_type(&key_bytes) + ); + std::process::exit(1); + }); + + let result: Vec = ciphertext + .decrypt_with_secret_key(&key) + .unwrap_or_else(|e| { + eprintln!("Error: decryption failed: {}.", e); + std::process::exit(1); + }); + println!("{}", String::from_utf8_lossy(&result)); } fn decrypt_asymmetric(data: String, key: String) { - let data = base64::decode(&data).unwrap(); - let data = devolutions_crypto::ciphertext::Ciphertext::try_from(data.as_slice()).unwrap(); - let key = base64::decode(&key).unwrap(); - let key = devolutions_crypto::key::PrivateKey::try_from(key.as_slice()).unwrap(); - - let data: Vec = data.decrypt_asymmetric(&key).unwrap(); - println!("{}", String::from_utf8_lossy(&data)); + use devolutions_crypto::ciphertext::Ciphertext; + use devolutions_crypto::key::PrivateKey; + + let data_bytes = decode_base64_arg("data", &data); + let ciphertext = Ciphertext::try_from(data_bytes.as_slice()).unwrap_or_else(|_| { + eprintln!( + "Error: 'data' - expected Ciphertext, received {}.", + detect_dc_type(&data_bytes) + ); + std::process::exit(1); + }); + + let key_bytes = decode_base64_arg("key", &key); + let key = PrivateKey::try_from(key_bytes.as_slice()).unwrap_or_else(|_| { + eprintln!( + "Error: 'key' - expected PrivateKey, received {}.", + detect_dc_type(&key_bytes) + ); + std::process::exit(1); + }); + + let result: Vec = ciphertext.decrypt_asymmetric(&key).unwrap_or_else(|e| { + eprintln!("Error: decryption failed: {}.", e); + std::process::exit(1); + }); + println!("{}", String::from_utf8_lossy(&result)); } fn hash_password(password: String, iterations: Option) { diff --git a/src/enums.rs b/src/enums.rs index 2639fc49..afd23e24 100644 --- a/src/enums.rs +++ b/src/enums.rs @@ -29,8 +29,10 @@ pub enum DataType { SigningKey = 5, /// A wrapped signature. Signature = 6, - /// A wrapped online ciphertextr that can be encrypted/decrypted chunk by chunk + /// A wrapped online ciphertext that can be encrypted/decrypted chunk by chunk OnlineCiphertext = 7, + /// Serialized key derivation parameters. + KeyDerivation = 8, } /// The versions of the encryption scheme to use. @@ -182,3 +184,28 @@ pub enum SignatureSubtype { #[default] None = 0, } + +/// The versions of the key derivation scheme to use. +#[cfg_attr(feature = "wbindgen", wasm_bindgen())] +#[cfg_attr(feature = "fuzz", derive(Arbitrary))] +#[derive(Clone, Copy, PartialEq, Eq, Zeroize, IntoPrimitive, TryFromPrimitive, Debug)] +#[repr(u16)] +#[derive(Default)] +pub enum KeyDerivationVersion { + /// Uses the latest version. + #[default] + Latest = 0, + /// Uses version 1: PBKDF2-HMAC-SHA256. + V1 = 1, + /// Uses version 2: Argon2id. + V2 = 2, +} + +#[derive(Clone, Copy, PartialEq, Eq, Zeroize, IntoPrimitive, TryFromPrimitive, Debug)] +#[cfg_attr(feature = "fuzz", derive(Arbitrary))] +#[repr(u16)] +#[derive(Default)] +pub enum KeyDerivationSubtype { + #[default] + None = 0, +} diff --git a/src/key/mod.rs b/src/key/mod.rs index ef76e88d..714c5b50 100644 --- a/src/key/mod.rs +++ b/src/key/mod.rs @@ -55,6 +55,8 @@ use secret_key_v1::SecretKeyV1; use std::borrow::Borrow; use std::convert::TryFrom; +use zeroize::Zeroizing; + #[cfg(feature = "fuzz")] use arbitrary::Arbitrary; @@ -370,6 +372,15 @@ impl SecretKey { } } +/// Constructs a `SecretKey` from raw derived key bytes (must be exactly 32 bytes). +/// Takes ownership of the `Zeroizing` wrapper to ensure the raw bytes are cleared on drop. +pub(crate) fn secret_key_from_raw(bytes: Zeroizing>) -> Result { + let mut header: Header = Header::default(); + header.version = KeyVersion::V1; + let payload = SecretKeyPayload::V1(SecretKeyV1::try_from(bytes.as_slice())?); + Ok(SecretKey { header, payload }) +} + /// Generates a `SecretKey` for use with symmetric encryption. /// # Arguments /// * `version` - Version of the key scheme to use. Use `KeyVersion::Latest` if you're not dealing with shared data. diff --git a/src/key_derivation/key_derivation_v1.rs b/src/key_derivation/key_derivation_v1.rs new file mode 100644 index 00000000..ff9a19a5 --- /dev/null +++ b/src/key_derivation/key_derivation_v1.rs @@ -0,0 +1,124 @@ +//! Key Derivation V1: PBKDF2-HMAC-SHA256 +use std::convert::TryFrom; +use std::io::{Cursor, Read, Write}; + +use byteorder::{LittleEndian, ReadBytesExt, WriteBytesExt}; +use zeroize::Zeroizing; + +use rand::TryRngCore; + +use crate::key::{secret_key_from_raw, SecretKey}; +use crate::utils::derive_key_pbkdf2; +use crate::{Error, Header, KeyDerivationVersion, Result, DEFAULT_PBKDF2_ITERATIONS}; + +use super::{DerivationParameters, DerivationParametersPayload}; + +pub const KEY_LENGTH: usize = 32; + +#[derive(Clone, Debug)] +pub struct KeyDerivationV1 { + pub iterations: u32, + pub salt: Vec, +} + +impl KeyDerivationV1 { + pub fn derive(&self, key: &[u8]) -> Zeroizing> { + Zeroizing::new(derive_key_pbkdf2( + key, + &self.salt, + self.iterations, + KEY_LENGTH, + )) + } +} + +impl From<&KeyDerivationV1> for Vec { + fn from(params: &KeyDerivationV1) -> Self { + let mut data = Vec::with_capacity(8 + params.salt.len()); + data.write_u32::(params.iterations).unwrap(); + data.write_u32::(params.salt.len() as u32) + .unwrap(); + data.write_all(¶ms.salt).unwrap(); + data + } +} + +impl TryFrom<&[u8]> for KeyDerivationV1 { + type Error = Error; + + fn try_from(data: &[u8]) -> Result { + let mut cursor = Cursor::new(data); + let iterations = cursor.read_u32::()?; + let salt_len = cursor.read_u32::()? as usize; + let remaining = data.len() - (cursor.position() as usize); + + if remaining < salt_len { + return Err(Error::InvalidLength); + } + + let mut salt = vec![0u8; salt_len]; + cursor.read_exact(&mut salt)?; + Ok(KeyDerivationV1 { iterations, salt }) + } +} + +// ── Pbkdf2 ─────────────────────────────────────────────────────────────────── + +/// Derives a key using PBKDF2-HMAC-SHA256 (V1). +pub struct Pbkdf2 { + iterations: u32, +} + +impl Pbkdf2 { + /// Creates a `Pbkdf2` key derivation object with default parameters (600,000 iterations). + pub fn new() -> Self { + Self { + iterations: DEFAULT_PBKDF2_ITERATIONS, + } + } + + /// Creates a `Pbkdf2` key derivation object with a custom iteration count. + /// The output key length is always 32 bytes to match `SecretKey`'s contract. + pub fn with_params(iterations: u32) -> Self { + Self { iterations } + } + + /// Derives the key using a randomly generated salt. + pub fn derive(&self, key: &[u8]) -> Result<(SecretKey, DerivationParameters)> { + let mut salt = vec![0u8; 16]; + rand::rngs::OsRng + .try_fill_bytes(&mut salt) + .map_err(|_| Error::RandomError)?; + self.derive_with_salt(key, &salt) + } + + /// Derives the key using the provided salt. + pub fn derive_with_salt( + &self, + key: &[u8], + salt: &[u8], + ) -> Result<(SecretKey, DerivationParameters)> { + let params = KeyDerivationV1 { + iterations: self.iterations, + salt: salt.to_vec(), + }; + let raw = params.derive(key); + let secret_key = secret_key_from_raw(raw)?; + + let mut header: Header = Header::default(); + header.version = KeyDerivationVersion::V1; + + let derivation_params = DerivationParameters { + header, + payload: DerivationParametersPayload::V1(params), + }; + + Ok((secret_key, derivation_params)) + } +} + +impl Default for Pbkdf2 { + fn default() -> Self { + Self::new() + } +} diff --git a/src/key_derivation/key_derivation_v2.rs b/src/key_derivation/key_derivation_v2.rs new file mode 100644 index 00000000..a2c111c9 --- /dev/null +++ b/src/key_derivation/key_derivation_v2.rs @@ -0,0 +1,90 @@ +//! Key Derivation V2: Argon2id +use std::convert::TryFrom; + +use zeroize::Zeroizing; + +use crate::key::{secret_key_from_raw, SecretKey}; +use crate::{Argon2Parameters, Error, Header, KeyDerivationVersion, Result}; + +use super::{DerivationParameters, DerivationParametersPayload}; + +#[derive(Clone)] +pub struct KeyDerivationV2 { + pub params: Argon2Parameters, +} + +impl core::fmt::Debug for KeyDerivationV2 { + fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { + write!(f, "KeyDerivationV2") + } +} + +impl KeyDerivationV2 { + pub fn derive(&self, key: &[u8]) -> Result>> { + Ok(Zeroizing::new(self.params.compute(key)?)) + } +} + +impl From<&KeyDerivationV2> for Vec { + fn from(v2: &KeyDerivationV2) -> Self { + Vec::from(&v2.params) + } +} + +impl TryFrom<&[u8]> for KeyDerivationV2 { + type Error = Error; + + fn try_from(data: &[u8]) -> Result { + Ok(KeyDerivationV2 { + params: Argon2Parameters::try_from(data)?, + }) + } +} + +// ── Argon2 ─────────────────────────────────────────────────────────────────── + +/// Derives a key using Argon2id (V2, the default). +pub struct Argon2 { + params: Argon2Parameters, +} + +impl Argon2 { + /// Creates an `Argon2` key derivation object with default parameters. + pub fn new() -> Self { + Self { + params: Argon2Parameters::default(), + } + } + + /// Creates an `Argon2` key derivation object with custom `Argon2Parameters`. + /// The caller is responsible for managing the salt (use `params.set_salt()` if needed). + pub fn with_params(params: Argon2Parameters) -> Self { + Self { params } + } + + /// Derives the key using the configured Argon2 parameters. + /// The salt is embedded in `Argon2Parameters` (generated at construction time when using `new()`). + pub fn derive(&self, key: &[u8]) -> Result<(SecretKey, DerivationParameters)> { + let v2 = KeyDerivationV2 { + params: self.params.clone(), + }; + let raw = v2.derive(key)?; + let secret_key = secret_key_from_raw(raw)?; + + let mut header: Header = Header::default(); + header.version = KeyDerivationVersion::V2; + + let derivation_params = DerivationParameters { + header, + payload: DerivationParametersPayload::V2(v2), + }; + + Ok((secret_key, derivation_params)) + } +} + +impl Default for Argon2 { + fn default() -> Self { + Self::new() + } +} diff --git a/src/key_derivation/mod.rs b/src/key_derivation/mod.rs new file mode 100644 index 00000000..bcf8cdba --- /dev/null +++ b/src/key_derivation/mod.rs @@ -0,0 +1,305 @@ +//! Module for key derivation. Derives a key or password into a `SecretKey` +//! and returns the `DerivationParameters` needed to reproduce the derivation. +//! +//! # Example (Argon2 — default) +//! ```rust +//! use devolutions_crypto::key_derivation::Argon2; +//! +//! let password = b"a very strong password"; +//! let argon2 = Argon2::new(); +//! let (secret_key, params) = argon2.derive(password).expect("derivation should not fail"); +//! // Serialize params to re-derive later: +//! let params_bytes: Vec = params.into(); +//! ``` +//! +//! # Example (PBKDF2) +//! ```rust +//! use devolutions_crypto::key_derivation::Pbkdf2; +//! +//! let password = b"a very strong password"; +//! let pbkdf2 = Pbkdf2::new(); +//! let (secret_key, params) = pbkdf2.derive(password).expect("derivation should not fail"); +//! ``` + +mod key_derivation_v1; +mod key_derivation_v2; + +pub use key_derivation_v1::Pbkdf2; +pub use key_derivation_v2::Argon2; + +use key_derivation_v1::KeyDerivationV1; +use key_derivation_v2::KeyDerivationV2; + +use std::borrow::Borrow; +use std::convert::TryFrom; + +#[cfg(feature = "fuzz")] +use arbitrary::Arbitrary; + +use crate::key::SecretKey; +#[cfg(feature = "fuzz")] +use crate::Argon2Parameters; +use crate::{DataType, Error, Header, HeaderType, KeyDerivationVersion, Result}; + +use super::enums::KeyDerivationSubtype; + +// ── DerivationParameters ───────────────────────────────────────────────────── + +/// Serializable parameters that fully describe a completed key derivation. +/// Can be stored alongside a user record to re-derive the same key later. +#[derive(Clone, Debug)] +#[cfg_attr(feature = "fuzz", derive(Arbitrary))] +pub struct DerivationParameters { + pub(crate) header: Header, + pub(super) payload: DerivationParametersPayload, +} + +impl HeaderType for DerivationParameters { + type Version = KeyDerivationVersion; + type Subtype = KeyDerivationSubtype; + + fn data_type() -> DataType { + DataType::KeyDerivation + } +} + +#[derive(Clone, Debug)] +#[cfg_attr(feature = "fuzz", derive(Arbitrary))] +pub(super) enum DerivationParametersPayload { + V1(KeyDerivationV1), + V2(KeyDerivationV2), +} + +#[cfg(feature = "fuzz")] +impl Arbitrary for KeyDerivationV1 { + fn arbitrary(u: &mut arbitrary::Unstructured<'_>) -> arbitrary::Result { + Ok(KeyDerivationV1 { + iterations: u32::arbitrary(u)?, + salt: Vec::::arbitrary(u)?, + }) + } +} + +#[cfg(feature = "fuzz")] +impl Arbitrary for KeyDerivationV2 { + fn arbitrary(u: &mut arbitrary::Unstructured<'_>) -> arbitrary::Result { + Ok(KeyDerivationV2 { + params: Argon2Parameters::default(), + }) + } +} + +impl From for Vec { + fn from(data: DerivationParameters) -> Self { + let mut header: Self = data.header.borrow().into(); + let mut payload: Self = data.payload.into(); + header.append(&mut payload); + header + } +} + +impl TryFrom<&[u8]> for DerivationParameters { + type Error = Error; + + fn try_from(data: &[u8]) -> Result { + if data.len() < Header::len() { + return Err(Error::InvalidLength); + } + + let header = Header::try_from(&data[0..Header::len()])?; + + let payload = match header.version { + KeyDerivationVersion::V1 => { + DerivationParametersPayload::V1(KeyDerivationV1::try_from(&data[Header::len()..])?) + } + KeyDerivationVersion::V2 => { + DerivationParametersPayload::V2(KeyDerivationV2::try_from(&data[Header::len()..])?) + } + KeyDerivationVersion::Latest => return Err(Error::UnknownVersion), + }; + + Ok(Self { header, payload }) + } +} + +impl From for Vec { + fn from(payload: DerivationParametersPayload) -> Self { + match payload { + DerivationParametersPayload::V1(v1) => Vec::from(&v1), + DerivationParametersPayload::V2(v2) => Vec::from(&v2), + } + } +} + +/// Derives a `SecretKey` from `password` using the algorithm selected by `version`. +/// +/// * `KeyDerivationVersion::Latest` and `KeyDerivationVersion::V2` use **Argon2id** (recommended). +/// * `KeyDerivationVersion::V1` uses **PBKDF2-HMAC-SHA256**. +/// +/// A random salt is generated automatically; the returned [`DerivationParameters`] can be +/// stored alongside the protected data so the same key can be reproduced later. +/// +/// # Example +/// ```rust +/// use devolutions_crypto::key_derivation::{derive_key, DerivationParameters}; +/// use devolutions_crypto::KeyDerivationVersion; +/// +/// let password = b"a very strong password"; +/// let (secret_key, params) = derive_key(password, KeyDerivationVersion::Latest).expect("derivation should not fail"); +/// // Serialize params to re-derive later: +/// let params_bytes: Vec = params.into(); +/// ``` +pub fn derive_key( + password: &[u8], + version: KeyDerivationVersion, +) -> Result<(SecretKey, DerivationParameters)> { + match version { + KeyDerivationVersion::V1 => Pbkdf2::new().derive(password), + KeyDerivationVersion::V2 | KeyDerivationVersion::Latest => Argon2::new().derive(password), + } +} + +// ── Tests ───────────────────────────────────────────────────────────────────── + +#[cfg(test)] +mod tests { + use std::convert::TryFrom; + + use crate::key::secret_key_from_raw; + use crate::Argon2Parameters; + + use super::*; + + // ── Pbkdf2 ─────────────────────────────────────────────────────────────── + + #[test] + fn pbkdf2_derive_same_input_same_salt_produces_same_key() { + let pbkdf2 = Pbkdf2::with_params(10); + let salt = b"fixed_salt_value"; + let (key1, _) = pbkdf2.derive_with_salt(b"password", salt).unwrap(); + let (key2, _) = pbkdf2.derive_with_salt(b"password", salt).unwrap(); + assert_eq!(key1.as_bytes(), key2.as_bytes()); + } + + #[test] + fn pbkdf2_derive_different_password_produces_different_key() { + let pbkdf2 = Pbkdf2::with_params(10); + let salt = b"fixed_salt_value"; + let (key1, _) = pbkdf2.derive_with_salt(b"password1", salt).unwrap(); + let (key2, _) = pbkdf2.derive_with_salt(b"password2", salt).unwrap(); + assert_ne!(key1.as_bytes(), key2.as_bytes()); + } + + #[test] + fn pbkdf2_derive_different_salt_produces_different_key() { + let pbkdf2 = Pbkdf2::with_params(10); + let (key1, _) = pbkdf2.derive_with_salt(b"password", b"salt_one").unwrap(); + let (key2, _) = pbkdf2.derive_with_salt(b"password", b"salt_two").unwrap(); + assert_ne!(key1.as_bytes(), key2.as_bytes()); + } + + #[test] + fn pbkdf2_derive_generates_random_salt() { + let pbkdf2 = Pbkdf2::with_params(10); + let (_, params1) = pbkdf2.derive(b"password").unwrap(); + let (_, params2) = pbkdf2.derive(b"password").unwrap(); + let bytes1: Vec = params1.into(); + let bytes2: Vec = params2.into(); + assert_ne!(bytes1, bytes2); + } + + #[test] + fn pbkdf2_derive_with_salt_roundtrip() { + let pbkdf2 = Pbkdf2::with_params(10); + let salt = b"roundtrip_salt!!"; + let (key1, params) = pbkdf2.derive_with_salt(b"password", salt).unwrap(); + + // Serialize and deserialize params + let params_bytes: Vec = params.into(); + let params2 = DerivationParameters::try_from(params_bytes.as_slice()).unwrap(); + + // Re-derive using deserialized params + let payload_bytes: Vec = params2.payload.into(); + let v1 = KeyDerivationV1::try_from(payload_bytes.as_slice()).unwrap(); + let raw = v1.derive(b"password"); + let key2 = secret_key_from_raw(raw).unwrap(); + + assert_eq!(key1.as_bytes(), key2.as_bytes()); + } + + #[test] + fn pbkdf2_derivation_parameters_serialize_roundtrip() { + let pbkdf2 = Pbkdf2::with_params(12345); + let (_, params) = pbkdf2 + .derive_with_salt(b"password", b"some_salt_here!!") + .unwrap(); + let bytes: Vec = params.into(); + let params2 = DerivationParameters::try_from(bytes.as_slice()).unwrap(); + assert_eq!(params2.header.version, KeyDerivationVersion::V1); + } + + // ── Argon2 ─────────────────────────────────────────────────────────────── + + #[test] + fn argon2_derive_same_params_same_input_produces_same_key() { + let mut argon2_params = Argon2Parameters::default(); + argon2_params.iterations = 2; + argon2_params.memory = 64; + // Fix the salt so derivation is deterministic + argon2_params.set_salt(b"fixed_salt_16byt".to_vec()); + + let argon2 = Argon2::with_params(argon2_params.clone()); + let (key1, _) = argon2.derive(b"password").unwrap(); + + let argon2 = Argon2::with_params(argon2_params); + let (key2, _) = argon2.derive(b"password").unwrap(); + + assert_eq!(key1.as_bytes(), key2.as_bytes()); + } + + #[test] + fn argon2_derive_different_salt_produces_different_key() { + // Two calls to new() generate different salts + let (key1, _) = Argon2::new().derive(b"password").unwrap(); + let (key2, _) = Argon2::new().derive(b"password").unwrap(); + assert_ne!(key1.as_bytes(), key2.as_bytes()); + } + + #[test] + fn argon2_derivation_parameters_serialize_roundtrip() { + let mut argon2_params = Argon2Parameters::default(); + argon2_params.iterations = 2; + argon2_params.memory = 64; + let (_, params) = Argon2::with_params(argon2_params) + .derive(b"password") + .unwrap(); + let bytes: Vec = params.into(); + let params2 = DerivationParameters::try_from(bytes.as_slice()).unwrap(); + assert_eq!(params2.header.version, KeyDerivationVersion::V2); + } + + // ── validate_header ─────────────────────────────────────────────────────── + + #[test] + fn validate_header_accepts_key_derivation() { + use crate::utils::validate_header; + let (_, params) = Pbkdf2::with_params(10) + .derive_with_salt(b"pw", b"salt_16bytes!!!") + .unwrap(); + let bytes: Vec = params.into(); + assert!(validate_header(&bytes, DataType::KeyDerivation)); + } + + #[test] + fn validate_header_rejects_wrong_type() { + use crate::utils::validate_header; + use crate::DataType; + let (_, params) = Pbkdf2::with_params(10) + .derive_with_salt(b"pw", b"salt_16bytes!!!") + .unwrap(); + let bytes: Vec = params.into(); + assert!(!validate_header(&bytes, DataType::Ciphertext)); + assert!(!validate_header(&bytes, DataType::Key)); + assert!(!validate_header(&bytes, DataType::PasswordHash)); + } +} diff --git a/src/lib.rs b/src/lib.rs index 9fc1dbc9..1cb508cf 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -6,13 +6,14 @@ //! * [Symmetric Encryption](#symmetric) //! * [Asymmetric Encryption](#asymmetric) //! * [Key Module](#key) -//! * [Key Generation/Derivation](#generationderivation) +//! * [Key Generation](#generation) //! * [Key Exchange](#key-exchange) +//! * [Key Derivation](#key-derivation) //! * [PasswordHash Module](#passwordhash) //! * [SecretSharing Module](#secretsharing) //! * [Utils Module](#utils) //! * [Key Generation](#key-generation) -//! * [Key Derivation](#key-derivation) +//! * [Key Derivation](#key-derivation-1) //! //! ## Overview //! @@ -22,39 +23,43 @@ //! //! These structures all implement `TryFrom<&[u8]>` and `Into>` to serialize and deserialize data. //! +//! +//! ## Ciphertext +//! +//! This module contains everything related to encryption. You can use it to encrypt and decrypt data using either a shared secret key or a keypair. +//! The encryption will give you a `Ciphertext`, which has a method to decrypt it. +//! +//! ### Symmetric +//! The library provides a `SecretKey` which can be used as a shared secret to encrypt messages. +//! //! ```rust //! use std::convert::TryFrom as _; -//! use devolutions_crypto::utils::generate_key; -//! use devolutions_crypto::ciphertext::{ encrypt, CiphertextVersion, Ciphertext }; +//! use devolutions_crypto::key::{generate_secret_key, KeyVersion, SecretKey}; +//! use devolutions_crypto::ciphertext::{ encrypt_with_secret_key, CiphertextVersion, Ciphertext }; //! -//! let key: Vec = generate_key(32).expect("generate key shouldn't fail");; +//! let secret_key = generate_secret_key(KeyVersion::Latest); //! let data = b"somesecretdata"; -//! let encrypted_data: Ciphertext = encrypt(data, &key, CiphertextVersion::Latest).expect("encryption shouldn't fail"); +//! let encrypted_data = encrypt_with_secret_key(data, &secret_key, CiphertextVersion::Latest).expect("encryption shouldn't fail"); //! //! // The ciphertext can be serialized to be saved somewhere, passed to another language or over the network. //! let encrypted_data_vec: Vec = encrypted_data.into(); //! //! // When you receive the data as a byte array, you can deserialize it into a struct using TryFrom //! let ciphertext = Ciphertext::try_from(encrypted_data_vec.as_slice()).expect("deserialization shouldn't fail"); -//! let decrypted_data = ciphertext.decrypt(&key).expect("The decryption shouldn't fail"); +//! let decrypted_data = ciphertext.decrypt_with_secret_key(&secret_key).expect("The decryption shouldn't fail"); //! assert_eq!(decrypted_data, data); //! ``` //! -//! ## Ciphertext -//! -//! This module contains everything related to encryption. You can use it to encrypt and decrypt data using either a shared key of a keypair. -//! Either way, the encryption will give you a `Ciphertext`, which has a method to decrypt it. -//! -//! ### Symmetric +//! The key can also be passed as raw bytes. //! //! ```rust //! use devolutions_crypto::utils::generate_key; -//! use devolutions_crypto::ciphertext::{encrypt, CiphertextVersion, Ciphertext}; +//! use devolutions_crypto::ciphertext::{encrypt_with_raw_key, CiphertextVersion, Ciphertext}; //! //! let key: Vec = generate_key(32).expect("generate key shouldn't fail"); //! let data = b"somesecretdata"; //! -//! let encrypted_data: Ciphertext = encrypt(data, &key, CiphertextVersion::Latest).expect("encryption shouldn't fail"); +//! let encrypted_data: Ciphertext = encrypt_with_raw_key(data, &key, CiphertextVersion::Latest).expect("encryption shouldn't fail"); //! let decrypted_data = encrypted_data.decrypt(&key).expect("The decryption shouldn't fail"); //! assert_eq!(decrypted_data, data); //! ``` @@ -80,7 +85,7 @@ //! //! This module provides secret keys and keypairs. //! -//! ### Generation/Derivation +//! ### Generation //! //! Use `generate_secret_key` to a generate a random symmetric key and `generate_keypair` to generate a random keypair. //! @@ -92,16 +97,51 @@ //! let keypair: KeyPair = generate_keypair(KeyVersion::Latest); //! ``` //! +//! ## Key Derivation +//! +//! The Key Derivation module provides a way to derive a `SecretKey` from a password or passphrase. The derive operation +//! returns a `SecretKey`, and a `DerivationParameters` that can be serialized and reused to derive the same key at a +//! later time. +//! +//! Example with `derive_key`: +//! ```rust +//! use devolutions_crypto::key_derivation::{derive_key, DerivationParameters}; +//! use devolutions_crypto::KeyDerivationVersion; +//! +//! let password = b"a very strong password"; +//! let (secret_key, params) = derive_key(password, KeyDerivationVersion::Latest).expect("derivation should not fail"); +//! // Serialize params to re-derive later: +//! let params_bytes: Vec = params.into(); +//! ``` +//! +//! Example with Argon2 (recommended): +//! ```rust +//! use devolutions_crypto::key_derivation::Argon2; +//! let password = b"a very strong password"; +//! let argon2 = Argon2::new(); +//! let (secret_key, params) = argon2.derive(password).expect("derivation should not fail"); +//! // Serialize params to re-derive later: +//! let params_bytes: Vec = params.into(); +//! ``` +//! +//! Example with PBKDF2: +//! ```rust +//! use devolutions_crypto::key_derivation::Pbkdf2; +//! let password = b"a very strong password"; +//! let pbkdf2 = Pbkdf2::new(); +//! let (secret_key, params) = pbkdf2.derive(password).expect("derivation should not fail"); +//! ``` +//! //! ### Key Exchange //! //! The goal of using a key exchange is to get a shared secret key between //! two parties without making it possible for users listening on the conversation //! to guess that shared key. -//! 1. Alice and Bob generates a `KeyPair` each. -//! 2. Alice and Bob exchanges their `PublicKey`. -//! 3. Alice mix her `PrivateKey` with Bob's `PublicKey`. This gives her the shared key. +//! 1. Alice and Bob generate a `KeyPair` each. +//! 2. Alice and Bob exchange their `PublicKey`. +//! 3. Alice mixes her `PrivateKey` with Bob's `PublicKey`. This gives her the shared key. //! 4. Bob mixes his `PrivateKey` with Alice's `PublicKey`. This gives him the shared key. -//! 5. Both Bob and Alice has the same shared key, which they can use for symmetric encryption for further communications. +//! 5. Both Bob and Alice have the same shared key, which they can use for symmetric encryption for further communications. //! //! ```rust //! use devolutions_crypto::key::{generate_keypair, mix_key_exchange, KeyVersion, KeyPair}; @@ -165,11 +205,10 @@ //! assert_eq!(32, key.len()); //! ``` //! +//! //! ### Key Derivation //! -//! This is a method used to generate a key from a password or another key. Useful for password-dependent -//! cryptography. Salt should be a random 16 bytes array if possible and iterations should be 600,000 or configurable -//! by the user. +//! The library exposes raw methods for key derivation with argon2 and PBKDF2. We recommend using the managed [Key Derivation](#key-derivation) module. //! //! ```rust //! use devolutions_crypto::utils::{generate_key, derive_key_pbkdf2}; @@ -183,11 +222,13 @@ //! assert_eq!(32, new_key.len()); //! ``` //! +//! //! # Underlying algorithms //! As of the current version: //! * Symmetric cryptography uses XChaCha20Poly1305 //! * Asymmetric cryptography uses Curve25519. //! * Asymmetric encryption uses ECIES. +//! * Key derivation uses Argon2 or PBKDF2 //! * Key exchange uses x25519, or ECDH over Curve25519 //! * Password Hashing uses PBKDF2-HMAC-SHA2-256 //! * Secret Sharing uses Shamir Secret sharing over GF256 @@ -200,6 +241,7 @@ mod header; pub mod ciphertext; pub mod key; +pub mod key_derivation; pub mod online_ciphertext; pub mod password_hash; pub mod secret_sharing; @@ -211,8 +253,9 @@ use enums::{CiphertextSubtype, PasswordHashSubtype, ShareSubtype, SignatureSubty pub use header::{Header, HeaderType}; pub use enums::{ - CiphertextVersion, DataType, KeySubtype, KeyVersion, OnlineCiphertextVersion, - PasswordHashVersion, SecretSharingVersion, SignatureVersion, SigningKeyVersion, + CiphertextVersion, DataType, KeyDerivationVersion, KeySubtype, KeyVersion, + OnlineCiphertextVersion, PasswordHashVersion, SecretSharingVersion, SignatureVersion, + SigningKeyVersion, }; pub use argon2::Variant as Argon2Variant; @@ -221,6 +264,7 @@ pub use argon2parameters::defaults as argon2parameters_defaults; pub use argon2parameters::Argon2Parameters; pub use argon2parameters::Argon2ParametersBuilder; pub use error::{Error, Result}; +pub use key_derivation::{derive_key, Argon2, DerivationParameters, Pbkdf2}; pub const DEFAULT_KEY_SIZE: usize = 32; pub const DEFAULT_PBKDF2_ITERATIONS: u32 = 600000; diff --git a/src/utils.rs b/src/utils.rs index 3f41df30..2a7e7322 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -94,7 +94,7 @@ pub fn derive_key_argon2(key: &[u8], parameters: &Argon2Parameters) -> Result = encrypt(b"test", &key, CiphertextVersion::Latest).unwrap().into(); +/// let ciphertext: `Vec` = encrypt(b"test", &key, CiphertextVersion::Latest).unwrap().into(); /// /// assert!(validate_header(&ciphertext, DataType::Ciphertext); /// assert!(!validate_header(&ciphertext, DataType::PasswordHash); @@ -128,13 +128,17 @@ pub fn validate_header(data: &[u8], data_type: DataType) -> bool { DataType::OnlineCiphertext => { Header::::try_from(&data[0..Header::len()]).is_ok() } + DataType::KeyDerivation => { + use super::key_derivation::DerivationParameters; + Header::::try_from(&data[0..Header::len()]).is_ok() + } } } /// Temporarly binded here for a specific use case, don't rely on this. /// /// Copied and modified from: -/// https://github.com/RustCrypto/password-hashing/blob/master/scrypt/src/simple.rs +/// /// Because rand is outdated, I cannot use the crate directly pub fn scrypt_simple(password: &[u8], salt: &[u8], log_n: u8, r: u32, p: u32) -> String { use byteorder::{ByteOrder, LittleEndian}; diff --git a/tests/conformity.rs b/tests/conformity.rs index 3c6e4af6..c04e1efe 100644 --- a/tests/conformity.rs +++ b/tests/conformity.rs @@ -29,6 +29,26 @@ fn test_derive_key_argon2() { ); } +#[test] +fn test_derive_key_argon2_struct() { + use devolutions_crypto::key_derivation::Argon2; + let params = Argon2Parameters::try_from( + general_purpose::STANDARD + .decode("AQAAACAAAAABAAAAIAAAAAEAAAACEwAAAAAQAAAAimFBkm3f8+f+YfLRnF5OoQ==") + .unwrap() + .as_slice(), + ) + .unwrap(); + let argon2 = Argon2::with_params(params.clone()); + let (key, _derivation_params) = argon2.derive(b"password").unwrap(); + assert_eq!( + key.as_bytes(), + &general_purpose::STANDARD + .decode("AcEN6Cb1Om6tomZScAM725qiXMzaxaHlj3iMiT/Ukq0=") + .unwrap()[..] + ); +} + #[test] fn test_derive_key_default() { let password = b"testpassword"; @@ -43,6 +63,21 @@ fn test_derive_key_default() { ); } +#[test] +fn test_derive_key_pbkdf2_struct_default() { + use devolutions_crypto::key_derivation::Pbkdf2; + let password = b"testpassword"; + let salt = b""; + let pbkdf2 = Pbkdf2::with_params(600000); + let (key, _params) = pbkdf2.derive_with_salt(password, salt).unwrap(); + assert_eq!( + key.as_bytes(), + &general_purpose::STANDARD + .decode("wdU+cxAOpTFddVhTQlKQTSzmVjZqPAXVx1cRrAqTGek=") + .unwrap()[..] + ); +} + #[test] fn test_derive_key_iterations() { let password = b"testPa$$"; @@ -57,6 +92,21 @@ fn test_derive_key_iterations() { ); } +#[test] +fn test_derive_key_pbkdf2_struct_iterations() { + use devolutions_crypto::key_derivation::Pbkdf2; + let password = b"testPa$$"; + let salt = b""; + let pbkdf2 = Pbkdf2::with_params(100); + let (key, _params) = pbkdf2.derive_with_salt(password, salt).unwrap(); + assert_eq!( + key.as_bytes(), + &general_purpose::STANDARD + .decode("ev/GiJLvOgIkkWrnIrHSi2fdZE5qJBIrW+DLeMLIXK4=") + .unwrap()[..] + ); +} + #[test] fn test_derive_key_salt() { let password = b"testPa$$"; @@ -73,6 +123,23 @@ fn test_derive_key_salt() { ); } +#[test] +fn test_derive_key_pbkdf2_struct_salt() { + use devolutions_crypto::key_derivation::Pbkdf2; + let password = b"testPa$$"; + let salt = general_purpose::STANDARD + .decode("tdTt5wgeqQYLvkiXKkFirqy2hMbzadBtL+jekVeNCRA=") + .unwrap(); + let pbkdf2 = Pbkdf2::with_params(100); + let (key, _params) = pbkdf2.derive_with_salt(password, &salt).unwrap(); + assert_eq!( + key.as_bytes(), + &general_purpose::STANDARD + .decode("ZaYRZeQiIPJ+Jl511AgHZjv4/HbCFq4eUP9yNa3gowI=") + .unwrap()[..] + ); +} + #[test] fn test_symmetric_decrypt_v1() { let key = general_purpose::STANDARD diff --git a/uniffi/devolutions-crypto-uniffi/src/lib.rs b/uniffi/devolutions-crypto-uniffi/src/lib.rs index 82d03d4e..b56b5ead 100644 --- a/uniffi/devolutions-crypto-uniffi/src/lib.rs +++ b/uniffi/devolutions-crypto-uniffi/src/lib.rs @@ -31,6 +31,7 @@ pub enum DataType { SigningKey, Signature, OnlineCiphertext, + KeyDerivation, } #[uniffi::remote(Enum)]