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
106 changes: 105 additions & 1 deletion ffi/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ use devolutions_crypto::derive_encrypt::{encrypt_with_password_and_aad, KdfEncry
use devolutions_crypto::key::{
generate_keypair, generate_secret_key, mix_key_exchange, KeyVersion, PrivateKey, PublicKey,
};
use devolutions_crypto::key_derivation::{derive_key, Argon2, DerivationParameters, Pbkdf2};
use devolutions_crypto::key_derivation::{Argon2, DerivationParameters, Pbkdf2};
use devolutions_crypto::password_hash::{
hash_password, hash_password_with_parameters, PasswordHash, PasswordHashVersion,
};
Expand Down Expand Up @@ -344,6 +344,110 @@ pub unsafe extern "C" fn DeriveEncryptData(
}
}

/// Derive a key from a password using caller-supplied serialized [`DerivationParameters`] and encrypt data.
/// # 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.
/// * `params` - Pointer to serialized `DerivationParameters` bytes.
/// * `params_length` - Length of serialized `DerivationParameters` 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 `DeriveEncryptDataWithParamsSize(...)`.
/// * `ciphertext_version` - Ciphertext version (0 latest, 1 AES-CBC, 2 XChaCha20).
/// # 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 DeriveEncryptDataWithParams(
data: *const u8,
data_length: usize,
password: *const u8,
password_length: usize,
params: *const u8,
params_length: usize,
aad: *const u8,
aad_length: usize,
result: *mut u8,
result_length: usize,
ciphertext_version: u16,
) -> i64 {
if data.is_null() || password.is_null() || params.is_null() || result.is_null() {
return Error::NullPointer.error_code();
}

if result_length
!= DeriveEncryptDataWithParamsSize(data_length, params_length, ciphertext_version) as usize
{
return Error::InvalidOutputLength.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 params_raw = slice::from_raw_parts(params, params_length);
let result = slice::from_raw_parts_mut(result, result_length);

let derivation_parameters = match DerivationParameters::try_from(params_raw) {
Ok(p) => p,
Err(e) => return e.error_code(),
};

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(),
}
}

/// Get the size of the resulting derive_encrypt blob when using pre-built serialized [`DerivationParameters`].
/// # Arguments
/// * `data_length` - Length of the plaintext.
/// * `params_length` - Length of the serialized `DerivationParameters`.
/// * `ciphertext_version` - Version for ciphertext (0 latest, 1 AES-CBC, 2 XChaCha20).
/// # Returns
/// Returns the exact output length expected by `DeriveEncryptDataWithParams()`.
#[no_mangle]
pub extern "C" fn DeriveEncryptDataWithParamsSize(
data_length: usize,
params_length: usize,
ciphertext_version: u16,
) -> i64 {
let ciphertext_size = EncryptSize(data_length, ciphertext_version);

if ciphertext_size < 0 {
return ciphertext_size;
}

// 8 = KdfEncryptedData header
// 4 = u32 derivation_parameters length prefix
// 4 = u32 ciphertext length prefix
(8 + 4 + params_length + 4 + ciphertext_size as usize) as i64
}

/// Decrypt a derive_encrypt blob using a password.
/// # Arguments
/// * `data` - Pointer to derive_encrypt bytes.
Expand Down
14 changes: 13 additions & 1 deletion tests/conformity.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ use devolutions_crypto::{
key::{PrivateKey, SecretKey},
password_hash::PasswordHash,
utils::{derive_key_argon2, derive_key_pbkdf2},
Argon2Parameters,
Argon2Parameters, KdfEncryptedData,
};

use std::convert::TryFrom as _;
Expand Down Expand Up @@ -311,6 +311,18 @@ fn test_utils_base64() {
assert_eq!(base64_encode(data), "QmFzZTY0VGVzdA==");
}

