Skip to content

Commit

Permalink
Add chacha20-poly1305-blake3-ctx cipher
Browse files Browse the repository at this point in the history
  • Loading branch information
quexten committed Dec 1, 2024
1 parent 15f1d9c commit 041769b
Show file tree
Hide file tree
Showing 5 changed files with 209 additions and 0 deletions.
53 changes: 53 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 8 additions & 0 deletions crates/bitwarden-crypto/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -27,13 +27,17 @@ argon2 = { version = ">=0.5.0, <0.6", features = [
"zeroize",
], default-features = false }
base64 = ">=0.22.1, <0.23"
blake3 = { version = "1.5.5", features = ["zeroize", "neon"] }
cbc = { version = ">=0.1.2, <0.2", features = ["alloc", "zeroize"] }
chacha20 = "0.9.1"
chacha20poly1305 = "0.10.1"
generic-array = { version = ">=0.14.7, <1.0", features = ["zeroize"] }
hkdf = ">=0.12.3, <0.13"
hmac = ">=0.12.1, <0.13"
num-bigint = ">=0.4, <0.5"
num-traits = ">=0.2.15, <0.3"
pbkdf2 = { version = ">=0.12.1, <0.13", default-features = false }
poly1305 = "0.8.0"
rand = ">=0.8.5, <0.9"
rayon = ">=1.8.1, <2.0"
rsa = ">=0.9.2, <0.10"
Expand Down Expand Up @@ -64,5 +68,9 @@ name = "zeroizing_allocator"
harness = false
required-features = ["no-memory-hardening"]

[[bench]]
name = "ciphers"
harness = false

[lints]
workspace = true
23 changes: 23 additions & 0 deletions crates/bitwarden-crypto/benches/ciphers.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
use bitwarden_crypto::chacha20::{decrypt_xchacha20_poly1305_blake3_ctx, encrypt_xchacha20_poly1305_blake3_ctx};
use criterion::{black_box, criterion_group, criterion_main, Criterion};

pub fn criterion_benchmark(c: &mut Criterion) {
let key = [0u8; 32];
let plaintext_secret_data = vec![0u8; 1024];
let plaintext_secret_data = plaintext_secret_data.as_slice();
let authenticated_data = vec![0u8; 256];
let authenticated_data = authenticated_data.as_slice();

c.bench_function("encrypt_xchacha20_poly1305_blake3_ctx", |b| {
b.iter(|| encrypt_xchacha20_poly1305_blake3_ctx(black_box(&key), black_box(plaintext_secret_data), black_box(authenticated_data)))
});

let encrypted = encrypt_xchacha20_poly1305_blake3_ctx(&key, plaintext_secret_data, authenticated_data).unwrap();

c.bench_function("encrypt_xchacha20_poly1305_blake3_ctx", |b| {
b.iter(|| decrypt_xchacha20_poly1305_blake3_ctx(black_box(&key), black_box(&encrypted)))
});
}

criterion_group!(benches, criterion_benchmark);
criterion_main!(benches);
124 changes: 124 additions & 0 deletions crates/bitwarden-crypto/src/chacha20.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
/**
* Note:
* XChaCha20Poly1305-CTX encrypts data, and authenticates associated data using XChaCha20Poly1305
* Specifically, this uses the CTX construction, proposed here: https://par.nsf.gov/servlets/purl/10391723
* using blake3 as the cryptographic hash function. This provides not only key-commitment, but full-commitment.
* In total, this scheme prevents attacks such as invisible salamanders.
*/

use crate::CryptoError;
use chacha20poly1305::AeadCore;
use chacha20poly1305::AeadInPlace;
use chacha20poly1305::KeyInit;
use chacha20poly1305::XChaCha20Poly1305;
use chacha20::XChaCha20;
use generic_array::GenericArray;
use poly1305::Poly1305;
use subtle::ConstantTimeEq;
use chacha20::cipher::{KeyIvInit, StreamCipher};

use poly1305::universal_hash::UniversalHash;

pub struct XChaCha20Poly1305CTXCiphetext {
nonce: [u8; 24],
tag: [u8; 32],
ciphertext: Vec<u8>,
authenticated_data: Vec<u8>,
}

#[allow(dead_code)]
pub fn encrypt_xchacha20_poly1305_blake3_ctx(
key: &[u8; 32],
plaintext_secret_data: &[u8],
authenticated_data: &[u8],
) -> Result<XChaCha20Poly1305CTXCiphetext, CryptoError> {
encrypt_xchacha20_poly1305_blake3_ctx_internal(rand::thread_rng(), key, plaintext_secret_data, authenticated_data)
}

