bitwarden_organization_crypto/
account_recovery.rs1use 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#[derive(Debug, Error)]
32pub enum AccountRecoveryParseError {
33 #[error("The account recovery key is malformed")]
35 Malformed,
36}
37
38#[derive(Debug, Error, PartialEq)]
40pub enum AccountRecoveryMakeError {
41 #[error("The private key of the trust proof is not in the key store context")]
45 PrivateKeyNotInContext,
46 #[error("The member's user key could not be encapsulated to the organization's public key")]
48 EncapsulationFailed,
49 #[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#[derive(Debug, Error)]
65pub enum AccountRecoveryMakeUntrustedError {
66 #[error("The member's user key could not be encapsulated to the organization's public key")]
68 EncapsulationFailed,
69}
70
71#[derive(Debug, Error)]
73pub enum AccountRecoveryUnsealError {
74 #[error(
77 "The account recovery key could not be decapsulated with the organization's private key"
78 )]
79 DecapsulationFailed,
80 #[error("The organization's private key does not exist in the key store context")]
82 PrivateKeyNotInContext,
83}
84
85pub enum AccountRecovery {
87 V1(UnsignedSharedKey),
92}
93
94impl AccountRecovery {
95 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 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 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 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}