#[test]
fn test_derive_decrypt_with_password_v1() {
let data = general_purpose::STANDARD
.decode("DQwJAAAAAQA2AAAAQgAAAA0MCAAAAAIAAQAAACAAAAABAAAAABAAAAIAAAACEwAAAAAQAAAAToyZHBBdwMfQ/nSt8fAG2g0MAgABAAIAOy6I4UgmX2jX+ji691rHdSKa5r4X1ItGiT6BszvL1eagyovyr/0DPMM2eIOmctQzuiQHgQ2BXrULGQ==")
.unwrap();

let blob = KdfEncryptedData::try_from(data.as_slice()).unwrap();
let result = blob.decrypt_with_password(b"DevoCrypto!").unwrap();

assert_eq!(result, b"Derive and Encrypt");
}

#[test]
fn test_symmetric_decrypt_with_secret_key_v2() {
// SecretKey wrapping the known key ozJVEme4+5e/4NG3C+Rl26GQbGWAqGc0QPX8/1xvaFM=
Expand Down
7 changes: 6 additions & 1 deletion wrappers/csharp/src/Enums.cs
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,12 @@ public enum DataType
/// <summary>
/// TODO
/// </summary>
OnlineCiphertext = 7
OnlineCiphertext = 7,

/// <summary>
/// A blob containing key derivation parameters and an encrypted ciphertext.
/// </summary>
KdfEncryptedData = 9
}

/// <summary>
Expand Down
118 changes: 118 additions & 0 deletions wrappers/csharp/src/Managed.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1390,6 +1390,124 @@ public static byte[][] GenerateSharedKey(int nbShares, int threshold, int secret
return result;
}

/// <summary>
/// Encrypts data with a key derived from a password and embeds the derivation parameters in the output blob.
/// Defaults to Argon2id with default parameters and XChaCha20-Poly1305 encryption.
/// </summary>
/// <param name="data">The data to encrypt.</param>
/// <param name="password">The password to derive the key from.</param>
/// <param name="aad">Additional authenticated data (optional).</param>
/// <param name="derivationParameters">Pre-built derivation parameters. Defaults to Argon2id when <c>null</c>.</param>
/// <param name="cipherTextVersion">The ciphertext version to use. Defaults to <see cref="CipherTextVersion.Latest"/> (XChaCha20-Poly1305).</param>
/// <returns>A self-contained blob containing the derivation parameters and the ciphertext.</returns>
public static byte[] DeriveEncryptWithPassword(byte[] data, byte[] password, byte[]? aad = null, DerivationParameters? derivationParameters = null, CipherTextVersion cipherTextVersion = CipherTextVersion.Latest)
{
if (data == null || data.Length == 0 || password == null || password.Length == 0)
{
throw new DevolutionsCryptoException(ManagedError.InvalidParameter);
}

long aadLength = aad?.Length ?? 0;

if (derivationParameters != null)
{
byte[] paramsRaw = derivationParameters.ToByteArray();

long resultLength = Native.DeriveEncryptDataWithParamsSizeNative((UIntPtr)data.Length, (UIntPtr)paramsRaw.Length, (ushort)cipherTextVersion);

if (resultLength < 0)
{
Utils.HandleError(resultLength);
}

byte[] result = new byte[resultLength];
long res = Native.DeriveEncryptDataWithParamsNative(
data, (UIntPtr)data.Length,
password, (UIntPtr)password.Length,
paramsRaw, (UIntPtr)paramsRaw.Length,
aad, (UIntPtr)aadLength,
result, (UIntPtr)result.Length,
(ushort)cipherTextVersion);

if (res < 0)
{
Utils.HandleError(res);
}

Array.Resize(ref result, (int)res);

return result;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Trim the derive-encrypt buffer to bytes written

When callers pass a DerivationParameters created from serialized bytes with trailing padding, the Rust parser accepts the parameters but reserializes only the canonical bytes, so DeriveEncryptDataWithParamsNative can return res < result.Length. Returning the untrimmed buffer appends zero padding to the KDF-encrypted blob, and DeriveDecryptWithPassword then rejects it because KdfEncryptedDataV1::try_from requires the serialized length to match exactly. Resize to res here as the decrypt path already does.

Useful? React with 👍 / 👎.

}
else
{
// Default: Argon2id with default parameters (key_derivation_version = 0 = Latest)
long resultLength = Native.DeriveEncryptSizeNative((UIntPtr)data.Length, 0, (ushort)cipherTextVersion);

if (resultLength < 0)
{
Utils.HandleError(resultLength);
}

byte[] result = new byte[resultLength];
long res = Native.DeriveEncryptDataNative(
data, (UIntPtr)data.Length,
password, (UIntPtr)password.Length,
aad, (UIntPtr)aadLength,
result, (UIntPtr)result.Length,
0,
(ushort)cipherTextVersion);

if (res < 0)
{
Utils.HandleError(res);
}

Array.Resize(ref result, (int)res);

return result;
}
}

