Skip to main content

bitwarden_organization_crypto/
account_recovery.rs

1//! Account recovery: a member's user key, held by the organization so that admins can recover the
2//! member's account by resetting the member's master password.
3//!
4//! The server and the API name this value the "reset password key" (`resetPasswordKey`).
5//!
6//! # Versions
7//!
8//! - V1: the member's user key encapsulated to the organization's public key. Nothing authenticates
9//!   the sender.
10//! - V2 (planned): the member's user key sealed from the member's identity to the organization's
11//!   identity in an identity-sealed key envelope, which authenticates the member as the sender.
12//!
13//! [`AccountRecovery::make`] takes the [`TrustProof`] for the organization's public key. Some V1
14//! flows have no way to verify the public key, and use [`AccountRecovery::make_untrusted`]. V2
15//! has no untrusted variant.
16//!
17//! Until identities exist, [`PublicKey`] and [`OrganizationPrivateKey`] stand in for the
18//! organization's identity.
19
20use std::str::FromStr;
21
22use bitwarden_crypto::{CryptoError, KeySlotIds, KeyStoreContext, PublicKey, UnsignedSharedKey};
23use thiserror::Error;
24
25use crate::{
26    organization_private_key::OrganizationPrivateKey,
27    trust_proof::{TrustProof, TrustProofError},
28};
29
30/// Errors that can occur when parsing an account recovery.
31#[derive(Debug, Error)]
32pub enum AccountRecoveryParseError {
33    /// The value stored for the member is not in a known account recovery format.
34    #[error("The account recovery key is malformed")]
35    Malformed,
36}
37
38/// Errors that can occur when making an account recovery with a trust proof.
39#[derive(Debug, Error, PartialEq)]
40pub enum AccountRecoveryMakeError {
41    /// The private key of
42    /// [`CryptographicProof::OwnPrivateKey`](crate::trust_proof::CryptographicProof::OwnPrivateKey)
43    /// is not in the key store context.
44    #[error("The private key of the trust proof is not in the key store context")]
45    PrivateKeyNotInContext,
46    /// The member's user key is not in the key store context.
47    #[error("The member's user key could not be encapsulated to the organization's public key")]
48    EncapsulationFailed,
49    /// The public key did not pass the trust proof.
50    #[error("The organization's public key could not be trusted")]
51    UntrustedPublicKey,
52}
53
54impl From<TrustProofError> for AccountRecoveryMakeError {
55    fn from(error: TrustProofError) -> Self {
56        match error {
57            TrustProofError::PrivateKeyNotInContext => Self::PrivateKeyNotInContext,
58            TrustProofError::UntrustedPublicKey => Self::UntrustedPublicKey,
59        }
60    }
61}
62
63/// Errors that can occur when making an account recovery without a trust proof.
64#[derive(Debug, Error)]
65pub enum AccountRecoveryMakeUntrustedError {
66    /// The member's user key is not in the key store context.
67    #[error("The member's user key could not be encapsulated to the organization's public key")]
68    EncapsulationFailed,
69}
70
71/// Errors that can occur when unsealing an account recovery.
72#[derive(Debug, Error)]
73pub enum AccountRecoveryUnsealError {
74    /// The account recovery was made for a different organization key pair, or the stored value is
75    /// corrupt. V1 cannot tell these apart.
76    #[error(
77        "The account recovery key could not be decapsulated with the organization's private key"
78    )]
79    DecapsulationFailed,
80    /// The private key of [`OrganizationPrivateKey`] is not in the key store context.
81    #[error("The organization's private key does not exist in the key store context")]
82    PrivateKeyNotInContext,
83}
84
85/// A member's user key, held by the organization so that admins can recover the account.
86pub enum AccountRecovery {
87    /// The member's user key encapsulated to the organization's public key.
88    ///
89    /// Encapsulation does not authenticate the sender. A swapped public key hands the member's user
90    /// key to whoever holds the matching private key.
91    V1(UnsignedSharedKey),
92}
93
94impl AccountRecovery {
95    /// Makes an account recovery of the member's user key, after the organization's public key
96    /// passes `trust_proof`.
97    pub fn make<Ids: KeySlotIds>(
98        member_user_key: Ids::Symmetric,
99        organization_public_key: &PublicKey,
100        trust_proof: &TrustProof<Ids>,
101        ctx: &KeyStoreContext<Ids>,
102    ) -> Result<Self, AccountRecoveryMakeError> {
103        trust_proof.verify(organization_public_key, ctx)?;
104
105        let encapsulated_key =
106            UnsignedSharedKey::encapsulate(member_user_key, organization_public_key, ctx)
107                .map_err(|_| AccountRecoveryMakeError::EncapsulationFailed)?;
108
109        Ok(Self::V1(encapsulated_key))
110    }
111
112    /// Makes a V1 account recovery of the member's user key, without verifying the
113    /// organization's public key.
114    ///
115    /// Only for V1 flows that have no way to verify the public key. A swapped public key hands the
116    /// member's user key to whoever holds the matching private key.
117    pub fn make_untrusted<Ids: KeySlotIds>(
118        member_user_key: Ids::Symmetric,
119        organization_public_key: &PublicKey,
120        ctx: &KeyStoreContext<Ids>,
121    ) -> Result<Self, AccountRecoveryMakeUntrustedError> {
122        let encapsulated_key =
123            UnsignedSharedKey::encapsulate(member_user_key, organization_public_key, ctx)
124                .map_err(|_| AccountRecoveryMakeUntrustedError::EncapsulationFailed)?;
125
126        Ok(Self::V1(encapsulated_key))
127    }
128
129    /// Opens the member's user key, returning the id it was added to the context under.
130    pub fn unseal_member_user_key<Ids: KeySlotIds>(
131        &self,
132        organization_private_key: &OrganizationPrivateKey<Ids>,
133        ctx: &mut KeyStoreContext<Ids>,
134    ) -> Result<Ids::Symmetric, AccountRecoveryUnsealError> {
135        let Self::V1(encapsulated_key) = self;
136
137        organization_private_key
138            .decapsulate_key(encapsulated_key, ctx)
139            .map_err(|error| match error {
140                CryptoError::MissingKeyId(_) => AccountRecoveryUnsealError::PrivateKeyNotInContext,
141                _ => AccountRecoveryUnsealError::DecapsulationFailed,
142            })
143    }
144}
145
146impl FromStr for AccountRecovery {
147    type Err = AccountRecoveryParseError;
148
149    fn from_str(value: &str) -> Result<Self, Self::Err> {
150        value
151            .parse()
152            .map(Self::V1)
153            .map_err(|_| AccountRecoveryParseError::Malformed)
154    }
155}
156
157impl From<&AccountRecovery> for String {
158    fn from(value: &AccountRecovery) -> Self {
159        let AccountRecovery::V1(encapsulated_key) = value;
160
161        encapsulated_key.to_string()
162    }
163}
164
165#[cfg(test)]
166mod tests {
167    use bitwarden_crypto::{
168        KeyStore, PublicKeyEncryptionAlgorithm, SymmetricKeyAlgorithm::Aes256CbcHmac,
169    };
170
171    use super::*;
172    use crate::test_support::{
173        TEST_VECTOR_V1_MEMBER_USER_KEY, TestIds, add_symmetric_key,
174        unwrap_v1_organization_private_key,
175    };
176
177    /// [`TEST_VECTOR_V1_MEMBER_USER_KEY`] encapsulated to the pinned organization public key.
178    const TEST_VECTOR_ACCOUNT_RECOVERY: &str = "4.GMFvOS56Osw1wUBwRx+lL6/wmiRxLZ+6/jWuDwMVCL1LoHKPn71uI+I5bofMog7AUFOZfnHYvkY7DezYDVj635x8DrD3msVrpHDis2WX9KDV3mffFVQ8SItiIJI4iD2x98uDYCDClMqI5CzFI/IroOYUDxleS3VVOncjUY2AlFSPXsfBClrC1nLsP6dkseh8kI27ZznWYdb2bxq+6a9nBuXz0ykiwrYJy+DIgCNqjpZhWOB8L+2LFGNfVR+Xz3DNDgmfo+t5ufdu9Y5HcKDOfFpkDOy7lngBtQjLBSaQcX3sh7lRxgDzAGzzcNQ+aTsak8YAf+e+lrWwhva7RxTaeA==";
179
180    #[test]
181    fn test_account_recovery_unseals_to_the_member_user_key() {
182        let key_store = KeyStore::<TestIds>::default();
183        let mut ctx = key_store.context_mut();
184        let organization_private_key = unwrap_v1_organization_private_key(&mut ctx);
185        let member_user_key = ctx.make_symmetric_key(Aes256CbcHmac);
186
187        let account_recovery = AccountRecovery::make(
188            member_user_key,
189            &organization_private_key.public_key(&ctx).unwrap(),
190            &TrustProof::TestStub(Ok(())),
191            &ctx,
192        )
193        .unwrap();
194        let unsealed = account_recovery
195            .unseal_member_user_key(&organization_private_key, &mut ctx)
196            .unwrap();
197
198        ctx.assert_symmetric_keys_equal(member_user_key, unsealed);
199    }
200
201    #[test]
202    fn test_untrusted_account_recovery_unseals_to_the_member_user_key() {
203        let key_store = KeyStore::<TestIds>::default();
204        let mut ctx = key_store.context_mut();
205        let organization_private_key = unwrap_v1_organization_private_key(&mut ctx);
206        let member_user_key = ctx.make_symmetric_key(Aes256CbcHmac);
207
208        let account_recovery = AccountRecovery::make_untrusted(
209            member_user_key,
210            &organization_private_key.public_key(&ctx).unwrap(),
211            &ctx,
212        )
213        .unwrap();
214        let unsealed = account_recovery
215            .unseal_member_user_key(&organization_private_key, &mut ctx)
216            .unwrap();
217
218        ctx.assert_symmetric_keys_equal(member_user_key, unsealed);
219    }
220
221    #[test]
222    fn test_account_recovery_survives_a_string_round_trip() {
223        let key_store = KeyStore::<TestIds>::default();
224        let mut ctx = key_store.context_mut();
225        let organization_private_key = unwrap_v1_organization_private_key(&mut ctx);
226        let member_user_key = ctx.make_symmetric_key(Aes256CbcHmac);
227        let account_recovery = AccountRecovery::make(
228            member_user_key,
229            &organization_private_key.public_key(&ctx).unwrap(),
230            &TrustProof::TestStub(Ok(())),
231            &ctx,
232        )
233        .unwrap();
234
235        let parsed: AccountRecovery = String::from(&account_recovery).parse().unwrap();
236
237        let unsealed = parsed
238            .unseal_member_user_key(&organization_private_key, &mut ctx)
239            .unwrap();
240        ctx.assert_symmetric_keys_equal(member_user_key, unsealed);
241    }
242
243    #[test]
244    fn test_parsing_a_key_in_no_known_format_fails() {
245        let result: Result<AccountRecovery, _> = "not an encapsulated key".parse();
246
247        assert!(matches!(result, Err(AccountRecoveryParseError::Malformed)));
248    }
249
250    #[test]
251    fn test_making_with_a_failed_trust_proof_fails_with_the_matching_error() {
252        let key_store = KeyStore::<TestIds>::default();
253        let mut ctx = key_store.context_mut();
254        let organization_private_key = unwrap_v1_organization_private_key(&mut ctx);
255        let organization_public_key = organization_private_key.public_key(&ctx).unwrap();
256        let member_user_key = ctx.make_symmetric_key(Aes256CbcHmac);
257
258        for (trust_proof_error, expected_error) in [
259            (
260                TrustProofError::PrivateKeyNotInContext,
261                AccountRecoveryMakeError::PrivateKeyNotInContext,
262            ),
263            (
264                TrustProofError::UntrustedPublicKey,
265                AccountRecoveryMakeError::UntrustedPublicKey,
266            ),
267        ] {
268            let result = AccountRecovery::make(
269                member_user_key,
270                &organization_public_key,
271                &TrustProof::TestStub(Err(trust_proof_error)),
272                &ctx,
273            );
274
275            assert_eq!(result.err(), Some(expected_error));
276        }
277    }
278
279    #[test]
280    fn test_making_with_a_missing_member_user_key_fails() {
281        let key_store = KeyStore::<TestIds>::default();
282        let mut ctx = key_store.context_mut();
283        let organization_private_key = unwrap_v1_organization_private_key(&mut ctx);
284        let other_key_store = KeyStore::<TestIds>::default();
285        let mut other_ctx = other_key_store.context_mut();
286        let missing_member_user_key = other_ctx.make_symmetric_key(Aes256CbcHmac);
287
288        let result = AccountRecovery::make(
289            missing_member_user_key,
290            &organization_private_key.public_key(&ctx).unwrap(),
291            &TrustProof::TestStub(Ok(())),
292            &ctx,
293        );
294
295        assert!(matches!(
296            result,
297            Err(AccountRecoveryMakeError::EncapsulationFailed)
298        ));
299    }
300
301    #[test]
302    fn test_making_untrusted_with_a_missing_member_user_key_fails() {
303        let key_store = KeyStore::<TestIds>::default();
304        let mut ctx = key_store.context_mut();
305        let organization_private_key = unwrap_v1_organization_private_key(&mut ctx);
306        let organization_public_key = organization_private_key.public_key(&ctx).unwrap();
307        let other_key_store = KeyStore::<TestIds>::default();
308        let mut other_ctx = other_key_store.context_mut();
309        let missing_member_user_key = other_ctx.make_symmetric_key(Aes256CbcHmac);
310
311        let result = AccountRecovery::make_untrusted(
312            missing_member_user_key,
313            &organization_public_key,
314            &ctx,
315        );
316
317        assert!(matches!(
318            result,
319            Err(AccountRecoveryMakeUntrustedError::EncapsulationFailed)
320        ));
321    }
322
323    #[test]
324    fn test_unsealing_an_account_recovery_for_another_organization_fails() {
325        let key_store = KeyStore::<TestIds>::default();
326        let mut ctx = key_store.context_mut();
327        let organization_private_key = unwrap_v1_organization_private_key(&mut ctx);
328        let other_private_key = ctx.make_private_key(PublicKeyEncryptionAlgorithm::RsaOaepSha1);
329        let other_public_key = ctx.get_public_key(other_private_key).unwrap();
330        let member_user_key = ctx.make_symmetric_key(Aes256CbcHmac);
331        let account_recovery =
332            AccountRecovery::make_untrusted(member_user_key, &other_public_key, &ctx).unwrap();
333
334        let result = account_recovery.unseal_member_user_key(&organization_private_key, &mut ctx);
335
336        assert!(matches!(
337            result,
338            Err(AccountRecoveryUnsealError::DecapsulationFailed)
339        ));
340    }
341
342    #[test]
343    fn test_unsealing_with_a_private_key_from_another_context_fails() {
344        let key_store = KeyStore::<TestIds>::default();
345        let mut ctx = key_store.context_mut();
346        let organization_private_key = unwrap_v1_organization_private_key(&mut ctx);
347        let member_user_key = ctx.make_symmetric_key(Aes256CbcHmac);
348        let account_recovery = AccountRecovery::make_untrusted(
349            member_user_key,
350            &organization_private_key.public_key(&ctx).unwrap(),
351            &ctx,
352        )
353        .unwrap();
354        let other_key_store = KeyStore::<TestIds>::default();
355        let mut other_ctx = other_key_store.context_mut();
356
357        let result =
358            account_recovery.unseal_member_user_key(&organization_private_key, &mut other_ctx);
359
360        assert!(matches!(
361            result,
362            Err(AccountRecoveryUnsealError::PrivateKeyNotInContext)
363        ));
364    }
365
366    #[test]
367    #[ignore = "Manual test to generate test vectors"]
368    fn generate_test_vectors() {
369        let key_store = KeyStore::<TestIds>::default();
370        let mut ctx = key_store.context_mut();
371        let organization_private_key = unwrap_v1_organization_private_key(&mut ctx);
372        let member_user_key = add_symmetric_key(TEST_VECTOR_V1_MEMBER_USER_KEY, &mut ctx);
373
374        let account_recovery = AccountRecovery::make_untrusted(
375            member_user_key,
376            &organization_private_key.public_key(&ctx).unwrap(),
377            &ctx,
378        )
379        .unwrap();
380
381        println!(
382            "const TEST_VECTOR_ACCOUNT_RECOVERY: &str = \"{}\";",
383            String::from(&account_recovery)
384        );
385    }
386
387    #[test]
388    fn test_account_recovery_test_vector() {
389        let key_store = KeyStore::<TestIds>::default();
390        let mut ctx = key_store.context_mut();
391        let organization_private_key = unwrap_v1_organization_private_key(&mut ctx);
392        let member_user_key = add_symmetric_key(TEST_VECTOR_V1_MEMBER_USER_KEY, &mut ctx);
393
394        let account_recovery: AccountRecovery = TEST_VECTOR_ACCOUNT_RECOVERY.parse().unwrap();
395        let unsealed = account_recovery
396            .unseal_member_user_key(&organization_private_key, &mut ctx)
397            .unwrap();
398
399        ctx.assert_symmetric_keys_equal(member_user_key, unsealed);
400    }
401}