diff --git a/ffi/src/lib.rs b/ffi/src/lib.rs index e42d34e3..ca61f51e 100644 --- a/ffi/src/lib.rs +++ b/ffi/src/lib.rs @@ -25,7 +25,7 @@ use devolutions_crypto::derive_encrypt::{encrypt_with_password_and_aad, KdfEncry use devolutions_crypto::key::{ generate_keypair, generate_secret_key, mix_key_exchange, KeyVersion, PrivateKey, PublicKey, }; -use devolutions_crypto::key_derivation::{derive_key, Argon2, DerivationParameters, Pbkdf2}; +use devolutions_crypto::key_derivation::{Argon2, DerivationParameters, Pbkdf2}; use devolutions_crypto::password_hash::{ hash_password, hash_password_with_parameters, PasswordHash, PasswordHashVersion, }; @@ -344,6 +344,110 @@ pub unsafe extern "C" fn DeriveEncryptData( } } +/// Derive a key from a password using caller-supplied serialized [`DerivationParameters`] and encrypt data. +/// # Arguments +/// * `data` - Pointer to the plaintext data to encrypt. +/// * `data_length` - Length of plaintext data. +/// * `password` - Pointer to the password bytes. +/// * `password_length` - Length of password bytes. +/// * `params` - Pointer to serialized `DerivationParameters` bytes. +/// * `params_length` - Length of serialized `DerivationParameters` bytes. +/// * `aad` - Pointer to additional authenticated data, or null. +/// * `aad_length` - Length of additional authenticated data. +/// * `result` - Pointer to output buffer. +/// * `result_length` - Length of output buffer, must equal `DeriveEncryptDataWithParamsSize(...)`. +/// * `ciphertext_version` - Ciphertext version (0 latest, 1 AES-CBC, 2 XChaCha20). +/// # Returns +/// The number of bytes written on success, or a negative DevoCryptoError code on failure. +/// # Safety +/// This method is made to be called by C, so it is therefore unsafe. The caller should make sure it passes the right pointers and sizes. +#[no_mangle] +pub unsafe extern "C" fn DeriveEncryptDataWithParams( + data: *const u8, + data_length: usize, + password: *const u8, + password_length: usize, + params: *const u8, + params_length: usize, + aad: *const u8, + aad_length: usize, + result: *mut u8, + result_length: usize, + ciphertext_version: u16, +) -> i64 { + if data.is_null() || password.is_null() || params.is_null() || result.is_null() { + return Error::NullPointer.error_code(); + } + + if result_length + != DeriveEncryptDataWithParamsSize(data_length, params_length, ciphertext_version) as usize + { + return Error::InvalidOutputLength.error_code(); + } + + let ciphertext_version = match CiphertextVersion::try_from(ciphertext_version) { + Ok(v) => v, + Err(_) => return Error::UnknownVersion.error_code(), + }; + + let aad = if aad.is_null() { + &[] + } else { + slice::from_raw_parts(aad, aad_length) + }; + + let data = slice::from_raw_parts(data, data_length); + let password = Zeroizing::new(slice::from_raw_parts(password, password_length).to_vec()); + let params_raw = slice::from_raw_parts(params, params_length); + let result = slice::from_raw_parts_mut(result, result_length); + + let derivation_parameters = match DerivationParameters::try_from(params_raw) { + Ok(p) => p, + Err(e) => return e.error_code(), + }; + + match encrypt_with_password_and_aad( + data, + &password, + aad, + derivation_parameters, + ciphertext_version, + ) { + Ok(res) => { + let res: Vec = res.into(); + let length = res.len(); + result[0..length].copy_from_slice(&res); + length as i64 + } + Err(e) => e.error_code(), + } +} + +/// Get the size of the resulting derive_encrypt blob when using pre-built serialized [`DerivationParameters`]. +/// # Arguments +/// * `data_length` - Length of the plaintext. +/// * `params_length` - Length of the serialized `DerivationParameters`. +/// * `ciphertext_version` - Version for ciphertext (0 latest, 1 AES-CBC, 2 XChaCha20). +/// # Returns +/// Returns the exact output length expected by `DeriveEncryptDataWithParams()`. +#[no_mangle] +pub extern "C" fn DeriveEncryptDataWithParamsSize( + data_length: usize, + params_length: usize, + ciphertext_version: u16, +) -> i64 { + let ciphertext_size = EncryptSize(data_length, ciphertext_version); + + if ciphertext_size < 0 { + return ciphertext_size; + } + + // 8 = KdfEncryptedData header + // 4 = u32 derivation_parameters length prefix + // 4 = u32 ciphertext length prefix + (8 + 4 + params_length + 4 + ciphertext_size as usize) as i64 +} + /// Decrypt a derive_encrypt blob using a password. /// # Arguments /// * `data` - Pointer to derive_encrypt bytes. diff --git a/tests/conformity.rs b/tests/conformity.rs index 088f127e..e189b9b2 100644 --- a/tests/conformity.rs +++ b/tests/conformity.rs @@ -4,7 +4,7 @@ use devolutions_crypto::{ key::{PrivateKey, SecretKey}, password_hash::PasswordHash, utils::{derive_key_argon2, derive_key_pbkdf2}, - Argon2Parameters, + Argon2Parameters, KdfEncryptedData, }; use std::convert::TryFrom as _; @@ -311,6 +311,18 @@ fn test_utils_base64() { assert_eq!(base64_encode(data), "QmFzZTY0VGVzdA=="); } +#[test] +fn test_derive_decrypt_with_password_v1() { + let data = general_purpose::STANDARD + .decode("DQwJAAAAAQA2AAAAQgAAAA0MCAAAAAIAAQAAACAAAAABAAAAABAAAAIAAAACEwAAAAAQAAAAToyZHBBdwMfQ/nSt8fAG2g0MAgABAAIAOy6I4UgmX2jX+ji691rHdSKa5r4X1ItGiT6BszvL1eagyovyr/0DPMM2eIOmctQzuiQHgQ2BXrULGQ==") + .unwrap(); + + let blob = KdfEncryptedData::try_from(data.as_slice()).unwrap(); + let result = blob.decrypt_with_password(b"DevoCrypto!").unwrap(); + + assert_eq!(result, b"Derive and Encrypt"); +} + #[test] fn test_symmetric_decrypt_with_secret_key_v2() { // SecretKey wrapping the known key ozJVEme4+5e/4NG3C+Rl26GQbGWAqGc0QPX8/1xvaFM= diff --git a/wrappers/csharp/src/Enums.cs b/wrappers/csharp/src/Enums.cs index 934dd2c3..ec7b2bdd 100644 --- a/wrappers/csharp/src/Enums.cs +++ b/wrappers/csharp/src/Enums.cs @@ -46,7 +46,12 @@ public enum DataType /// /// TODO /// - OnlineCiphertext = 7 + OnlineCiphertext = 7, + + /// + /// A blob containing key derivation parameters and an encrypted ciphertext. + /// + KdfEncryptedData = 9 } /// diff --git a/wrappers/csharp/src/Managed.cs b/wrappers/csharp/src/Managed.cs index 56e17dba..148ca25a 100644 --- a/wrappers/csharp/src/Managed.cs +++ b/wrappers/csharp/src/Managed.cs @@ -1390,6 +1390,124 @@ public static byte[][] GenerateSharedKey(int nbShares, int threshold, int secret return result; } + /// + /// Encrypts data with a key derived from a password and embeds the derivation parameters in the output blob. + /// Defaults to Argon2id with default parameters and XChaCha20-Poly1305 encryption. + /// + /// The data to encrypt. + /// The password to derive the key from. + /// Additional authenticated data (optional). + /// Pre-built derivation parameters. Defaults to Argon2id when null. + /// The ciphertext version to use. Defaults to (XChaCha20-Poly1305). + /// A self-contained blob containing the derivation parameters and the ciphertext. + public static byte[] DeriveEncryptWithPassword(byte[] data, byte[] password, byte[]? aad = null, DerivationParameters? derivationParameters = null, CipherTextVersion cipherTextVersion = CipherTextVersion.Latest) + { + if (data == null || data.Length == 0 || password == null || password.Length == 0) + { + throw new DevolutionsCryptoException(ManagedError.InvalidParameter); + } + + long aadLength = aad?.Length ?? 0; + + if (derivationParameters != null) + { + byte[] paramsRaw = derivationParameters.ToByteArray(); + + long resultLength = Native.DeriveEncryptDataWithParamsSizeNative((UIntPtr)data.Length, (UIntPtr)paramsRaw.Length, (ushort)cipherTextVersion); + + if (resultLength < 0) + { + Utils.HandleError(resultLength); + } + + byte[] result = new byte[resultLength]; + long res = Native.DeriveEncryptDataWithParamsNative( + data, (UIntPtr)data.Length, + password, (UIntPtr)password.Length, + paramsRaw, (UIntPtr)paramsRaw.Length, + aad, (UIntPtr)aadLength, + result, (UIntPtr)result.Length, + (ushort)cipherTextVersion); + + if (res < 0) + { + Utils.HandleError(res); + } + + Array.Resize(ref result, (int)res); + + return result; + } + else + { + // Default: Argon2id with default parameters (key_derivation_version = 0 = Latest) + long resultLength = Native.DeriveEncryptSizeNative((UIntPtr)data.Length, 0, (ushort)cipherTextVersion); + + if (resultLength < 0) + { + Utils.HandleError(resultLength); + } + + byte[] result = new byte[resultLength]; + long res = Native.DeriveEncryptDataNative( + data, (UIntPtr)data.Length, + password, (UIntPtr)password.Length, + aad, (UIntPtr)aadLength, + result, (UIntPtr)result.Length, + 0, + (ushort)cipherTextVersion); + + if (res < 0) + { + Utils.HandleError(res); + } + + Array.Resize(ref result, (int)res); + + return result; + } + } + + /// + /// Decrypts a derive-encrypt blob created by . + /// + /// The blob to decrypt (derivation parameters + ciphertext). + /// The password that was used to encrypt the data. + /// Additional authenticated data (optional, must match the value used during encryption). + /// The decrypted plaintext bytes, or null if is null or empty. + public static byte[]? DeriveDecryptWithPassword(byte[]? data, byte[] password, byte[]? aad = null) + { + if (data == null || data.Length == 0) + { + return null; + } + + if (password == null || password.Length == 0) + { + throw new DevolutionsCryptoException(ManagedError.InvalidParameter); + } + + long aadLength = aad?.Length ?? 0; + + // Allocate a buffer the size of the ciphertext blob; the FFI will return the actual plaintext length. + byte[] result = new byte[data.Length]; + long res = Native.DeriveDecryptDataNative( + data, (UIntPtr)data.Length, + password, (UIntPtr)password.Length, + aad, (UIntPtr)aadLength, + result, (UIntPtr)result.Length); + + if (res < 0) + { + Utils.HandleError(res); + } + + // Trim to actual plaintext length. + Array.Resize(ref result, (int)res); + + return result; + } + private static bool SharesLengthAreValid(byte[][] shares) { if (shares.Length == 0) diff --git a/wrappers/csharp/src/Native.Core.cs b/wrappers/csharp/src/Native.Core.cs index 9242649f..d8a8cd38 100644 --- a/wrappers/csharp/src/Native.Core.cs +++ b/wrappers/csharp/src/Native.Core.cs @@ -228,6 +228,21 @@ public static partial class Native [DllImport(LibName, EntryPoint = "ConstantTimeEquals", CallingConvention = CallingConvention.Cdecl)] internal static extern long ConstantTimeEquals(byte[] x, UIntPtr xLength, byte[] y, UIntPtr yLength); + + [DllImport(LibName, EntryPoint = "DeriveEncryptSize", CallingConvention = CallingConvention.Cdecl)] + internal static extern long DeriveEncryptSizeNative(UIntPtr dataLength, ushort keyDerivationVersion, ushort ciphertextVersion); + + [DllImport(LibName, EntryPoint = "DeriveEncryptData", CallingConvention = CallingConvention.Cdecl)] + internal static extern long DeriveEncryptDataNative(byte[] data, UIntPtr dataLength, byte[] password, UIntPtr passwordLength, byte[]? aad, UIntPtr aadLength, byte[] result, UIntPtr resultLength, ushort keyDerivationVersion, ushort ciphertextVersion); + + [DllImport(LibName, EntryPoint = "DeriveEncryptDataWithParamsSize", CallingConvention = CallingConvention.Cdecl)] + internal static extern long DeriveEncryptDataWithParamsSizeNative(UIntPtr dataLength, UIntPtr paramsLength, ushort ciphertextVersion); + + [DllImport(LibName, EntryPoint = "DeriveEncryptDataWithParams", CallingConvention = CallingConvention.Cdecl)] + internal static extern long DeriveEncryptDataWithParamsNative(byte[] data, UIntPtr dataLength, byte[] password, UIntPtr passwordLength, byte[] derivationParams, UIntPtr derivationParamsLength, byte[]? aad, UIntPtr aadLength, byte[] result, UIntPtr resultLength, ushort ciphertextVersion); + + [DllImport(LibName, EntryPoint = "DeriveDecryptData", CallingConvention = CallingConvention.Cdecl)] + internal static extern long DeriveDecryptDataNative(byte[] data, UIntPtr dataLength, byte[] password, UIntPtr passwordLength, byte[]? aad, UIntPtr aadLength, byte[] result, UIntPtr resultLength); } } #endif \ No newline at end of file diff --git a/wrappers/csharp/tests/unit-tests/Conformity.cs b/wrappers/csharp/tests/unit-tests/Conformity.cs index 3af82801..332f4d7e 100644 --- a/wrappers/csharp/tests/unit-tests/Conformity.cs +++ b/wrappers/csharp/tests/unit-tests/Conformity.cs @@ -3,10 +3,8 @@ namespace Devolutions.Crypto.Tests { using Microsoft.VisualStudio.TestTools.UnitTesting; - using System; using System.Text; - using Devolutions.Cryptography; using Devolutions.Cryptography.Argon2; using Devolutions.Cryptography.Signature; @@ -18,7 +16,8 @@ public class Conformity public void DecryptAsymmetricV2() { byte[]? decryptResult = Managed.DecryptAsymmetric( - Utils.Base64StringToByteArray("DQwCAAIAAgCIG9L2MTiumytn7H/p5I3aGVdhV3WUL4i8nIeMWIJ1YRbNQ6lEiQDAyfYhbs6gg1cD7+5Ft2Q5cm7ArsGfiFYWnscm1y7a8tAGfjFFTonzrg=="), + Utils.Base64StringToByteArray( + "DQwCAAIAAgCIG9L2MTiumytn7H/p5I3aGVdhV3WUL4i8nIeMWIJ1YRbNQ6lEiQDAyfYhbs6gg1cD7+5Ft2Q5cm7ArsGfiFYWnscm1y7a8tAGfjFFTonzrg=="), Utils.Base64StringToByteArray("DQwBAAEAAQAAwQ3oJvU6bq2iZlJwAzvbmqJczNrFoeWPeIyJP9SSbQ==")!); Assert.IsTrue(Utils.ByteArrayToUtf8String(decryptResult) == "testdata"); @@ -28,7 +27,8 @@ public void DecryptAsymmetricV2() public void DecryptAsymmetricAadV2() { byte[]? decryptResult = Managed.DecryptAsymmetric( - Utils.Base64StringToByteArray("DQwCAAIAAgB1u62xYeyppWf83QdWwbwGUt5QuiAFZr+hIiFEvMRbXiNCE3RMBNbmgQkLr/vME0BeQa+uUTXZARvJcyNXHyAE4tSdw6o/psU/kw/Z/FbsPw=="), + Utils.Base64StringToByteArray( + "DQwCAAIAAgB1u62xYeyppWf83QdWwbwGUt5QuiAFZr+hIiFEvMRbXiNCE3RMBNbmgQkLr/vME0BeQa+uUTXZARvJcyNXHyAE4tSdw6o/psU/kw/Z/FbsPw=="), Utils.Base64StringToByteArray("DQwBAAEAAQC9qf9UY1ovL/48ALGHL9SLVpVozbdjYsw0EPerUl3zYA==")!, aad: Utils.StringToUtf8ByteArray("this is some public data")); @@ -39,7 +39,8 @@ public void DecryptAsymmetricAadV2() public void DecryptV1() { byte[] encryptedData = Utils.Base64StringToByteArray( - "DQwCAAAAAQCK1twEut+TeJfFbTWCRgHjyS6bOPOZUEQAeBtSFFRl2jHggM/34n68zIZWGbsZHkufVzU6mTN5N2Dx9bTplrycv5eNVevT4P9FdVHJ751D+A==")!; + "DQwCAAAAAQCK1twEut+TeJfFbTWCRgHjyS6bOPOZUEQAeBtSFFRl2jHggM/34n68zIZWGbsZHkufVzU6mTN5N2Dx9bTplrycv5eNVevT4P9FdVHJ751D+A==") + !; byte[] encryptKey = Utils.Base64StringToByteArray("ozJVEme4+5e/4NG3C+Rl26GQbGWAqGc0QPX8/1xvaFM=")!; byte[]? decryptResult = Managed.Decrypt(encryptedData, encryptKey); @@ -51,7 +52,8 @@ public void DecryptV1() public void DecryptAadV1() { byte[] encryptedData = Utils.Base64StringToByteArray( - "DQwCAAEAAQCeKfbTqYjfVCEPEiAJjiypBstPmZz0AnpliZKoR+WXTKdj2f/4ops0++dDBVZ+XdyE1KfqxViWVc9djy/HSCcPR4nDehtNI69heGCIFudXfQ==")!; + "DQwCAAEAAQCeKfbTqYjfVCEPEiAJjiypBstPmZz0AnpliZKoR+WXTKdj2f/4ops0++dDBVZ+XdyE1KfqxViWVc9djy/HSCcPR4nDehtNI69heGCIFudXfQ==") + !; byte[] encryptKey = Utils.Base64StringToByteArray("ozJVEme4+5e/4NG3C+Rl26GQbGWAqGc0QPX8/1xvaFM=")!; byte[] aad = Utils.StringToUtf8ByteArray("this is some public data"); @@ -63,7 +65,9 @@ public void DecryptAadV1() [TestMethod] public void DecryptV2() { - byte[] encryptedData = Utils.Base64StringToByteArray("DQwCAAAAAgAA0iPpI4IEzcrWAQiy6tqDqLbRYduGvlMC32mVH7tpIN2CXDUu5QHF91I7pMrmjt/61pm5CeR/IcU=")!; + byte[] encryptedData = + Utils.Base64StringToByteArray( + "DQwCAAAAAgAA0iPpI4IEzcrWAQiy6tqDqLbRYduGvlMC32mVH7tpIN2CXDUu5QHF91I7pMrmjt/61pm5CeR/IcU=")!; byte[] encryptKey = Utils.Base64StringToByteArray("ozJVEme4+5e/4NG3C+Rl26GQbGWAqGc0QPX8/1xvaFM=")!; byte[]? decryptResult = Managed.Decrypt(encryptedData, encryptKey); @@ -87,7 +91,8 @@ public void DecryptAadV2() [TestMethod] public void DeriveKeyArgon2_Default() { - Argon2Parameters? parameters = Argon2Parameters.FromByteArray(Utils.Base64StringToByteArray("AQAAACAAAAABAAAAIAAAAAEAAAACEwAAAAAQAAAAimFBkm3f8+f+YfLRnF5OoQ==")!); + Argon2Parameters? parameters = Argon2Parameters.FromByteArray( + Utils.Base64StringToByteArray("AQAAACAAAAABAAAAIAAAAAEAAAACEwAAAAAQAAAAimFBkm3f8+f+YfLRnF5OoQ==")!); Assert.IsNotNull(parameters); byte[] password = Utils.StringToUtf8ByteArray("password"); byte[] derivedPassword = Managed.DeriveKeyArgon2(password, parameters); @@ -129,7 +134,9 @@ public void VerifyPasswordV1_Default() { bool result = Managed.VerifyPassword( Utils.StringToUtf8ByteArray("password1"), - Utils.DecodeFromBase64("DQwDAAAAAQAQJwAAXCzLFoyeZhFSDYBAPiIWhCk04aoP/lalOoCl7D+skIY/i+3WT7dn6L8WvnfEq6flCd7i+IcKb3GEK4rCpzhDlw==")!); + Utils.DecodeFromBase64( + "DQwDAAAAAQAQJwAAXCzLFoyeZhFSDYBAPiIWhCk04aoP/lalOoCl7D+skIY/i+3WT7dn6L8WvnfEq6flCd7i+IcKb3GEK4rCpzhDlw==") + !); Assert.IsTrue(result); } @@ -139,7 +146,9 @@ public void VerifyPasswordV1_Iterations() { bool result = Managed.VerifyPassword( Utils.StringToUtf8ByteArray("password1"), - Utils.DecodeFromBase64("DQwDAAAAAQAKAAAAmH1BBckBJYDD0xfiwkAk1xwKgw8a57YQT0Igm+Faa9LFamTeEJgqn/qHc2R/8XEyK2iLPkVy+IErdGLLtLKJ2g==")!); + Utils.DecodeFromBase64( + "DQwDAAAAAQAKAAAAmH1BBckBJYDD0xfiwkAk1xwKgw8a57YQT0Igm+Faa9LFamTeEJgqn/qHc2R/8XEyK2iLPkVy+IErdGLLtLKJ2g==") + !); Assert.IsTrue(result); } @@ -150,9 +159,12 @@ public void SignatureV1() byte[] data = Encoding.UTF8.GetBytes("this is a test"); byte[] wrongData = Encoding.UTF8.GetBytes("this is wrong"); - SigningPublicKey publicKey = SigningPublicKey.FromByteArray(Convert.FromBase64String("DQwFAAIAAQDeEvwlEigK5AXoTorhmlKP6+mbiUU2rYrVQ25JQ5xang==")); + SigningPublicKey publicKey = + SigningPublicKey.FromByteArray( + Convert.FromBase64String("DQwFAAIAAQDeEvwlEigK5AXoTorhmlKP6+mbiUU2rYrVQ25JQ5xang==")); - byte[] signature = Convert.FromBase64String("DQwGAAAAAQD82uRk4sFC8vEni6pDNw/vOdN1IEDg9cAVfprWJZ/JBls9Gi61cUt5u6uBJtseNGZFT7qKLvp4NUZrAOL8FH0K"); + byte[] signature = Convert.FromBase64String( + "DQwGAAAAAQD82uRk4sFC8vEni6pDNw/vOdN1IEDg9cAVfprWJZ/JBls9Gi61cUt5u6uBJtseNGZFT7qKLvp4NUZrAOL8FH0K"); Assert.IsTrue(Managed.VerifySignature(data, publicKey, signature)); Assert.IsFalse(Managed.VerifySignature(wrongData, publicKey, signature)); @@ -172,6 +184,17 @@ public void DecryptWithSecretKeyV2() byte[]? result = Managed.Decrypt(ciphertext, secretKey); Assert.AreEqual("test Ciph3rtext~2", Utils.ByteArrayToUtf8String(result)); } + + [TestMethod] + public void DeriveDecryptWithPassword_V1() + { + byte[] data = Utils.Base64StringToByteArray( + "DQwJAAAAAQA2AAAAQgAAAA0MCAAAAAIAAQAAACAAAAABAAAAABAAAAIAAAACEwAAAAAQAAAAToyZHBBdwMfQ/nSt8fAG2g0MAgABAAIAOy6I4UgmX2jX+ji691rHdSKa5r4X1ItGiT6BszvL1eagyovyr/0DPMM2eIOmctQzuiQHgQ2BXrULGQ==") + !; + byte[] password = Utils.StringToUtf8ByteArray("DevoCrypto!"); + byte[]? result = Managed.DeriveDecryptWithPassword(data, password); + Assert.AreEqual("Derive and Encrypt", Utils.ByteArrayToUtf8String(result)); + } } } #pragma warning restore SA1600 // Elements should be documented \ No newline at end of file diff --git a/wrappers/csharp/tests/unit-tests/TestManaged.cs b/wrappers/csharp/tests/unit-tests/TestManaged.cs index 5496d95c..1e7b8e58 100644 --- a/wrappers/csharp/tests/unit-tests/TestManaged.cs +++ b/wrappers/csharp/tests/unit-tests/TestManaged.cs @@ -506,6 +506,90 @@ public void DeriveSecretKeyPbkdf2WithSalt_FixedSalt_ProducesSameKey() CollectionAssert.AreEqual(result1.Parameters.ToByteArray(), result2.Parameters.ToByteArray()); } + [TestMethod] + public void DeriveEncryptDecryptWithPassword_RoundTrip() + { + byte[] data = "Hello, derive-encrypt!"u8.ToArray(); + byte[] password = "s3cr3tPa$$w0rd"u8.ToArray(); + + byte[] blob = Managed.DeriveEncryptWithPassword(data, password); + Assert.IsNotNull(blob); + Assert.IsTrue(Utils.ValidateHeader(blob, DataType.KdfEncryptedData)); + + byte[]? decrypted = Managed.DeriveDecryptWithPassword(blob, password); + Assert.IsNotNull(decrypted); + CollectionAssert.AreEqual(data, decrypted); + } + + [TestMethod] + public void DeriveEncryptDecryptWithPassword_WithAad_RoundTrip() + { + byte[] data = "sensitive payload"u8.ToArray(); + byte[] password = "pa$$word"u8.ToArray(); + byte[] aad = "public context"u8.ToArray(); + byte[] wrongAad = "tampered context"u8.ToArray(); + + byte[] blob = Managed.DeriveEncryptWithPassword(data, password, aad); + Assert.IsNotNull(blob); + + byte[]? decrypted = Managed.DeriveDecryptWithPassword(blob, password, aad); + Assert.IsNotNull(decrypted); + CollectionAssert.AreEqual(data, decrypted); + + Assert.ThrowsException(() => Managed.DeriveDecryptWithPassword(blob, password, wrongAad)); + } + + [TestMethod] + public void DeriveEncryptDecryptWithPassword_WrongPassword_Throws() + { + byte[] data = "secret"u8.ToArray(); + byte[] password = "correct-password"u8.ToArray(); + byte[] wrongPassword = "wrong-password"u8.ToArray(); + + byte[] blob = Managed.DeriveEncryptWithPassword(data, password); + + Assert.ThrowsException(() => Managed.DeriveDecryptWithPassword(blob, wrongPassword)); + } + + [TestMethod] + public void DeriveEncryptDecryptWithPassword_AesVersion_RoundTrip() + { + byte[] data = "AES-CBC encrypt test"u8.ToArray(); + byte[] password = "aes-password"u8.ToArray(); + + byte[] blob = Managed.DeriveEncryptWithPassword(data, password, cipherTextVersion: CipherTextVersion.V1); + Assert.IsNotNull(blob); + + byte[]? decrypted = Managed.DeriveDecryptWithPassword(blob, password); + Assert.IsNotNull(decrypted); + CollectionAssert.AreEqual(data, decrypted); + } + + [TestMethod] + public void DeriveEncryptDecryptWithPassword_WithDerivationParameters_RoundTrip() + { + byte[] data = "using pre-built params"u8.ToArray(); + byte[] password = "params-password"u8.ToArray(); + + // Generate derivation parameters first, then reuse them + KeyDerivationResult derivResult = Managed.DeriveSecretKeyArgon2(password, Managed.GetDefaultArgon2Parameters()); + DerivationParameters derivParams = derivResult.Parameters; + + byte[] blob = Managed.DeriveEncryptWithPassword(data, password, derivationParameters: derivParams); + Assert.IsNotNull(blob); + + byte[]? decrypted = Managed.DeriveDecryptWithPassword(blob, password); + Assert.IsNotNull(decrypted); + CollectionAssert.AreEqual(data, decrypted); + } + + [TestMethod] + public void DeriveDecryptWithPassword_NullData_ReturnsNull() + { + byte[]? result = Managed.DeriveDecryptWithPassword(null, "password"u8.ToArray()); + Assert.IsNull(result); + } + private static byte[][] GetSharesKeys() { const int nbShares = 3; diff --git a/wrappers/kotlin/lib/src/test/kotlin/org/devolutions/crypto/ConformityTest.kt b/wrappers/kotlin/lib/src/test/kotlin/org/devolutions/crypto/ConformityTest.kt index a8759c28..a3de64e9 100644 --- a/wrappers/kotlin/lib/src/test/kotlin/org/devolutions/crypto/ConformityTest.kt +++ b/wrappers/kotlin/lib/src/test/kotlin/org/devolutions/crypto/ConformityTest.kt @@ -127,4 +127,13 @@ class ConformityTest { assert(verifySignature("this is a test".toByteArray(), publicKey, signature)) assertFalse(verifySignature("this is wrong".toByteArray(), publicKey, signature)) } + + @Test + fun deriveDecryptWithPasswordV1Test() { + val data = base64Decode("DQwJAAAAAQA2AAAAQgAAAA0MCAAAAAIAAQAAACAAAAABAAAAABAAAAIAAAACEwAAAAAQAAAAToyZHBBdwMfQ/nSt8fAG2g0MAgABAAIAOy6I4UgmX2jX+ji691rHdSKa5r4X1ItGiT6BszvL1eagyovyr/0DPMM2eIOmctQzuiQHgQ2BXrULGQ==") + val password = "DevoCrypto!".toByteArray() + val result = deriveDecryptWithPassword(data, password) + + assertContentEquals("Derive and Encrypt".toByteArray(), result) + } } \ No newline at end of file diff --git a/wrappers/python/tests/conformity.py b/wrappers/python/tests/conformity.py index 6111e9ab..4807c21a 100644 --- a/wrappers/python/tests/conformity.py +++ b/wrappers/python/tests/conformity.py @@ -53,5 +53,10 @@ def test_signature(self): self.assertTrue(devolutions_crypto.verify_signature(b"this is a test", public_key, signature)) self.assertFalse(devolutions_crypto.verify_signature(b"this is wrong", public_key, signature)) + def test_derive_decrypt_with_password_v1(self): + data = b64decode(b'DQwJAAAAAQA2AAAAQgAAAA0MCAAAAAIAAQAAACAAAAABAAAAABAAAAIAAAACEwAAAAAQAAAAToyZHBBdwMfQ/nSt8fAG2g0MAgABAAIAOy6I4UgmX2jX+ji691rHdSKa5r4X1ItGiT6BszvL1eagyovyr/0DPMM2eIOmctQzuiQHgQ2BXrULGQ==') + password = b'DevoCrypto!' + self.assertEqual(devolutions_crypto.derive_decrypt_with_password(data, password), b'Derive and Encrypt') + if __name__ == "__main__": unittest.main() diff --git a/wrappers/swift/DevolutionsCryptoSwift/Tests/DevolutionsCryptoSwiftTests/ConformityTests.swift b/wrappers/swift/DevolutionsCryptoSwift/Tests/DevolutionsCryptoSwiftTests/ConformityTests.swift index 0e64fa3d..7297b313 100644 --- a/wrappers/swift/DevolutionsCryptoSwift/Tests/DevolutionsCryptoSwiftTests/ConformityTests.swift +++ b/wrappers/swift/DevolutionsCryptoSwift/Tests/DevolutionsCryptoSwiftTests/ConformityTests.swift @@ -127,4 +127,16 @@ class ConformityTests: XCTestCase { try verifySignature( data: Data("this is wrong".utf8), publicKey: publicKey, signature: signature)) } + + func testDeriveDecryptWithPasswordV1() throws { + let data = try base64Decode( + data: + "DQwJAAAAAQA2AAAAQgAAAA0MCAAAAAIAAQAAACAAAAABAAAAABAAAAIAAAACEwAAAAAQAAAAToyZHBBdwMfQ/nSt8fAG2g0MAgABAAIAOy6I4UgmX2jX+ji691rHdSKa5r4X1ItGiT6BszvL1eagyovyr/0DPMM2eIOmctQzuiQHgQ2BXrULGQ==" + ) + let password = Data("DevoCrypto!".utf8) + let result = try deriveDecryptWithPassword(data: data, password: password) + let expected = Data("Derive and Encrypt".utf8) + + XCTAssertEqual(result, expected) + } } diff --git a/wrappers/wasm/tests/tests/conformity.ts b/wrappers/wasm/tests/tests/conformity.ts index aaf65f99..c5aa0c02 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, deriveSecretKeyPbkdf2, deriveSecretKeyArgon2, KeyDerivationResult, DerivationParameters + KeyPair, deriveKeyPbkdf2, base64encode, base64decode, decrypt, Argon2Parameters, PrivateKey, SigningPublicKey, decryptAsymmetric, verifyPassword, verifySignature, deriveKeyArgon2, deriveSecretKeyPbkdf2, deriveSecretKeyArgon2, KeyDerivationResult, DerivationParameters, deriveDecryptWithPassword } from 'devolutions-crypto' import { describe, test } from 'node:test' import assert from 'node:assert/strict' @@ -113,4 +113,11 @@ describe('Conformity Tests', () => { assert.strictEqual(base64encode(result.secretKey.bytes), 'DQwBAAQAAQCoRRraZaLaR3nJn0E+1ZYBcM3DBCwINJpWAuA2tcvr6w==') }) + + test('DeriveDecryptWithPassword V1', () => { + const data: Uint8Array = base64decode('DQwJAAAAAQA2AAAAQgAAAA0MCAAAAAIAAQAAACAAAAABAAAAABAAAAIAAAACEwAAAAAQAAAAToyZHBBdwMfQ/nSt8fAG2g0MAgABAAIAOy6I4UgmX2jX+ji691rHdSKa5r4X1ItGiT6BszvL1eagyovyr/0DPMM2eIOmctQzuiQHgQ2BXrULGQ==') + const result: Uint8Array = deriveDecryptWithPassword(data, encoder.encode('DevoCrypto!')) + + assert.strictEqual(decoder.decode(result), 'Derive and Encrypt') + }) })