From 3c3644df8ce93b44b9fbb1d665da5eaef9368617 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Duquette?= Date: Tue, 26 May 2026 14:54:22 -0400 Subject: [PATCH 1/5] feat(derivation): Expose key derivation to FFI and language wrappers This change exposes the `key_derivation` module introduced in the previous commit to all language wrappers: the C FFI layer, C# managed wrappers, UniFFI (Kotlin/Swift/Python), and WASM/TypeScript. Four new FFI functions are added to `ffi/src/lib.rs`: `DeriveSecretKeyPbkdf2` and `DeriveSecretKeyArgon2` with accompanying `DeriveSecretKeyPbkdf2Size` and `DeriveSecretKeyArgon2ParametersSize` size-query functions. The C# managed layer receives matching P/Invoke declarations in `Native.Core.cs` and public `DeriveSecretKeyPbkdf2` / `DeriveSecretKeyArgon2` methods in `Managed.cs`. A new `key_derivation` UniFFI module is added under `uniffi/devolutions-crypto-uniffi/src/`, exposing a `KeyDerivationResult` record and the two derive functions for Kotlin, Swift, and Python consumers. The WASM demo application gains a new Key Derivation page with an algorithm selector (defaulting to Argon2, with PBKDF2 as an option), The Inspect (Debug) page is also extended to decode `KeyDerivation` blobs. --- ffi/src/lib.rs | 240 +++++++++++++++++- src/key_derivation/mod.rs | 4 + src/wasm.rs | 65 ++++- .../src/key_derivation.rs | 33 +++ uniffi/devolutions-crypto-uniffi/src/lib.rs | 13 +- wrappers/csharp/src/DerivationParameters.cs | 48 ++++ wrappers/csharp/src/KeyDerivationResult.cs | 29 +++ wrappers/csharp/src/Managed.cs | 80 ++++++ wrappers/csharp/src/Native.Core.cs | 12 + .../csharp/tests/unit-tests/TestManaged.cs | 61 +++++ wrappers/wasm/demo/src/app/app.component.html | 9 +- wrappers/wasm/demo/src/app/app.routes.ts | 2 + .../demo/src/app/inspect/inspect.component.ts | 194 ++++++++++++++ .../key-derivation.component.html | 94 +++++++ .../key-derivation.component.ts | 104 ++++++++ .../app/service/encryption.inner.service.ts | 12 +- wrappers/wasm/tests/package.json | 2 +- wrappers/wasm/tests/tests/conformity.ts | 19 +- wrappers/wasm/tests/tests/key-derivation.ts | 57 +++++ 19 files changed, 1065 insertions(+), 13 deletions(-) create mode 100644 uniffi/devolutions-crypto-uniffi/src/key_derivation.rs create mode 100644 wrappers/csharp/src/DerivationParameters.cs create mode 100644 wrappers/csharp/src/KeyDerivationResult.cs create mode 100644 wrappers/wasm/demo/src/app/key-derivation/key-derivation.component.html create mode 100644 wrappers/wasm/demo/src/app/key-derivation/key-derivation.component.ts create mode 100644 wrappers/wasm/tests/tests/key-derivation.ts diff --git a/ffi/src/lib.rs b/ffi/src/lib.rs index 5d312939..c0f10f79 100644 --- a/ffi/src/lib.rs +++ b/ffi/src/lib.rs @@ -24,6 +24,7 @@ use devolutions_crypto::ciphertext::{ use devolutions_crypto::key::{ generate_keypair, generate_secret_key, mix_key_exchange, KeyVersion, PrivateKey, PublicKey, }; +use devolutions_crypto::key_derivation::{Argon2, Pbkdf2}; use devolutions_crypto::password_hash::{hash_password, PasswordHash, PasswordHashVersion}; use devolutions_crypto::secret_sharing::{ generate_shared_key, join_shares, SecretSharingVersion, Share, @@ -1401,7 +1402,146 @@ pub unsafe extern "C" fn DeriveKeyPbkdf2( 0 } -/// Validate if the header of the data is valid and consistant. +/// Derive a key with PBKDF2 and return both the SecretKey and the DerivationParameters. +/// # Arguments +/// * password - Pointer to the password to derive. +/// * password_length - Length of the password to derive. +/// * iterations - Number of PBKDF2 iterations. +/// * secret_key - Pointer to the buffer to write the derived SecretKey to. +/// Must be `GenerateSecretKeySize()` bytes. +/// * secret_key_length - Length of the secret key output buffer. +/// * params_out - Pointer to the buffer to write the DerivationParameters to. +/// Must be `DeriveSecretKeyPbkdf2Size()` bytes. +/// * params_out_length - Length of the params output buffer. +/// # Returns +/// Returns 0 if the operation is successful. If there is an error, +/// it will return the appropriate error code defined in DevoCryptoError. +/// # 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 DeriveSecretKeyPbkdf2( + password: *const u8, + password_length: usize, + iterations: u32, + secret_key: *mut u8, + secret_key_length: usize, + params_out: *mut u8, + params_out_length: usize, +) -> i64 { + if password.is_null() || secret_key.is_null() || params_out.is_null() { + return Error::NullPointer.error_code(); + } + + if secret_key_length != GenerateSecretKeySize() as usize { + return Error::InvalidOutputLength.error_code(); + } + + if params_out_length != DeriveSecretKeyPbkdf2Size() as usize { + return Error::InvalidOutputLength.error_code(); + } + + let password = slice::from_raw_parts(password, password_length); + + let (sk, params) = match Pbkdf2::with_params(iterations).derive(password) { + Ok(x) => x, + Err(e) => return e.error_code(), + }; + + let sk_bytes: Zeroizing> = Zeroizing::new(sk.into()); + let params_bytes: Vec = params.into(); + + let secret_key = slice::from_raw_parts_mut(secret_key, secret_key_length); + let params_out = slice::from_raw_parts_mut(params_out, params_out_length); + + secret_key.copy_from_slice(&sk_bytes); + params_out.copy_from_slice(¶ms_bytes); + 0 +} + +/// Returns the size of the DerivationParameters output buffer for `DeriveSecretKeyPbkdf2()`. +/// The size is fixed: 8 (header) + 4 (iterations) + 4 (salt length) + 16 (salt) = 32 bytes. +#[no_mangle] +pub extern "C" fn DeriveSecretKeyPbkdf2Size() -> i64 { + 32 // 8 header + 4 iterations + 4 salt_len + 16 salt +} + +/// Derive a key with Argon2 and return both the SecretKey and the DerivationParameters. +/// # Arguments +/// * password - Pointer to the password to derive. +/// * password_length - Length of the password to derive. +/// * argon2_parameters - Pointer to the buffer containing the serialized Argon2Parameters. +/// * argon2_parameters_length - Length of the Argon2Parameters buffer. +/// * secret_key - Pointer to the buffer to write the derived SecretKey to. +/// Must be `GenerateSecretKeySize()` bytes. +/// * secret_key_length - Length of the secret key output buffer. +/// * params_out - Pointer to the buffer to write the DerivationParameters to. +/// Must be `DeriveSecretKeyArgon2ParametersSize(argon2_parameters_length)` bytes. +/// * params_out_length - Length of the params output buffer. +/// # Returns +/// Returns 0 if the operation is successful. If there is an error, +/// it will return the appropriate error code defined in DevoCryptoError. +/// # 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 DeriveSecretKeyArgon2( + password: *const u8, + password_length: usize, + argon2_parameters: *const u8, + argon2_parameters_length: usize, + secret_key: *mut u8, + secret_key_length: usize, + params_out: *mut u8, + params_out_length: usize, +) -> i64 { + if password.is_null() + || argon2_parameters.is_null() + || secret_key.is_null() + || params_out.is_null() + { + return Error::NullPointer.error_code(); + } + + if secret_key_length != GenerateSecretKeySize() as usize { + return Error::InvalidOutputLength.error_code(); + } + + if params_out_length != DeriveSecretKeyArgon2ParametersSize(argon2_parameters_length) as usize { + return Error::InvalidOutputLength.error_code(); + } + + let password = slice::from_raw_parts(password, password_length); + let argon2_parameters_raw = slice::from_raw_parts(argon2_parameters, argon2_parameters_length); + + let argon2_params = match Argon2Parameters::try_from(argon2_parameters_raw) { + Ok(x) => x, + Err(e) => return e.error_code(), + }; + + let (sk, params) = match Argon2::with_params(argon2_params).derive(password) { + Ok(x) => x, + Err(e) => return e.error_code(), + }; + + let sk_bytes: Zeroizing> = Zeroizing::new(sk.into()); + let params_bytes: Vec = params.into(); + + let secret_key = slice::from_raw_parts_mut(secret_key, secret_key_length); + let params_out = slice::from_raw_parts_mut(params_out, params_out_length); + + secret_key.copy_from_slice(&sk_bytes); + params_out.copy_from_slice(¶ms_bytes); + 0 +} + +/// Returns the size of the DerivationParameters output buffer for `DeriveSecretKeyArgon2()`. +/// The size is 8 (header) + the length of the serialized Argon2Parameters. +/// # Arguments +/// * argon2_parameters_length - The length of the Argon2Parameters buffer passed to `DeriveSecretKeyArgon2()`. +#[no_mangle] +pub extern "C" fn DeriveSecretKeyArgon2ParametersSize(argon2_parameters_length: usize) -> i64 { + (8 + argon2_parameters_length) as i64 +} + /// # Arguments /// * `data` - Pointer to the input buffer. /// * `data_length` - Length of the input buffer. @@ -1859,3 +1999,101 @@ fn test_generate_secret_key() { .expect("should parse as SecretKey"); assert_eq!(key.as_bytes().len(), 32); } + +#[test] +fn test_derive_secret_key_pbkdf2() { + let password = b"test_password"; + let sk_size = GenerateSecretKeySize() as usize; + let params_size = DeriveSecretKeyPbkdf2Size() as usize; + let mut sk_buf = vec![0u8; sk_size]; + let mut params_buf = vec![0u8; params_size]; + + let res = unsafe { + DeriveSecretKeyPbkdf2( + password.as_ptr(), + password.len(), + 10, + sk_buf.as_mut_ptr(), + sk_size, + params_buf.as_mut_ptr(), + params_size, + ) + }; + assert_eq!(res, 0); + + let sk = devolutions_crypto::key::SecretKey::try_from(sk_buf.as_slice()) + .expect("should parse as SecretKey"); + assert_eq!(sk.as_bytes().len(), 32); + + let params = DerivationParameters::try_from(params_buf.as_slice()) + .expect("should parse as DerivationParameters"); + let _round_trip: Vec = params.into(); +} + +#[test] +fn test_derive_secret_key_pbkdf2_deterministic() { + let password = b"test_password"; + let sk_size = GenerateSecretKeySize() as usize; + let params_size = DeriveSecretKeyPbkdf2Size() as usize; + let mut sk1 = vec![0u8; sk_size]; + let mut params1 = vec![0u8; params_size]; + let mut sk2 = vec![0u8; sk_size]; + let mut params2 = vec![0u8; params_size]; + + unsafe { + DeriveSecretKeyPbkdf2( + password.as_ptr(), + password.len(), + 10, + sk1.as_mut_ptr(), + sk_size, + params1.as_mut_ptr(), + params_size, + ); + DeriveSecretKeyPbkdf2( + password.as_ptr(), + password.len(), + 10, + sk2.as_mut_ptr(), + sk_size, + params2.as_mut_ptr(), + params_size, + ); + } + + // Random salt → different params and different derived keys each call + assert_ne!(params1, params2); + assert_ne!(sk1, sk2); +} + +#[test] +fn test_derive_secret_key_argon2() { + let password = b"test_password"; + let argon2_params: Vec = (&Argon2Parameters::default()).into(); + let sk_size = GenerateSecretKeySize() as usize; + let params_size = DeriveSecretKeyArgon2ParametersSize(argon2_params.len()) as usize; + let mut sk_buf = vec![0u8; sk_size]; + let mut params_buf = vec![0u8; params_size]; + + let res = unsafe { + DeriveSecretKeyArgon2( + password.as_ptr(), + password.len(), + argon2_params.as_ptr(), + argon2_params.len(), + sk_buf.as_mut_ptr(), + sk_size, + params_buf.as_mut_ptr(), + params_size, + ) + }; + assert_eq!(res, 0); + + let sk = devolutions_crypto::key::SecretKey::try_from(sk_buf.as_slice()) + .expect("should parse as SecretKey"); + assert_eq!(sk.as_bytes().len(), 32); + + let params = DerivationParameters::try_from(params_buf.as_slice()) + .expect("should parse as DerivationParameters"); + let _round_trip: Vec = params.into(); +} diff --git a/src/key_derivation/mod.rs b/src/key_derivation/mod.rs index bcf8cdba..e6f4e5c7 100644 --- a/src/key_derivation/mod.rs +++ b/src/key_derivation/mod.rs @@ -36,6 +36,9 @@ use std::convert::TryFrom; #[cfg(feature = "fuzz")] use arbitrary::Arbitrary; +#[cfg(feature = "wbindgen")] +use wasm_bindgen::prelude::*; + use crate::key::SecretKey; #[cfg(feature = "fuzz")] use crate::Argon2Parameters; @@ -49,6 +52,7 @@ use super::enums::KeyDerivationSubtype; /// Can be stored alongside a user record to re-derive the same key later. #[derive(Clone, Debug)] #[cfg_attr(feature = "fuzz", derive(Arbitrary))] +#[cfg_attr(feature = "wbindgen", wasm_bindgen(inspectable))] pub struct DerivationParameters { pub(crate) header: Header, pub(super) payload: DerivationParametersPayload, diff --git a/src/wasm.rs b/src/wasm.rs index d2b1a86a..309ae1e6 100644 --- a/src/wasm.rs +++ b/src/wasm.rs @@ -15,6 +15,9 @@ use super::{ key, key::{KeyVersion, PrivateKey, PublicKey, SecretKey}, }; +use super::{ + key_derivation::{DerivationParameters, Pbkdf2, Argon2}, +}; use super::{ password_hash, password_hash::{PasswordHash, PasswordHashVersion}, @@ -35,8 +38,40 @@ use super::{ // Local KeyPair have private fields with getters instead of public field, for wasm_bindgen #[wasm_bindgen(inspectable)] #[derive(Clone)] -pub struct KeyPair { - private_key: PrivateKey, +pub struct KeyDerivationResult { + secret_key: SecretKey, + parameters: DerivationParameters, +} + +#[wasm_bindgen] +impl KeyDerivationResult { + #[wasm_bindgen(getter, js_name = "secretKey")] + pub fn secret_key(&self) -> SecretKey { + self.secret_key.clone() + } + + #[wasm_bindgen(getter)] + pub fn parameters(&self) -> DerivationParameters { + self.parameters.clone() + } +} + +#[wasm_bindgen] +impl DerivationParameters { + #[wasm_bindgen(getter)] + pub fn bytes(&self) -> Vec { + self.clone().into() + } + + #[wasm_bindgen(js_name = "fromBytes")] + pub fn from_bytes(buffer: &[u8]) -> Result { + Ok(Self::try_from(buffer)?) + } +} + +#[wasm_bindgen(inspectable)] +#[derive(Clone)] +pub struct KeyPair {private_key: PrivateKey, public_key: PublicKey, } @@ -377,6 +412,32 @@ pub fn derive_key_argon2(key: &[u8], parameters: &Argon2Parameters) -> Result, +) -> Result { + let (secret_key, parameters) = + Pbkdf2::with_params(iterations.unwrap_or(DEFAULT_PBKDF2_ITERATIONS)).derive(password)?; + Ok(KeyDerivationResult { + secret_key, + parameters, + }) +} + +#[wasm_bindgen(js_name = "deriveSecretKeyArgon2")] +pub fn derive_secret_key_argon2( + password: &[u8], + parameters: &Argon2Parameters, +) -> Result { + let (secret_key, derivation_params) = + Argon2::with_params(parameters.clone()).derive(password)?; + Ok(KeyDerivationResult { + secret_key, + parameters: derivation_params, + }) +} + #[wasm_bindgen(js_name = "validateHeader")] pub fn validate_header(data: &[u8], data_type: DataType) -> bool { utils::validate_header(data, data_type) diff --git a/uniffi/devolutions-crypto-uniffi/src/key_derivation.rs b/uniffi/devolutions-crypto-uniffi/src/key_derivation.rs new file mode 100644 index 00000000..931da1d7 --- /dev/null +++ b/uniffi/devolutions-crypto-uniffi/src/key_derivation.rs @@ -0,0 +1,33 @@ +use std::sync::Arc; + +use crate::{Argon2Parameters, Result}; + +#[derive(uniffi::Record)] +pub struct KeyDerivationResult { + pub secret_key: Vec, + pub parameters: Vec, +} + +#[uniffi::export(default(iterations = 600000))] +pub fn derive_secret_key_pbkdf2(key: &[u8], iterations: u32) -> Result { + let (sk, params) = + devolutions_crypto::key_derivation::Pbkdf2::with_params(iterations).derive(key)?; + Ok(KeyDerivationResult { + secret_key: sk.into(), + parameters: params.into(), + }) +} + +#[uniffi::export] +pub fn derive_secret_key_argon2( + key: &[u8], + parameters: &Arc, +) -> Result { + let (sk, params) = + devolutions_crypto::key_derivation::Argon2::with_params(parameters.inner.clone()) + .derive(key)?; + Ok(KeyDerivationResult { + secret_key: sk.into(), + parameters: params.into(), + }) +} diff --git a/uniffi/devolutions-crypto-uniffi/src/lib.rs b/uniffi/devolutions-crypto-uniffi/src/lib.rs index b56b5ead..a3fb121d 100644 --- a/uniffi/devolutions-crypto-uniffi/src/lib.rs +++ b/uniffi/devolutions-crypto-uniffi/src/lib.rs @@ -1,6 +1,7 @@ mod argon2parameters; mod ciphertext; mod key; +mod key_derivation; mod password_hash; mod secret_sharing; mod signature; @@ -10,6 +11,7 @@ mod utils; pub use argon2parameters::*; pub use ciphertext::*; pub use key::*; +pub use key_derivation::*; pub use password_hash::*; pub use secret_sharing::*; pub use signature::*; @@ -17,8 +19,8 @@ pub use signing_key::*; pub use utils::*; pub use devolutions_crypto::{ - CiphertextVersion, DataType, Error as DevolutionsCryptoError, KeyVersion, PasswordHashVersion, - Result, SecretSharingVersion, SignatureVersion, SigningKeyVersion, + CiphertextVersion, DataType, Error as DevolutionsCryptoError, KeyDerivationVersion, KeyVersion, + PasswordHashVersion, Result, SecretSharingVersion, SignatureVersion, SigningKeyVersion, }; #[uniffi::remote(Enum)] @@ -47,6 +49,13 @@ pub enum KeyVersion { V1, } +#[uniffi::remote(Enum)] +pub enum KeyDerivationVersion { + Latest, + V1, + V2, +} + #[uniffi::remote(Enum)] pub enum PasswordHashVersion { Latest, diff --git a/wrappers/csharp/src/DerivationParameters.cs b/wrappers/csharp/src/DerivationParameters.cs new file mode 100644 index 00000000..cb300618 --- /dev/null +++ b/wrappers/csharp/src/DerivationParameters.cs @@ -0,0 +1,48 @@ +namespace Devolutions.Cryptography +{ + using System; + + /// + /// Serializable parameters that fully describe a completed key derivation. + /// Can be stored alongside a user record to re-derive the same key later. + /// + public class DerivationParameters + { + /// + /// Initializes a new instance of the class. + /// + public DerivationParameters(byte[] payload) + { + this.Payload = payload; + } + + /// + /// Gets the raw serialized parameters data. + /// + internal byte[] Payload { get; } + + /// + /// Gets the raw serialized parameters data as a base64 string. + /// + public string PayloadString => Convert.ToBase64String(this.Payload); + + /// + /// Deserialize a from a byte array. + /// + /// The serialized parameters bytes. + /// Returns the deserialized . + public static DerivationParameters FromByteArray(byte[] data) + { + return new DerivationParameters(data); + } + + /// + /// Serialize the to a byte array. + /// + /// Returns the raw serialized parameters bytes. + public byte[] ToByteArray() + { + return this.Payload; + } + } +} diff --git a/wrappers/csharp/src/KeyDerivationResult.cs b/wrappers/csharp/src/KeyDerivationResult.cs new file mode 100644 index 00000000..f4ceec63 --- /dev/null +++ b/wrappers/csharp/src/KeyDerivationResult.cs @@ -0,0 +1,29 @@ +namespace Devolutions.Cryptography +{ + /// + /// Holds the result of a structured key derivation: the derived + /// and the needed to reproduce the same derivation. + /// + public class KeyDerivationResult + { + /// + /// Initializes a new instance of the class. + /// + public KeyDerivationResult(SecretKey secretKey, DerivationParameters parameters) + { + this.SecretKey = secretKey; + this.Parameters = parameters; + } + + /// + /// Gets the derived secret key for symmetric encryption. + /// + public SecretKey SecretKey { get; } + + /// + /// Gets the derivation parameters used to produce the key. + /// Store these alongside the protected data to re-derive the same key later. + /// + public DerivationParameters Parameters { get; } + } +} diff --git a/wrappers/csharp/src/Managed.cs b/wrappers/csharp/src/Managed.cs index 72bbd304..9629d21d 100644 --- a/wrappers/csharp/src/Managed.cs +++ b/wrappers/csharp/src/Managed.cs @@ -230,6 +230,86 @@ public static byte[] DerivePassword(string password, string? salt, uint iteratio return DeriveKey(Utils.StringToUtf8ByteArray(password), Utils.StringToUtf8ByteArray(salt), iterations); } + /// + /// Derives a password using PBKDF2 and returns the structured and . + /// + /// The password or key material to derive. + /// The number of PBKDF2 iterations (defaults to 600,000). + /// Returns a containing the derived key and the parameters needed to reproduce it. + public static KeyDerivationResult DeriveSecretKeyPbkdf2(byte[] key, uint iterations = DEFAULT_PBKDF2_ITERATIONS) + { + if (key == null || key.Length == 0) + { + throw new DevolutionsCryptoException(ManagedError.InvalidParameter); + } + + long skSize = Native.GenerateSecretKeySizeNative(); + long paramsSize = Native.DeriveSecretKeyPbkdf2SizeNative(); + + if (skSize < 0) + { + Utils.HandleError(skSize); + } + + if (paramsSize < 0) + { + Utils.HandleError(paramsSize); + } + + byte[] skBuf = new byte[skSize]; + byte[] paramsBuf = new byte[paramsSize]; + + long res = Native.DeriveSecretKeyPbkdf2Native(key, (UIntPtr)key.Length, iterations, skBuf, (UIntPtr)skBuf.Length, paramsBuf, (UIntPtr)paramsBuf.Length); + + if (res < 0) + { + Utils.HandleError(res); + } + + return new KeyDerivationResult(SecretKey.FromByteArray(skBuf), DerivationParameters.FromByteArray(paramsBuf)); + } + + /// + /// Derives a password using Argon2 and returns the structured and . + /// + /// The password or key material to derive. + /// The Argon2 parameters to use for derivation. + /// Returns a containing the derived key and the parameters needed to reproduce it. + public static KeyDerivationResult DeriveSecretKeyArgon2(byte[] key, Argon2Parameters parameters) + { + if (key == null || key.Length == 0 || parameters == null) + { + throw new DevolutionsCryptoException(ManagedError.InvalidParameter); + } + + byte[] parametersRaw = parameters.ToByteArray(); + + long skSize = Native.GenerateSecretKeySizeNative(); + long paramsSize = Native.DeriveSecretKeyArgon2ParametersSizeNative((UIntPtr)parametersRaw.Length); + + if (skSize < 0) + { + Utils.HandleError(skSize); + } + + if (paramsSize < 0) + { + Utils.HandleError(paramsSize); + } + + byte[] skBuf = new byte[skSize]; + byte[] paramsBuf = new byte[paramsSize]; + + long res = Native.DeriveSecretKeyArgon2Native(key, (UIntPtr)key.Length, parametersRaw, (UIntPtr)parametersRaw.Length, skBuf, (UIntPtr)skBuf.Length, paramsBuf, (UIntPtr)paramsBuf.Length); + + if (res < 0) + { + Utils.HandleError(res); + } + + return new KeyDerivationResult(SecretKey.FromByteArray(skBuf), DerivationParameters.FromByteArray(paramsBuf)); + } + /// /// Decrypts the data with the provided key. /// diff --git a/wrappers/csharp/src/Native.Core.cs b/wrappers/csharp/src/Native.Core.cs index 603f7411..5f2cf3fc 100644 --- a/wrappers/csharp/src/Native.Core.cs +++ b/wrappers/csharp/src/Native.Core.cs @@ -48,6 +48,18 @@ public static partial class Native [DllImport(LibName, EntryPoint = "DeriveKeyPbkdf2", CallingConvention = CallingConvention.Cdecl)] internal static extern long DeriveKeyPbkdf2Native(byte[] key, UIntPtr keyLength, byte[]? salt, UIntPtr saltLength, System.UInt32 iterations, byte[] result, UIntPtr resultLength); + [DllImport(LibName, EntryPoint = "DeriveSecretKeyPbkdf2", CallingConvention = CallingConvention.Cdecl)] + internal static extern long DeriveSecretKeyPbkdf2Native(byte[] key, UIntPtr keyLength, System.UInt32 iterations, byte[] secretKey, UIntPtr secretKeyLength, byte[] paramsOut, UIntPtr paramsOutLength); + + [DllImport(LibName, EntryPoint = "DeriveSecretKeyPbkdf2Size", CallingConvention = CallingConvention.Cdecl)] + internal static extern long DeriveSecretKeyPbkdf2SizeNative(); + + [DllImport(LibName, EntryPoint = "DeriveSecretKeyArgon2", CallingConvention = CallingConvention.Cdecl)] + internal static extern long DeriveSecretKeyArgon2Native(byte[] key, UIntPtr keyLength, byte[] argon2Parameters, UIntPtr argon2ParametersLength, byte[] secretKey, UIntPtr secretKeyLength, byte[] paramsOut, UIntPtr paramsOutLength); + + [DllImport(LibName, EntryPoint = "DeriveSecretKeyArgon2ParametersSize", CallingConvention = CallingConvention.Cdecl)] + internal static extern long DeriveSecretKeyArgon2ParametersSizeNative(UIntPtr argon2ParametersLength); + [DllImport(LibName, EntryPoint = "Encode", CallingConvention = CallingConvention.Cdecl)] internal static extern long EncodeNative(byte[] input, UIntPtr input_length, byte[] output, UIntPtr output_length); diff --git a/wrappers/csharp/tests/unit-tests/TestManaged.cs b/wrappers/csharp/tests/unit-tests/TestManaged.cs index 2e875e2f..9c86a433 100644 --- a/wrappers/csharp/tests/unit-tests/TestManaged.cs +++ b/wrappers/csharp/tests/unit-tests/TestManaged.cs @@ -413,6 +413,67 @@ public void VerifyPassword() Assert.IsTrue(Managed.VerifyPassword(TestData.BytesTestKey, TestData.TestHash)); } + [TestMethod] + public void DeriveSecretKeyPbkdf2_ReturnsDifferentKeysAndParams() + { + byte[] password = "my test password"u8.ToArray(); + + KeyDerivationResult result1 = Managed.DeriveSecretKeyPbkdf2(password, 10); + KeyDerivationResult result2 = Managed.DeriveSecretKeyPbkdf2(password, 10); + + Assert.IsNotNull(result1.SecretKey); + Assert.IsNotNull(result1.Parameters); + + // Random salt → different params and different derived keys on each call + CollectionAssert.AreNotEqual(result1.Parameters.ToByteArray(), result2.Parameters.ToByteArray()); + CollectionAssert.AreNotEqual(result1.SecretKey.ToByteArray(), result2.SecretKey.ToByteArray()); + } + + [TestMethod] + public void DeriveSecretKeyPbkdf2_ParametersRoundTrip() + { + byte[] password = "round-trip password"u8.ToArray(); + + KeyDerivationResult result = Managed.DeriveSecretKeyPbkdf2(password, 10); + + byte[] paramsBytes = result.Parameters.ToByteArray(); + Assert.IsTrue(paramsBytes.Length > 0); + + // Parameters can be round-tripped through byte array + DerivationParameters restored = DerivationParameters.FromByteArray(paramsBytes); + CollectionAssert.AreEqual(paramsBytes, restored.ToByteArray()); + } + + [TestMethod] + public void DeriveSecretKeyArgon2_WithFixedSalt_ProducesSameKey() + { + byte[] password = "argon2 test password"u8.ToArray(); + + // Use a pre-serialized Argon2Parameters with a known fixed salt for deterministic derivation + byte[] fixedParamsBytes = Convert.FromBase64String(TestData.Argon2DefaultParametersb64); + Argon2Parameters parameters1 = Argon2Parameters.FromByteArray(fixedParamsBytes)!; + Argon2Parameters parameters2 = Argon2Parameters.FromByteArray(fixedParamsBytes)!; + + KeyDerivationResult result1 = Managed.DeriveSecretKeyArgon2(password, parameters1); + KeyDerivationResult result2 = Managed.DeriveSecretKeyArgon2(password, parameters2); + + // Same parameters (same salt) + same password → same derived key + CollectionAssert.AreEqual(result1.SecretKey.ToByteArray(), result2.SecretKey.ToByteArray()); + } + + [TestMethod] + public void DeriveSecretKeyArgon2_DifferentSalts_ProduceDifferentKeys() + { + byte[] password = "argon2 test password"u8.ToArray(); + + // Default params generate a random salt on each call + KeyDerivationResult result1 = Managed.DeriveSecretKeyArgon2(password, Managed.GetDefaultArgon2Parameters()); + KeyDerivationResult result2 = Managed.DeriveSecretKeyArgon2(password, Managed.GetDefaultArgon2Parameters()); + + CollectionAssert.AreNotEqual(result1.SecretKey.ToByteArray(), result2.SecretKey.ToByteArray()); + CollectionAssert.AreNotEqual(result1.Parameters.ToByteArray(), result2.Parameters.ToByteArray()); + } + private static byte[][] GetSharesKeys() { const int nbShares = 3; diff --git a/wrappers/wasm/demo/src/app/app.component.html b/wrappers/wasm/demo/src/app/app.component.html index b3c8c24a..5a3b248e 100644 --- a/wrappers/wasm/demo/src/app/app.component.html +++ b/wrappers/wasm/demo/src/app/app.component.html @@ -8,12 +8,13 @@

