Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
79 changes: 79 additions & 0 deletions cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,30 @@ enum Commands {
shares: Vec<String>,
},

/// Combine key derivation and encryption in a single operation
#[command(arg_required_else_help = true)]
DeriveAndEncrypt {
/// The plaintext to encrypt
data: String,

/// The password from which the encryption key is derived
password: String,

/// The cipher version to use (default: latest)
#[arg(short, long)]
version: Option<u16>,
},

/// Decrypt a KdfEncryptedData blob with a password
#[command(arg_required_else_help = true)]
DeriveAndDecrypt {
/// The KdfEncryptedData blob to decrypt, in base64
data: String,

/// The password used during encryption
password: String,
},

/// Print the header information
#[command(arg_required_else_help = true)]
PrintHeader {
Expand Down Expand Up @@ -198,6 +222,14 @@ fn main() {
Commands::VerifyPassword { password, hash } => verify_password(hash, password),
Commands::MixKeyExchange { private, public } => mix_key_exchange(private, public),
Commands::JoinShares { shares } => join_shares(shares),
Commands::DeriveAndEncrypt {
data,
password,
version,
} => derive_and_encrypt_password(data, password, version),
Commands::DeriveAndDecrypt { data, password } => {
derive_and_decrypt_password(data, password)
}
Commands::PrintHeader { data } => print_header(data),
}
}
Expand Down Expand Up @@ -258,6 +290,42 @@ fn derive_key(data: String, salt: Option<String>, iterations: Option<u32>) {
println!("DerivationParameters: {}", base64::encode(&params_bytes));
}

fn derive_and_encrypt_password(data: String, password: String, version: Option<u16>) {
use devolutions_crypto::derive_encrypt::encrypt_with_password;
use devolutions_crypto::key_derivation::Argon2;

let version = version.unwrap_or(0);
let version = devolutions_crypto::CiphertextVersion::try_from(version).unwrap();

let params = Argon2::new().parameters();
let result: Vec<u8> =
encrypt_with_password(data.as_bytes(), password.as_bytes(), params, version)
.unwrap()
.into();
println!("{}", base64::encode(&result));
}

fn derive_and_decrypt_password(data: String, password: String) {
use devolutions_crypto::derive_encrypt::KdfEncryptedData;

let data_bytes = decode_base64_arg("data", &data);
let blob = KdfEncryptedData::try_from(data_bytes.as_slice()).unwrap_or_else(|_| {
eprintln!(
"Error: 'data' - expected KdfEncryptedData, received {}.",
detect_dc_type(&data_bytes)
);
std::process::exit(1);
});

let result: Vec<u8> = blob
.decrypt_with_password(password.as_bytes())
.unwrap_or_else(|e| {
eprintln!("Error: decryption failed: {}.", e);
std::process::exit(1);
});
println!("{}", String::from_utf8_lossy(&result));
}

/// 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 {
Expand All @@ -284,6 +352,7 @@ fn detect_dc_type(bytes: &[u8]) -> String {
Ok(DataType::SigningKey) => "SigningKey".to_string(),
Ok(DataType::Signature) => "Signature".to_string(),
Ok(DataType::KeyDerivation) => "DerivationParameters".to_string(),
Ok(DataType::KdfEncryptedData) => "KdfEncryptedData".to_string(),
_ => "unknown".to_string(),
}
}
Expand Down Expand Up @@ -541,6 +610,16 @@ fn print_header(data: String) {
println!("Invalid Header");
}
}
Ok(devolutions_crypto::DataType::KdfEncryptedData) => {
if let Ok(header) = devolutions_crypto::Header::<
devolutions_crypto::derive_encrypt::KdfEncryptedData,
>::try_from(&data[0..8])
{
println!("{:?}", &header);
} else {
println!("Invalid Header");
}
}
_ => println!("Invalid Header"),
}
}
181 changes: 180 additions & 1 deletion ffi/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,16 +21,18 @@ use base64::{engine::general_purpose, Engine as _};
use devolutions_crypto::ciphertext::{
encrypt_asymmetric_with_aad, encrypt_with_aad, Ciphertext, CiphertextVersion,
};
use devolutions_crypto::derive_encrypt::{encrypt_with_password_and_aad, KdfEncryptedData};
use devolutions_crypto::key::{
generate_keypair, generate_secret_key, mix_key_exchange, KeyVersion, PrivateKey, PublicKey,
};
use devolutions_crypto::key_derivation::{Argon2, DerivationParameters, Pbkdf2};
use devolutions_crypto::key_derivation::{derive_key, Argon2, DerivationParameters, Pbkdf2};
use devolutions_crypto::password_hash::{
hash_password, hash_password_with_parameters, PasswordHash, PasswordHashVersion,
};
use devolutions_crypto::secret_sharing::{
generate_shared_key, join_shares, SecretSharingVersion, Share,
};
use devolutions_crypto::KeyDerivationVersion;
use devolutions_crypto::OnlineCiphertextVersion;
use devolutions_crypto::{
signature,
Expand Down Expand Up @@ -221,6 +223,183 @@ pub extern "C" fn EncryptAsymmetricSize(data_length: usize, version: u16) -> i64
}
}

