Skip to content

Commit aaae971

Browse files
quextenMGibson1HintonThomas-Avery
authored
[PM-24127] Implement password-protected key envelope (#335)
## 🎟️ Tracking https://bitwarden.atlassian.net/browse/PM-24127 ## 📔 Objective The current masterkey logic is complex to understand for using teams (auth), and also prone to error. When any setting changes / gets out of sync, such as the email, or kdf, then decryption fails. The masterkey is further too widely scoped, used both in an authentication protocol, and in unlock decryption. This PR introduces a PasswordProtectedKeyEnvelope. The goal is to protect a symmetric key with a password securely. Internally, this uses a KDF, and the KDF settings (argon2 parameters, and random salt) are stored on the serialized object. That means that the only thing needed to unlock this structure is the correct password, everything else is stored on the object, making this process much less error prone. At the same time the interface is easier to use. An example is provided to show usage. A follow-up PR will add an unlock method / enrollment for PIN based on this new cryptographic API. Note: Only argon2 is supported here. The PasswordProtectedKeyEnvelope's settings are completely decoupled from the account settings, and we don't need to provide backwards compatibility to non-recommended legacy cryptographic algorithms (pbkdf2). ## ⏰ 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 --------- Co-authored-by: Matt Gibson <[email protected]> Co-authored-by: Oscar Hinton <[email protected]> Co-authored-by: Thomas Avery <[email protected]>
1 parent 1a956d4 commit aaae971

File tree

12 files changed

+901
-4
lines changed

12 files changed

+901
-4
lines changed

crates/bitwarden-core/src/key_management/mod.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,11 @@ mod crypto_client;
1818
#[cfg(feature = "internal")]
1919
pub use crypto_client::CryptoClient;
2020

21+
#[cfg(feature = "internal")]
22+
mod non_generic_wrappers;
23+
#[allow(unused_imports)]
24+
#[cfg(feature = "internal")]
25+
pub(crate) use non_generic_wrappers::*;
2126
#[cfg(feature = "internal")]
2227
mod security_state;
2328
#[cfg(feature = "internal")]
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
//! Structs with generic parameters cannot be moved across FFI bounds (uniffi/wasm).
2+
//! This module contains wrapper structs that hide the generic parameter with instantiated versions.
3+
4+
use std::ops::Deref;
5+
6+
use serde::{Deserialize, Serialize};
7+
#[cfg(feature = "wasm")]
8+
use tsify::Tsify;
9+
10+
use crate::key_management::KeyIds;
11+
12+
/// A non-generic wrapper around `bitwarden-crypto`'s `PasswordProtectedKeyEnvelope`.
13+
#[derive(Serialize, Deserialize)]
14+
#[serde(transparent)]
15+
#[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))]
16+
pub struct PasswordProtectedKeyEnvelope(
17+
#[cfg_attr(
18+
feature = "wasm",
19+
tsify(type = r#"Tagged<string, "PasswordProtectedKeyEnvelope">"#)
20+
)]
21+
pub(crate) bitwarden_crypto::safe::PasswordProtectedKeyEnvelope<KeyIds>,
22+
);
23+
24+
impl Deref for PasswordProtectedKeyEnvelope {
25+
type Target = bitwarden_crypto::safe::PasswordProtectedKeyEnvelope<KeyIds>;
26+
27+
fn deref(&self) -> &Self::Target {
28+
&self.0
29+
}
30+
}
31+
32+
impl std::fmt::Debug for PasswordProtectedKeyEnvelope {
33+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
34+
self.0.fmt(f)
35+
}
36+
}

crates/bitwarden-core/src/uniffi_support.rs

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
//! This module contains custom type converters for Uniffi.
22
3-
use std::num::NonZeroU32;
3+
use std::{num::NonZeroU32, str::FromStr};
44

55
use bitwarden_crypto::CryptoError;
66
use uuid::Uuid;
77

8-
use crate::key_management::SignedSecurityState;
8+
use crate::key_management::{PasswordProtectedKeyEnvelope, SignedSecurityState};
99

1010
uniffi::use_remote_type!(bitwarden_crypto::NonZeroU32);
1111