DevolutionsCrypto

diff --git a/wrappers/wasm/demo/src/app/app.routes.ts b/wrappers/wasm/demo/src/app/app.routes.ts index eab16e7d..1335b0c8 100644 --- a/wrappers/wasm/demo/src/app/app.routes.ts +++ b/wrappers/wasm/demo/src/app/app.routes.ts @@ -6,12 +6,14 @@ import { UtilitiesComponent } from './utilities/utilities.component'; import { AsymmetricComponent } from './asymmetric/asymmetric.component'; import { SecretKeyEncryptionComponent } from './secret-key-encryption/secret-key-encryption.component'; import { InspectComponent } from './inspect/inspect.component'; +import { KeyDerivationComponent } from './key-derivation/key-derivation.component'; export const routes: Routes = [ { path: '', redirectTo: '/encryption', pathMatch: 'full' }, { path: 'encryption', component: EncryptionComponent }, { path: 'secret-sharing', component: SecretSharingComponent }, { path: 'password', component: PasswordComponent }, + { path: 'key-derivation', component: KeyDerivationComponent }, { path: 'utilities', component: UtilitiesComponent }, { path: 'asymmetric', component: AsymmetricComponent }, { path: 'secret-key-encryption', component: SecretKeyEncryptionComponent }, diff --git a/wrappers/wasm/demo/src/app/inspect/inspect.component.ts b/wrappers/wasm/demo/src/app/inspect/inspect.component.ts index b91912e1..c2ecff67 100644 --- a/wrappers/wasm/demo/src/app/inspect/inspect.component.ts +++ b/wrappers/wasm/demo/src/app/inspect/inspect.component.ts @@ -20,6 +20,7 @@ const DATA_TYPE_NAMES: Record = { 5: 'SigningKey', 6: 'Signature', 7: 'OnlineCiphertext', + 8: 'KeyDerivation', }; const SUBTYPE_NAMES: Record> = { @@ -29,6 +30,7 @@ const SUBTYPE_NAMES: Record> = { 4: { 0: 'None' }, 5: { 0: 'None', 1: 'Pair', 2: 'Public' }, 6: { 0: 'None' }, + 8: { 0: 'None' }, }; const VERSION_NAMES: Record> = { @@ -38,6 +40,7 @@ const VERSION_NAMES: Record> = { 4: { 0: 'Latest', 1: 'V1 – Shamir Secret Sharing over GF256' }, 5: { 0: 'Latest', 1: 'V1 – Ed25519' }, 6: { 0: 'Latest', 1: 'V1 – Ed25519' }, + 8: { 0: 'Latest', 1: 'V1 – PBKDF2-HMAC-SHA256', 2: 'V2 – Argon2id' }, }; export interface PayloadField { @@ -191,6 +194,8 @@ export class InspectComponent implements OnInit { return this.parsePasswordHashPayload(payload, abs); case 4: return this.parseSharePayload(payload, abs); + case 8: + return this.parseDerivationParametersPayload(payload, version, abs); default: return [ { @@ -391,6 +396,195 @@ export class InspectComponent implements OnInit { ]; } + private parseDerivationParametersPayload( + payload: Uint8Array, + version: number, + abs: (n: number) => number + ): PayloadField[] { + const fields: PayloadField[] = []; + const dv = new DataView(payload.buffer, payload.byteOffset, payload.byteLength); + + if (version === 1) { + // V1 PBKDF2: iterations(4 LE u32) + salt_len(4 LE u32) + salt(N bytes) + if (payload.length < 8) { + fields.push({ + name: 'Error', + offset: abs(0), + size: payload.length, + hex: toHex(payload), + description: `Payload too short for V1 derivation parameters (min 8 bytes, got ${payload.length})`, + }); + return fields; + } + const iterations = dv.getUint32(0, true); + const saltLen = dv.getUint32(4, true); + const salt = payload.slice(8, 8 + saltLen); + fields.push({ + name: 'Iterations', + offset: abs(0), + size: 4, + hex: toHex(payload.slice(0, 4)), + description: `PBKDF2 iteration count: ${iterations.toLocaleString()}`, + }); + fields.push({ + name: 'Salt Length', + offset: abs(4), + size: 4, + hex: toHex(payload.slice(4, 8)), + description: `Salt length: ${saltLen} bytes`, + }); + fields.push({ + name: 'Salt', + offset: abs(8), + size: saltLen, + hex: toHex(salt), + description: `Random salt for PBKDF2 (${saltLen} bytes)`, + }); + return fields; + } + + if (version === 2) { + // V2 Argon2: Argon2Parameters binary format + // dc_version(4) + length(4) + lanes(4) + memory(4) + iterations(4) + variant(1) + version(1) + assoc_data_len(4) + assoc_data(N) + salt_len(4) + salt(M) + if (payload.length < 26) { + fields.push({ + name: 'Error', + offset: abs(0), + size: payload.length, + hex: toHex(payload), + description: `Payload too short for V2 derivation parameters (min 26 bytes, got ${payload.length})`, + }); + return fields; + } + let pos = 0; + + const dcVersion = dv.getUint32(pos, true); + fields.push({ + name: 'DC Version', + offset: abs(pos), + size: 4, + hex: toHex(payload.slice(pos, pos + 4)), + description: `Devolutions-Crypto Argon2Parameters version: ${dcVersion}`, + }); + pos += 4; + + const hashLength = dv.getUint32(pos, true); + fields.push({ + name: 'Hash Length', + offset: abs(pos), + size: 4, + hex: toHex(payload.slice(pos, pos + 4)), + description: `Argon2 output length: ${hashLength} bytes`, + }); + pos += 4; + + const lanes = dv.getUint32(pos, true); + fields.push({ + name: 'Lanes', + offset: abs(pos), + size: 4, + hex: toHex(payload.slice(pos, pos + 4)), + description: `Argon2 parallelism (lanes): ${lanes}`, + }); + pos += 4; + + const memory = dv.getUint32(pos, true); + fields.push({ + name: 'Memory', + offset: abs(pos), + size: 4, + hex: toHex(payload.slice(pos, pos + 4)), + description: `Argon2 memory usage: ${memory.toLocaleString()} KiB`, + }); + pos += 4; + + const iterations = dv.getUint32(pos, true); + fields.push({ + name: 'Iterations', + offset: abs(pos), + size: 4, + hex: toHex(payload.slice(pos, pos + 4)), + description: `Argon2 time cost (iterations): ${iterations}`, + }); + pos += 4; + + const variantByte = payload[pos]; + const variantName = variantByte === 0 ? 'Argon2d' : variantByte === 1 ? 'Argon2i' : variantByte === 2 ? 'Argon2id' : `Unknown (${variantByte})`; + fields.push({ + name: 'Variant', + offset: abs(pos), + size: 1, + hex: toHex(payload.slice(pos, pos + 1)), + description: `Argon2 variant: ${variantName}`, + }); + pos += 1; + + const versionByte = payload[pos]; + const versionName = versionByte === 0x13 ? '1.3 (0x13)' : `0x${versionByte.toString(16).padStart(2, '0')}`; + fields.push({ + name: 'Argon2 Version', + offset: abs(pos), + size: 1, + hex: toHex(payload.slice(pos, pos + 1)), + description: `Argon2 algorithm version: ${versionName}`, + }); + pos += 1; + + const assocDataLen = dv.getUint32(pos, true); + fields.push({ + name: 'Assoc. Data Length', + offset: abs(pos), + size: 4, + hex: toHex(payload.slice(pos, pos + 4)), + description: `Associated data length: ${assocDataLen} bytes`, + }); + pos += 4; + + if (assocDataLen > 0) { + fields.push({ + name: 'Assoc. Data', + offset: abs(pos), + size: assocDataLen, + hex: toHex(payload.slice(pos, pos + assocDataLen)), + description: `Associated data (${assocDataLen} bytes)`, + }); + pos += assocDataLen; + } + + const saltLen = dv.getUint32(pos, true); + fields.push({ + name: 'Salt Length', + offset: abs(pos), + size: 4, + hex: toHex(payload.slice(pos, pos + 4)), + description: `Salt length: ${saltLen} bytes`, + }); + pos += 4; + + const salt = payload.slice(pos, pos + saltLen); + fields.push({ + name: 'Salt', + offset: abs(pos), + size: saltLen, + hex: toHex(salt), + description: `Random salt for Argon2 (${saltLen} bytes)`, + }); + + return fields; + } + + // Unknown version — show raw payload + return [ + { + name: 'Raw Payload', + offset: abs(0), + size: payload.length, + hex: toHex(payload, 32), + description: `${payload.length} bytes — unknown derivation parameters version`, + }, + ]; + } + private errorResult(totalBytes: number, message: string): ParseResult { return { signatureHex: '', diff --git a/wrappers/wasm/demo/src/app/key-derivation/key-derivation.component.html b/wrappers/wasm/demo/src/app/key-derivation/key-derivation.component.html new file mode 100644 index 00000000..56835707 --- /dev/null +++ b/wrappers/wasm/demo/src/app/key-derivation/key-derivation.component.html @@ -0,0 +1,94 @@ + +
+ + Devolutions Crypto +
+ +
+ +
+ +

Key Derivation

+
+ +
+
Derive Secret Key
+
+
+ + +
+ +
+ + +
+
+ + +
+ + +
+ + + +
+ + +
+
+ + +
+
+ + +
+

+ A random salt is generated automatically on each derivation and embedded in the output parameters. +

+
+ + + +
+ + +
+

+ A random salt is generated automatically and embedded in the output parameters. +

+
+ +
+ +
+ + +
+ Secret Key (base64) + +
+
+ Derivation Parameters (base64) + +
+ +
+
+
+ +
diff --git a/wrappers/wasm/demo/src/app/key-derivation/key-derivation.component.ts b/wrappers/wasm/demo/src/app/key-derivation/key-derivation.component.ts new file mode 100644 index 00000000..3a4598c4 --- /dev/null +++ b/wrappers/wasm/demo/src/app/key-derivation/key-derivation.component.ts @@ -0,0 +1,104 @@ +import { Component, OnInit } from '@angular/core'; +import { FormGroup, FormControl, ReactiveFormsModule } from '@angular/forms'; +import { EncryptionService } from '../service/encryption.service'; +import { FaIconComponent } from '@fortawesome/angular-fontawesome'; +import { faKey } from '@fortawesome/free-solid-svg-icons'; +import { NgIf } from '@angular/common'; +import * as functions from '../shared/shared.component'; + +type EncryptionServiceInner = typeof import('../service/encryption.inner.service'); + +@Component({ + selector: 'app-key-derivation', + standalone: true, + imports: [ReactiveFormsModule, FaIconComponent, NgIf], + templateUrl: './key-derivation.component.html', +}) +export class KeyDerivationComponent implements OnInit { + faKey = faKey; + + algorithm: 'argon2' | 'pbkdf2' = 'argon2'; + + deriveForm: FormGroup; + + encoder: TextEncoder; + + constructor(private encryptionService: EncryptionService) { + this.encoder = new TextEncoder(); + + this.deriveForm = new FormGroup({ + password: new FormControl(''), + // Argon2 parameters + argon2Memory: new FormControl(''), + argon2Iterations: new FormControl(''), + argon2Lanes: new FormControl(''), + // PBKDF2 parameters + pbkdf2Iterations: new FormControl(''), + pbkdf2Salt: new FormControl(''), + // Outputs + secretKeyResult: new FormControl(''), + parametersResult: new FormControl(''), + }); + } + + ngOnInit() {} + + setAlgorithm(algo: 'argon2' | 'pbkdf2') { + this.algorithm = algo; + this.deriveForm.patchValue({ secretKeyResult: '', parametersResult: '' }); + } + + w3Open() { functions.w3_open(); } + w3Close() { functions.w3_close(); } + + async derive() { + const service: EncryptionServiceInner = await this.encryptionService.innerModule; + + const password: string = this.deriveForm.value.password; + if (!password) { return; } + + const passwordBytes: Uint8Array = this.encoder.encode(password); + + try { + if (this.algorithm === 'argon2') { + const params = new service.Argon2Parameters(); + + const memory: string = this.deriveForm.value.argon2Memory; + const iterations: string = this.deriveForm.value.argon2Iterations; + const lanes: string = this.deriveForm.value.argon2Lanes; + + if (memory) { params.memory = Number(memory); } + if (iterations) { params.iterations = Number(iterations); } + if (lanes) { params.lanes = Number(lanes); } + + const result = service.deriveSecretKeyArgon2(passwordBytes, params); + + this.deriveForm.patchValue({ + secretKeyResult: service.base64encode(result.secretKey.bytes), + parametersResult: service.base64encode(result.parameters.bytes), + }); + } else { + const iterStr: string = this.deriveForm.value.pbkdf2Iterations; + const saltStr: string = this.deriveForm.value.pbkdf2Salt; + + const iterations: number | undefined = iterStr ? Number(iterStr) : undefined; + const salt: Uint8Array | undefined = saltStr ? this.encoder.encode(saltStr) : undefined; + + // PBKDF2 uses a random salt unless one is embedded via derive_with_salt (not directly exposed to WASM). + // We call deriveSecretKeyPbkdf2 with the requested iterations; salt is always random on this path. + // If a custom salt was provided, inform the user it is not used on this path. + const result = service.deriveSecretKeyPbkdf2(passwordBytes, iterations); + + this.deriveForm.patchValue({ + secretKeyResult: service.base64encode(result.secretKey.bytes), + parametersResult: service.base64encode(result.parameters.bytes), + }); + } + } catch (e: any) { + this.deriveForm.patchValue({ + secretKeyResult: `Error: ${e?.message ?? e}`, + parametersResult: '', + }); + } + } +} diff --git a/wrappers/wasm/demo/src/app/service/encryption.inner.service.ts b/wrappers/wasm/demo/src/app/service/encryption.inner.service.ts index ca39e6a1..929fb030 100644 --- a/wrappers/wasm/demo/src/app/service/encryption.inner.service.ts +++ b/wrappers/wasm/demo/src/app/service/encryption.inner.service.ts @@ -1,5 +1,5 @@ -import wasmInit, { CiphertextVersion, KeyVersion, KeyPair, PrivateKey, PublicKey, SecretKey, Argon2Parameters, PasswordHashVersion } from '@devolutions/devolutions-crypto-web'; -export { CiphertextVersion, KeyVersion, KeyPair, PrivateKey, PublicKey, SecretKey, Argon2Parameters, PasswordHashVersion } from '@devolutions/devolutions-crypto-web'; +import wasmInit, { CiphertextVersion, KeyVersion, KeyPair, PrivateKey, PublicKey, SecretKey, Argon2Parameters, PasswordHashVersion, KeyDerivationResult, DerivationParameters } from '@devolutions/devolutions-crypto-web'; +export { CiphertextVersion, KeyVersion, KeyPair, PrivateKey, PublicKey, SecretKey, Argon2Parameters, PasswordHashVersion, KeyDerivationResult, DerivationParameters } from '@devolutions/devolutions-crypto-web'; import * as devolutionsCrypto from '@devolutions/devolutions-crypto-web'; // Initialize WASM before any functions are used @@ -73,3 +73,11 @@ export function encryptWithSecretKey(data: Uint8Array, key: SecretKey, version?: export function decryptWithSecretKey(data: Uint8Array, key: SecretKey): Uint8Array { return devolutionsCrypto.decryptWithSecretKey(data, key); } + +export function deriveSecretKeyPbkdf2(password: Uint8Array, iterations?: number): KeyDerivationResult { + return devolutionsCrypto.deriveSecretKeyPbkdf2(password, iterations); +} + +export function deriveSecretKeyArgon2(password: Uint8Array, parameters: Argon2Parameters): KeyDerivationResult { + return devolutionsCrypto.deriveSecretKeyArgon2(password, parameters); +} diff --git a/wrappers/wasm/tests/package.json b/wrappers/wasm/tests/package.json index 19fb786f..73f2471b 100644 --- a/wrappers/wasm/tests/package.json +++ b/wrappers/wasm/tests/package.json @@ -4,7 +4,7 @@ "description": "Tests for the devolutions cryptographic library", "type": "module", "scripts": { - "test": "tsx --test tests/asymmetric.ts tests/conformity.ts tests/hashing.ts tests/secret-sharing.ts tests/signature.ts tests/symmetric.ts tests/utils.ts", + "test": "tsx --test tests/asymmetric.ts tests/conformity.ts tests/hashing.ts tests/key-derivation.ts tests/secret-sharing.ts tests/signature.ts tests/symmetric.ts tests/utils.ts", "test:watch": "tsx --test --watch tests/*.ts" }, "repository": { diff --git a/wrappers/wasm/tests/tests/conformity.ts b/wrappers/wasm/tests/tests/conformity.ts index c710f1da..aaf65f99 100644 --- a/wrappers/wasm/tests/tests/conformity.ts +++ b/wrappers/wasm/tests/tests/conformity.ts @@ -1,6 +1,6 @@ // These tests are there to make sure that the implementations are compatible between one language and another import { - KeyPair, deriveKeyPbkdf2, base64encode, base64decode, decrypt, Argon2Parameters, PrivateKey, SigningPublicKey, decryptAsymmetric, verifyPassword, verifySignature, deriveKeyArgon2 + KeyPair, deriveKeyPbkdf2, base64encode, base64decode, decrypt, Argon2Parameters, PrivateKey, SigningPublicKey, decryptAsymmetric, verifyPassword, verifySignature, deriveKeyArgon2, deriveSecretKeyPbkdf2, deriveSecretKeyArgon2, KeyDerivationResult, DerivationParameters } from 'devolutions-crypto' import { describe, test } from 'node:test' import assert from 'node:assert/strict' @@ -96,4 +96,21 @@ describe('Conformity Tests', () => { assert.strictEqual(verifySignature(encoder.encode('this is a test'), public_key, signature), true) assert.strictEqual(verifySignature(encoder.encode('this is wrong'), public_key, signature), false) }) + + test('DeriveSecretKey PBKDF2 V1 - Parameters format conformity', () => { + // Known PBKDF2 DerivationParameters bytes (password='testpassword', salt='fixed_salt_16byt', iterations=10) + const knownParamsBytes = base64decode('DQwIAAAAAQAKAAAAEAAAAGZpeGVkX3NhbHRfMTZieXQ=') + const params: DerivationParameters = DerivationParameters.fromBytes(knownParamsBytes) + + // Parameters round-trip through bytes + assert.strictEqual(base64encode(params.bytes), base64encode(knownParamsBytes)) + }) + + test('DeriveSecretKey Argon2 V2', () => { + // Derived with password='testpassword' and fixed argon2 parameters + const parameters: Argon2Parameters = Argon2Parameters.fromBytes(base64decode('AQAAACAAAAABAAAAIAAAAAEAAAACEwAAAAAQAAAAimFBkm3f8+f+YfLRnF5OoQ==')) + const result: KeyDerivationResult = deriveSecretKeyArgon2(encoder.encode('testpassword'), parameters) + + assert.strictEqual(base64encode(result.secretKey.bytes), 'DQwBAAQAAQCoRRraZaLaR3nJn0E+1ZYBcM3DBCwINJpWAuA2tcvr6w==') + }) }) diff --git a/wrappers/wasm/tests/tests/key-derivation.ts b/wrappers/wasm/tests/tests/key-derivation.ts new file mode 100644 index 00000000..93e80f72 --- /dev/null +++ b/wrappers/wasm/tests/tests/key-derivation.ts @@ -0,0 +1,57 @@ +import { deriveSecretKeyPbkdf2, deriveSecretKeyArgon2, Argon2Parameters, DerivationParameters, KeyDerivationResult, base64encode, base64decode } from 'devolutions-crypto' +import { describe, test } from 'node:test' +import assert from 'node:assert/strict' + +const encoder: TextEncoder = new TextEncoder() + +describe('key derivation', () => { + test('deriveSecretKeyPbkdf2 returns a secret key and parameters', () => { + const result: KeyDerivationResult = deriveSecretKeyPbkdf2(encoder.encode('test password'), 10) + + assert.ok(result.secretKey.bytes.length > 0) + assert.ok(result.parameters.bytes.length > 0) + }) + + test('deriveSecretKeyPbkdf2 produces different results for each call (random salt)', () => { + const password = encoder.encode('same password') + + const result1: KeyDerivationResult = deriveSecretKeyPbkdf2(password, 10) + const result2: KeyDerivationResult = deriveSecretKeyPbkdf2(password, 10) + + // Random salt → different parameters and different derived keys + assert.notStrictEqual(base64encode(result1.parameters.bytes), base64encode(result2.parameters.bytes)) + assert.notStrictEqual(base64encode(result1.secretKey.bytes), base64encode(result2.secretKey.bytes)) + }) + + test('deriveSecretKeyArgon2 with fixed parameters produces the same key', () => { + const password = encoder.encode('test password') + const fixedParams: Argon2Parameters = Argon2Parameters.fromBytes( + base64decode('AQAAACAAAAABAAAAIAAAAAEAAAACEwAAAAAQAAAAimFBkm3f8+f+YfLRnF5OoQ==') + ) + + const result1: KeyDerivationResult = deriveSecretKeyArgon2(password, fixedParams) + const result2: KeyDerivationResult = deriveSecretKeyArgon2(password, fixedParams) + + // Same parameters (same salt) + same password → same derived key + assert.strictEqual(base64encode(result1.secretKey.bytes), base64encode(result2.secretKey.bytes)) + }) + + test('deriveSecretKeyArgon2 with default parameters produces different results (random salt)', () => { + const password = encoder.encode('test password') + + const result1: KeyDerivationResult = deriveSecretKeyArgon2(password, new Argon2Parameters()) + const result2: KeyDerivationResult = deriveSecretKeyArgon2(password, new Argon2Parameters()) + + assert.notStrictEqual(base64encode(result1.secretKey.bytes), base64encode(result2.secretKey.bytes)) + assert.notStrictEqual(base64encode(result1.parameters.bytes), base64encode(result2.parameters.bytes)) + }) + + test('DerivationParameters round-trip through bytes', () => { + const result: KeyDerivationResult = deriveSecretKeyPbkdf2(encoder.encode('round trip'), 10) + + const paramsBytes = result.parameters.bytes + const restored: DerivationParameters = DerivationParameters.fromBytes(paramsBytes) + + assert.strictEqual(base64encode(restored.bytes), base64encode(paramsBytes)) + }) +}) From 1836b008a8b0f3bec8b626c3e791df1a9507ccaf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Duquette?= Date: Tue, 26 May 2026 17:10:59 -0400 Subject: [PATCH 2/5] Address copilot comments --- ffi/src/lib.rs | 134 +++++++++++++++++- src/wasm.rs | 24 +++- .../src/key_derivation.rs | 14 ++ wrappers/csharp/src/Managed.cs | 47 ++++++ wrappers/csharp/src/Native.Core.cs | 6 + .../csharp/tests/unit-tests/TestManaged.cs | 14 ++ .../demo/src/app/inspect/inspect.component.ts | 38 +++++ .../key-derivation.component.html | 7 +- .../key-derivation.component.ts | 12 +- .../app/service/encryption.inner.service.ts | 4 + wrappers/wasm/tests/tests/key-derivation.ts | 24 +++- 11 files changed, 306 insertions(+), 18 deletions(-) diff --git a/ffi/src/lib.rs b/ffi/src/lib.rs index c0f10f79..9c5791e4 100644 --- a/ffi/src/lib.rs +++ b/ffi/src/lib.rs @@ -49,6 +49,9 @@ use std::sync::Mutex; use zeroize::Zeroizing; +#[cfg(test)] +use devolutions_crypto::key_derivation::DerivationParameters; + const VERSION: &str = env!("CARGO_PKG_VERSION"); /// Encrypt a data blob @@ -1533,10 +1536,80 @@ pub unsafe extern "C" fn DeriveSecretKeyArgon2( 0 } +/// Derive a key with PBKDF2 and return both the SecretKey and the DerivationParameters. +/// # Arguments +/// * password - Pointer to the password to derive. +/// * password_length - Length of the password to derive. +/// * iterations - Number of PBKDF2 iterations. +/// * salt - Pointer to the salt to use for derivation. +/// * salt_length - Length of the salt. +/// * secret_key - Pointer to the buffer to write the derived SecretKey to. +/// Must be `GenerateSecretKeySize()` bytes. +/// * secret_key_length - Length of the secret key output buffer. +/// * params_out - Pointer to the buffer to write the DerivationParameters to. +/// Must be `DeriveSecretKeyPbkdf2WithSaltSize(salt_length)` bytes. +/// * params_out_length - Length of the params output buffer. +/// # Returns +/// Returns 0 if the operation is successful. If there is an error, +/// it will return the appropriate error code defined in DevoCryptoError. +/// # 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 DeriveSecretKeyPbkdf2WithSalt( + password: *const u8, + password_length: usize, + iterations: u32, + salt: *const u8, + salt_length: usize, + secret_key: *mut u8, + secret_key_length: usize, + params_out: *mut u8, + params_out_length: usize, +) -> i64 { + if password.is_null() || salt.is_null() || secret_key.is_null() || params_out.is_null() { + return Error::NullPointer.error_code(); + } + + if secret_key_length != GenerateSecretKeySize() as usize { + return Error::InvalidOutputLength.error_code(); + } + + if params_out_length != DeriveSecretKeyPbkdf2WithSaltSize(salt_length) as usize { + return Error::InvalidOutputLength.error_code(); + } + + let password = slice::from_raw_parts(password, password_length); + let salt = slice::from_raw_parts(salt, salt_length); + + let (sk, params) = match Pbkdf2::with_params(iterations).derive_with_salt(password, salt) { + Ok(x) => x, + Err(e) => return e.error_code(), + }; + + let sk_bytes: Zeroizing> = Zeroizing::new(sk.into()); + let params_bytes: Vec = params.into(); + + let secret_key = slice::from_raw_parts_mut(secret_key, secret_key_length); + let params_out = slice::from_raw_parts_mut(params_out, params_out_length); + + secret_key.copy_from_slice(&sk_bytes); + params_out.copy_from_slice(¶ms_bytes); + 0 +} + +/// Returns the size of the DerivationParameters output buffer for `DeriveSecretKeyPbkdf2WithSalt()`. +/// The size is: 8 (header) + 4 (iterations) + 4 (salt length field) + salt_length (salt bytes). +/// # Arguments +/// * salt_length - The length of the salt that will be passed to `DeriveSecretKeyPbkdf2WithSalt()`. +#[no_mangle] +pub extern "C" fn DeriveSecretKeyPbkdf2WithSaltSize(salt_length: usize) -> i64 { + (8 + 4 + 4 + salt_length) as i64 +} + /// Returns the size of the DerivationParameters output buffer for `DeriveSecretKeyArgon2()`. -/// The size is 8 (header) + the length of the serialized Argon2Parameters. +/// The size is: 8 (header) + argon2_parameters_length (serialized Argon2Parameters bytes). /// # Arguments -/// * argon2_parameters_length - The length of the Argon2Parameters buffer passed to `DeriveSecretKeyArgon2()`. +/// * argon2_parameters_length - The length of the Argon2Parameters that will be passed to `DeriveSecretKeyArgon2()`. #[no_mangle] pub extern "C" fn DeriveSecretKeyArgon2ParametersSize(argon2_parameters_length: usize) -> i64 { (8 + argon2_parameters_length) as i64 @@ -2041,7 +2114,7 @@ fn test_derive_secret_key_pbkdf2_deterministic() { let mut params2 = vec![0u8; params_size]; unsafe { - DeriveSecretKeyPbkdf2( + let retval1 = DeriveSecretKeyPbkdf2( password.as_ptr(), password.len(), 10, @@ -2050,7 +2123,7 @@ fn test_derive_secret_key_pbkdf2_deterministic() { params1.as_mut_ptr(), params_size, ); - DeriveSecretKeyPbkdf2( + let retval2 = DeriveSecretKeyPbkdf2( password.as_ptr(), password.len(), 10, @@ -2059,6 +2132,9 @@ fn test_derive_secret_key_pbkdf2_deterministic() { params2.as_mut_ptr(), params_size, ); + + assert_eq!(retval1, 0); + assert_eq!(retval2, 0); } // Random salt → different params and different derived keys each call @@ -2097,3 +2173,53 @@ fn test_derive_secret_key_argon2() { .expect("should parse as DerivationParameters"); let _round_trip: Vec = params.into(); } + +#[test] +fn test_derive_secret_key_pbkdf2_with_salt() { + let password = b"test_password"; + let salt = b"fixed_salt_16byt"; + let sk_size = GenerateSecretKeySize() as usize; + let params_size = DeriveSecretKeyPbkdf2WithSaltSize(salt.len()) as usize; + let mut sk1 = vec![0u8; sk_size]; + let mut params1 = vec![0u8; params_size]; + let mut sk2 = vec![0u8; sk_size]; + let mut params2 = vec![0u8; params_size]; + + unsafe { + let retval1 = DeriveSecretKeyPbkdf2WithSalt( + password.as_ptr(), + password.len(), + 10, + salt.as_ptr(), + salt.len(), + sk1.as_mut_ptr(), + sk_size, + params1.as_mut_ptr(), + params_size, + ); + let retval2 = DeriveSecretKeyPbkdf2WithSalt( + password.as_ptr(), + password.len(), + 10, + salt.as_ptr(), + salt.len(), + sk2.as_mut_ptr(), + sk_size, + params2.as_mut_ptr(), + params_size, + ); + assert_eq!(retval1, 0); + assert_eq!(retval2, 0); + } + + assert_eq!(sk1, sk2); + assert_eq!(params1, params2); + + let sk = devolutions_crypto::key::SecretKey::try_from(sk1.as_slice()) + .expect("should parse as SecretKey"); + assert_eq!(sk.as_bytes().len(), 32); + + let params = DerivationParameters::try_from(params1.as_slice()) + .expect("should parse as DerivationParameters"); + let _round_trip: Vec = params.into(); +} diff --git a/src/wasm.rs b/src/wasm.rs index 309ae1e6..6abfc1f2 100644 --- a/src/wasm.rs +++ b/src/wasm.rs @@ -3,6 +3,7 @@ use std::convert::{TryFrom as _, TryInto as _}; use js_sys::Array; use wasm_bindgen::prelude::*; +use super::key_derivation::{Argon2, DerivationParameters, Pbkdf2}; use super::utils; use super::Argon2Parameters; use super::DataType; @@ -15,9 +16,6 @@ use super::{ key, key::{KeyVersion, PrivateKey, PublicKey, SecretKey}, }; -use super::{ - key_derivation::{DerivationParameters, Pbkdf2, Argon2}, -}; use super::{ password_hash, password_hash::{PasswordHash, PasswordHashVersion}, @@ -35,7 +33,6 @@ use super::{ signing_key::{SigningKeyPair, SigningKeyVersion, SigningPublicKey}, }; -// Local KeyPair have private fields with getters instead of public field, for wasm_bindgen #[wasm_bindgen(inspectable)] #[derive(Clone)] pub struct KeyDerivationResult { @@ -71,7 +68,9 @@ impl DerivationParameters { #[wasm_bindgen(inspectable)] #[derive(Clone)] -pub struct KeyPair {private_key: PrivateKey, +// Local KeyPair have private fields with getters instead of public field, for wasm_bindgen +pub struct KeyPair { + private_key: PrivateKey, public_key: PublicKey, } @@ -425,6 +424,21 @@ pub fn derive_secret_key_pbkdf2( }) } +#[wasm_bindgen(js_name = "deriveSecretKeyPbkdf2WithSalt")] +pub fn derive_secret_key_pbkdf2_with_salt( + password: &[u8], + salt: &[u8], + iterations: Option, +) -> Result { + let (secret_key, parameters) = + Pbkdf2::with_params(iterations.unwrap_or(DEFAULT_PBKDF2_ITERATIONS)) + .derive_with_salt(password, salt)?; + Ok(KeyDerivationResult { + secret_key, + parameters, + }) +} + #[wasm_bindgen(js_name = "deriveSecretKeyArgon2")] pub fn derive_secret_key_argon2( password: &[u8], diff --git a/uniffi/devolutions-crypto-uniffi/src/key_derivation.rs b/uniffi/devolutions-crypto-uniffi/src/key_derivation.rs index 931da1d7..d23835d1 100644 --- a/uniffi/devolutions-crypto-uniffi/src/key_derivation.rs +++ b/uniffi/devolutions-crypto-uniffi/src/key_derivation.rs @@ -18,6 +18,20 @@ pub fn derive_secret_key_pbkdf2(key: &[u8], iterations: u32) -> Result Result { + let (sk, params) = devolutions_crypto::key_derivation::Pbkdf2::with_params(iterations) + .derive_with_salt(key, salt)?; + Ok(KeyDerivationResult { + secret_key: sk.into(), + parameters: params.into(), + }) +} + #[uniffi::export] pub fn derive_secret_key_argon2( key: &[u8], diff --git a/wrappers/csharp/src/Managed.cs b/wrappers/csharp/src/Managed.cs index 9629d21d..527d5870 100644 --- a/wrappers/csharp/src/Managed.cs +++ b/wrappers/csharp/src/Managed.cs @@ -310,6 +310,53 @@ public static KeyDerivationResult DeriveSecretKeyArgon2(byte[] key, Argon2Parame return new KeyDerivationResult(SecretKey.FromByteArray(skBuf), DerivationParameters.FromByteArray(paramsBuf)); } + /// + /// Derives a from a password using PBKDF2-HMAC-SHA256 with a caller-supplied salt. + /// Use this overload when you need a deterministic derivation (e.g. re-deriving the same key from stored parameters). + /// For new derivations prefer , which generates a random salt automatically. + /// + /// The password to derive from. + /// The salt to use. Must not be empty. + /// Number of PBKDF2 iterations. Defaults to 600 000. + /// Returns a containing the derived key and the parameters needed to reproduce it. + public static KeyDerivationResult DeriveSecretKeyPbkdf2WithSalt(byte[] key, byte[] salt, uint iterations = DEFAULT_PBKDF2_ITERATIONS) + { + if (key == null || key.Length == 0) + { + throw new DevolutionsCryptoException(ManagedError.InvalidParameter); + } + + if (salt == null || salt.Length == 0) + { + throw new DevolutionsCryptoException(ManagedError.InvalidParameter); + } + + long skSize = Native.GenerateSecretKeySizeNative(); + long paramsSize = Native.DeriveSecretKeyPbkdf2WithSaltSizeNative((UIntPtr)salt.Length); + + if (skSize < 0) + { + Utils.HandleError(skSize); + } + + if (paramsSize < 0) + { + Utils.HandleError(paramsSize); + } + + byte[] skBuf = new byte[skSize]; + byte[] paramsBuf = new byte[paramsSize]; + + long res = Native.DeriveSecretKeyPbkdf2WithSaltNative(key, (UIntPtr)key.Length, iterations, salt, (UIntPtr)salt.Length, skBuf, (UIntPtr)skBuf.Length, paramsBuf, (UIntPtr)paramsBuf.Length); + + if (res < 0) + { + Utils.HandleError(res); + } + + return new KeyDerivationResult(SecretKey.FromByteArray(skBuf), DerivationParameters.FromByteArray(paramsBuf)); + } + /// /// Decrypts the data with the provided key. /// diff --git a/wrappers/csharp/src/Native.Core.cs b/wrappers/csharp/src/Native.Core.cs index 5f2cf3fc..cd2c0c75 100644 --- a/wrappers/csharp/src/Native.Core.cs +++ b/wrappers/csharp/src/Native.Core.cs @@ -60,6 +60,12 @@ public static partial class Native [DllImport(LibName, EntryPoint = "DeriveSecretKeyArgon2ParametersSize", CallingConvention = CallingConvention.Cdecl)] internal static extern long DeriveSecretKeyArgon2ParametersSizeNative(UIntPtr argon2ParametersLength); + [DllImport(LibName, EntryPoint = "DeriveSecretKeyPbkdf2WithSalt", CallingConvention = CallingConvention.Cdecl)] + internal static extern long DeriveSecretKeyPbkdf2WithSaltNative(byte[] key, UIntPtr keyLength, System.UInt32 iterations, byte[] salt, UIntPtr saltLength, byte[] secretKey, UIntPtr secretKeyLength, byte[] paramsOut, UIntPtr paramsOutLength); + + [DllImport(LibName, EntryPoint = "DeriveSecretKeyPbkdf2WithSaltSize", CallingConvention = CallingConvention.Cdecl)] + internal static extern long DeriveSecretKeyPbkdf2WithSaltSizeNative(UIntPtr saltLength); + [DllImport(LibName, EntryPoint = "Encode", CallingConvention = CallingConvention.Cdecl)] internal static extern long EncodeNative(byte[] input, UIntPtr input_length, byte[] output, UIntPtr output_length); diff --git a/wrappers/csharp/tests/unit-tests/TestManaged.cs b/wrappers/csharp/tests/unit-tests/TestManaged.cs index 9c86a433..94584db0 100644 --- a/wrappers/csharp/tests/unit-tests/TestManaged.cs +++ b/wrappers/csharp/tests/unit-tests/TestManaged.cs @@ -474,6 +474,20 @@ public void DeriveSecretKeyArgon2_DifferentSalts_ProduceDifferentKeys() CollectionAssert.AreNotEqual(result1.Parameters.ToByteArray(), result2.Parameters.ToByteArray()); } + [TestMethod] + public void DeriveSecretKeyPbkdf2WithSalt_FixedSalt_ProducesSameKey() + { + byte[] password = "pbkdf2 test password"u8.ToArray(); + byte[] salt = "fixed_salt_16byt"u8.ToArray(); + + KeyDerivationResult result1 = Managed.DeriveSecretKeyPbkdf2WithSalt(password, salt, 10); + KeyDerivationResult result2 = Managed.DeriveSecretKeyPbkdf2WithSalt(password, salt, 10); + + // Same password + same salt + same iterations → same key and same parameters + CollectionAssert.AreEqual(result1.SecretKey.ToByteArray(), result2.SecretKey.ToByteArray()); + CollectionAssert.AreEqual(result1.Parameters.ToByteArray(), result2.Parameters.ToByteArray()); + } + private static byte[][] GetSharesKeys() { const int nbShares = 3; diff --git a/wrappers/wasm/demo/src/app/inspect/inspect.component.ts b/wrappers/wasm/demo/src/app/inspect/inspect.component.ts index c2ecff67..bca44dee 100644 --- a/wrappers/wasm/demo/src/app/inspect/inspect.component.ts +++ b/wrappers/wasm/demo/src/app/inspect/inspect.component.ts @@ -418,6 +418,12 @@ export class InspectComponent implements OnInit { } const iterations = dv.getUint32(0, true); const saltLen = dv.getUint32(4, true); + if (8 + saltLen > payload.length) { + fields.push({ name: 'Iterations', offset: abs(0), size: 4, hex: toHex(payload.slice(0, 4)), description: `PBKDF2 iteration count: ${iterations.toLocaleString()}` }); + fields.push({ name: 'Salt Length', offset: abs(4), size: 4, hex: toHex(payload.slice(4, 8)), description: `Salt length: ${saltLen} bytes` }); + fields.push({ name: 'Error', offset: abs(8), size: payload.length - 8, hex: toHex(payload.slice(8)), description: `Truncated: salt claims ${saltLen} bytes but only ${payload.length - 8} bytes remain` }); + return fields; + } const salt = payload.slice(8, 8 + saltLen); fields.push({ name: 'Iterations', @@ -541,6 +547,16 @@ export class InspectComponent implements OnInit { pos += 4; if (assocDataLen > 0) { + if (pos + assocDataLen > payload.length) { + fields.push({ + name: 'Error', + offset: abs(pos), + size: payload.length - pos, + hex: toHex(payload.slice(pos)), + description: `Truncated: assoc data claims ${assocDataLen} bytes but only ${payload.length - pos} bytes remain`, + }); + return fields; + } fields.push({ name: 'Assoc. Data', offset: abs(pos), @@ -551,6 +567,17 @@ export class InspectComponent implements OnInit { pos += assocDataLen; } + if (pos + 4 > payload.length) { + fields.push({ + name: 'Error', + offset: abs(pos), + size: payload.length - pos, + hex: toHex(payload.slice(pos)), + description: `Truncated: expected salt-length field (4 bytes) but only ${payload.length - pos} bytes remain`, + }); + return fields; + } + const saltLen = dv.getUint32(pos, true); fields.push({ name: 'Salt Length', @@ -561,6 +588,17 @@ export class InspectComponent implements OnInit { }); pos += 4; + if (pos + saltLen > payload.length) { + fields.push({ + name: 'Error', + offset: abs(pos), + size: payload.length - pos, + hex: toHex(payload.slice(pos)), + description: `Truncated: salt claims ${saltLen} bytes but only ${payload.length - pos} bytes remain`, + }); + return fields; + } + const salt = payload.slice(pos, pos + saltLen); fields.push({ name: 'Salt', diff --git a/wrappers/wasm/demo/src/app/key-derivation/key-derivation.component.html b/wrappers/wasm/demo/src/app/key-derivation/key-derivation.component.html index 56835707..16160b40 100644 --- a/wrappers/wasm/demo/src/app/key-derivation/key-derivation.component.html +++ b/wrappers/wasm/demo/src/app/key-derivation/key-derivation.component.html @@ -66,9 +66,10 @@

Key Derivation

-

- A random salt is generated automatically and embedded in the output parameters. -

+
+ + +
diff --git a/wrappers/wasm/demo/src/app/key-derivation/key-derivation.component.ts b/wrappers/wasm/demo/src/app/key-derivation/key-derivation.component.ts index 3a4598c4..1041dc14 100644 --- a/wrappers/wasm/demo/src/app/key-derivation/key-derivation.component.ts +++ b/wrappers/wasm/demo/src/app/key-derivation/key-derivation.component.ts @@ -82,12 +82,14 @@ export class KeyDerivationComponent implements OnInit { const saltStr: string = this.deriveForm.value.pbkdf2Salt; const iterations: number | undefined = iterStr ? Number(iterStr) : undefined; - const salt: Uint8Array | undefined = saltStr ? this.encoder.encode(saltStr) : undefined; - // PBKDF2 uses a random salt unless one is embedded via derive_with_salt (not directly exposed to WASM). - // We call deriveSecretKeyPbkdf2 with the requested iterations; salt is always random on this path. - // If a custom salt was provided, inform the user it is not used on this path. - const result = service.deriveSecretKeyPbkdf2(passwordBytes, iterations); + let result; + if (saltStr) { + const salt: Uint8Array = this.encoder.encode(saltStr); + result = service.deriveSecretKeyPbkdf2WithSalt(passwordBytes, salt, iterations); + } else { + result = service.deriveSecretKeyPbkdf2(passwordBytes, iterations); + } this.deriveForm.patchValue({ secretKeyResult: service.base64encode(result.secretKey.bytes), diff --git a/wrappers/wasm/demo/src/app/service/encryption.inner.service.ts b/wrappers/wasm/demo/src/app/service/encryption.inner.service.ts index 929fb030..e75cbdd7 100644 --- a/wrappers/wasm/demo/src/app/service/encryption.inner.service.ts +++ b/wrappers/wasm/demo/src/app/service/encryption.inner.service.ts @@ -78,6 +78,10 @@ export function deriveSecretKeyPbkdf2(password: Uint8Array, iterations?: number) return devolutionsCrypto.deriveSecretKeyPbkdf2(password, iterations); } +export function deriveSecretKeyPbkdf2WithSalt(password: Uint8Array, salt: Uint8Array, iterations?: number): KeyDerivationResult { + return devolutionsCrypto.deriveSecretKeyPbkdf2WithSalt(password, salt, iterations); +} + export function deriveSecretKeyArgon2(password: Uint8Array, parameters: Argon2Parameters): KeyDerivationResult { return devolutionsCrypto.deriveSecretKeyArgon2(password, parameters); } diff --git a/wrappers/wasm/tests/tests/key-derivation.ts b/wrappers/wasm/tests/tests/key-derivation.ts index 93e80f72..3b729f4e 100644 --- a/wrappers/wasm/tests/tests/key-derivation.ts +++ b/wrappers/wasm/tests/tests/key-derivation.ts @@ -1,4 +1,4 @@ -import { deriveSecretKeyPbkdf2, deriveSecretKeyArgon2, Argon2Parameters, DerivationParameters, KeyDerivationResult, base64encode, base64decode } from 'devolutions-crypto' +import { deriveSecretKeyPbkdf2, deriveSecretKeyPbkdf2WithSalt, deriveSecretKeyArgon2, Argon2Parameters, DerivationParameters, KeyDerivationResult, base64encode, base64decode } from 'devolutions-crypto' import { describe, test } from 'node:test' import assert from 'node:assert/strict' @@ -23,6 +23,28 @@ describe('key derivation', () => { assert.notStrictEqual(base64encode(result1.secretKey.bytes), base64encode(result2.secretKey.bytes)) }) + test('deriveSecretKeyPbkdf2WithSalt with fixed salt produces the same key', () => { + const password = encoder.encode('test password') + const salt = encoder.encode('fixed_salt_16byt') + + const result1: KeyDerivationResult = deriveSecretKeyPbkdf2WithSalt(password, salt, 10) + const result2: KeyDerivationResult = deriveSecretKeyPbkdf2WithSalt(password, salt, 10) + + // Fixed salt → same key and same parameters both times + assert.strictEqual(base64encode(result1.secretKey.bytes), base64encode(result2.secretKey.bytes)) + assert.strictEqual(base64encode(result1.parameters.bytes), base64encode(result2.parameters.bytes)) + }) + + test('deriveSecretKeyPbkdf2WithSalt with different salts produces different keys', () => { + const password = encoder.encode('test password') + + const result1: KeyDerivationResult = deriveSecretKeyPbkdf2WithSalt(password, encoder.encode('salt_one_16bytes'), 10) + const result2: KeyDerivationResult = deriveSecretKeyPbkdf2WithSalt(password, encoder.encode('salt_two_16bytes'), 10) + + assert.notStrictEqual(base64encode(result1.secretKey.bytes), base64encode(result2.secretKey.bytes)) + assert.notStrictEqual(base64encode(result1.parameters.bytes), base64encode(result2.parameters.bytes)) + }) + test('deriveSecretKeyArgon2 with fixed parameters produces the same key', () => { const password = encoder.encode('test password') const fixedParams: Argon2Parameters = Argon2Parameters.fromBytes( From 2f02cbfba6f2cc1a0fdb4d9437c58e8cb692e0c0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Duquette?= Date: Wed, 27 May 2026 09:32:50 -0400 Subject: [PATCH 3/5] Small touchups --- ffi/src/lib.rs | 6 +++--- .../src/app/key-derivation/key-derivation.component.html | 8 ++++---- .../src/app/key-derivation/key-derivation.component.ts | 3 +-- 3 files changed, 8 insertions(+), 9 deletions(-) diff --git a/ffi/src/lib.rs b/ffi/src/lib.rs index 9c5791e4..39acd0f8 100644 --- a/ffi/src/lib.rs +++ b/ffi/src/lib.rs @@ -42,6 +42,9 @@ use devolutions_crypto::{ use devolutions_crypto::Result; +#[cfg(test)] +use devolutions_crypto::key_derivation::DerivationParameters; + use std::borrow::Borrow; use std::ffi::c_void; use std::slice; @@ -49,9 +52,6 @@ use std::sync::Mutex; use zeroize::Zeroizing; -#[cfg(test)] -use devolutions_crypto::key_derivation::DerivationParameters; - const VERSION: &str = env!("CARGO_PKG_VERSION"); /// Encrypt a data blob diff --git a/wrappers/wasm/demo/src/app/key-derivation/key-derivation.component.html b/wrappers/wasm/demo/src/app/key-derivation/key-derivation.component.html index 16160b40..a679d605 100644 --- a/wrappers/wasm/demo/src/app/key-derivation/key-derivation.component.html +++ b/wrappers/wasm/demo/src/app/key-derivation/key-derivation.component.html @@ -42,7 +42,7 @@

Key Derivation

- + @if (algorithm === 'argon2') {
@@ -58,10 +58,10 @@

Key Derivation

A random salt is generated automatically on each derivation and embedded in the output parameters.

- + } - + @if (algorithm === 'pbkdf2') {
@@ -70,7 +70,7 @@

Key Derivation

-
+ }
diff --git a/wrappers/wasm/demo/src/app/key-derivation/key-derivation.component.ts b/wrappers/wasm/demo/src/app/key-derivation/key-derivation.component.ts index 1041dc14..135773db 100644 --- a/wrappers/wasm/demo/src/app/key-derivation/key-derivation.component.ts +++ b/wrappers/wasm/demo/src/app/key-derivation/key-derivation.component.ts @@ -3,7 +3,6 @@ import { FormGroup, FormControl, ReactiveFormsModule } from '@angular/forms'; import { EncryptionService } from '../service/encryption.service'; import { FaIconComponent } from '@fortawesome/angular-fontawesome'; import { faKey } from '@fortawesome/free-solid-svg-icons'; -import { NgIf } from '@angular/common'; import * as functions from '../shared/shared.component'; type EncryptionServiceInner = typeof import('../service/encryption.inner.service'); @@ -11,7 +10,7 @@ type EncryptionServiceInner = typeof import('../service/encryption.inner.service @Component({ selector: 'app-key-derivation', standalone: true, - imports: [ReactiveFormsModule, FaIconComponent, NgIf], + imports: [ReactiveFormsModule, FaIconComponent], templateUrl: './key-derivation.component.html', }) export class KeyDerivationComponent implements OnInit { From 6589237b5e2baebd834ab5d52346539a303b826f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Duquette?= Date: Wed, 27 May 2026 10:42:42 -0400 Subject: [PATCH 4/5] Validate size of argon2 params against params_out_length --- ffi/src/lib.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/ffi/src/lib.rs b/ffi/src/lib.rs index 39acd0f8..a535f529 100644 --- a/ffi/src/lib.rs +++ b/ffi/src/lib.rs @@ -1528,6 +1528,10 @@ pub unsafe extern "C" fn DeriveSecretKeyArgon2( let sk_bytes: Zeroizing> = Zeroizing::new(sk.into()); let params_bytes: Vec = params.into(); + if params_bytes.len() != params_out_length { + return Error::InvalidOutputLength.error_code(); + } + let secret_key = slice::from_raw_parts_mut(secret_key, secret_key_length); let params_out = slice::from_raw_parts_mut(params_out, params_out_length); From d57277536b1b5cae883b1fe2929bb6f7e1c968b0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Duquette?= Date: Wed, 27 May 2026 11:59:13 -0400 Subject: [PATCH 5/5] Remove cbindgen files --- ffi/cbindgen.toml | 158 --------- ffi/devolutions-crypto.h | 723 --------------------------------------- 2 files changed, 881 deletions(-) delete mode 100644 ffi/cbindgen.toml delete mode 100644 ffi/devolutions-crypto.h diff --git a/ffi/cbindgen.toml b/ffi/cbindgen.toml deleted file mode 100644 index 30e68bba..00000000 --- a/ffi/cbindgen.toml +++ /dev/null @@ -1,158 +0,0 @@ -# This is a template cbindgen.toml file with all of the default values. -# Some values are commented out because their absence is the real default. -# -# See https://github.com/mozilla/cbindgen/blob/master/docs.md#cbindgentoml -# for detailed documentation of every option here. - - - -language = "C" - - - -############## Options for Wrapping the Contents of the Header ################# - -# header = "/* Text to put at the beginning of the generated file. Probably a license. */" -# trailer = "/* Text to put at the end of the generated file */" -# include_guard = "my_bindings_h" -# pragma_once = true -# autogen_warning = "/* Warning, this file is autogenerated by cbindgen. Don't modify this manually. */" -include_version = false -#namespace = "ffi" -namespaces = [] -using_namespaces = [] -sys_includes = [] -includes = [] -no_includes = false -after_includes = "" - - - - -############################ Code Style Options ################################ - -braces = "SameLine" -line_length = 100 -tab_width = 2 -documentation = true -documentation_style = "auto" -documentation_length = "full" -line_endings = "LF" # also "CR", "CRLF", "Native" - - - - -############################# Codegen Options ################################## - -style = "both" -sort_by = "Name" # default for `fn.sort_by` and `const.sort_by` -usize_is_size_t = true - - - -[defines] -# "target_os = freebsd" = "DEFINE_FREEBSD" -# "feature = serde" = "DEFINE_SERDE" - - - -[export] -include = [] -exclude = [] -# prefix = "CAPI_" -item_types = [] -renaming_overrides_prefixing = false - - - -[export.rename] - - - -[export.body] - - -[export.mangle] - - -[fn] -rename_args = "None" -# must_use = "MUST_USE_FUNC" -# deprecated = "DEPRECATED_FUNC" -# deprecated_with_note = "DEPRECATED_FUNC_WITH_NOTE" -# no_return = "NO_RETURN" -# prefix = "START_FUNC" -# postfix = "END_FUNC" -args = "auto" -sort_by = "Name" - - - - -[struct] -rename_fields = "None" -# must_use = "MUST_USE_STRUCT" -# deprecated = "DEPRECATED_STRUCT" -# deprecated_with_note = "DEPRECATED_STRUCT_WITH_NOTE" -derive_constructor = false -derive_eq = false -derive_neq = false -derive_lt = false -derive_lte = false -derive_gt = false -derive_gte = false - - - - -[enum] -rename_variants = "None" -# must_use = "MUST_USE_ENUM" -# deprecated = "DEPRECATED_ENUM" -# deprecated_with_note = "DEPRECATED_ENUM_WITH_NOTE" -add_sentinel = false -prefix_with_name = false -derive_helper_methods = false -derive_const_casts = false -derive_mut_casts = false -# cast_assert_name = "ASSERT" -derive_tagged_enum_destructor = false -derive_tagged_enum_copy_constructor = false -enum_class = true -private_default_tagged_enum_constructor = false - - - - -[const] -allow_static_const = true -allow_constexpr = false -sort_by = "Name" - - - - -[macro_expansion] -bitflags = false - - - - - - -############## Options for How Your Rust library Should Be Parsed ############## - -[parse] -parse_deps = false -# include = [] -exclude = [] -clean = false -extra_bindings = [] - - - -[parse.expand] -crates = [] -all_features = false -default_features = true -features = [] \ No newline at end of file diff --git a/ffi/devolutions-crypto.h b/ffi/devolutions-crypto.h deleted file mode 100644 index fec3c283..00000000 --- a/ffi/devolutions-crypto.h +++ /dev/null @@ -1,723 +0,0 @@ -#include -#include -#include -#include -#include - - -/** - * Compare two byte arrays with constant-time equality. - * # Arguments - * * `x` - Pointer to the first value to compare. - * * `x_length` - Length of the first value to compare. - * * `y` - Pointer to the second value to compare. - * * `y_length` - Length of the second value to compare. - * # Returns - * Returns 0 if the values are not equal is invalid or 1 if the values are equal. If there is an error, - * it will return the appropriate error code defined in DevoCryptoError. - * # 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. - */ -int64_t ConstantTimeEquals(const uint8_t *x, - size_t x_length, - const uint8_t *y, - size_t y_length); - -/** - * Decode a base64 string to bytes. - * # Arguments - * * input - Pointer to the string to decode. - * * input_length - Length of the string to decode. - * * output - Pointer to the output buffer. - * * output_length - Length of the output buffer. - * # Returns - * Returns the size of the decoded string. - * # 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. - */ -int64_t Decode(const uint8_t *input, - size_t input_length, - uint8_t *output, - size_t output_length); - -/** - * Decode a base64 string to bytes using base64url. - * # Arguments - * * input - Pointer to the string to decode. - * * input_length - Length of the string to decode. - * * output - Pointer to the output buffer. - * * output_length - Length of the output buffer. - * # Returns - * Returns the size of the decoded string. - * # 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. - */ -int64_t DecodeUrl(const uint8_t *input, - size_t input_length, - uint8_t *output, - size_t output_length); - -/** - * Decrypt a data blob - * # Arguments - * * `data` - Pointer to the data to decrypt. - * * `data_length` - Length of the data to decrypt. - * * `key` - Pointer to the key to use to decrypt. - * * `key_length` - Length of the key to use to decrypt. - * * `aad` - Pointer to additionnal data to authenticate alongside the ciphertext. - * Pass null if there is not additionnal data to authenticate. - * * `aad_length` - Length of the additionnal data to authenticate. Pass 0 if there is no data. - * * `result` - Pointer to the buffer to write the plaintext to. - * * `result_length` - Length of the buffer to write the plaintext to. - * The safest size is the same size as the ciphertext. - * # Returns - * This returns the length of the plaintext. If there is an error, it will return the - * appropriate error code defined in DevoCryptoError. - * # 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. - */ -int64_t Decrypt(const uint8_t *data, - size_t data_length, - const uint8_t *key, - size_t key_length, - const uint8_t *aad, - size_t aad_length, - uint8_t *result, - size_t result_length); - -/** - * Decrypt a data blob - * # Arguments - * * `data` - Pointer to the data to decrypt. - * * `data_length` - Length of the data to decrypt. - * * `private_key` - Pointer to the private key to use to decrypt. - * * `private_key_length` - Length of the private key to use to decrypt. - * * `aad` - Pointer to additionnal data to authenticate alongside the ciphertext. - * Pass null if there is not additionnal data to authenticate. - * * `aad_length` - Length of the additionnal data to authenticate. Pass 0 if there is no data. - * * `result` - Pointer to the buffer to write the plaintext to. - * * `result_length` - Length of the buffer to write the plaintext to. - * The safest size is the same size as the ciphertext. - * # Returns - * This returns the length of the plaintext. If there is an error, it will return the - * appropriate error code defined in DevoCryptoError. - * # 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. - */ -int64_t DecryptAsymmetric(const uint8_t *data, - size_t data_length, - const uint8_t *private_key, - size_t private_key_length, - const uint8_t *aad, - size_t aad_length, - uint8_t *result, - size_t result_length); - -/** - * Derive a key with Argon2 to create a new one. Can be used with a password. - * # Arguments - * * key - Pointer to the key to derive. - * * key_length - Length of the key to derive. - * * argon2_parameters - Pointer to the buffer containing the argon2 parameters. - * * argon2_parameters_length - Length of the argon2 parameters to use. - * * result - Pointer to the buffer to write the new key to. - * * result_length - Length of buffer to write the key to. - * # Returns - * Returns 0 if the operation is successful. If there is an error, - * it will return the appropriate error code defined in DevoCryptoError. - * # 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. - */ -int64_t DeriveKeyArgon2(const uint8_t *key, - size_t key_length, - const uint8_t *argon2_parameters, - size_t argon2_parameters_length, - uint8_t *result, - size_t result_length); - -/** - * Derive a key with PBKDF2 to create a new one. Can be used with a password. - * # Arguments - * * key - Pointer to the key to derive. - * * key_length - Length of the key to derive. - * * salt - Pointer to the buffer containing the salt. Can be null. - * * salt_length - Length of the salt to use. - * * result - Pointer to the buffer to write the new key to. - * * result_length - Length of buffer to write the key to. - * # Returns - * Returns 0 if the operation is successful. If there is an error, - * it will return the appropriate error code defined in DevoCryptoError. - * # 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. - */ -int64_t DeriveKeyPbkdf2(const uint8_t *key, - size_t key_length, - const uint8_t *salt, - size_t salt_length, - uint32_t niterations, - uint8_t *result, - size_t result_length); - -/** - * Encode a byte array to a base64 string. - * # Arguments - * * input - Pointer to the buffer to encode. - * * input_length - Length of the buffer to encode. - * * output - Pointer to the output buffer. - * * output_length - Length of the output buffer. - * # Returns - * Returns the size, in bytes, of the output buffer. - * # 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. - */ -int64_t Encode(const uint8_t *input, - size_t input_length, - uint8_t *output, - size_t output_length); - -/** - * Encode a byte array to a base64 string using base64url. - * # Arguments - * * input - Pointer to the buffer to encode. - * * input_length - Length of the buffer to encode. - * * output - Pointer to the output buffer. - * * output_length - Length of the output buffer. - * # Returns - * Returns the size, in bytes, of the output buffer. - * # 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. - */ -int64_t EncodeUrl(const uint8_t *input, - size_t input_length, - uint8_t *output, - size_t output_length); - -/** - * Encrypt a data blob - * # Arguments - * * `data` - Pointer to the data to encrypt. - * * `data_length` - Length of the data to encrypt. - * * `key` - Pointer to the key to use to encrypt. - * * `key_length` - Length of the key to use to encrypt. - * * `aad` - Pointer to additionnal data to authenticate alongside the ciphertext. - * Pass null if there is not additionnal data to authenticate. - * * `aad_length` - Length of the additionnal data to authenticate. Pass 0 if there is no data. - * * `result` - Pointer to the buffer to write the ciphertext to. - * * `result_length` - Length of the buffer to write the ciphertext to. You can get the value by - * calling EncryptSize() beforehand. - * * `version` - Version to use. Use 0 for the latest one. - * # Returns - * This returns the length of the ciphertext. If there is an error, it will return the - * appropriate error code defined in DevoCryptoError. - * # 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. - */ -int64_t Encrypt(const uint8_t *data, - size_t data_length, - const uint8_t *key, - size_t key_length, - const uint8_t *aad, - size_t aad_length, - uint8_t *result, - size_t result_length, - uint16_t version); - -/** - * Encrypt a data blob - * # Arguments - * * `data` - Pointer to the data to encrypt. - * * `data_length` - Length of the data to encrypt. - * * `public_key` - Pointer to the public key to use to encrypt. - * * `public_key_length` - Length of the public key to use to encrypt. - * * `aad` - Pointer to additionnal data to authenticate alongside the ciphertext. - * Pass null if there is not additionnal data to authenticate. - * * `aad_length` - Length of the additionnal data to authenticate. Pass 0 if there is no data. - * * `result` - Pointer to the buffer to write the ciphertext to. - * * `result_length` - Length of the buffer to write the ciphertext to. You can get the value by - * calling EncryptAsymmetricSize() beforehand. - * * `version` - Version to use. Use 0 for the latest one. - * # Returns - * This returns the length of the asymmetric ciphertext. If there is an error, it will return the - * appropriate error code defined in DevoCryptoError. - * # 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. - */ -int64_t EncryptAsymmetric(const uint8_t *data, - size_t data_length, - const uint8_t *public_key, - size_t public_key_length, - const uint8_t *aad, - size_t aad_length, - uint8_t *result, - size_t result_length, - uint16_t version); - -/** - * Get the size of the resulting asymmetric ciphertext. - * # Arguments - * * data_length - Length of the plaintext. - * # Returns - * Returns the length of the asymmetric ciphertext to input as `result_length` in `EncryptAsymmetric()`. - */ -int64_t EncryptAsymmetricSize(size_t data_length, - uint16_t version); - -/** - * Get the size of the resulting ciphertext. - * # Arguments - * * data_length - Length of the plaintext. - * # Returns - * Returns the length of the ciphertext to input as `result_length` in `Encrypt()`. - */ -int64_t EncryptSize(size_t data_length, uint16_t version); - -void FreeOnlineDecryptor(void *ptr); - -void FreeOnlineEncryptor(void *ptr); - -/** - * Generate a key using a CSPRNG. - * # Arguments - * * key - Pointer to the buffer to fill with random values. - * * key_length - Length of the buffer to fill. - * # Returns - * Returns 0 if the operation is successful. If there is an error, - * it will return the appropriate error code defined in DevoCryptoError. - * # 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. - */ -int64_t GenerateKey(uint8_t *key, - size_t key_length); - -/** - * Generate a key pair to perform a key exchange. Must be used with MixKey() - * # Arguments - * * `private` - Pointer to the buffer to write the private key to. - * * `private_length` - Length of the buffer to write the private key to. - * You can get the value by calling `GenerateKeyPairSize()` beforehand. - * * `public` - Pointer to the buffer to write the public key to. - * * `public_length` - Length of the buffer to write the public key to. - * You can get the value by calling `GenerateKeyPairSize()` beforehand. - * # Returns - * Returns 0 if the generation worked. If there is an error, - * it will return the appropriate error code defined in DevoCryptoError. - * # 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. - */ -int64_t GenerateKeyPair(uint8_t *private_, - size_t private_length, - uint8_t *public_, - size_t public_length); - -/** - * Get the size of the keys in the key exchange key pair. - * # Returns - * Returns the length of the keys to input as `private_length` - * and `public_length` in `GenerateKeyPair()`. - */ -int64_t GenerateKeyPairSize(void); - -/** - * Generate a secret key for symmetric encryption. - * # Arguments - * * `result` - Pointer to the buffer to write the secret key to. - * * `result_length` - Length of the buffer to write the secret key to. - * You can get the value by calling `GenerateSecretKeySize()` beforehand. - * # Returns - * Returns 0 if the generation worked. If there is an error, - * it will return the appropriate error code defined in DevoCryptoError. - * # 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. - */ -int64_t GenerateSecretKey(uint8_t *result, size_t result_length); - -/** - * Get the size of a serialized secret key. - * # Returns - * Returns the length of the buffer to pass as `result_length` in `GenerateSecretKey()`. - */ -int64_t GenerateSecretKeySize(void); - -/** - * Generates a secret key shared amongst multiple actor. - * # Arguments - * * n_shares - The number of shares to generate. - * * threshold - The number of shares required to regenerate the secret. - * * length - The length of the generated secret - * * shares - The output buffers. This is a 2-dimensionnal array representing the shares. - * # Returns - * Returns 0 if the operation is successful. If there is an error, - * it will return the appropriate error code defined in DevoCryptoError. - * # 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. - */ -int64_t GenerateSharedKey(uint8_t n_shares, - uint8_t threshold, - size_t length, - uint8_t *const *shares); - -/** - * The size, in bytes, of each resulting shares - * # Arguments - * * secret_length - The length of the desired secret - * # Returns - * Returns the size, in bytes, of each resulting shares. - */ -int64_t GenerateSharedKeySize(size_t secret_length); - -/** - * Generate a key pair to sign and verify data with. - * # Arguments - * * `keypair` - Pointer to the buffer to write the keypair to. - * * `keypair_length` - Length of the buffer to write the keypair to. - * You can get the value by calling `GenerateSigningKeyPairSize()` beforehand. - * * `version` - Version to use. Use 0 for the latest one. - * # Returns - * Returns 0 if the generation worked. If there is an error, - * it will return the appropriate error code defined in DevoCryptoError. - * # 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. - */ -int64_t GenerateSigningKeyPair(uint8_t *keypair, - size_t keypair_length, - uint16_t version); - -/** - * Get the size of the keypair used for signing. - * # Returns - * Returns the length of the keypair to input as `keypair_length` - * in `GenerateSigningKeyPair()`. - */ -int64_t GenerateSigningKeyPairSize(uint16_t _version); - -/** - * Get the default Argon2Parameters struct values. - * # Arguments - * * argon2_parameters - Pointer to the output buffer. - * * argon2_parameters_length - Length of the output buffer. - * # Returns - * Returns 0 if the operation is successful. - * # 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. - */ -int64_t GetDefaultArgon2Parameters(uint8_t *argon2_parameters, - size_t argon2_parameters_length); - -/** - * Size of the Argon2Parameters struct. - * # Returns - * Returns 0 if the operation is successful. - */ -int64_t GetDefaultArgon2ParametersSize(void); - -/** - * Get the public part of a keypair used to sign data. - * # Arguments - * * `keypair` - Pointer to the buffer containing the keypair. - * * `keypair_length` - Length of the buffer containing the keypair. - * * `public` - Pointer to the buffer to write the public key to. - * * `public_length` - Length of the buffer to write the public key to. - * You can get the value by calling `GetSigningPublicKeySize()` beforehand. - * # 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. - */ -int64_t GetSigningPublicKey(const uint8_t *keypair, - size_t keypair_length, - uint8_t *public_, - size_t public_length); - -/** - * Get the size of the public key used for signing. - * # Returns - * Returns the length of the public key to input as `public_length` - * in `GetSigningPublicKey()`. - */ -int64_t GetSigningPublicKeySize(const uint8_t *_keypair, size_t _keypair_length); - -/** - * Hash a password using a high-cost algorithm. - * # Arguments - * * `password` - Pointer to the password to hash. - * * `password_length` - Length of the password to hash. - * * `iterations` - Number of iterations of the password hash. - * A higher number is slower but harder to brute-force. The recommended value is 600,000. - * * `result` - Pointer to the buffer to write the hash to. - * * `result_length` - Length of the buffer to write the hash to. You can get the value by - * calling HashPasswordLength() beforehand. - * # Returns - * This returns the length of the hash. If there is an error, it will return the - * appropriate error code defined in DevoCryptoError. - * # 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. - */ -int64_t HashPassword(const uint8_t *password, - size_t password_length, - uint32_t iterations, - uint8_t *result, - size_t result_length); - -/** - * Get the size of the resulting hash. - * # Returns - * Returns the length of the hash to input as `result_length` in `HashPassword()`. - */ -int64_t HashPasswordLength(void); - -/** - * Join multiple shares to regenerate a shared secret. - * # Arguments - * * n_shares - The number of shares sent to the method - * * share_length - The length of each share - * * shares - The shares to join - * * secret - The output buffer to write the shared secret to. - * * secret_length - The length of the output buffer. Get the value with JoinSharesSize. - * # Returns - * Returns 0 if the operation is successful. If there is an error, - * it will return the appropriate error code defined in DevoCryptoError. - * # 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. - */ -int64_t JoinShares(size_t n_shares, - size_t share_length, - const uint8_t *const *shares, - uint8_t *secret, - size_t secret_length); - -/** - * The size, in bytes, of the resulting secret - * # Arguments - * * share_length - The length of a share - * # Returns - * Returns the size, in bytes, of each resulting secret. - */ -int64_t JoinSharesSize(size_t share_length); - -/** - * Size, in bits, of the key used for the current Encrypt() implementation. - * # Returns - * Returns the size, in bits, of the key used fot the current Encrypt() implementation. - */ -uint32_t KeySize(void); - -/** - * Performs a key exchange. - * # Arguments - * * `private` - Pointer to the buffer containing the private key. - * * `private_length` - Length of the buffer containing the private key. - * * `public` - Pointer to the buffer containing the public key. - * * `public_length` - Length of the buffer containing the public key. - * * `shared` - Pointer to the buffer to write the resulting shared key. - * * `shared_size` - Length of the buffer containing the shared key. - * # Returns - * Returns 0 if the key exchange worked. If there is an error, - * it will return the appropriate error code defined in DevoCryptoError. - * # 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. - */ -int64_t MixKeyExchange(const uint8_t *private_, - size_t private_size, - const uint8_t *public_, - size_t public_size, - uint8_t *shared, - size_t shared_size); - -/** - * Get the size of the keys in the key exchange key pair. - * # Returns - * Returns the length of the keys to input as `shared_length` in `MixKeyExchange()`. - */ -int64_t MixKeyExchangeSize(void); - -int64_t NewOnlineDecryptor(const uint8_t *key, - size_t key_size, - const uint8_t *aad, - size_t aad_size, - const uint8_t *header, - size_t header_size, - bool asymmetric, - void **output); - -int64_t NewOnlineEncryptor(const uint8_t *key, - size_t key_size, - const uint8_t *aad, - size_t aad_size, - uint32_t chunk_size, - bool asymmetric, - uint16_t version, - void **output); - -int64_t OnlineDecryptorGetChunkSize(const void *ptr); - -int64_t OnlineDecryptorGetHeader(const void *ptr, uint8_t *result, size_t result_size); - -int64_t OnlineDecryptorGetHeaderSize(const void *ptr); - -int64_t OnlineDecryptorGetTagSize(const void *ptr); - -int64_t OnlineDecryptorLastChunk(void *ptr, - const uint8_t *data, - size_t data_size, - const uint8_t *aad, - size_t aad_size, - uint8_t *result, - size_t result_size); - -int64_t OnlineDecryptorNextChunk(void *ptr, - const uint8_t *data, - size_t data_size, - const uint8_t *aad, - size_t aad_size, - uint8_t *result, - size_t result_size); - -int64_t OnlineEncryptorGetChunkSize(const void *ptr); - -int64_t OnlineEncryptorGetHeader(const void *ptr, uint8_t *result, size_t result_size); - -int64_t OnlineEncryptorGetHeaderSize(const void *ptr); - -int64_t OnlineEncryptorGetTagSize(const void *ptr); - -int64_t OnlineEncryptorLastChunk(void *ptr, - const uint8_t *data, - size_t data_size, - const uint8_t *aad, - size_t aad_size, - uint8_t *result, - size_t result_size); - -int64_t OnlineEncryptorNextChunk(void *ptr, - const uint8_t *data, - size_t data_size, - const uint8_t *aad, - size_t aad_size, - uint8_t *result, - size_t result_size); - -/** - * This is binded here for one specific use case, do not use it if you don't know what you're doing. - * # 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. - */ -int64_t ScryptSimple(const uint8_t *password, - size_t password_length, - const uint8_t *salt, - size_t salt_length, - uint8_t log_n, - uint32_t r, - uint32_t p, - uint8_t *output, - size_t output_length); - -/** - * This is binded here for one specific use case, do not use it if you don't know what you're doing. - * # 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. - */ -int64_t ScryptSimpleSize(void); - -/** - * Sign data using a keypair to certify its authenticity. - * # Arguments - * * `data` - Pointer to the data to sign. - * * `data_length` - Length of the data to sign. - * * `keypair` - pointer to the keypair to use to sign the data. - * * `keypair_length` - Length of the keypair to use to sign the data. - * * `result` - Pointer to the buffer to write the signature to. - * * `result_length` - Length of the buffer to write the signature to. You can get the value by - * calling SignSize() beforehand. - * # Returns - * This returns 0 if the operation worked. If there is an error, it will return the - * appropriate error code defined in DevoCryptoError. - * # 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. - */ -int64_t Sign(const uint8_t *data, - size_t data_length, - const uint8_t *keypair, - size_t keypair_length, - uint8_t *result, - size_t result_length, - uint16_t version); - -/** - * Get the size of the resulting signature. - * # Returns - * Returns the length of the signature to input as `result_length` in `Sign()`. - */ -int64_t SignSize(uint16_t _version); - -/** - * Validate if the header of the data is valid and consistant. - * # Arguments - * * `data` - Pointer to the input buffer. - * * `data_length` - Length of the input buffer. - * * `data_type` - Type of the data. - * # Returns - * 1 if the header is valid, 0 if it's not, and a negative value if there is an error. - * # 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. - */ -int64_t ValidateHeader(const uint8_t *data, - size_t data_length, - uint16_t data_type); - -/** - * Verify a password against a hash with constant-time equality. - * # Arguments - * * `password` - Pointer to the password to verify. - * * `password_length` - Length of the password to verify. - * * `hash` - Pointer to the hash to verify. - * * `hash_length` - Length of the hash to verify. - * # Returns - * Returns 0 if the password is invalid or 1 if the password is valid. If there is an error, - * it will return the appropriate error code defined in DevoCryptoError. - * # 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. - */ -int64_t VerifyPassword(const uint8_t *password, - size_t password_length, - const uint8_t *hash, - size_t hash_length); - -/** - * Verify some data using a signature and the corresponding public key. - * # Arguments - * * `data` - Pointer to the data to verify. - * * `data_length` - Length of the data to verify. - * * `public_key` - Pointer to the public part of the keypair that was used to sign the data. - * * `public_key` - Length of the public part of the keypair that was used to sign the data. - * * `signature` - Pointer to the signature to verify. - * * `signature_length` - Length of the signature to verify. - * # Returns - * Returns 0 if the data, the signature or the public key is invalid or 1 if everything is valid. If there is an error, - * it will return the appropriate error code defined in DevoCryptoError. - * # 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. - */ -int64_t VerifySignature(const uint8_t *data, - size_t data_length, - const uint8_t *public_key, - size_t public_key_length, - const uint8_t *signature, - size_t signature_length); - -/** - * Fill the output buffer with the version string - * # Arguments - * * output - Pointer to the output buffer. - * * output_length - Length of the output buffer. - * # Returns - * Returns the size, in bytes, of the output buffer. - * # 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. - */ -int64_t Version(uint8_t *output, - size_t output_length); - -/** - * Size of the version string - * # Returns - * Returns the size of the version string - */ -int64_t VersionSize(void);