Skip to content
Open
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
26 changes: 26 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,32 @@ assert_eq!(
);
```

### With AAD

Context (e.g., a key name) can be authenticated alongside the message
with `encrypt_with_aad`/`decrypt_with_aad`. It is not stored in the
ciphertext, so decryption must be given the same context, and fails if
it does not match:

```rust
use ecies::{decrypt_with_aad, encrypt_with_aad, utils::generate_keypair};

let (sk, pk) = generate_keypair();
#[cfg(all(not(feature = "x25519"), not(feature = "ed25519")))]
let (sk, pk) = (&sk.serialize(), &pk.serialize());
#[cfg(feature = "x25519")]
let (sk, pk) = (sk.as_bytes(), pk.as_bytes());
#[cfg(feature = "ed25519")]
let (sk, pk) = (&sk, &pk);

let encrypted = encrypt_with_aad(pk, b"msg", b"context").unwrap();
assert_eq!(
b"msg",
decrypt_with_aad(sk, &encrypted, b"context").unwrap().as_slice()
);
assert!(decrypt_with_aad(sk, &encrypted, b"other").is_err());
```

## Elliptic curve configuration

### Optional x25519/ed25519 support
Expand Down
38 changes: 35 additions & 3 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,10 @@ mod sync;

use config::{get_ephemeral_key_size, is_ephemeral_key_compressed, is_hkdf_key_compressed};
use elliptic::{decapsulate, encapsulate, generate_keypair, parse_pk, parse_sk, pk_to_vec, Error};
use symmetric::{sym_decrypt, sym_encrypt};
use symmetric::{sym_decrypt_with_aad, sym_encrypt_with_aad};

use crate::compat::Vec;
use crate::consts::EMPTY_BYTES;
pub use elliptic::{PublicKey, SecretKey};

/// Encrypt a message by a public key
Expand All @@ -36,11 +37,27 @@ pub use elliptic::{PublicKey, SecretKey};
/// * `receiver_pub` - The u8 array reference of a receiver's public key
/// * `msg` - The u8 array reference of the message to encrypt
pub fn encrypt(receiver_pub: &[u8], msg: &[u8]) -> Result<Vec<u8>, Error> {
encrypt_with_aad(receiver_pub, msg, &EMPTY_BYTES)
}

/// Encrypt a message by a public key, with additional authenticated data
/// (AAD).
///
/// The AAD is authenticated but not encrypted, and is not stored in the
/// ciphertext; the same AAD must be given to [`decrypt_with_aad`]. An
/// empty AAD produces output identical to [`encrypt`].
///
/// # Arguments
///
/// * `receiver_pub` - The u8 array reference of a receiver's public key
/// * `msg` - The u8 array reference of the message to encrypt
/// * `aad` - The u8 array reference of the additional authenticated data
pub fn encrypt_with_aad(receiver_pub: &[u8], msg: &[u8], aad: &[u8]) -> Result<Vec<u8>, Error> {
let receiver_pk = parse_pk(receiver_pub)?;
let (ephemeral_sk, ephemeral_pk) = generate_keypair();

let sym_key = encapsulate(&ephemeral_sk, &receiver_pk, is_hkdf_key_compressed())?;
let encrypted = sym_encrypt(&sym_key, msg).ok_or(Error::InvalidMessage)?;
let encrypted = sym_encrypt_with_aad(&sym_key, msg, aad).ok_or(Error::InvalidMessage)?;

let is_compressed = is_ephemeral_key_compressed();
let key_size = get_ephemeral_key_size();
Expand All @@ -61,6 +78,21 @@ pub fn encrypt(receiver_pub: &[u8], msg: &[u8]) -> Result<Vec<u8>, Error> {
/// * `receiver_sec` - The u8 array reference of a receiver's secret key
/// * `msg` - The u8 array reference of the encrypted message
pub fn decrypt(receiver_sec: &[u8], msg: &[u8]) -> Result<Vec<u8>, Error> {
decrypt_with_aad(receiver_sec, msg, &EMPTY_BYTES)
}

/// Decrypt a message by a secret key, with additional authenticated data
/// (AAD).
///
/// Authentication fails with [`Error::InvalidMessage`] if the AAD does
/// not match the one given to [`encrypt_with_aad`].
///
/// # Arguments
///
/// * `receiver_sec` - The u8 array reference of a receiver's secret key
/// * `msg` - The u8 array reference of the encrypted message
/// * `aad` - The u8 array reference of the additional authenticated data
pub fn decrypt_with_aad(receiver_sec: &[u8], msg: &[u8], aad: &[u8]) -> Result<Vec<u8>, Error> {
let receiver_sk = parse_sk(receiver_sec)?;
let key_size = get_ephemeral_key_size();

Expand All @@ -72,5 +104,5 @@ pub fn decrypt(receiver_sec: &[u8], msg: &[u8]) -> Result<Vec<u8>, Error> {
let encrypted = &msg[key_size..];

let sym_key = decapsulate(&ephemeral_pk, &receiver_sk, is_hkdf_key_compressed())?;
sym_decrypt(&sym_key, encrypted).ok_or(Error::InvalidMessage)
sym_decrypt_with_aad(&sym_key, encrypted, aad).ok_or(Error::InvalidMessage)
}
23 changes: 21 additions & 2 deletions src/symmetric/aead.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,16 @@ use crate::consts::{AEAD_TAG_LENGTH, EMPTY_BYTES, NONCE_LENGTH, NONCE_TAG_LENGTH
///
/// It's basically safe to just `unwrap` the returned `Option<Vec<u8>>`.
pub fn encrypt(key: &[u8], nonce: &[u8], msg: &[u8]) -> Option<Vec<u8>> {
encrypt_with_aad(key, nonce, msg, &EMPTY_BYTES)
}

/// Pure Rust AES-256-GCM or XChaCha20-Poly1305 encryption wrapper with
/// additional authenticated data (AAD).
///
/// The AAD is authenticated but not encrypted, and is not stored in the
/// output; the same AAD must be given to [`decrypt_with_aad`]. An empty
/// AAD is equivalent to [`encrypt`].
pub fn encrypt_with_aad(key: &[u8], nonce: &[u8], msg: &[u8], aad: &[u8]) -> Option<Vec<u8>> {
let key = GenericArray::from_slice(key);
let aead = Cipher::new(key);

Expand All @@ -32,7 +42,7 @@ pub fn encrypt(key: &[u8], nonce: &[u8], msg: &[u8]) -> Option<Vec<u8>> {
output.extend(msg);

let nonce = GenericArray::from_slice(nonce);
aead.encrypt_in_place_detached(nonce, &EMPTY_BYTES, &mut output[NONCE_TAG_LENGTH..])
aead.encrypt_in_place_detached(nonce, aad, &mut output[NONCE_TAG_LENGTH..])
.map(|tag| {
output[NONCE_LENGTH..NONCE_TAG_LENGTH].copy_from_slice(tag.as_slice());
output
Expand All @@ -42,6 +52,15 @@ pub fn encrypt(key: &[u8], nonce: &[u8], msg: &[u8]) -> Option<Vec<u8>> {

/// Pure Rust AES-256-GCM or XChaCha20-Poly1305 decryption wrapper
pub fn decrypt(key: &[u8], encrypted: &[u8]) -> Option<Vec<u8>> {
decrypt_with_aad(key, encrypted, &EMPTY_BYTES)
}

/// Pure Rust AES-256-GCM or XChaCha20-Poly1305 decryption wrapper with
/// additional authenticated data (AAD).
///
/// Authentication fails if the AAD does not match the one given to
/// [`encrypt_with_aad`].
pub fn decrypt_with_aad(key: &[u8], encrypted: &[u8], aad: &[u8]) -> Option<Vec<u8>> {
if encrypted.len() < NONCE_TAG_LENGTH {
return None;
}
Expand All @@ -54,7 +73,7 @@ pub fn decrypt(key: &[u8], encrypted: &[u8]) -> Option<Vec<u8>> {
let mut out = Vec::with_capacity(encrypted.len() - NONCE_TAG_LENGTH);
out.extend(&encrypted[NONCE_TAG_LENGTH..]);

aead.decrypt_in_place_detached(nonce, &EMPTY_BYTES, &mut out, tag)
aead.decrypt_in_place_detached(nonce, aad, &mut out, tag)
.map(|_| out)
.ok()
}
76 changes: 73 additions & 3 deletions src/symmetric/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,12 @@ use crate::consts::NONCE_LENGTH;
#[cfg(any(feature = "aes-rust", feature = "xchacha20"))]
mod aead;
#[cfg(any(feature = "aes-rust", feature = "xchacha20"))]
use aead::{decrypt, encrypt};
use aead::{decrypt, decrypt_with_aad, encrypt, encrypt_with_aad};

#[cfg(feature = "aes-openssl")]
mod openssl_aes;
#[cfg(feature = "aes-openssl")]
use openssl_aes::{decrypt, encrypt};
use openssl_aes::{decrypt, decrypt_with_aad, encrypt, encrypt_with_aad};

mod hash;

Expand All @@ -28,16 +28,36 @@ pub fn sym_encrypt(key: &[u8], msg: &[u8]) -> Option<Vec<u8>> {
encrypt(key, &nonce, msg)
}

/// Symmetric encryption wrapper with additional authenticated data (AAD).
///
/// The AAD is authenticated but not encrypted, and is not stored in the
/// output; the same AAD must be given to [`sym_decrypt_with_aad`]. An
/// empty AAD is equivalent to [`sym_encrypt`].
pub fn sym_encrypt_with_aad(key: &[u8], msg: &[u8], aad: &[u8]) -> Option<Vec<u8>> {
let mut nonce = [0u8; NONCE_LENGTH];
OsRng.fill_bytes(&mut nonce);
encrypt_with_aad(key, &nonce, msg, aad)
}

/// Symmetric decryption wrapper
pub fn sym_decrypt(key: &[u8], encrypted: &[u8]) -> Option<Vec<u8>> {
decrypt(key, encrypted)
}

/// Symmetric decryption wrapper with additional authenticated data
/// (AAD).
///
/// Authentication fails if the AAD does not match the one given to
/// [`sym_encrypt_with_aad`].
pub fn sym_decrypt_with_aad(key: &[u8], encrypted: &[u8], aad: &[u8]) -> Option<Vec<u8>> {
decrypt_with_aad(key, encrypted, aad)
}

#[cfg(test)]
mod tests {
use super::*;
use crate::{
consts::{NONCE_TAG_LENGTH, ZERO_SECRET},
consts::{EMPTY_BYTES, NONCE_TAG_LENGTH, ZERO_SECRET},
utils::tests::decode_hex,
};

Expand All @@ -60,6 +80,56 @@ mod tests {
}
}

#[test]
pub(super) fn test_aad() {
let mut key = ZERO_SECRET;
OsRng.fill_bytes(&mut key);
let msg = b"bound to this context";

let encrypted = sym_encrypt_with_aad(&key, msg, b"entry-name").unwrap();
// Correct AAD decrypts.
assert_eq!(
msg.to_vec(),
sym_decrypt_with_aad(&key, &encrypted, b"entry-name").unwrap()
);
// Wrong or missing AAD fails authentication.
assert!(sym_decrypt_with_aad(&key, &encrypted, b"other-name").is_none());
assert!(sym_decrypt(&key, &encrypted).is_none());

// Empty AAD is equivalent to the legacy wrappers.
let legacy = sym_encrypt(&key, msg).unwrap();
assert_eq!(msg.to_vec(), sym_decrypt_with_aad(&key, &legacy, &EMPTY_BYTES).unwrap());
assert_eq!(msg.to_vec(), sym_decrypt(&key, &legacy).unwrap());
}

#[test]
#[cfg(all(feature = "aes-rust", not(feature = "aes-short-nonce"), not(feature = "xchacha20")))]
pub(super) fn test_aes_aad_layout() {
// Verify the wire layout (nonce || tag || ct) and the AAD
// placement against a direct aes-gcm call.
use aes_gcm::aead::{generic_array::GenericArray, AeadInPlace};
use aes_gcm::{aes::Aes256, AesGcm, KeyInit};

type Cipher = AesGcm<Aes256, typenum::consts::U16>;

let key = [7u8; 32];
let nonce = [3u8; NONCE_LENGTH];
let aad = b"entry-name";
let msg = b"bound plaintext";

// Reference: ct and tag computed by the aes-gcm crate directly,
// bypassing our wrapper entirely.
let mut ref_ct = msg.to_vec();
let ref_tag = Cipher::new(GenericArray::from_slice(&key))
.encrypt_in_place_detached(GenericArray::from_slice(&nonce), aad, &mut ref_ct)
.unwrap();

let ours = encrypt_with_aad(&key, &nonce, msg, aad).unwrap();
assert_eq!(&ours[..NONCE_LENGTH], &nonce);
assert_eq!(&ours[NONCE_LENGTH..NONCE_TAG_LENGTH], ref_tag.as_slice());
assert_eq!(&ours[NONCE_TAG_LENGTH..], ref_ct.as_slice());
}

#[test]
#[cfg(all(not(feature = "aes-short-nonce"), not(feature = "xchacha20")))]
pub(super) fn test_aes_known_key() {
Expand Down
23 changes: 21 additions & 2 deletions src/symmetric/openssl_aes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,24 @@ use crate::Vec;

/// AES-256-GCM encryption wrapper
pub fn encrypt(key: &[u8], nonce: &[u8], msg: &[u8]) -> Option<Vec<u8>> {
encrypt_with_aad(key, nonce, msg, &EMPTY_BYTES)
}

/// AES-256-GCM encryption wrapper with additional authenticated data
/// (AAD).
///
/// The AAD is authenticated but not encrypted, and is not stored in the
/// output; the same AAD must be given to [`decrypt_with_aad`]. An empty
/// AAD is equivalent to [`encrypt`].
pub fn encrypt_with_aad(key: &[u8], nonce: &[u8], msg: &[u8], aad: &[u8]) -> Option<Vec<u8>> {
let cipher = Cipher::aes_256_gcm();

let mut output = Vec::with_capacity(NONCE_TAG_LENGTH + msg.len());
output.extend(nonce);
output.extend([0u8; AEAD_TAG_LENGTH]);

let tag = &mut output[NONCE_LENGTH..NONCE_TAG_LENGTH];
encrypt_aead(cipher, key, Some(nonce), &EMPTY_BYTES, msg, tag)
encrypt_aead(cipher, key, Some(nonce), aad, msg, tag)
.map(|encrypted| {
output.extend(encrypted);
output
Expand All @@ -22,6 +32,15 @@ pub fn encrypt(key: &[u8], nonce: &[u8], msg: &[u8]) -> Option<Vec<u8>> {

/// AES-256-GCM decryption wrapper
pub fn decrypt(key: &[u8], encrypted: &[u8]) -> Option<Vec<u8>> {
decrypt_with_aad(key, encrypted, &EMPTY_BYTES)
}

/// AES-256-GCM decryption wrapper with additional authenticated data
/// (AAD).
///
/// Authentication fails if the AAD does not match the one given to
/// [`encrypt_with_aad`].
pub fn decrypt_with_aad(key: &[u8], encrypted: &[u8], aad: &[u8]) -> Option<Vec<u8>> {
if encrypted.len() < NONCE_TAG_LENGTH {
return None;
}
Expand All @@ -31,5 +50,5 @@ pub fn decrypt(key: &[u8], encrypted: &[u8]) -> Option<Vec<u8>> {
let nonce = &encrypted[..NONCE_LENGTH];
let tag = &encrypted[NONCE_LENGTH..NONCE_TAG_LENGTH];
let encrypted = &encrypted[NONCE_TAG_LENGTH..];
decrypt_aead(cipher, key, Some(nonce), &EMPTY_BYTES, encrypted, tag).ok()
decrypt_aead(cipher, key, Some(nonce), aad, encrypted, tag).ok()
}
26 changes: 26 additions & 0 deletions tests/integration.rs
Original file line number Diff line number Diff line change
Expand Up @@ -54,3 +54,29 @@ fn is_compatible_with_python() {

assert_eq!(res.as_bytes(), MSG.as_bytes());
}

#[test]
#[cfg(all(not(target_arch = "wasm32"), not(feature = "x25519"), not(feature = "ed25519"),))]
fn test_aad() {
use ecies::utils::generate_keypair;
use ecies::{decrypt, decrypt_with_aad, encrypt, encrypt_with_aad};
let (sk, pk) = generate_keypair();
let (sk, pk) = (sk.serialize(), pk.serialize());

const MSG: &[u8] = b"hello aad";
const AAD: &[u8] = b"entry:DATABASE_PASSWORD";

// Correct AAD round-trips.
let ct = encrypt_with_aad(&pk, MSG, AAD).unwrap();
assert_eq!(decrypt_with_aad(&sk, &ct, AAD).unwrap(), MSG);

// Wrong or missing AAD fails closed.
assert!(decrypt_with_aad(&sk, &ct, b"entry:API_TOKEN").is_err());
assert!(decrypt(&sk, &ct).is_err());

// Empty AAD is equivalent to the legacy API in both directions.
let legacy = encrypt(&pk, MSG).unwrap();
assert_eq!(decrypt_with_aad(&sk, &legacy, b"").unwrap(), MSG);
assert_eq!(decrypt(&sk, &legacy).unwrap(), MSG);
assert_eq!(encrypt_with_aad(&pk, MSG, b"").unwrap().len(), legacy.len());
}
Loading