Skip to content

Commit b0335ef

Browse files
authored
[PM-22621] Fix missing key ID and add testvectors for cose decryption (#305)
## 🎟️ Tracking https://bitwarden.atlassian.net/browse/PM-22621 ## 📔 Objective This PR adds the key identifier value on encryption to the encrypt0 message. To quote the cose spec: > This parameter identifies one piece of data that can be used as input to find the needed cryptographic key. The value of this parameter can be matched against the 'kid' member in a COSE_Key structure. Other methods of key distribution can define an equivalent field to be matched. Applications MUST NOT assume that 'kid' values are unique. There may be more than one key with the same 'kid' value, so all of the keys associated with this 'kid' may need to be checked. The internal structure of 'kid' values is not defined and cannot be relied on by applications. Key identifier values are hints about which key to use. This is not a security-critical field. For this reason, it can be placed in the unprotected headers bucket. While our code does not make use yet of the KID value for hinting at the correct key, we do want to include it, and it had not been included so far. Since we do not use cose yet, no migration is needed. We also fail decryption on a mismatched key-id, but this is not a security feature, instead this is a traceability feature, and makes the failure reason clearer. ## ⏰ 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 0e948af commit b0335ef

File tree

2 files changed

+70
-1
lines changed

2 files changed

+70
-1
lines changed

crates/bitwarden-crypto/src/cose.rs

Lines changed: 67 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,9 @@ pub(crate) fn encrypt_xchacha20_poly1305(
2121
plaintext: &[u8],
2222
key: &crate::XChaCha20Poly1305Key,
2323
) -> Result<Vec<u8>, CryptoError> {
24-
let mut protected_header = coset::HeaderBuilder::new().build();
24+
let mut protected_header = coset::HeaderBuilder::new()
25+
.key_id(key.key_id.to_vec())
26+
.build();
2527
// This should be adjusted to use the builder pattern once implemented in coset.
2628
// The related coset upstream issue is:
2729
// https://github.com/google/coset/issues/105
@@ -59,6 +61,9 @@ pub(crate) fn decrypt_xchacha20_poly1305(
5961
if *alg != coset::Algorithm::PrivateUse(XCHACHA20_POLY1305) {
6062
return Err(CryptoError::WrongKeyType);
6163
}
64+
if key.key_id != *msg.protected.header.key_id {
65+
return Err(CryptoError::WrongCoseKeyId);
66+
}
6267

6368
let decrypted_message = msg.decrypt(&[], |data, aad| {
6469
let nonce = msg.unprotected.iv.as_slice();
@@ -116,6 +121,21 @@ impl TryFrom<&coset::CoseKey> for SymmetricCryptoKey {
116121
mod test {
117122
use super::*;
118123

124+
const KEY_ID: [u8; 16] = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15];
125+
const KEY_DATA: [u8; 32] = [
126+
0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e,
127+
0x0f, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d,
128+
0x1e, 0x1f,
129+
];
130+
const TEST_VECTOR_PLAINTEXT: &[u8] = b"Message test vector";
131+
const TEST_VECTOR_COSE_ENCRYPT0: &[u8] = &[
132+
131, 88, 25, 162, 1, 58, 0, 1, 17, 111, 4, 80, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12,
133+
13, 14, 15, 161, 5, 88, 24, 39, 48, 159, 48, 215, 77, 21, 100, 241, 209, 216, 65, 99, 221,
134+
83, 63, 118, 204, 200, 175, 126, 202, 53, 33, 88, 35, 218, 136, 132, 223, 131, 246, 169,
135+
120, 134, 49, 56, 173, 169, 133, 232, 109, 248, 101, 59, 226, 90, 97, 210, 181, 76, 68,
136+
158, 159, 94, 65, 67, 23, 112, 253, 83,
137+
];
138+
119139
#[test]
120140
fn test_encrypt_decrypt_roundtrip() {
121141
let SymmetricCryptoKey::XChaCha20Poly1305Key(ref key) =
@@ -129,4 +149,50 @@ mod test {
129149
let decrypted = decrypt_xchacha20_poly1305(&encrypted, key).unwrap();
130150
assert_eq!(decrypted, plaintext);
131151
}
152+
153+
#[test]
154+
fn test_decrypt_test_vector() {
155+
let key = XChaCha20Poly1305Key {
156+
key_id: KEY_ID,
157+
enc_key: Box::pin(*GenericArray::from_slice(&KEY_DATA)),
158+
};
159+
let decrypted = decrypt_xchacha20_poly1305(TEST_VECTOR_COSE_ENCRYPT0, &key).unwrap();
160+
assert_eq!(decrypted, TEST_VECTOR_PLAINTEXT);
161+
}
162+
163+
#[test]
164+
fn test_fail_wrong_key_id() {
165+
let key = XChaCha20Poly1305Key {
166+
key_id: [1; 16], // Different key ID
167+
enc_key: Box::pin(*GenericArray::from_slice(&KEY_DATA)),
168+
};
169+
assert!(matches!(
170+
decrypt_xchacha20_poly1305(TEST_VECTOR_COSE_ENCRYPT0, &key),
171+
Err(CryptoError::WrongCoseKeyId)
172+
));
173+
}
174+
175+
#[test]
176+
fn test_fail_wrong_algorithm() {
177+
let protected_header = coset::HeaderBuilder::new()
178+
.algorithm(iana::Algorithm::A256GCM)
179+
.key_id(KEY_ID.to_vec())
180+
.build();
181+
let nonce = [0u8; 16];
182+
let cose_encrypt0 = coset::CoseEncrypt0Builder::new()
183+
.protected(protected_header)
184+
.create_ciphertext(&[], &[], |_, _| Vec::new())
185+
.unprotected(coset::HeaderBuilder::new().iv(nonce.to_vec()).build())
186+
.build();
187+
let serialized_message = cose_encrypt0.to_vec().unwrap();
188+
189+
let key = XChaCha20Poly1305Key {
190+
key_id: KEY_ID,
191+
enc_key: Box::pin(*GenericArray::from_slice(&KEY_DATA)),
192+
};
193+
assert!(matches!(
194+
decrypt_xchacha20_poly1305(&serialized_message, &key),
195+
Err(CryptoError::WrongKeyType)
196+
));
197+
}
132198
}

crates/bitwarden-crypto/src/error.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,9 @@ pub enum CryptoError {
5555
#[error("Key algorithm does not match encrypted data type")]
5656
WrongKeyType,
5757

58+
#[error("Key ID in the COSE Encrypt0 message does not match the key ID in the key")]
59+
WrongCoseKeyId,
60+
5861
#[error("Invalid nonce length")]
5962
InvalidNonceLength,
6063
}

0 commit comments

Comments
 (0)