/// Get the size of the resulting derive_encrypt blob.
/// # Arguments
/// * data_length - Length of the plaintext.
/// * key_derivation_version - Version for key derivation (0 latest, 1 PBKDF2, 2 Argon2).
/// * ciphertext_version - Version for ciphertext (0 latest, 1 V1, 2 V2).
/// # Returns
/// Returns the exact output length expected by `DeriveEncryptData()`.
#[no_mangle]
pub extern "C" fn DeriveEncryptSize(
data_length: usize,
key_derivation_version: u16,
ciphertext_version: u16,
) -> i64 {
let key_derivation_version = match KeyDerivationVersion::try_from(key_derivation_version) {
Ok(v) => v,
Err(_) => return Error::UnknownVersion.error_code(),
};

let ciphertext_size = EncryptSize(data_length, ciphertext_version);

if ciphertext_size < 0 {
return ciphertext_size;
}

let derivation_parameters_size: usize = match key_derivation_version {
KeyDerivationVersion::V1 => DeriveSecretKeyPbkdf2Size() as usize,
KeyDerivationVersion::V2 | KeyDerivationVersion::Latest => {
DeriveSecretKeyArgon2ParametersSize(GetDefaultArgon2ParametersSize() as usize) as usize
}
};

// The length is calculated based on these values:
// 8 = KdfEncryptedData header
// 4 = u32 derivation_parameters length prefix
// 4 = u32 ciphertext length prefix
(8 + 4 + derivation_parameters_size + 4 + ciphertext_size as usize) as i64
Comment thread
sduquette-devolutions marked this conversation as resolved.
}

/// Derive a key from a password and encrypt data in one managed blob.
/// # Arguments
/// * `data` - Pointer to the plaintext data to encrypt.
/// * `data_length` - Length of plaintext data.
/// * `password` - Pointer to the password bytes.
/// * `password_length` - Length of password bytes.
/// * `aad` - Pointer to additional authenticated data, or null.
/// * `aad_length` - Length of additional authenticated data.
/// * `result` - Pointer to output buffer.
/// * `result_length` - Length of output buffer, must equal `DeriveEncryptSize(...)`.
/// * `key_derivation_version` - Key derivation version.
/// * `ciphertext_version` - Ciphertext version.
/// # Returns
/// The number of bytes written on success, or a negative DevoCryptoError code on failure.
/// # Safety
/// This method is made to be called by C, so it is therefore unsafe. The caller should make sure it passes the right pointers and sizes.
#[no_mangle]
pub unsafe extern "C" fn DeriveEncryptData(
data: *const u8,
data_length: usize,
password: *const u8,
password_length: usize,
aad: *const u8,
aad_length: usize,
result: *mut u8,
result_length: usize,
key_derivation_version: u16,
ciphertext_version: u16,
) -> i64 {
if data.is_null() || password.is_null() || result.is_null() {
return Error::NullPointer.error_code();
}

if result_length
!= DeriveEncryptSize(data_length, key_derivation_version, ciphertext_version) as usize
{
return Error::InvalidOutputLength.error_code();
}

let key_derivation_version = match KeyDerivationVersion::try_from(key_derivation_version) {
Ok(v) => v,
Err(_) => return Error::UnknownVersion.error_code(),
};

let ciphertext_version = match CiphertextVersion::try_from(ciphertext_version) {
Ok(v) => v,
Err(_) => return Error::UnknownVersion.error_code(),
};

let aad = if aad.is_null() {
&[]
} else {
slice::from_raw_parts(aad, aad_length)
};

let data = slice::from_raw_parts(data, data_length);
let password = Zeroizing::new(slice::from_raw_parts(password, password_length).to_vec());
let result = slice::from_raw_parts_mut(result, result_length);

let derivation_parameters = match key_derivation_version {
KeyDerivationVersion::Latest | KeyDerivationVersion::V2 => Argon2::new().parameters(),
KeyDerivationVersion::V1 => Pbkdf2::new()
.parameters()
.expect("default PKBDF2 parameters shouldn't fail"),
};

match encrypt_with_password_and_aad(
data,
&password,
aad,
derivation_parameters,
ciphertext_version,
) {
Ok(res) => {
let res: Vec<u8> = res.into();
let length = res.len();
result[0..length].copy_from_slice(&res);
length as i64
}
Err(e) => e.error_code(),
}
}