#[allow(dead_code)]
fn encrypt_xchacha20_poly1305_blake3_ctx_internal(
rng: impl rand::CryptoRng + rand::RngCore,
key: &[u8; 32],
plaintext_secret_data: &[u8],
associated_data: &[u8],
) -> Result<XChaCha20Poly1305CTXCiphetext, CryptoError> {
let mut buffer = Vec::from(plaintext_secret_data);
let cipher = XChaCha20Poly1305::new(&GenericArray::from_slice(key));
let nonce = XChaCha20Poly1305::generate_nonce(rng);

let poly1305_tag = cipher.encrypt_in_place_detached(&nonce, associated_data, &mut buffer).map_err(|_| CryptoError::InvalidKey)?;

// T* = H(K, N, A, T )
let ctx_tag = blake3::hash(&[key, nonce.as_slice(), associated_data, poly1305_tag.as_slice()].concat());
let ctx_tag = ctx_tag.as_bytes();

Ok(XChaCha20Poly1305CTXCiphetext {
nonce: nonce.as_slice().try_into().unwrap(),
ciphertext: buffer,
authenticated_data: associated_data.to_vec(),
tag: *ctx_tag,
})
}

#[allow(dead_code)]
pub fn decrypt_xchacha20_poly1305_blake3_ctx(
key: &[u8; 32],
ctx: &XChaCha20Poly1305CTXCiphetext,
) -> Result<Vec<u8>, CryptoError> {
let buffer = ctx.ciphertext.clone();
let associated_data = ctx.authenticated_data.as_slice();

// First, get the original polynomial tag, since this is required to calculate the ctx_tag
let poly1305_tag = get_tag_expected_for_xchacha20_poly1305_ctx(key, &ctx.nonce, associated_data, &buffer);

let ctx_tag = blake3::hash(&[key, ctx.nonce.as_slice(), associated_data, poly1305_tag.as_slice()].concat());
let ctx_tag = ctx_tag.as_bytes();

if ctx_tag.ct_eq(&ctx.tag).into() {
// At this point the commitment is verified, so we can decrypt the data using regular XChaCha20Poly1305
let cipher = XChaCha20Poly1305::new(&GenericArray::from_slice(key));
let mut buffer = ctx.ciphertext.clone();
let nonce_array = GenericArray::from_slice(&ctx.nonce);
cipher.decrypt_in_place_detached(nonce_array, associated_data, &mut buffer, &poly1305_tag)
.map_err(|_| CryptoError::InvalidKey)?;
return Ok(buffer);
}

Err(CryptoError::InvalidKey)

Check warning on line 87 in crates/bitwarden-crypto/src/chacha20.rs

View check run for this annotation

Codecov / codecov/patch

crates/bitwarden-crypto/src/chacha20.rs#L85-L87

Added lines #L85 - L87 were not covered by tests
}

fn get_tag_expected_for_xchacha20_poly1305_ctx(key: &[u8; 32], nonce: &[u8; 24], associated_data: &[u8], buffer: &[u8]) -> chacha20poly1305::Tag {
let mut chacha20 = XChaCha20::new(GenericArray::from_slice(key), GenericArray::from_slice(nonce));
let mut mac_key = poly1305::Key::default();
chacha20.apply_keystream(&mut *mac_key);
let mut mac = Poly1305::new(GenericArray::from_slice(&*mac_key));
mac.update_padded(&associated_data);
mac.update_padded(&buffer);
authenticate_lengths(&associated_data, &buffer, &mut mac);
mac.finalize()
}

fn authenticate_lengths(associated_data: &[u8], buffer: &[u8], mac: &mut Poly1305) -> () {
let associated_data_len: u64 = associated_data.len() as u64;
let buffer_len: u64 = buffer.len() as u64;

let mut block = GenericArray::default();
block[..8].copy_from_slice(&associated_data_len.to_le_bytes());
block[8..].copy_from_slice(&buffer_len.to_le_bytes());
mac.update(&[block]);
}

mod tests {
use super::*;

#[test]
fn test_encrypt_decrypt_xchacha20_poly1305_ctx() {
let key = [0u8; 32];
let plaintext_secret_data = b"My secret data";
let authenticated_data = b"My authenticated data";

let encrypted = encrypt_xchacha20_poly1305_blake3_ctx(&key, plaintext_secret_data, authenticated_data).unwrap();
let decrypted = decrypt_xchacha20_poly1305_blake3_ctx(&key, &encrypted).unwrap();
assert_eq!(plaintext_secret_data, decrypted.as_slice());
}
}
1 change: 1 addition & 0 deletions crates/bitwarden-crypto/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ mod wordlist;
pub use wordlist::EFF_LONG_WORD_LIST;
mod allocator;
pub use allocator::ZeroizingAllocator;
pub mod chacha20;

#[cfg(feature = "uniffi")]
uniffi::setup_scaffolding!();
Expand Down

0 comments on commit 041769b

Please sign in to comment.