diff --git a/.github/dependabot.yml b/.github/dependabot.yml index b9414fadd..b66eeb313 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -4,8 +4,5 @@ updates: directory: "/" schedule: interval: "daily" - assignees: - - "pdugre" - - "MathieuMorrissette" # Disable version updates, we only want security updates. open-pull-requests-limit: 0 \ No newline at end of file diff --git a/README.md b/README.md index f0786f8fe..da1e91dd1 100644 --- a/README.md +++ b/README.md @@ -59,7 +59,6 @@ Unless you explicitly state otherwise, any contribution intentionally submitted This header represents : A Curve25519 private key from Devolutions Crypto - - Signature Bytes - The first two bytes specifies that the data is from Devolutions Crypto (DC) - Data type @@ -70,6 +69,7 @@ A Curve25519 private key from Devolutions Crypto - The fourth two bytes (pos: 7, 8) represents the version. ## Data Type + | Data Types | Value | Description | |---------------------|--------|------------------------------------------------------------------------------| | None | 0x00 | No data type. Only used as a default value. | @@ -82,7 +82,6 @@ A Curve25519 private key from Devolutions Crypto | OnlineCiphertext | 0x70 | A wrapped online ciphertext that can be encrypted/decrypted chunk by chunk | - ## Sub types | Key Sub Types | Value | @@ -91,6 +90,7 @@ A Curve25519 private key from Devolutions Crypto | Private | 0x10 | | Public | 0x20 | | Pair | 0x30 | +| Secret | 0x40 | | Ciphertext Sub Types | Value | |----------------------|--------| @@ -149,3 +149,35 @@ A Curve25519 private key from Devolutions Crypto | V1 | 0x10 | Uses version 1: XChaCha20-Poly1305 wrapped in a STREAM construction. | +# Local development setup + +## Rust + +Build the project with cargo. + +``` +cargo build +cargo test +``` + +## C# + +Build the rust library, then open the solution wrappers\csharp\tests\unit-tests\local\devolutions-crypto-tests\devolutions-crypto-tests.sln. + +## WebAssembly + +**Setup** + +- Install the wasm32-unknown-unknown target with `rustup target add wasm32-unknown-unknown` +- Install `wasm-pack` with `cargo install wasm-pack`. + +**Building** + +Run `wasm_build.ps1` or `wasm_build.sh` in the wrappers/wasm folder. + +Tests can then be run in the wrappers/wasm/tests folder. + +``` +npm install +npm run test +``` \ No newline at end of file diff --git a/README_RUST.md b/README_RUST.md index 1d0d4d548..3337eabd9 100644 --- a/README_RUST.md +++ b/README_RUST.md @@ -1,5 +1,4 @@ # devolutions-crypto -[![Build Status](https://dev.azure.com/devolutions-net/Open%20Source/_apis/build/status/devolutions-crypto?branchName=master)](https://dev.azure.com/devolutions-net/Open%20Source/_build/latest?definitionId=170&branchName=master) [![](https://meritbadge.herokuapp.com/devolutions-crypto)](https://crates.io/crates/devolutions-crypto) Cryptographic library used in Devolutions products. It is made to be fast, easy to use and misuse-resistant. [![crates.io](https://img.shields.io/crates/v/devolutions-crypto.svg)](https://crates.io/crates/devolutions-crypto) [Documentation](https://docs.rs/devolutions-crypto/) diff --git a/ffi/devolutions-crypto.h b/ffi/devolutions-crypto.h index af2544b6d..fec3c283a 100644 --- a/ffi/devolutions-crypto.h +++ b/ffi/devolutions-crypto.h @@ -317,6 +317,27 @@ int64_t GenerateKeyPair(uint8_t *private_, */ 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 diff --git a/ffi/src/lib.rs b/ffi/src/lib.rs index 345d83596..5d312939e 100644 --- a/ffi/src/lib.rs +++ b/ffi/src/lib.rs @@ -22,7 +22,7 @@ use devolutions_crypto::ciphertext::{ encrypt_asymmetric_with_aad, encrypt_with_aad, Ciphertext, CiphertextVersion, }; use devolutions_crypto::key::{ - generate_keypair, mix_key_exchange, KeyVersion, PrivateKey, PublicKey, + generate_keypair, generate_secret_key, mix_key_exchange, KeyVersion, PrivateKey, PublicKey, }; use devolutions_crypto::password_hash::{hash_password, PasswordHash, PasswordHashVersion}; use devolutions_crypto::secret_sharing::{ @@ -672,6 +672,43 @@ pub extern "C" fn GenerateKeyPairSize() -> i64 { 8 + 32 // header + key length } +/// 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. +#[no_mangle] +pub unsafe extern "C" fn GenerateSecretKey(result: *mut u8, result_length: usize) -> i64 { + if result.is_null() { + return Error::NullPointer.error_code(); + } + + if result_length != GenerateSecretKeySize() as usize { + return Error::InvalidOutputLength.error_code(); + } + + let result = slice::from_raw_parts_mut(result, result_length); + + let key = generate_secret_key(KeyVersion::Latest); + let key_bytes: Zeroizing> = Zeroizing::new(key.into()); + + result[0..key_bytes.len()].copy_from_slice(&key_bytes); + 0 +} + +/// Get the size of a serialized secret key. +/// # Returns +/// Returns the length of the buffer to pass as `result_length` in `GenerateSecretKey()`. +#[no_mangle] +pub extern "C" fn GenerateSecretKeySize() -> i64 { + 8 + 32 // header + key length +} + /// Get the size of the keypair used for signing. /// # Returns /// Returns the length of the keypair to input as `keypair_length` @@ -1809,3 +1846,16 @@ fn test_decode() { assert!(res > 0i64) } } + +#[test] +fn test_generate_secret_key() { + let size = GenerateSecretKeySize() as usize; + let mut key_buf = vec![0u8; size]; + + let res = unsafe { GenerateSecretKey(key_buf.as_mut_ptr(), size) }; + assert_eq!(res, 0); + + let key = devolutions_crypto::key::SecretKey::try_from(key_buf.as_slice()) + .expect("should parse as SecretKey"); + assert_eq!(key.as_bytes().len(), 32); +} diff --git a/python/devolutions_crypto.pyi b/python/devolutions_crypto.pyi index ef851d935..c0e8a8920 100644 --- a/python/devolutions_crypto.pyi +++ b/python/devolutions_crypto.pyi @@ -258,6 +258,76 @@ def generate_signing_keypair(version: int = 0) -> bytes: """ ... +def generate_secret_key(version: int = 0) -> bytes: + """ + Generate a random secret key for symmetric encryption. + + Args: + version: Key version (default: 0) + + Returns: + The serialized secret key as bytes (header + 32 raw key bytes) + + Raises: + DevolutionsCryptoException: If key generation fails or invalid version + + Example: + >>> secret_key = generate_secret_key() + >>> ciphertext = encrypt_with_secret_key(b'data', secret_key) + """ + ... + +def encrypt_with_secret_key( + data: bytes, + key: bytes, + aad: Optional[bytes] = None, + version: int = 0 +) -> bytes: + """ + Encrypt data using a SecretKey. + + Args: + data: The plaintext data to encrypt + key: The serialized SecretKey (generated by generate_secret_key) + aad: Optional Additional Authenticated Data for AEAD + version: Ciphertext version (default: 0) + + Returns: + The encrypted ciphertext as bytes + + Raises: + DevolutionsCryptoException: If encryption fails or invalid key provided + + Example: + >>> secret_key = generate_secret_key() + >>> ciphertext = encrypt_with_secret_key(b'Hello', secret_key) + """ + ... + +def decrypt_with_secret_key( + data: bytes, + key: bytes, + aad: Optional[bytes] = None +) -> bytes: + """ + Decrypt data that was encrypted with a SecretKey. + + Args: + data: The ciphertext to decrypt + key: The serialized SecretKey used for encryption + aad: Optional Additional Authenticated Data (must match encryption AAD) + + Returns: + The decrypted plaintext as bytes + + Raises: + DevolutionsCryptoException: If decryption fails, authentication fails, or invalid key + + Example: + >>> plaintext = decrypt_with_secret_key(ciphertext, secret_key) + """ + ... + def get_signing_public_key(keypair: bytes) -> bytes: """ Extract the public key from a signing keypair. @@ -342,4 +412,7 @@ __all__ = [ 'get_signing_public_key', 'sign', 'verify_signature', + 'generate_secret_key', + 'encrypt_with_secret_key', + 'decrypt_with_secret_key', ] diff --git a/python/src/lib.rs b/python/src/lib.rs index 1c99a62ce..e33ca90b9 100644 --- a/python/src/lib.rs +++ b/python/src/lib.rs @@ -10,7 +10,7 @@ use devolutions_crypto::Error; use devolutions_crypto::{ciphertext, ciphertext::Ciphertext}; use devolutions_crypto::{ key, - key::{PrivateKey, PublicKey}, + key::{PrivateKey, PublicKey, SecretKey}, }; use devolutions_crypto::{signature, signature::Signature}; use devolutions_crypto::{ @@ -242,6 +242,63 @@ fn generate_keypair(py: Python, version: u16) -> Result { Ok(keypair) } +#[pyfunction] +#[pyo3(name = "generate_secret_key")] +#[pyo3(signature = (version=0))] +fn generate_secret_key(py: Python, version: u16) -> Result> { + let version = match KeyVersion::try_from(version) { + Ok(v) => v, + Err(_) => { + let error: DevolutionsCryptoError = Error::UnknownVersion.into(); + return Err(error); + } + }; + + let key = key::generate_secret_key(version); + let bytes: Vec = key.into(); + Ok(PyBytes::new(py, &bytes).into()) +} + +#[pyfunction] +#[pyo3(name = "encrypt_with_secret_key")] +#[pyo3(signature = (data, key, aad=None, version=0))] +fn encrypt_with_secret_key( + py: Python, + data: &[u8], + key: &[u8], + aad: Option<&[u8]>, + version: u16, +) -> Result> { + let version = match CiphertextVersion::try_from(version) { + Ok(v) => v, + Err(_) => { + let error: DevolutionsCryptoError = Error::UnknownVersion.into(); + return Err(error); + } + }; + + let key = SecretKey::try_from(key)?; + let aad = aad.unwrap_or(&[]); + let ct: Vec = ciphertext::encrypt_with_secret_key_and_aad(data, &key, aad, version)?.into(); + Ok(PyBytes::new(py, &ct).into()) +} + +#[pyfunction] +#[pyo3(name = "decrypt_with_secret_key")] +#[pyo3(signature = (data, key, aad=None))] +fn decrypt_with_secret_key( + py: Python, + data: &[u8], + key: &[u8], + aad: Option<&[u8]>, +) -> Result> { + let key = SecretKey::try_from(key)?; + let aad = aad.unwrap_or(&[]); + let ct = ciphertext::Ciphertext::try_from(data)?; + let plaintext = ct.decrypt_with_secret_key_and_aad(&key, aad)?; + Ok(PyBytes::new(py, &plaintext).into()) +} + #[pyfunction] #[pyo3(name = "generate_signing_keypair")] #[pyo3(signature = (version=0))] @@ -287,6 +344,9 @@ fn devolutions_crypto_module(m: &Bound<'_, PyModule>) -> PyResult<()> { m.add_function(wrap_pyfunction!(generate_keypair, m)?)?; m.add_function(wrap_pyfunction!(generate_signing_keypair, m)?)?; m.add_function(wrap_pyfunction!(get_signing_public_key, m)?)?; + m.add_function(wrap_pyfunction!(generate_secret_key, m)?)?; + m.add_function(wrap_pyfunction!(encrypt_with_secret_key, m)?)?; + m.add_function(wrap_pyfunction!(decrypt_with_secret_key, m)?)?; m.add_class::()?; m.add( "DevolutionsCryptoException", diff --git a/src/ciphertext/mod.rs b/src/ciphertext/mod.rs index 62bcbeb53..4b8ad5a8a 100644 --- a/src/ciphertext/mod.rs +++ b/src/ciphertext/mod.rs @@ -51,7 +51,7 @@ use super::Header; use super::HeaderType; use super::Result; -use super::key::{PrivateKey, PublicKey}; +use super::key::{PrivateKey, PublicKey, SecretKey}; use ciphertext_v1::CiphertextV1; use ciphertext_v2::{CiphertextV2Asymmetric, CiphertextV2Symmetric}; @@ -87,11 +87,11 @@ enum CiphertextPayload { V2Asymmetric(CiphertextV2Asymmetric), } -/// Returns an `Ciphertext` from cleartext data and a key. +/// Returns a `Ciphertext` from cleartext data and a key. /// # Arguments /// * `data` - The data to encrypt. /// * `key` - The key to use. The recommended size is 32 bytes. -/// * `version` - Version of the library to encrypt with. Use `CiphertTextVersion::Latest` if you're not dealing with shared data. +/// * `version` - Version of the library to encrypt with. Use `CiphertextVersion::Latest` if you're not dealing with shared data. /// # Returns /// Returns a `Ciphertext` containing the encrypted data. /// # Example @@ -104,15 +104,39 @@ enum CiphertextPayload { /// let encrypted_data = encrypt(data, key, CiphertextVersion::Latest).unwrap(); /// ``` pub fn encrypt(data: &[u8], key: &[u8], version: CiphertextVersion) -> Result { + encrypt_with_raw_key(data, key, version) +} + +/// Returns a `Ciphertext` from cleartext data and a key. +/// # Arguments +/// * `data` - The data to encrypt. +/// * `key` - The key to use. The recommended size is 32 bytes. +/// * `version` - Version of the library to encrypt with. Use `CiphertextVersion::Latest` if you're not dealing with shared data. +/// # Returns +/// Returns a `Ciphertext` containing the encrypted data. +/// # Example +/// ```rust +/// use devolutions_crypto::ciphertext::{ encrypt_with_raw_key, CiphertextVersion }; +/// +/// let data = b"somesecretdata"; +/// let key = b"somesecretkey"; +/// +/// let encrypted_data = encrypt_with_raw_key(data, key, CiphertextVersion::Latest).unwrap(); +/// ``` +pub fn encrypt_with_raw_key( + data: &[u8], + key: &[u8], + version: CiphertextVersion, +) -> Result { encrypt_with_aad(data, key, [].as_slice(), version) } -/// Returns an `Ciphertext` from cleartext data and a key. +/// Returns a `Ciphertext` from cleartext data and a key. /// # Arguments /// * `data` - The data to encrypt. /// * `key` - The key to use. The recommended size is 32 bytes. /// * `aad` - Additionnal data to authenticate alongside the ciphertext. -/// * `version` - Version of the library to encrypt with. Use `CiphertTextVersion::Latest` if you're not dealing with shared data. +/// * `version` - Version of the library to encrypt with. Use `CiphertextVersion::Latest` if you're not dealing with shared data. /// # Returns /// Returns a `Ciphertext` containing the encrypted data, which also authenticates the `aad` argument. /// # Example @@ -149,12 +173,12 @@ pub fn encrypt_with_aad( Ok(Ciphertext { header, payload }) } -/// Returns an `Ciphertext` from cleartext data and a `PublicKey`. +/// Returns a `Ciphertext` from cleartext data and a `PublicKey`. /// You will need the corresponding `PrivateKey` to decrypt it. /// # Arguments /// * `data` - The data to encrypt. /// * `public_key` - The `PublicKey` to use. Use `generate_keypair` to generate a keypair. -/// * `version` - Version of the library to encrypt with. Use `CiphertTextVersion::Latest` if you're not dealing with shared data. +/// * `version` - Version of the library to encrypt with. Use `CiphertextVersion::Latest` if you're not dealing with shared data. /// # Returns /// Returns a `Ciphertext` containing the encrypted data. /// # Example @@ -175,13 +199,13 @@ pub fn encrypt_asymmetric( encrypt_asymmetric_with_aad(data, public_key, [].as_slice(), version) } -/// Returns an `Ciphertext` from cleartext data and a `PublicKey`. +/// Returns a `Ciphertext` from cleartext data and a `PublicKey`. /// You will need the corresponding `PrivateKey` to decrypt it. /// # Arguments /// * `data` - The data to encrypt. /// * `public_key` - The `PublicKey` to use. Use `generate_keypair` to generate a keypair. /// * `aad` - Additionnal data to authenticate alongside the ciphertext. -/// * `version` - Version of the library to encrypt with. Use `CiphertTextVersion::Latest` if you're not dealing with shared data. +/// * `version` - Version of the library to encrypt with. Use `CiphertextVersion::Latest` if you're not dealing with shared data. /// # Returns /// Returns a `Ciphertext` containing the encrypted data. /// # Example @@ -218,6 +242,59 @@ pub fn encrypt_asymmetric_with_aad( Ok(Ciphertext { header, payload }) } +/// Returns a `Ciphertext` from cleartext data and a `SecretKey`. +/// # Arguments +/// * `data` - The data to encrypt. +/// * `key` - The `SecretKey` to use. Generate one with `generate_secret_key`. +/// * `version` - Version of the library to encrypt with. Use `CiphertextVersion::Latest` if you're not dealing with shared data. +/// # Returns +/// Returns a `Ciphertext` containing the encrypted data. +/// # Example +/// ```rust +/// use devolutions_crypto::ciphertext::{ encrypt_with_secret_key, CiphertextVersion }; +/// use devolutions_crypto::key::{ generate_secret_key, KeyVersion }; +/// +/// let data = b"somesecretdata"; +/// let key = generate_secret_key(KeyVersion::Latest); +/// +/// let encrypted_data = encrypt_with_secret_key(data, &key, CiphertextVersion::Latest).unwrap(); +/// ``` +pub fn encrypt_with_secret_key( + data: &[u8], + key: &SecretKey, + version: CiphertextVersion, +) -> Result { + encrypt(data, key.as_bytes(), version) +} + +/// Returns a `Ciphertext` from cleartext data and a `SecretKey` with additional authenticated data. +/// # Arguments +/// * `data` - The data to encrypt. +/// * `key` - The `SecretKey` to use. Generate one with `generate_secret_key`. +/// * `aad` - Additional data to authenticate alongside the ciphertext. +/// * `version` - Version of the library to encrypt with. Use `CiphertextVersion::Latest` if you're not dealing with shared data. +/// # Returns +/// Returns a `Ciphertext` containing the encrypted data, which also authenticates the `aad` argument. +/// # Example +/// ```rust +/// use devolutions_crypto::ciphertext::{ encrypt_with_secret_key_and_aad, CiphertextVersion }; +/// use devolutions_crypto::key::{ generate_secret_key, KeyVersion }; +/// +/// let data = b"somesecretdata"; +/// let aad = b"somepublicdata"; +/// let key = generate_secret_key(KeyVersion::Latest); +/// +/// let encrypted_data = encrypt_with_secret_key_and_aad(data, &key, aad, CiphertextVersion::Latest).unwrap(); +/// ``` +pub fn encrypt_with_secret_key_and_aad( + data: &[u8], + key: &SecretKey, + aad: &[u8], + version: CiphertextVersion, +) -> Result { + encrypt_with_aad(data, key.as_bytes(), aad, version) +} + impl Ciphertext { /// Decrypt the data blob using a key. /// # Arguments @@ -237,6 +314,27 @@ impl Ciphertext { /// assert_eq!(data.to_vec(), decrypted_data); ///``` pub fn decrypt(&self, key: &[u8]) -> Result> { + self.decrypt_with_raw_key(key) + } + + /// Decrypt the data blob using a key. + /// # Arguments + /// * `key` - Key to use. The recommended size is 32 bytes. + /// # Returns + /// Returns the decrypted data. + /// # Example + /// ```rust + /// use devolutions_crypto::ciphertext::{ encrypt_with_raw_key, CiphertextVersion}; + /// + /// let data = b"somesecretdata"; + /// let key = b"somesecretkey"; + /// + /// let encrypted_data = encrypt_with_raw_key(data, key, CiphertextVersion::Latest).unwrap(); + /// let decrypted_data = encrypted_data.decrypt_with_raw_key(key).unwrap(); + /// + /// assert_eq!(data.to_vec(), decrypted_data); + ///``` + pub fn decrypt_with_raw_key(&self, key: &[u8]) -> Result> { self.decrypt_with_aad(key, [].as_slice()) } @@ -320,6 +418,52 @@ impl Ciphertext { _ => Err(Error::InvalidDataType), } } + + /// Decrypt the data blob using a `SecretKey`. + /// # Arguments + /// * `key` - The `SecretKey` used for encryption. + /// # Returns + /// Returns the decrypted data. + /// # Example + /// ```rust + /// use devolutions_crypto::ciphertext::{ encrypt_with_secret_key, CiphertextVersion }; + /// use devolutions_crypto::key::{ generate_secret_key, KeyVersion }; + /// + /// let data = b"somesecretdata"; + /// let key = generate_secret_key(KeyVersion::Latest); + /// + /// let encrypted_data = encrypt_with_secret_key(data, &key, CiphertextVersion::Latest).unwrap(); + /// let decrypted_data = encrypted_data.decrypt_with_secret_key(&key).unwrap(); + /// + /// assert_eq!(decrypted_data, data); + ///``` + pub fn decrypt_with_secret_key(&self, key: &SecretKey) -> Result> { + self.decrypt(key.as_bytes()) + } + + /// Decrypt the data blob using a `SecretKey` and additional authenticated data. + /// # Arguments + /// * `key` - The `SecretKey` used for encryption. + /// * `aad` - Additional data to authenticate alongside the ciphertext. + /// # Returns + /// Returns the decrypted data. + /// # Example + /// ```rust + /// use devolutions_crypto::ciphertext::{ encrypt_with_secret_key_and_aad, CiphertextVersion }; + /// use devolutions_crypto::key::{ generate_secret_key, KeyVersion }; + /// + /// let data = b"somesecretdata"; + /// let aad = b"somepublicdata"; + /// let key = generate_secret_key(KeyVersion::Latest); + /// + /// let encrypted_data = encrypt_with_secret_key_and_aad(data, &key, aad, CiphertextVersion::Latest).unwrap(); + /// let decrypted_data = encrypted_data.decrypt_with_secret_key_and_aad(&key, aad).unwrap(); + /// + /// assert_eq!(decrypted_data, data); + ///``` + pub fn decrypt_with_secret_key_and_aad(&self, key: &SecretKey, aad: &[u8]) -> Result> { + self.decrypt_with_aad(key.as_bytes(), aad) + } } impl From for Vec { @@ -626,3 +770,75 @@ fn asymmetric_aad_test_v2() { assert!(decrypted.is_err()); } + +#[test] +fn encrypt_decrypt_with_secret_key() { + use super::key::{generate_secret_key, KeyVersion}; + + let data = b"somesecretdata"; + let key = generate_secret_key(KeyVersion::Latest); + + let encrypted = encrypt_with_secret_key(data, &key, CiphertextVersion::Latest).unwrap(); + let decrypted = encrypted.decrypt_with_secret_key(&key).unwrap(); + + assert_eq!(decrypted, data); +} + +#[test] +fn encrypt_decrypt_with_secret_key_aad() { + use super::key::{generate_secret_key, KeyVersion}; + + let data = b"somesecretdata"; + let aad = b"somepublicdata"; + let wrong_aad = b"somewrongdata"; + let key = generate_secret_key(KeyVersion::Latest); + + let encrypted = + encrypt_with_secret_key_and_aad(data, &key, aad, CiphertextVersion::Latest).unwrap(); + let decrypted = encrypted + .decrypt_with_secret_key_and_aad(&key, aad) + .unwrap(); + + assert_eq!(decrypted, data); + + assert!(encrypted + .decrypt_with_secret_key_and_aad(&key, wrong_aad) + .is_err()); + assert!(encrypted.decrypt_with_secret_key(&key).is_err()); +} + +#[test] +fn encrypt_decrypt_with_secret_key_v1() { + use super::key::{generate_secret_key, KeyVersion}; + + let data = b"somesecretdata"; + let key = generate_secret_key(KeyVersion::Latest); + + let encrypted = encrypt_with_secret_key(data, &key, CiphertextVersion::V1).unwrap(); + + assert_eq!(encrypted.header.version, CiphertextVersion::V1); + + let encrypted_bytes: Vec = encrypted.into(); + let encrypted = Ciphertext::try_from(encrypted_bytes.as_slice()).unwrap(); + let decrypted = encrypted.decrypt_with_secret_key(&key).unwrap(); + + assert_eq!(decrypted, data); +} + +#[test] +fn encrypt_decrypt_with_secret_key_v2() { + use super::key::{generate_secret_key, KeyVersion}; + + let data = b"somesecretdata"; + let key = generate_secret_key(KeyVersion::Latest); + + let encrypted = encrypt_with_secret_key(data, &key, CiphertextVersion::V2).unwrap(); + + assert_eq!(encrypted.header.version, CiphertextVersion::V2); + + let encrypted_bytes: Vec = encrypted.into(); + let encrypted = Ciphertext::try_from(encrypted_bytes.as_slice()).unwrap(); + let decrypted = encrypted.decrypt_with_secret_key(&key).unwrap(); + + assert_eq!(decrypted, data); +} diff --git a/src/enums.rs b/src/enums.rs index fd4ceb08d..2639fc497 100644 --- a/src/enums.rs +++ b/src/enums.rs @@ -153,6 +153,7 @@ pub enum KeySubtype { Private = 1, Public = 2, Pair = 3, + Secret = 4, } #[derive(Clone, Copy, PartialEq, Eq, Zeroize, IntoPrimitive, TryFromPrimitive, Debug)] diff --git a/src/key/mod.rs b/src/key/mod.rs index da5a7c0a3..ef76e88d7 100644 --- a/src/key/mod.rs +++ b/src/key/mod.rs @@ -1,7 +1,5 @@ //! Module for dealing with wrapped keys and key exchange. //! -//! For now, this module only deal with keypairs, as the symmetric keys are not wrapped yet. -//! //! ### Generation/Derivation //! //! Using `generate_keypair` will generate a random keypair. @@ -41,6 +39,7 @@ //! ``` mod key_v1; +mod secret_key_v1; use super::DataType; use super::Error; @@ -51,6 +50,7 @@ pub use super::KeyVersion; use super::Result; use key_v1::{KeyV1Private, KeyV1Public}; +use secret_key_v1::SecretKeyV1; use std::borrow::Borrow; use std::convert::TryFrom; @@ -333,6 +333,111 @@ impl From<&PrivateKey> for x25519_dalek::StaticSecret { } } +/// A secret key for symmetric encryption. Should never be sent over an insecure channel or stored unsecurely. +#[cfg_attr(feature = "wbindgen", wasm_bindgen(inspectable))] +#[cfg_attr(feature = "fuzz", derive(Arbitrary))] +#[derive(Clone, Debug)] +pub struct SecretKey { + pub(crate) header: Header, + payload: SecretKeyPayload, +} + +impl HeaderType for SecretKey { + type Version = KeyVersion; + type Subtype = KeySubtype; + + fn data_type() -> DataType { + DataType::Key + } + + fn subtype() -> Self::Subtype { + KeySubtype::Secret + } +} + +#[derive(Clone, Debug)] +#[cfg_attr(feature = "fuzz", derive(Arbitrary))] +enum SecretKeyPayload { + V1(SecretKeyV1), +} + +impl SecretKey { + /// Returns the raw key bytes for use with symmetric encryption primitives. + pub fn as_bytes(&self) -> &[u8] { + match &self.payload { + SecretKeyPayload::V1(k) => k.as_bytes(), + } + } +} + +/// Generates a `SecretKey` for use with symmetric encryption. +/// # Arguments +/// * `version` - Version of the key scheme to use. Use `KeyVersion::Latest` if you're not dealing with shared data. +/// # Returns +/// Returns a `SecretKey` containing the raw key material. +/// # Example +/// ```rust +/// use devolutions_crypto::key::{generate_secret_key, KeyVersion}; +/// +/// let key = generate_secret_key(KeyVersion::Latest); +/// ``` +pub fn generate_secret_key(version: KeyVersion) -> SecretKey { + let mut header: Header = Header::default(); + + match version { + KeyVersion::V1 | KeyVersion::Latest => { + header.version = KeyVersion::V1; + } + } + + SecretKey { + header, + payload: SecretKeyPayload::V1(SecretKeyV1::generate()), + } +} + +impl From for Vec { + /// Serialize the structure into a `Vec`, for storage, transmission or use in another language. + fn from(data: SecretKey) -> Self { + let mut header: Self = data.header.borrow().into(); + let mut payload: Self = data.payload.into(); + header.append(&mut payload); + header + } +} + +impl TryFrom<&[u8]> for SecretKey { + type Error = Error; + + /// Parses the data. Can return an Error if the data is invalid or unrecognized. + fn try_from(data: &[u8]) -> Result { + if data.len() < Header::len() { + return Err(Error::InvalidLength); + }; + + let header = Header::try_from(&data[0..Header::len()])?; + + if header.data_subtype != KeySubtype::Secret { + return Err(Error::InvalidDataType); + } + + let payload = match header.version { + KeyVersion::V1 => SecretKeyPayload::V1(SecretKeyV1::try_from(&data[Header::len()..])?), + _ => return Err(Error::UnknownVersion), + }; + + Ok(Self { header, payload }) + } +} + +impl From for Vec { + fn from(data: SecretKeyPayload) -> Self { + match data { + SecretKeyPayload::V1(x) => x.into(), + } + } +} + #[test] fn ecdh_test() { let bob_keypair = generate_keypair(KeyVersion::Latest); @@ -344,3 +449,31 @@ fn ecdh_test() { assert_eq!(bob_shared, alice_shared); } + +#[test] +fn secret_key_generate_roundtrip() { + let key = generate_secret_key(KeyVersion::Latest); + let original_bytes = key.as_bytes().to_vec(); + + let serialized: Vec = key.into(); + let deserialized = SecretKey::try_from(serialized.as_slice()).unwrap(); + + assert_eq!(deserialized.as_bytes(), original_bytes.as_slice()); +} + +#[test] +fn secret_key_wrong_subtype_rejected() { + use std::convert::TryFrom as _; + let keypair = generate_keypair(KeyVersion::Latest); + let private_bytes: Vec = keypair.private_key.into(); + + let result = SecretKey::try_from(private_bytes.as_slice()); + assert!(matches!(result, Err(Error::InvalidDataType))); +} + +#[test] +fn secret_key_wrong_length_rejected() { + let short = [0u8; 4]; + let result = SecretKey::try_from(short.as_slice()); + assert!(matches!(result, Err(Error::InvalidLength))); +} diff --git a/src/key/secret_key_v1.rs b/src/key/secret_key_v1.rs new file mode 100644 index 000000000..508cc5a18 --- /dev/null +++ b/src/key/secret_key_v1.rs @@ -0,0 +1,66 @@ +//! SecretKey V1: 32 raw random bytes +use super::Error; +use super::Result; + +use rand_08::RngCore; +use zeroize::Zeroizing; + +use std::convert::TryFrom; + +#[cfg(feature = "fuzz")] +use arbitrary::Arbitrary; + +#[derive(Clone)] +pub struct SecretKeyV1 { + key: Zeroizing<[u8; 32]>, +} + +impl core::fmt::Debug for SecretKeyV1 { + fn fmt(&self, f: &mut core::fmt::Formatter) -> std::result::Result<(), core::fmt::Error> { + write!(f, "Secret Key") + } +} + +#[cfg(feature = "fuzz")] +impl Arbitrary for SecretKeyV1 { + fn arbitrary(u: &mut arbitrary::Unstructured<'_>) -> arbitrary::Result { + let key: [u8; 32] = Arbitrary::arbitrary(u)?; + Ok(Self { + key: Zeroizing::new(key), + }) + } +} + +impl SecretKeyV1 { + pub fn generate() -> Self { + let mut key = Zeroizing::new([0u8; 32]); + rand_08::rngs::OsRng.fill_bytes(key.as_mut()); + Self { key } + } + + pub fn as_bytes(&self) -> &[u8] { + self.key.as_ref() + } +} + +impl From for Vec { + fn from(key: SecretKeyV1) -> Self { + key.key.as_ref().to_vec() + } +} + +impl TryFrom<&[u8]> for SecretKeyV1 { + type Error = Error; + + fn try_from(data: &[u8]) -> Result { + if data.len() != 32 { + return Err(Error::InvalidLength); + } + + let mut key = [0u8; 32]; + key.copy_from_slice(data); + Ok(Self { + key: Zeroizing::new(key), + }) + } +} diff --git a/src/lib.rs b/src/lib.rs index b383807ea..9fc1dbc97 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,4 +1,3 @@ -//! [![Build Status](https://dev.azure.com/devolutions-net/Open%20Source/_apis/build/status/devolutions-crypto?branchName=master)](https://dev.azure.com/devolutions-net/Open%20Source/_build/latest?definitionId=170&branchName=master) [![](https://meritbadge.herokuapp.com/devolutions-crypto)](https://crates.io/crates/devolutions-crypto) //! Cryptographic library used in Devolutions products. It is made to be fast, easy to use and misuse-resistant. //! //! # Usage @@ -17,11 +16,11 @@ //! //! ## Overview //! -//! The library is splitted into multiple modules, which are explained below. When -//! dealing with "managed" data, that includes an header and versionning, you deal -//! with structures like `Ciphertext`, `PublicKey`, etc. +//! This library is split into multiple modules, which are explained below. When +//! dealing with "managed" data, that includes an header and versioning, you deal +//! with structures like `Ciphertext`, `SecretKey`, `PublicKey`, etc. //! -//! These all implements `TryFrom<&[u8]>` and `Into>` which are the implemented way to serialize and deserialize data. +//! These structures all implement `TryFrom<&[u8]>` and `Into>` to serialize and deserialize data. //! //! ```rust //! use std::convert::TryFrom as _; @@ -29,22 +28,15 @@ //! use devolutions_crypto::ciphertext::{ encrypt, CiphertextVersion, Ciphertext }; //! //! let key: Vec = generate_key(32).expect("generate key shouldn't fail");; -//! //! let data = b"somesecretdata"; -//! //! let encrypted_data: Ciphertext = encrypt(data, &key, CiphertextVersion::Latest).expect("encryption shouldn't fail"); //! -//! // The ciphertext can be serialized. +//! // The ciphertext can be serialized to be saved somewhere, passed to another language or over the network. //! let encrypted_data_vec: Vec = encrypted_data.into(); //! -//! // This data can be saved somewhere, passed to another language or over the network -//! // ... //! // When you receive the data as a byte array, you can deserialize it into a struct using TryFrom -//! //! let ciphertext = Ciphertext::try_from(encrypted_data_vec.as_slice()).expect("deserialization shouldn't fail"); -//! //! let decrypted_data = ciphertext.decrypt(&key).expect("The decryption shouldn't fail"); -//! //! assert_eq!(decrypted_data, data); //! ``` //! @@ -57,16 +49,13 @@ //! //! ```rust //! use devolutions_crypto::utils::generate_key; -//! use devolutions_crypto::ciphertext::{ encrypt, CiphertextVersion, Ciphertext }; -//! -//! let key: Vec = generate_key(32).expect("generate key shouldn't fail");; +//! use devolutions_crypto::ciphertext::{encrypt, CiphertextVersion, Ciphertext}; //! +//! let key: Vec = generate_key(32).expect("generate key shouldn't fail"); //! let data = b"somesecretdata"; //! //! let encrypted_data: Ciphertext = encrypt(data, &key, CiphertextVersion::Latest).expect("encryption shouldn't fail"); -//! //! let decrypted_data = encrypted_data.decrypt(&key).expect("The decryption shouldn't fail"); -//! //! assert_eq!(decrypted_data, data); //! ``` //! @@ -80,27 +69,23 @@ //! use devolutions_crypto::ciphertext::{ encrypt_asymmetric, CiphertextVersion, Ciphertext }; //! //! let keypair: KeyPair = generate_keypair(KeyVersion::Latest); -//! //! let data = b"somesecretdata"; //! //! let encrypted_data: Ciphertext = encrypt_asymmetric(data, &keypair.public_key, CiphertextVersion::Latest).expect("encryption shouldn't fail"); -//! //! let decrypted_data = encrypted_data.decrypt_asymmetric(&keypair.private_key).expect("The decryption shouldn't fail"); -//! //! assert_eq!(decrypted_data, data); //! ``` //! //! ## Key //! -//! For now, this module only deal with keypairs, as the symmetric keys are not wrapped yet. +//! This module provides secret keys and keypairs. //! //! ### Generation/Derivation //! -//! Using `generate_keypair` will generate a random keypair. +//! Use `generate_secret_key` to a generate a random symmetric key and `generate_keypair` to generate a random keypair. //! //! Asymmetric keys have two uses. They can be used to [encrypt and decrypt data](##asymmetric) and to perform a [key exchange](#key-exchange). //! -//! #### `generate_keypair` //! ```rust //! use devolutions_crypto::key::{generate_keypair, KeyVersion, KeyPair}; //! @@ -146,7 +131,7 @@ //! ``` //! //! ## SecretSharing -//! This module is used to generate a key that is splitted in multiple `Share` +//! This module is used to generate a key that is split in multiple `Share` //! and that requires a specific amount of them to regenerate the key. //! You can think of it as a "Break The Glass" scenario. You can //! generate a key using this, lock your entire data by encrypting it @@ -156,7 +141,7 @@ //! ```rust //! use devolutions_crypto::secret_sharing::{generate_shared_key, join_shares, SecretSharingVersion, Share}; //! -//! // You want a key of 32 bytes, splitted between 5 people, and I want a +//! // You want a key of 32 bytes, split between 5 people, and I want a //! // minimum of 3 of these shares to regenerate the key. //! let shares: Vec = generate_shared_key(5, 3, 32, SecretSharingVersion::Latest).expect("generation shouldn't fail with the right parameters"); //! @@ -222,12 +207,12 @@ pub mod signature; pub mod signing_key; pub mod utils; -use enums::{CiphertextSubtype, KeySubtype, PasswordHashSubtype, ShareSubtype, SignatureSubtype}; +use enums::{CiphertextSubtype, PasswordHashSubtype, ShareSubtype, SignatureSubtype}; pub use header::{Header, HeaderType}; pub use enums::{ - CiphertextVersion, DataType, KeyVersion, OnlineCiphertextVersion, PasswordHashVersion, - SecretSharingVersion, SignatureVersion, SigningKeyVersion, + CiphertextVersion, DataType, KeySubtype, KeyVersion, OnlineCiphertextVersion, + PasswordHashVersion, SecretSharingVersion, SignatureVersion, SigningKeyVersion, }; pub use argon2::Variant as Argon2Variant; diff --git a/src/secret_sharing/mod.rs b/src/secret_sharing/mod.rs index dc1b44861..ab29d0ad9 100644 --- a/src/secret_sharing/mod.rs +++ b/src/secret_sharing/mod.rs @@ -1,8 +1,8 @@ -//! Module for creating keys splitted between multiple parties. +//! Module for creating keys split between multiple parties. //! Use this for "Break The Glass" scenarios or when you want to cryptographically enforce //! approval of multiple users. //! -//! This module is used to generate a key that is splitted in multiple `Share` +//! This module is used to generate a key that is split in multiple `Share` //! and that requires a specific amount of them to regenerate the key. //! You can think of it as a "Break The Glass" scenario. You can //! generate a key using this, lock your entire data by encrypting it @@ -12,7 +12,7 @@ //! ```rust //! use devolutions_crypto::secret_sharing::{generate_shared_key, join_shares, SecretSharingVersion, Share}; //! -//! // You want a key of 32 bytes, splitted between 5 people, and I want a +//! // You want a key of 32 bytes, split between 5 people, and I want a //! // minimum of 3 of these shares to regenerate the key. //! let shares: Vec = generate_shared_key(5, 3, 32, SecretSharingVersion::Latest).expect("generation shouldn't fail with the right parameters"); //! diff --git a/src/wasm.rs b/src/wasm.rs index 49941d608..d2b1a86a5 100644 --- a/src/wasm.rs +++ b/src/wasm.rs @@ -13,7 +13,7 @@ use super::{ }; use super::{ key, - key::{KeyVersion, PrivateKey, PublicKey}, + key::{KeyVersion, PrivateKey, PublicKey, SecretKey}, }; use super::{ password_hash, @@ -219,6 +219,50 @@ pub fn generate_keypair(version: Option) -> KeyPair { key::generate_keypair(version.unwrap_or(KeyVersion::Latest)).into() } +#[wasm_bindgen] +impl SecretKey { + #[wasm_bindgen(getter)] + pub fn bytes(&self) -> Vec { + self.clone().into() + } + + #[wasm_bindgen(js_name = "fromBytes")] + pub fn from_bytes(buffer: &[u8]) -> Result { + Ok(SecretKey::try_from(buffer)?) + } +} + +#[wasm_bindgen(js_name = "generateSecretKey")] +pub fn generate_secret_key(version: Option) -> SecretKey { + key::generate_secret_key(version.unwrap_or(KeyVersion::Latest)) +} + +#[wasm_bindgen(js_name = "encryptWithSecretKey")] +pub fn encrypt_with_secret_key( + data: &[u8], + key: &SecretKey, + aad: Option>, + version: Option, +) -> Result, JsValue> { + Ok(ciphertext::encrypt_with_aad( + data, + key.as_bytes(), + &aad.unwrap_or_default(), + version.unwrap_or(CiphertextVersion::Latest), + )? + .into()) +} + +#[wasm_bindgen(js_name = "decryptWithSecretKey")] +pub fn decrypt_with_secret_key( + data: &[u8], + key: &SecretKey, + aad: Option>, +) -> Result, JsValue> { + let data_blob = Ciphertext::try_from(data)?; + Ok(data_blob.decrypt_with_aad(key.as_bytes(), &aad.unwrap_or_default())?) +} + #[wasm_bindgen(js_name = "generateSigningKeyPair")] pub fn generate_signing_keypair(version: Option) -> SigningKeyPair { signing_key::generate_signing_keypair(version.unwrap_or(SigningKeyVersion::Latest)).into() diff --git a/tests/conformity.rs b/tests/conformity.rs index 5aba9326e..3c6e4af68 100644 --- a/tests/conformity.rs +++ b/tests/conformity.rs @@ -1,7 +1,7 @@ use base64::{engine::general_purpose, Engine as _}; use devolutions_crypto::{ ciphertext::Ciphertext, - key::PrivateKey, + key::{PrivateKey, SecretKey}, password_hash::PasswordHash, utils::{derive_key_argon2, derive_key_pbkdf2}, Argon2Parameters, @@ -227,3 +227,28 @@ fn test_utils_base64() { assert_eq!(base64_encode(data), "QmFzZTY0VGVzdA=="); } + +#[test] +fn test_symmetric_decrypt_with_secret_key_v2() { + // SecretKey wrapping the known key ozJVEme4+5e/4NG3C+Rl26GQbGWAqGc0QPX8/1xvaFM= + // Header: sig=0x0D0C, DataType::Key=1, KeySubtype::Secret=4, KeyVersion::V1=1 + let secret_key = SecretKey::try_from( + general_purpose::STANDARD + .decode("DQwBAAQAAQCjMlUSZ7j7l7/g0bcL5GXboZBsZYCoZzRA9fz/XG9oUw==") + .unwrap() + .as_slice(), + ) + .unwrap(); + + let ciphertext = Ciphertext::try_from( + general_purpose::STANDARD + .decode("DQwCAAAAAgAA0iPpI4IEzcrWAQiy6tqDqLbRYduGvlMC32mVH7tpIN2CXDUu5QHF91I7pMrmjt/61pm5CeR/IcU=") + .unwrap() + .as_slice(), + ) + .unwrap(); + + let result = ciphertext.decrypt_with_secret_key(&secret_key).unwrap(); + + assert_eq!(result, b"test Ciph3rtext~2"); +} diff --git a/uniffi/devolutions-crypto-uniffi/src/ciphertext.rs b/uniffi/devolutions-crypto-uniffi/src/ciphertext.rs index eb96487ff..078aabb83 100644 --- a/uniffi/devolutions-crypto-uniffi/src/ciphertext.rs +++ b/uniffi/devolutions-crypto-uniffi/src/ciphertext.rs @@ -1,5 +1,6 @@ use crate::CiphertextVersion; use crate::Result; +use devolutions_crypto::key::SecretKey; #[uniffi::export(default(version = None))] pub fn encrypt(data: &[u8], key: &[u8], version: Option) -> Result> { @@ -69,3 +70,43 @@ fn decrypt_asymmetric_with_aad(data: &[u8], key: &[u8], aad: &[u8]) -> Result, +) -> Result> { + let version = version.unwrap_or(CiphertextVersion::Latest); + let key = SecretKey::try_from(key)?; + Ok(devolutions_crypto::ciphertext::encrypt_with_secret_key(data, &key, version)?.into()) +} + +#[uniffi::export(default(version = None))] +pub fn encrypt_with_secret_key_and_aad( + data: &[u8], + key: &[u8], + aad: &[u8], + version: Option, +) -> Result> { + let version = version.unwrap_or(CiphertextVersion::Latest); + let key = SecretKey::try_from(key)?; + Ok( + devolutions_crypto::ciphertext::encrypt_with_secret_key_and_aad(data, &key, aad, version)? + .into(), + ) +} + +#[uniffi::export] +pub fn decrypt_with_secret_key(data: &[u8], key: &[u8]) -> Result> { + let key = SecretKey::try_from(key)?; + let data = devolutions_crypto::ciphertext::Ciphertext::try_from(data)?; + data.decrypt_with_secret_key(&key) +} + +#[uniffi::export] +pub fn decrypt_with_secret_key_and_aad(data: &[u8], key: &[u8], aad: &[u8]) -> Result> { + let key = SecretKey::try_from(key)?; + let data = devolutions_crypto::ciphertext::Ciphertext::try_from(data)?; + data.decrypt_with_secret_key_and_aad(&key, aad) +} diff --git a/uniffi/devolutions-crypto-uniffi/src/key.rs b/uniffi/devolutions-crypto-uniffi/src/key.rs index cbfb43544..4ed395b89 100644 --- a/uniffi/devolutions-crypto-uniffi/src/key.rs +++ b/uniffi/devolutions-crypto-uniffi/src/key.rs @@ -22,6 +22,12 @@ pub fn generate_keypair(version: Option) -> KeyPair { devolutions_crypto::key::generate_keypair(version).into() } +#[uniffi::export(default(version = None))] +pub fn generate_secret_key(version: Option) -> Vec { + let version = version.unwrap_or(KeyVersion::Latest); + devolutions_crypto::key::generate_secret_key(version).into() +} + #[uniffi::export] pub fn mix_key_exchange(private_key: &[u8], public_key: &[u8]) -> Result> { let private_key = private_key.try_into()?; diff --git a/wrappers/csharp/src/Managed.cs b/wrappers/csharp/src/Managed.cs index adade3b2b..72bbd3040 100644 --- a/wrappers/csharp/src/Managed.cs +++ b/wrappers/csharp/src/Managed.cs @@ -541,6 +541,55 @@ public static KeyPair GenerateKeyPair() }; } + internal static byte[] GenerateSecretKeyBytes() + { + long size = Native.GenerateSecretKeySizeNative(); + byte[] result = new byte[size]; + + long res = Native.GenerateSecretKeyNative(result, (UIntPtr)result.Length); + + if (res < 0) + { + Utils.HandleError(res); + } + + return result; + } + + /// + /// Generates a random secret key for symmetric encryption. + /// + /// Returns the generated . + public static SecretKey GenerateSecretKey() + { + return new SecretKey(GenerateSecretKeyBytes()); + } + + /// + /// Encrypts data using a . + /// + /// The data to encrypt. + /// The to use for encryption. + /// Additional authenticated data. (Optional). + /// The cipher version to use. (Latest is recommended). + /// Returns the encrypted data as a byte array. + public static byte[]? Encrypt(byte[]? data, SecretKey key, byte[]? aad = null, CipherTextVersion version = CIPHERTEXT_VERSION) + { + return Encrypt(data, key.KeyMaterial, aad, version); + } + + /// + /// Decrypts data using a . + /// + /// The data to decrypt. + /// The used for encryption. + /// Additional authenticated data. (Optional). + /// Returns the decrypted data as a byte array. + public static byte[]? Decrypt(byte[]? data, SecretKey key, byte[]? aad = null) + { + return Decrypt(data, key.KeyMaterial, aad); + } + /// /// Encrypts the data. /// diff --git a/wrappers/csharp/src/Native.Core.cs b/wrappers/csharp/src/Native.Core.cs index 913ffd397..603f74117 100644 --- a/wrappers/csharp/src/Native.Core.cs +++ b/wrappers/csharp/src/Native.Core.cs @@ -73,6 +73,12 @@ public static partial class Native [DllImport(LibName, EntryPoint = "GenerateKeyPairSize", CallingConvention = CallingConvention.Cdecl)] internal static extern long GenerateKeyPairSizeNative(); + [DllImport(LibName, EntryPoint = "GenerateSecretKey", CallingConvention = CallingConvention.Cdecl)] + internal static extern long GenerateSecretKeyNative(byte[] result, UIntPtr resultLength); + + [DllImport(LibName, EntryPoint = "GenerateSecretKeySize", CallingConvention = CallingConvention.Cdecl)] + internal static extern long GenerateSecretKeySizeNative(); + [DllImport(LibName, EntryPoint = "GenerateKey", CallingConvention = CallingConvention.Cdecl)] internal static extern long GenerateKeyNative(byte[] key, UIntPtr keyLength); diff --git a/wrappers/csharp/src/SecretKey.cs b/wrappers/csharp/src/SecretKey.cs new file mode 100644 index 000000000..c9d6d0934 --- /dev/null +++ b/wrappers/csharp/src/SecretKey.cs @@ -0,0 +1,65 @@ +namespace Devolutions.Cryptography +{ + using System; + + /// + /// A secret key for symmetric encryption. Should never be sent over an insecure channel or stored unsecurely. + /// + public class SecretKey + { + /// + /// Initializes a new instance of the class. + /// + public SecretKey(byte[] payload) + { + this.Payload = payload; + } + + /// + /// Gets the raw serialized key data. + /// + internal byte[] Payload { get; } + + /// + /// Gets the raw key material, without the serialization header. This is the value used as the actual encryption key. + /// + internal byte[] KeyMaterial + { + get + { + if (this.Payload.Length < 8) + { + throw new InvalidOperationException("Invalid secret key payload: too short to contain header and key material."); + } + + byte[] result = new byte[this.Payload.Length - 8]; + Array.Copy(this.Payload, 8, result, 0, result.Length); + return result; + } + } + + /// + /// Gets the raw serialized key data as a base64 string. + /// + public string PayloadString => Convert.ToBase64String(this.Payload); + + /// + /// Deserialize a from a byte array. + /// + /// The serialized secret key bytes. + /// Returns the deserialized . + public static SecretKey FromByteArray(byte[] data) + { + return new SecretKey(data); + } + + /// + /// Serialize the to a byte array. + /// + /// Returns the raw serialized key bytes. + public byte[] ToByteArray() + { + return this.Payload; + } + } +} diff --git a/wrappers/csharp/tests/unit-tests/Conformity.cs b/wrappers/csharp/tests/unit-tests/Conformity.cs index 1fc9aafbf..3af828018 100644 --- a/wrappers/csharp/tests/unit-tests/Conformity.cs +++ b/wrappers/csharp/tests/unit-tests/Conformity.cs @@ -157,6 +157,21 @@ public void SignatureV1() Assert.IsTrue(Managed.VerifySignature(data, publicKey, signature)); Assert.IsFalse(Managed.VerifySignature(wrongData, publicKey, signature)); } + + [TestMethod] + public void DecryptWithSecretKeyV2() + { + // SecretKey wrapping the known key ozJVEme4+5e/4NG3C+Rl26GQbGWAqGc0QPX8/1xvaFM= + // Header: sig=0x0D0C, DataType::Key=1, KeySubtype::Secret=4, KeyVersion::V1=1 + SecretKey secretKey = SecretKey.FromByteArray( + Utils.Base64StringToByteArray("DQwBAAQAAQCjMlUSZ7j7l7/g0bcL5GXboZBsZYCoZzRA9fz/XG9oUw==")!); + + byte[] ciphertext = Utils.Base64StringToByteArray( + "DQwCAAAAAgAA0iPpI4IEzcrWAQiy6tqDqLbRYduGvlMC32mVH7tpIN2CXDUu5QHF91I7pMrmjt/61pm5CeR/IcU=")!; + + byte[]? result = Managed.Decrypt(ciphertext, secretKey); + Assert.AreEqual("test Ciph3rtext~2", 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 6fcbafdf0..2e875e2f8 100644 --- a/wrappers/csharp/tests/unit-tests/TestManaged.cs +++ b/wrappers/csharp/tests/unit-tests/TestManaged.cs @@ -36,7 +36,7 @@ public void Decrypt() } [TestMethod] - public void DecryptAsymmetric_Test() + public void DecryptAsymmetric() { byte[] encryptedData = Convert.FromBase64String("DQwCAAIAAgD5rUXkPQO55rzI69WSxtVTA43lDXougn6BxJ7evqf+Yq+SEGXZxpE49874fz/aEk39LTnh1yWnY2VNoAAqKVB5CWZryd6SSld8Sx8v"); byte[]? decryptedData = Managed.DecryptAsymmetric(encryptedData, TestData.AlicePrivateKey); @@ -105,9 +105,9 @@ public void Encrypt() } [TestMethod] - public void EncryptAsymmetric_Test() + public void EncryptAsymmetric() { - byte[] dataToEncrypt = Encoding.UTF8.GetBytes("test"); + byte[] dataToEncrypt = "test"u8.ToArray(); byte[]? encryptedData = Managed.EncryptAsymmetric(dataToEncrypt, TestData.AlicePublicKey); Assert.IsNotNull(encryptedData); @@ -196,6 +196,63 @@ public void GenerateKeyPair() Assert.AreEqual(Convert.ToBase64String(bobMix), Convert.ToBase64String(aliceMix)); } + [TestMethod] + public void GenerateSecretKeyObject() + { + SecretKey key = Managed.GenerateSecretKey(); + Assert.IsNotNull(key.ToByteArray()); + Assert.IsTrue(key.ToByteArray().Length > 0); + } + + [TestMethod] + public void EncryptDecryptWithSecretKey() + { + byte[] plaintext = "test secret message"u8.ToArray(); + SecretKey key = Managed.GenerateSecretKey(); + + byte[]? ciphertext = Managed.Encrypt(plaintext, key); + Assert.IsNotNull(ciphertext); + Assert.IsTrue(Utils.ValidateHeader(ciphertext, DataType.Cipher)); + + byte[]? decrypted = Managed.Decrypt(ciphertext, key); + Assert.IsNotNull(decrypted); + Assert.AreEqual("test secret message", Encoding.UTF8.GetString(decrypted)); + } + + [TestMethod] + public void EncryptDecryptWithSecretKeyAndAad() + { + byte[] plaintext = "test secret message"u8.ToArray(); + byte[] aad = "public metadata"u8.ToArray(); + byte[] wrongAad = "tampered metadata"u8.ToArray(); + SecretKey key = Managed.GenerateSecretKey(); + + byte[]? ciphertext = Managed.Encrypt(plaintext, key, aad); + Assert.IsNotNull(ciphertext); + + byte[]? decrypted = Managed.Decrypt(ciphertext, key, aad); + Assert.IsNotNull(decrypted); + Assert.AreEqual("test secret message", Encoding.UTF8.GetString(decrypted)); + + Assert.ThrowsException(() => Managed.Decrypt(ciphertext, key, wrongAad)); + } + + [TestMethod] + public void SecretKeyRoundTrip() + { + SecretKey original = Managed.GenerateSecretKey(); + byte[] serialized = original.ToByteArray(); + SecretKey restored = SecretKey.FromByteArray(serialized); + + byte[] plaintext = "round-trip test"u8.ToArray(); + + byte[]? ciphertext = Managed.Encrypt(plaintext, original); + byte[]? decrypted = Managed.Decrypt(ciphertext, restored); + + Assert.IsNotNull(decrypted); + Assert.AreEqual("round-trip test", Encoding.UTF8.GetString(decrypted)); + } + [TestMethod] public void GenerateSigningKeyPair() { @@ -213,7 +270,7 @@ public void Sign() byte[]? signature = Managed.Sign(data, keypair); Assert.IsNotNull(signature); - Assert.AreEqual(Convert.ToBase64String(signature), TestData.SignedTestingb64); + Assert.AreEqual(TestData.SignedTestingb64, Convert.ToBase64String(signature)); } [TestMethod] @@ -226,7 +283,7 @@ public void VerifySignature() bool res = Managed.VerifySignature(Encoding.UTF8.GetBytes(TestData.SignTesting), pubkey, signature); - Assert.AreEqual(res, true); + Assert.IsTrue(res); } [TestMethod] @@ -237,9 +294,9 @@ public void VerifySignature_FailBadData() SigningKeyPair keypair = SigningKeyPair.FromByteArray(Convert.FromBase64String(TestData.SigningKeyPairb64)); SigningPublicKey pubkey = SigningPublicKey.FromByteArray(Convert.FromBase64String(TestData.SigningPublicKeyb64)); - bool res = Managed.VerifySignature(Encoding.UTF8.GetBytes("bad data"), pubkey, signature); + bool res = Managed.VerifySignature("bad data"u8.ToArray(), pubkey, signature); - Assert.AreEqual(res, false); + Assert.IsFalse(res); } [TestMethod] @@ -266,7 +323,7 @@ public void VerifySignature_FailBadSignature() bool res = Managed.VerifySignature(Encoding.UTF8.GetBytes(TestData.SignTesting), pubkey, signature); - Assert.AreEqual(res, false); + Assert.IsFalse(res); } [TestMethod] diff --git a/wrappers/python/tests/symmetric.py b/wrappers/python/tests/symmetric.py index 11b8f04ad..d6595485a 100644 --- a/wrappers/python/tests/symmetric.py +++ b/wrappers/python/tests/symmetric.py @@ -27,6 +27,29 @@ def test_symmetric_with_aad(self): with self.assertRaises(devolutions_crypto.DevolutionsCryptoException): devolutions_crypto.decrypt(ciphertext, key, aad = b"Wrong AAD") + def test_symmetric_with_secret_key(self): + key = devolutions_crypto.generate_secret_key() + plaintext = b'Test plaintext' + + ciphertext = devolutions_crypto.encrypt_with_secret_key(plaintext, key) + + self.assertEqual(devolutions_crypto.decrypt_with_secret_key(ciphertext, key), plaintext) + + def test_symmetric_with_secret_key_and_aad(self): + key = devolutions_crypto.generate_secret_key() + plaintext = b'Test plaintext' + aad = b"Test AAD" + + ciphertext = devolutions_crypto.encrypt_with_secret_key(plaintext, key, aad) + + self.assertEqual(devolutions_crypto.decrypt_with_secret_key(ciphertext, key, aad), plaintext) + + with self.assertRaises(devolutions_crypto.DevolutionsCryptoException): + devolutions_crypto.decrypt_with_secret_key(ciphertext, key) + + with self.assertRaises(devolutions_crypto.DevolutionsCryptoException): + devolutions_crypto.decrypt_with_secret_key(ciphertext, key, aad = b"Wrong AAD") + if __name__ == "__main__": unittest.main() diff --git a/wrappers/wasm/demo/package-lock.json b/wrappers/wasm/demo/package-lock.json index 6204173e6..7c6bc1dab 100644 --- a/wrappers/wasm/demo/package-lock.json +++ b/wrappers/wasm/demo/package-lock.json @@ -16,7 +16,7 @@ "@angular/platform-browser": "^21.2.4", "@angular/platform-browser-dynamic": "^21.2.4", "@angular/router": "^21.2.4", - "@devolutions/devolutions-crypto-web": "0.9.2", + "@devolutions/devolutions-crypto-web": "file:../dist/web", "@fortawesome/angular-fontawesome": "^4.0.0", "@fortawesome/fontawesome-svg-core": "^7.1.0", "@fortawesome/free-solid-svg-icons": "^7.0.0", @@ -35,6 +35,11 @@ "typescript": "~5.9.0" } }, + "../dist/web": { + "name": "@devolutions/devolutions-crypto-web", + "version": "0.9.3", + "license": "MIT OR Apache-2.0" + }, "node_modules/@algolia/abtesting": { "version": "1.14.1", "resolved": "https://registry.npmjs.org/@algolia/abtesting/-/abtesting-1.14.1.tgz", @@ -2671,10 +2676,8 @@ } }, "node_modules/@devolutions/devolutions-crypto-web": { - "version": "0.9.2", - "resolved": "https://devolutions.jfrog.io/devolutions/api/npm/npm/@devolutions/devolutions-crypto-web/-/devolutions-crypto-web-0.9.2.tgz", - "integrity": "sha512-9MSex6zmx1Eoq2KT6/DQUdjnBtkfqWZ+AzQBBwiTsE+hKHdo4NTdOyhw4Dur9b5gPrbMelp8znrSz6cyh6Mo9g==", - "license": "MIT OR Apache-2.0" + "resolved": "../dist/web", + "link": true }, "node_modules/@discoveryjs/json-ext": { "version": "0.6.3", diff --git a/wrappers/wasm/demo/package.json b/wrappers/wasm/demo/package.json index a0fdaa107..1a4c3c508 100644 --- a/wrappers/wasm/demo/package.json +++ b/wrappers/wasm/demo/package.json @@ -17,7 +17,7 @@ "@angular/platform-browser": "^21.2.4", "@angular/platform-browser-dynamic": "^21.2.4", "@angular/router": "^21.2.4", - "@devolutions/devolutions-crypto-web": "0.9.2", + "@devolutions/devolutions-crypto-web": "file:../dist/web", "@fortawesome/angular-fontawesome": "^4.0.0", "@fortawesome/fontawesome-svg-core": "^7.1.0", "@fortawesome/free-solid-svg-icons": "^7.0.0", diff --git a/wrappers/wasm/demo/src/app/app.component.html b/wrappers/wasm/demo/src/app/app.component.html index 43228181e..989bfe04d 100644 --- a/wrappers/wasm/demo/src/app/app.component.html +++ b/wrappers/wasm/demo/src/app/app.component.html @@ -12,6 +12,7 @@