/// Decrypt a derive_encrypt blob using a password.
/// # Arguments
/// * `data` - Pointer to derive_encrypt bytes.
/// * `data_length` - Length of derive_encrypt bytes.
/// * `password` - Pointer to password bytes.
/// * `password_length` - Length of password bytes.
/// * `aad` - Pointer to additional authenticated data, or null.
/// * `aad_length` - Length of additional authenticated data.
/// * `result` - Pointer to output plaintext buffer.
/// * `result_length` - Length of output plaintext buffer.
/// # Returns
/// The number of plaintext bytes written on success, or a negative DevoCryptoError code on failure.
/// # Safety
/// This method is made to be called by C, so it is therefore unsafe. The caller should make sure it passes the right pointers and sizes.
#[no_mangle]
pub unsafe extern "C" fn DeriveDecryptData(
data: *const u8,
data_length: usize,
password: *const u8,
password_length: usize,
aad: *const u8,
aad_length: usize,
result: *mut u8,
result_length: usize,
) -> i64 {
if data.is_null() || password.is_null() || result.is_null() {
return Error::NullPointer.error_code();
}

let aad = if aad.is_null() {
&[]
} else {
slice::from_raw_parts(aad, aad_length)
};

let data = slice::from_raw_parts(data, data_length);
let password = Zeroizing::new(slice::from_raw_parts(password, password_length).to_vec());
let result = slice::from_raw_parts_mut(result, result_length);

match KdfEncryptedData::try_from(data) {
Ok(res) => match res.decrypt_with_password_and_aad(&password, aad) {
Ok(plaintext) => {
if result.len() >= plaintext.len() {
let length = plaintext.len();
result[0..length].copy_from_slice(&plaintext);
length as i64
} else {
Error::InvalidOutputLength.error_code()
}
}
Err(e) => e.error_code(),
},
Err(e) => e.error_code(),
}
}

/// Decrypt a data blob
/// # Arguments
/// * `data` - Pointer to the data to decrypt.
Expand Down
60 changes: 60 additions & 0 deletions python/devolutions_crypto.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -394,6 +394,64 @@ def decrypt_with_secret_key(
"""
...

def derive_encrypt_with_password(
data: bytes,
password: bytes,
aad: Optional[bytes] = None,
key_derivation_version: int = 0,
ciphertext_version: int = 0
) -> bytes:
"""
Derive a key from a password and encrypt data in a single blob.

The output contains the key derivation parameters and the ciphertext,
allowing decryption with only the password.

Args:
data: The plaintext data to encrypt
password: The password to derive the encryption key from
aad: Optional Additional Authenticated Data for AEAD
key_derivation_version: KDF version (0 = latest/Argon2, 1 = PBKDF2)
ciphertext_version: Ciphertext version (0 = latest)

Returns:
The DeriveEncrypt blob as bytes (KDF params + ciphertext)

Raises:
DevolutionsCryptoException: If encryption or key derivation fails

Example:
>>> blob = derive_encrypt_with_password(b'secret data', b'my password')
"""
...

def derive_decrypt_with_password(
data: bytes,
password: bytes,
aad: Optional[bytes] = None
) -> bytes:
"""
Decrypt a DeriveEncrypt blob using the original password.

Re-derives the encryption key from the embedded KDF parameters and password,
then decrypts the embedded ciphertext.

Args:
data: The DeriveEncrypt blob (produced by derive_encrypt_with_password)
password: The password used during encryption
aad: Optional Additional Authenticated Data (must match encryption AAD)

Returns:
The decrypted plaintext as bytes

Raises:
DevolutionsCryptoException: If decryption fails, wrong password, or invalid blob

Example:
>>> plaintext = derive_decrypt_with_password(blob, b'my password')
"""
...

def get_signing_public_key(keypair: bytes) -> bytes:
"""
Extract the public key from a signing keypair.
Expand Down Expand Up @@ -481,4 +539,6 @@ __all__ = [
'generate_secret_key',
'encrypt_with_secret_key',
'decrypt_with_secret_key',
'derive_encrypt_with_password',
'derive_decrypt_with_password',
]
Loading
Loading