/// <summary>
/// Decrypts a derive-encrypt blob created by <see cref="DeriveEncryptWithPassword"/>.
/// </summary>
/// <param name="data">The blob to decrypt (derivation parameters + ciphertext).</param>
/// <param name="password">The password that was used to encrypt the data.</param>
/// <param name="aad">Additional authenticated data (optional, must match the value used during encryption).</param>
/// <returns>The decrypted plaintext bytes, or <c>null</c> if <paramref name="data"/> is null or empty.</returns>
public static byte[]? DeriveDecryptWithPassword(byte[]? data, byte[] password, byte[]? aad = null)
{
if (data == null || data.Length == 0)
{
return null;
}

if (password == null || password.Length == 0)
{
throw new DevolutionsCryptoException(ManagedError.InvalidParameter);
}

long aadLength = aad?.Length ?? 0;

// Allocate a buffer the size of the ciphertext blob; the FFI will return the actual plaintext length.
byte[] result = new byte[data.Length];
long res = Native.DeriveDecryptDataNative(
data, (UIntPtr)data.Length,
password, (UIntPtr)password.Length,
aad, (UIntPtr)aadLength,
result, (UIntPtr)result.Length);

if (res < 0)
{
Utils.HandleError(res);
}

// Trim to actual plaintext length.
Array.Resize(ref result, (int)res);

return result;
}

private static bool SharesLengthAreValid(byte[][] shares)
{
if (shares.Length == 0)
Expand Down
15 changes: 15 additions & 0 deletions wrappers/csharp/src/Native.Core.cs
Original file line number Diff line number Diff line change
Expand Up @@ -228,6 +228,21 @@ public static partial class Native

[DllImport(LibName, EntryPoint = "ConstantTimeEquals", CallingConvention = CallingConvention.Cdecl)]
internal static extern long ConstantTimeEquals(byte[] x, UIntPtr xLength, byte[] y, UIntPtr yLength);

[DllImport(LibName, EntryPoint = "DeriveEncryptSize", CallingConvention = CallingConvention.Cdecl)]
internal static extern long DeriveEncryptSizeNative(UIntPtr dataLength, ushort keyDerivationVersion, ushort ciphertextVersion);

[DllImport(LibName, EntryPoint = "DeriveEncryptData", CallingConvention = CallingConvention.Cdecl)]
internal static extern long DeriveEncryptDataNative(byte[] data, UIntPtr dataLength, byte[] password, UIntPtr passwordLength, byte[]? aad, UIntPtr aadLength, byte[] result, UIntPtr resultLength, ushort keyDerivationVersion, ushort ciphertextVersion);

[DllImport(LibName, EntryPoint = "DeriveEncryptDataWithParamsSize", CallingConvention = CallingConvention.Cdecl)]
internal static extern long DeriveEncryptDataWithParamsSizeNative(UIntPtr dataLength, UIntPtr paramsLength, ushort ciphertextVersion);

[DllImport(LibName, EntryPoint = "DeriveEncryptDataWithParams", CallingConvention = CallingConvention.Cdecl)]
internal static extern long DeriveEncryptDataWithParamsNative(byte[] data, UIntPtr dataLength, byte[] password, UIntPtr passwordLength, byte[] derivationParams, UIntPtr derivationParamsLength, byte[]? aad, UIntPtr aadLength, byte[] result, UIntPtr resultLength, ushort ciphertextVersion);

[DllImport(LibName, EntryPoint = "DeriveDecryptData", CallingConvention = CallingConvention.Cdecl)]
internal static extern long DeriveDecryptDataNative(byte[] data, UIntPtr dataLength, byte[] password, UIntPtr passwordLength, byte[]? aad, UIntPtr aadLength, byte[] result, UIntPtr resultLength);
}
}
#endif
Loading
Loading