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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 0 additions & 3 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
36 changes: 34 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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. |
Expand All @@ -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 |
Expand All @@ -91,6 +90,7 @@ A Curve25519 private key from Devolutions Crypto
| Private | 0x10 |
| Public | 0x20 |
| Pair | 0x30 |
| Secret | 0x40 |

| Ciphertext Sub Types | Value |
|----------------------|--------|
Expand Down Expand Up @@ -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
```
1 change: 0 additions & 1 deletion README_RUST.md
Original file line number Diff line number Diff line change
@@ -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/)
Expand Down
21 changes: 21 additions & 0 deletions ffi/devolutions-crypto.h
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
52 changes: 51 additions & 1 deletion ffi/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::{
Expand Down Expand Up @@ -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<Vec<u8>> = 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`
Expand Down Expand Up @@ -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);
}
73 changes: 73 additions & 0 deletions python/devolutions_crypto.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -342,4 +412,7 @@ __all__ = [
'get_signing_public_key',
'sign',
'verify_signature',
'generate_secret_key',
'encrypt_with_secret_key',
'decrypt_with_secret_key',
]
62 changes: 61 additions & 1 deletion python/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::{
Expand Down Expand Up @@ -242,6 +242,63 @@ fn generate_keypair(py: Python, version: u16) -> Result<Keypair> {
Ok(keypair)
}

#[pyfunction]
#[pyo3(name = "generate_secret_key")]
#[pyo3(signature = (version=0))]
fn generate_secret_key(py: Python, version: u16) -> Result<Py<PyBytes>> {
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<u8> = 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<Py<PyBytes>> {
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<u8> = 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<Py<PyBytes>> {
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))]
Expand Down Expand Up @@ -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::<Keypair>()?;
m.add(
"DevolutionsCryptoException",
Expand Down
Loading
Loading