DevolutionsCrypto

Secret Sharing Password Hashing Asymmetric + Secret Key Encryption Utilities diff --git a/wrappers/wasm/demo/src/app/app.routes.ts b/wrappers/wasm/demo/src/app/app.routes.ts index 6df1d785a..3883d948f 100644 --- a/wrappers/wasm/demo/src/app/app.routes.ts +++ b/wrappers/wasm/demo/src/app/app.routes.ts @@ -4,6 +4,7 @@ import { SecretSharingComponent } from './secret-sharing/secret-sharing.componen import { PasswordComponent } from './password/password.component'; import { UtilitiesComponent } from './utilities/utilities.component'; import { AsymmetricComponent } from './asymmetric/asymmetric.component'; +import { SecretKeyEncryptionComponent } from './secret-key-encryption/secret-key-encryption.component'; export const routes: Routes = [ { path: '', redirectTo: '/encryption', pathMatch: 'full' }, @@ -11,5 +12,6 @@ export const routes: Routes = [ { path: 'secret-sharing', component: SecretSharingComponent }, { path: 'password', component: PasswordComponent }, { path: 'utilities', component: UtilitiesComponent }, - { path: 'asymmetric', component: AsymmetricComponent } + { path: 'asymmetric', component: AsymmetricComponent }, + { path: 'secret-key-encryption', component: SecretKeyEncryptionComponent } ]; diff --git a/wrappers/wasm/demo/src/app/secret-key-encryption/secret-key-encryption.component.html b/wrappers/wasm/demo/src/app/secret-key-encryption/secret-key-encryption.component.html new file mode 100644 index 000000000..c5e25acf5 --- /dev/null +++ b/wrappers/wasm/demo/src/app/secret-key-encryption/secret-key-encryption.component.html @@ -0,0 +1,75 @@ + +
+ + Devolutions Crypto +
+ +
+ +
+ +

