Skip to content

Commit ae3c08d

Browse files
authored
[PM-15096] Implement xchacha20-poly1305 cipher functions (#150)
## 🎟️ Tracking https://bitwarden.atlassian.net/browse/PM-15096 ## 📔 Objective Implements xchacha20-poly1305 (https://libsodium.gitbook.io/doc/secret-key_cryptography/aead/chacha20-poly1305/xchacha20-poly1305_construction) as a new encryption type. Further, this introduces a dependency patch override (to https://github.com/bitwarden/rustcrypto-formats.git at revision 2b27c63034217dd126bbf5ed874da51b84f8c705) for rustcrypto formats, until this gets updated upstream to fix a type incompatibility when building on WASM and with chacha as a dependency. These functions are unused until a follow-up PR (soon). ## ⏰ Reminders before review - Contributor guidelines followed - All formatters and local linters executed and passed - Written new unit and / or integration tests where applicable - Protected functional changes with optionality (feature flags) - Used internationalization (i18n) for all UI strings - CI builds passed - Communicated to DevOps any deployment requirements - Updated any necessary documentation (Confluence, contributing docs) or informed the documentation team ## 🦮 Reviewer guidelines <!-- Suggested interactions but feel free to use (or not) as you desire! --> - 👍 (`:+1:`) or similar for great changes - 📝 (`:memo:`) or ℹ️ (`:information_source:`) for notes or general info - ❓ (`:question:`) for questions - 🤔 (`:thinking:`) or 💭 (`:thought_balloon:`) for more open inquiry that's not quite a confirmed issue and could potentially benefit from discussion - 🎨 (`:art:`) for suggestions / improvements - ❌ (`:x:`) or ⚠️ (`:warning:`) for more significant problems or concerns needing attention - 🌱 (`:seedling:`) or ♻️ (`:recycle:`) for future improvements or indications of technical debt - ⛏ (`:pick:`) for minor or nitpick changes
1 parent aa9ed2a commit ae3c08d

File tree

5 files changed

+168
-2
lines changed

5 files changed

+168
-2
lines changed

Cargo.lock

Lines changed: 15 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/bitwarden-crypto/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ argon2 = { version = ">=0.5.0, <0.6", features = [
2929
base64 = ">=0.22.1, <0.23"
3030
bitwarden-error = { workspace = true }
3131
cbc = { version = ">=0.1.2, <0.2", features = ["alloc", "zeroize"] }
32+
chacha20poly1305 = { version = "0.10.1" }
3233
generic-array = { version = ">=0.14.7, <1.0", features = ["zeroize"] }
3334
hkdf = ">=0.12.3, <0.13"
3435
hmac = ">=0.12.1, <0.13"

crates/bitwarden-crypto/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,7 @@ pub use wordlist::EFF_LONG_WORD_LIST;
9191
mod store;
9292
pub use store::{KeyStore, KeyStoreContext};
9393
mod traits;
94+
mod xchacha20;
9495
pub use traits::{Decryptable, Encryptable, IdentifyKey, KeyId, KeyIds};
9596
pub use zeroizing_alloc::ZeroAlloc as ZeroizingAllocator;
9697

Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
//! # XChaCha20Poly1305 operations
2+
//!
3+
//! Contains low level XChaCha20Poly1305 operations used by the rest of the crate.
4+
//!
5+
//! In most cases you should use the [EncString][crate::EncString] with
6+
//! [KeyEncryptable][crate::KeyEncryptable] & [KeyDecryptable][crate::KeyDecryptable] instead.
7+
//!
8+
//! Note:
9+
//! XChaCha20Poly1305 encrypts data, and authenticates both the cipher text and associated
10+
//! data. This does not provide key-commitment, and assumes there can only be one key.
11+
//!
12+
//! If multiple keys are possible, a key-committing cipher such as
13+
//! XChaCha20Poly1305Blake3CTX should be used: `https://github.com/bitwarden/sdk-internal/pull/41` to prevent invisible-salamander style attacks.
14+
//! `https://eprint.iacr.org/2019/016.pdf`
15+
//! `https://soatok.blog/2024/09/10/invisible-salamanders-are-not-what-you-think/`
16+
17+
use chacha20poly1305::{AeadCore, AeadInPlace, KeyInit, XChaCha20Poly1305};
18+
use generic_array::GenericArray;
19+
use rand::{CryptoRng, RngCore};
20+
21+
use crate::CryptoError;
22+
23+
#[allow(unused)]
24+
pub(crate) struct XChaCha20Poly1305Ciphertext {
25+
nonce: GenericArray<u8, <XChaCha20Poly1305 as AeadCore>::NonceSize>,
26+
ciphertext: Vec<u8>,
27+
}
28+
29+
#[allow(unused)]
30+
fn encrypt_xchacha20_poly1305(
31+
key: &[u8; 32],
32+
plaintext_secret_data: &[u8],
33+
associated_data: &[u8],
34+
) -> XChaCha20Poly1305Ciphertext {
35+
let mut rng = rand::thread_rng();
36+
encrypt_xchacha20_poly1305_internal(rng, key, plaintext_secret_data, associated_data)
37+
}
38+
39+
fn encrypt_xchacha20_poly1305_internal(
40+
rng: impl RngCore + CryptoRng,
41+
key: &[u8; 32],
42+
plaintext_secret_data: &[u8],
43+
associated_data: &[u8],
44+
) -> XChaCha20Poly1305Ciphertext {
45+
let nonce = &XChaCha20Poly1305::generate_nonce(rng);
46+
// This buffer contains the plaintext, that will be encrypted in-place
47+
let mut buffer = plaintext_secret_data.to_vec();
48+
XChaCha20Poly1305::new(GenericArray::from_slice(key))
49+
.encrypt_in_place(nonce, associated_data, &mut buffer)
50+
.expect("encryption failed");
51+
52+
XChaCha20Poly1305Ciphertext {
53+
nonce: *nonce,
54+
ciphertext: buffer,
55+
}
56+
}
57+
58+
#[allow(unused)]
59+
pub(crate) fn decrypt_xchacha20_poly1305(
60+
nonce: &[u8; 24],
61+
key: &[u8; 32],
62+
ciphertext: &[u8],
63+
associated_data: &[u8],
64+
) -> Result<Vec<u8>, CryptoError> {
65+
let mut buffer = ciphertext.to_vec();
66+
XChaCha20Poly1305::new(GenericArray::from_slice(key))
67+
.decrypt_in_place(
68+
GenericArray::from_slice(nonce),
69+
associated_data,
70+
&mut buffer,
71+
)
72+
.map_err(|_| CryptoError::KeyDecrypt)?;
73+
Ok(buffer)
74+
}
75+
76+
mod tests {
77+
#[cfg(test)]
78+
use crate::xchacha20::*;
79+
80+
#[test]
81+
fn test_encrypt_decrypt_xchacha20() {
82+
let key = [0u8; 32];
83+
let plaintext_secret_data = b"My secret data";
84+
let authenticated_data = b"My authenticated data";
85+
let encrypted = encrypt_xchacha20_poly1305(&key, plaintext_secret_data, authenticated_data);
86+
let decrypted = decrypt_xchacha20_poly1305(
87+
&encrypted.nonce.into(),
88+
&key,
89+
&encrypted.ciphertext,
90+
authenticated_data,
91+
)
92+
.unwrap();
93+
assert_eq!(plaintext_secret_data, decrypted.as_slice());
94+
}
95+
96+
#[test]
97+
fn test_fails_when_ciphertext_changed() {
98+
let key = [0u8; 32];
99+
let plaintext_secret_data = b"My secret data";
100+
let authenticated_data = b"My authenticated data";
101+
102+
let mut encrypted =
103+
encrypt_xchacha20_poly1305(&key, plaintext_secret_data, authenticated_data);
104+
encrypted.ciphertext[0] = encrypted.ciphertext[0].wrapping_add(1);
105+
let result = decrypt_xchacha20_poly1305(
106+
&encrypted.nonce.into(),
107+
&key,
108+
&encrypted.ciphertext,
109+
authenticated_data,
110+
);
111+
assert!(result.is_err());
112+
}
113+
114+
#[test]
115+
fn test_fails_when_associated_data_changed() {
116+
let key = [0u8; 32];
117+
let plaintext_secret_data = b"My secret data";
118+
let mut authenticated_data = b"My authenticated data".to_vec();
119+
120+
let encrypted =
121+
encrypt_xchacha20_poly1305(&key, plaintext_secret_data, authenticated_data.as_slice());
122+
authenticated_data[0] = authenticated_data[0].wrapping_add(1);
123+
let result = decrypt_xchacha20_poly1305(
124+
&encrypted.nonce.into(),
125+
&key,
126+
&encrypted.ciphertext,
127+
authenticated_data.as_slice(),
128+
);
129+
assert!(result.is_err());
130+
}
131+
132+
#[test]
133+
fn test_fails_when_nonce_changed() {
134+
let key = [0u8; 32];
135+
let plaintext_secret_data = b"My secret data";
136+
let authenticated_data = b"My authenticated data";
137+
138+
let mut encrypted =
139+
encrypt_xchacha20_poly1305(&key, plaintext_secret_data, authenticated_data);
140+
encrypted.nonce[0] = encrypted.nonce[0].wrapping_add(1);
141+
let result = decrypt_xchacha20_poly1305(
142+
&encrypted.nonce.into(),
143+
&key,
144+
&encrypted.ciphertext,
145+
authenticated_data,
146+
);
147+
assert!(result.is_err());
148+
}
149+
}

crates/bitwarden-wasm-internal/build.sh

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,9 @@ fi
2323
# 1. It is required for wasm2js support
2424
# 2. While webpack supports it, it has some compatibility issues that lead to strange results
2525
# Note that this requirest build-std which is an unstable feature,
26-
# this normally requires a nightly build, but we can also use the
26+
# this normally requires a nightly build, but we can also use the
2727
# RUSTC_BOOTSTRAP hack to use the same stable version as the normal build
28-
RUSTFLAGS=-Ctarget-cpu=mvp RUSTC_BOOTSTRAP=1 cargo build -p bitwarden-wasm-internal -Zbuild-std=panic_abort,std --target wasm32-unknown-unknown ${RELEASE_FLAG}
28+
RUSTFLAGS=-Ctarget-cpu=mvp RUSTC_BOOTSTRAP=1 cargo build -p bitwarden-wasm-internal -Zbuild-std=panic_abort,std --target wasm32-unknown-unknown ${RELEASE_FLAG} --config 'patch.crates-io.pkcs5.git="https://github.com/bitwarden/rustcrypto-formats.git"' --config 'patch.crates-io.pkcs5.rev="2b27c63034217dd126bbf5ed874da51b84f8c705"'
2929
wasm-bindgen --target bundler --out-dir crates/bitwarden-wasm-internal/npm ./target/wasm32-unknown-unknown/${BUILD_FOLDER}/bitwarden_wasm_internal.wasm
3030
wasm-bindgen --target nodejs --out-dir crates/bitwarden-wasm-internal/npm/node ./target/wasm32-unknown-unknown/${BUILD_FOLDER}/bitwarden_wasm_internal.wasm
3131

0 commit comments

Comments
 (0)