@@ -35,3 +35,11 @@ uniffi::custom_type!(SignedSecurityState, String, {
3535
},
3636
lower: |obj| obj.into(),
3737
});
38+
39+
uniffi::custom_type!(PasswordProtectedKeyEnvelope, String, {
40+
remote,
41+
try_lift: |val| bitwarden_crypto::safe::PasswordProtectedKeyEnvelope::from_str(val.as_str())
42+
.map_err(|e| e.into())
43+
.map(PasswordProtectedKeyEnvelope),
44+
lower: |obj| obj.0.into(),
45+
});
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
//! This example demonstrates how to securely protect keys with a password using the
2+
//! [PasswordProtectedKeyEnvelope].
3+
4+
use bitwarden_crypto::{
5+
key_ids,
6+
safe::{PasswordProtectedKeyEnvelope, PasswordProtectedKeyEnvelopeError},
7+
KeyStore, KeyStoreContext,
8+
};
9+
10+
fn main() {
11+
let key_store = KeyStore::<ExampleIds>::default();
12+
let mut ctx: KeyStoreContext<'_, ExampleIds> = key_store.context_mut();
13+
let mut disk = MockDisk::new();
14+
15+
// Alice wants to protect a key with a password.
16+
// For example to:
17+
// - Protect her vault with a pin
18+
// - Protect her exported vault with a password
19+
// - Protect a send with a URL fragment secret
20+
// For this, the `PasswordProtectedKeyEnvelope` is used.
21+
22+
// Alice has a vault protected with a symmetric key. She wants the symmetric key protected with
23+
// a PIN.
24+
let vault_key = ctx
25+
.generate_symmetric_key(ExampleSymmetricKey::VaultKey)
26+
.expect("Generating vault key should work");
27+
28+
// Seal the key with the PIN
29+
// The KDF settings are chosen for you, and do not need to be separately tracked or synced
30+
// Next, store this protected key envelope on disk.
31+
let pin = "1234";
32+
let envelope =
33+
PasswordProtectedKeyEnvelope::seal(vault_key, pin, &ctx).expect("Sealing should work");
34+
disk.save("vault_key_envelope", (&envelope).into());
35+
36+
// Wipe the context to simulate new session
37+
ctx.clear_local();
38+
39+
// Load the envelope from disk and unseal it with the PIN, and store it in the context.
40+
let deserialized: PasswordProtectedKeyEnvelope<ExampleIds> =
41+
PasswordProtectedKeyEnvelope::try_from(
42+
disk.load("vault_key_envelope")
43+
.expect("Loading from disk should work"),
44+
)
45+
.expect("Deserializing envelope should work");
46+
deserialized
47+
.unseal(ExampleSymmetricKey::VaultKey, pin, &mut ctx)
48+
.expect("Unsealing should work");
49+
50+
// Alice wants to change her password; also her KDF settings are below the minimums.
51+
// Re-sealing will update the password, and KDF settings.
52+
let envelope = envelope
53+
.reseal(pin, "0000")
54+
.expect("The password should be valid");
55+
disk.save("vault_key_envelope", (&envelope).into());
56+
57+
// Alice wants to change the protected key. This requires creating a new envelope
58+
ctx.generate_symmetric_key(ExampleSymmetricKey::VaultKey)
59+
.expect("Generating vault key should work");
60+
let envelope = PasswordProtectedKeyEnvelope::seal(ExampleSymmetricKey::VaultKey, "0000", &ctx)
61+
.expect("Sealing should work");
62+
disk.save("vault_key_envelope", (&envelope).into());
63+
64+
// Alice tries the password but it is wrong
65+
assert!(matches!(
66+
envelope.unseal(ExampleSymmetricKey::VaultKey, "9999", &mut ctx),
67+
Err(PasswordProtectedKeyEnvelopeError::WrongPassword)
68+
));
69+
}
70+
71+
pub(crate) struct MockDisk {
72+
map: std::collections::HashMap<String, Vec<u8>>,
73+
}
74+
75+
impl MockDisk {
76+
pub(crate) fn new() -> Self {
77+
MockDisk {
78+
map: std::collections::HashMap::new(),
79+
}
80+
}
81+
82+
pub(crate) fn save(&mut self, key: &str, value: Vec<u8>) {
83+
self.map.insert(key.to_string(), value);
84+
}
85+
86+
pub(crate) fn load(&self, key: &str) -> Option<&Vec<u8>> {
87+
self.map.get(key)
88+
}
89+
}
90+
91+
key_ids! {
92+
#[symmetric]
93+
pub enum ExampleSymmetricKey {
94+
#[local]
95+
VaultKey
96+
}
97+
98+
#[asymmetric]
99+
pub enum ExampleAsymmetricKey {
100+
Key(u8),
101+
}
102+
103+
#[signing]
104+
pub enum ExampleSigningKey {
105+
Key(u8),
106+
}
107+
108+
pub ExampleIds => ExampleSymmetricKey, ExampleAsymmetricKey, ExampleSigningKey;
109+
}

crates/bitwarden-crypto/src/cose.rs

Lines changed: 54 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,10 @@
55
66
use coset::{
77
iana::{self, CoapContentFormat},
8-
CborSerializable, ContentType, Label,
8+
CborSerializable, ContentType, Header, Label,
99
};
1010
use generic_array::GenericArray;
11+
use thiserror::Error;
1112
use typenum::U32;
1213