Secret Key Encryption

+
+ +
+
Key Generation
+
+
+
+ +
+
+ Result + +
+
+
+
+ +
+
Encrypt
+
+
+
+ + +
+
+ + +
+
+ +
+
+ Result + +
+
+
+
+ +
+
Decrypt
+
+
+
+ + +
+
+ + +
+
+ +
+
+ Result + +
+
+
+
+ +
diff --git a/wrappers/wasm/demo/src/app/secret-key-encryption/secret-key-encryption.component.ts b/wrappers/wasm/demo/src/app/secret-key-encryption/secret-key-encryption.component.ts new file mode 100644 index 000000000..0bfa5cd8a --- /dev/null +++ b/wrappers/wasm/demo/src/app/secret-key-encryption/secret-key-encryption.component.ts @@ -0,0 +1,109 @@ +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 * as functions from '../shared/shared.component'; + +import { CiphertextVersion, KeyVersion, SecretKey } from '@devolutions/devolutions-crypto-web'; + +type EncryptionServiceInner = typeof import('../service/encryption.inner.service'); + +@Component({ + selector: 'app-secret-key-encryption', + standalone: true, + imports: [ReactiveFormsModule, FaIconComponent], + templateUrl: './secret-key-encryption.component.html', +}) +export class SecretKeyEncryptionComponent implements OnInit { + faKey = faKey; + + keyGenerationForm: FormGroup; + encryptForm: FormGroup; + decryptForm: FormGroup; + + decoder: TextDecoder; + encoder: TextEncoder; + + constructor(private encryptionService: EncryptionService) { + this.decoder = new TextDecoder(); + this.encoder = new TextEncoder(); + + this.keyGenerationForm = new FormGroup({ + generationResult: new FormControl(''), + }); + + this.encryptForm = new FormGroup({ + textToEncrypt: new FormControl(''), + secretKey: new FormControl(''), + encryptResult: new FormControl(''), + }); + + this.decryptForm = new FormGroup({ + textToDecrypt: new FormControl(''), + secretKey: new FormControl(''), + decryptResult: new FormControl(''), + }); + } + + ngOnInit() {} + + w3Open() { + functions.w3_open(); + } + + w3Close() { + functions.w3_close(); + } + + async generateSecretKey() { + const service: EncryptionServiceInner = await this.encryptionService.innerModule; + + const key: SecretKey = service.generateSecretKey(service.KeyVersion.Latest); + const keyText: string = service.base64encode(key.bytes); + + this.keyGenerationForm.setValue({ generationResult: keyText }); + } + + async encrypt() { + const service: EncryptionServiceInner = await this.encryptionService.innerModule; + + const textToEncrypt: string = this.encryptForm.value.textToEncrypt; + const secretKeyString: string = this.encryptForm.value.secretKey; + if (!textToEncrypt || !secretKeyString) { return; } + + const keyBytes: Uint8Array = service.base64decode(secretKeyString.trim()); + const key: SecretKey = service.SecretKey.fromBytes(keyBytes); + const data: Uint8Array = this.encoder.encode(textToEncrypt); + + const version: CiphertextVersion = service.CiphertextVersion.Latest; + const encrypted: Uint8Array = service.encryptWithSecretKey(data, key, version); + + this.encryptForm.setValue({ + textToEncrypt, + secretKey: secretKeyString, + encryptResult: service.base64encode(encrypted), + }); + } + + async decrypt() { + const service: EncryptionServiceInner = await this.encryptionService.innerModule; + + const textToDecrypt: string = this.decryptForm.value.textToDecrypt; + const secretKeyString: string = this.decryptForm.value.secretKey; + if (!textToDecrypt || !secretKeyString) { return; } + + const keyBytes: Uint8Array = service.base64decode(secretKeyString.trim()); + const key: SecretKey = service.SecretKey.fromBytes(keyBytes); + const ciphertext: Uint8Array = service.base64decode(textToDecrypt.trim()); + + const plaintext: Uint8Array = service.decryptWithSecretKey(ciphertext, key); + const text: string = this.decoder.decode(plaintext); + + this.decryptForm.setValue({ + textToDecrypt, + secretKey: secretKeyString, + decryptResult: text, + }); + } +} 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 b055e1643..ca39e6a17 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, Argon2Parameters, PasswordHashVersion } from '@devolutions/devolutions-crypto-web'; -export { CiphertextVersion, KeyVersion, KeyPair, PrivateKey, PublicKey, Argon2Parameters, PasswordHashVersion } from '@devolutions/devolutions-crypto-web'; +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 * as devolutionsCrypto from '@devolutions/devolutions-crypto-web'; // Initialize WASM before any functions are used @@ -61,3 +61,15 @@ export function mixKeyExchange(privateKey: PrivateKey, publicKey: PublicKey): Ui const result = devolutionsCrypto.mixKeyExchange(privateKey, publicKey); return result; } + +export function generateSecretKey(version?: KeyVersion): SecretKey { + return devolutionsCrypto.generateSecretKey(version); +} + +export function encryptWithSecretKey(data: Uint8Array, key: SecretKey, version?: CiphertextVersion): Uint8Array { + return devolutionsCrypto.encryptWithSecretKey(data, key, undefined, version); +} + +export function decryptWithSecretKey(data: Uint8Array, key: SecretKey): Uint8Array { + return devolutionsCrypto.decryptWithSecretKey(data, key); +} diff --git a/wrappers/wasm/tests/tests/symmetric.ts b/wrappers/wasm/tests/tests/symmetric.ts index 47ad7dd43..4df41900d 100644 --- a/wrappers/wasm/tests/tests/symmetric.ts +++ b/wrappers/wasm/tests/tests/symmetric.ts @@ -1,4 +1,11 @@ -import { generateKey, encrypt, decrypt } from 'devolutions-crypto' +import { + generateKey, + generateSecretKey, + encrypt, + encryptWithSecretKey, + decrypt, + decryptWithSecretKey, +} from 'devolutions-crypto' import { describe, test } from 'node:test' import assert from 'node:assert/strict' @@ -34,4 +41,34 @@ describe('encrypt/decrypt', () => { assert.throws(() => decrypt(encrypted, key)) assert.throws(() => decrypt(encrypted, key, wrongAad)) }) + + test('should be able to encrypt and decrypt with a secret key', () => { + const input: Uint8Array = encoder.encode('This is some test data') + const key = generateSecretKey() + const encrypted: Uint8Array = encryptWithSecretKey(input, key) + const decrypted: Uint8Array = decryptWithSecretKey(encrypted, key) + assert.notDeepStrictEqual(encrypted, input) + assert.deepStrictEqual(decrypted, input) + }) + + test('should be able to encrypt and decrypt with a secret key and AAD', () => { + const input: Uint8Array = encoder.encode('This is some test data') + const aad: Uint8Array = encoder.encode('This is some public data') + const key = generateSecretKey() + const encrypted: Uint8Array = encryptWithSecretKey(input, key, aad) + const decrypted: Uint8Array = decryptWithSecretKey(encrypted, key, aad) + assert.notDeepStrictEqual(encrypted, input) + assert.deepStrictEqual(decrypted, input) + }) + + test('should fail if secret key AAD is invalid', () => { + const input: Uint8Array = encoder.encode('This is some test data') + const aad: Uint8Array = encoder.encode('This is some public data') + const wrongAad: Uint8Array = encoder.encode('this is some public data') + const key = generateSecretKey() + const encrypted: Uint8Array = encryptWithSecretKey(input, key, aad) + + assert.throws(() => decryptWithSecretKey(encrypted, key)) + assert.throws(() => decryptWithSecretKey(encrypted, key, wrongAad)) + }) }) diff --git a/wrappers/wasm/wasm_build.bat b/wrappers/wasm/wasm_build.bat deleted file mode 100644 index e9f0d1789..000000000 --- a/wrappers/wasm/wasm_build.bat +++ /dev/null @@ -1,4 +0,0 @@ -wasm-pack build ../../ --out-dir ./wrappers/wasm/dist/bundler --target bundler --scope devolutions -- --features=wbindgen -wasm-pack build ../../ --out-dir ./wrappers/wasm/dist/node --target nodejs --scope devolutions -- --features=wbindgen -wasm-pack build ../../ --out-dir ./wrappers/wasm/dist/web --target web --scope devolutions -- --features=wbindgen -wasm-pack build ../../ --out-dir ./wrappers/wasm/dist/no-modules --target no-modules --scope devolutions -- --features=wbindgen diff --git a/wrappers/wasm/wasm_build.ps1 b/wrappers/wasm/wasm_build.ps1 new file mode 100644 index 000000000..464ab7f03 --- /dev/null +++ b/wrappers/wasm/wasm_build.ps1 @@ -0,0 +1,31 @@ +$ErrorActionPreference = 'Stop' + +$scriptRoot = Split-Path -Parent $MyInvocation.MyCommand.Path +$crateRoot = (Resolve-Path (Join-Path $scriptRoot '..\..')).Path +$distRoot = Join-Path $scriptRoot 'dist' + +function Invoke-WasmPackBuild { + param( + [Parameter(Mandatory = $true)] + [string]$Target, + + [Parameter(Mandatory = $true)] + [string]$OutDir + ) + + & wasm-pack build $crateRoot --out-dir $OutDir --target $Target --scope devolutions -- --features=wbindgen + + if ($LASTEXITCODE -ne 0) { + throw "wasm-pack build failed for target '$Target' with exit code $LASTEXITCODE." + } +} + +Invoke-WasmPackBuild -Target 'bundler' -OutDir (Join-Path $distRoot 'bundler') +Invoke-WasmPackBuild -Target 'nodejs' -OutDir (Join-Path $distRoot 'node') +Invoke-WasmPackBuild -Target 'web' -OutDir (Join-Path $distRoot 'web') +Invoke-WasmPackBuild -Target 'no-modules' -OutDir (Join-Path $distRoot 'no-modules') + +$webPackageJsonPath = Join-Path $distRoot 'web\package.json' +$webPackageJson = Get-Content $webPackageJsonPath -Raw | ConvertFrom-Json +$webPackageJson.name = '@devolutions/devolutions-crypto-web' +$webPackageJson | ConvertTo-Json -Depth 10 | Set-Content $webPackageJsonPath