1314
use crate::{
@@ -22,6 +23,12 @@ use crate::{
2223
pub(crate) const XCHACHA20_POLY1305: i64 = -70000;
2324
const XCHACHA20_TEXT_PAD_BLOCK_SIZE: usize = 32;
2425

26+
pub(crate) const ALG_ARGON2ID13: i64 = -71000;
27+
pub(crate) const ARGON2_SALT: i64 = -71001;
28+
pub(crate) const ARGON2_ITERATIONS: i64 = -71002;
29+
pub(crate) const ARGON2_MEMORY: i64 = -71003;
30+
pub(crate) const ARGON2_PARALLELISM: i64 = -71004;
31+
2532
// Note: These are in the "unregistered" tree: https://datatracker.ietf.org/doc/html/rfc6838#section-3.4
2633
// These are only used within Bitwarden, and not meant for exchange with other systems.
2734
const CONTENT_TYPE_PADDED_UTF8: &str = "application/x.bitwarden.utf8-padded";
@@ -221,6 +228,52 @@ pub trait CoseSerializable<T: CoseContentFormat + ConstContentFormat> {
221228
where
222229
Self: Sized;
223230
}
231+
232+
pub(crate) fn extract_integer(
233+
header: &Header,
234+
target_label: i64,
235+
value_name: &str,
236+
) -> Result<i128, CoseExtractError> {
237+
header
238+
.rest
239+
.iter()
240+
.find_map(|(label, value)| match (label, value) {
241+
(Label::Int(label_value), ciborium::Value::Integer(int_value))
242+
if *label_value == target_label =>
243+
{
244+
Some(*int_value)
245+
}
246+
_ => None,
247+
})
248+
.map(Into::into)
249+
.ok_or_else(|| CoseExtractError::MissingValue(value_name.to_string()))
250+
}
251+
252+
pub(crate) fn extract_bytes(
253+
header: &Header,
254+
target_label: i64,
255+
value_name: &str,
256+
) -> Result<Vec<u8>, CoseExtractError> {
257+
header
258+
.rest
259+
.iter()
260+
.find_map(|(label, value)| match (label, value) {
261+
(Label::Int(label_value), ciborium::Value::Bytes(byte_value))
262+
if *label_value == target_label =>
263+
{
264+
Some(byte_value.clone())
265+
}
266+
_ => None,
267+
})
268+
.ok_or(CoseExtractError::MissingValue(value_name.to_string()))
269+
}
270+
271+
#[derive(Debug, Error)]
272+
pub(crate) enum CoseExtractError {
273+
#[error("Missing value {0}")]
274+
MissingValue(String),
275+
}
276+
224277
#[cfg(test)]
225278
mod test {
226279
use super::*;

crates/bitwarden-crypto/src/keys/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ mod symmetric_crypto_key;
99
#[cfg(test)]
1010
pub use symmetric_crypto_key::derive_symmetric_key;
1111
pub use symmetric_crypto_key::{
12-
Aes256CbcHmacKey, Aes256CbcKey, SymmetricCryptoKey, XChaCha20Poly1305Key,
12+
Aes256CbcHmacKey, Aes256CbcKey, EncodedSymmetricKey, SymmetricCryptoKey, XChaCha20Poly1305Key,
1313
};
1414
mod asymmetric_crypto_key;
1515
pub use asymmetric_crypto_key::{

crates/bitwarden-crypto/src/keys/symmetric_crypto_key.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -407,6 +407,7 @@ impl From<EncodedSymmetricKey> for Vec<u8> {
407407
}
408408
}
409409
impl EncodedSymmetricKey {
410+
/// Returns the content format of the encoded symmetric key.
410411
#[allow(private_interfaces)]
411412
pub fn content_format(&self) -> ContentFormat {
412413
match self {

crates/bitwarden-crypto/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ pub use store::{
3636
};
3737
mod cose;
3838
pub use cose::CoseSerializable;
39+
pub mod safe;
3940
mod signing;
4041
pub use signing::*;
4142
mod traits;
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
# Bitwarden-crypto safe module
2+
3+
The safe module provides high-level cryptographic tools for building secure protocols and features.
4+
When developing new features, use this module first before considering lower-level primitives from
5+
other parts of `bitwarden-crypto`.
6+
7+
## Password-protected key envelope
8+
9+
Use the password protected key envelope to protect a symmetric key with a password. Examples
10+
include:
11+
12+
- locking a vault with a PIN/Password
13+
- protecting exports with a password
14+
15+
Internally, the module uses a KDF to protect against brute-forcing, but it does not expose this to
16+
the consumer. The consumer only provides a password and key.
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
#![doc = include_str!("./README.md")]
2+
3+
mod password_protected_key_envelope;
4+
pub use password_protected_key_envelope::*;

0 commit comments

Comments
 (0)