Skip to content

Commit 4becfdd

Browse files
committed
Implement key rotation for signing keys
1 parent c9bd7f9 commit 4becfdd

File tree

5 files changed

+88
-6
lines changed

5 files changed

+88
-6
lines changed

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

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,8 @@ use std::collections::HashMap;
99
use base64::{engine::general_purpose::STANDARD, Engine};
1010
use bitwarden_crypto::{
1111
AsymmetricCryptoKey, CoseSerializable, CryptoError, EncString, Encryptable, Kdf,
12-
KeyDecryptable, KeyEncryptable, MasterKey, SignatureAlgorithm, SignedPublicKey, SigningKey,
13-
SymmetricCryptoKey, UnsignedSharedKey, UserKey,
12+
KeyDecryptable, KeyEncryptable, MasterKey, RotateUserKeysResponse, SignatureAlgorithm,
13+
SignedPublicKey, SigningKey, SymmetricCryptoKey, UnsignedSharedKey, UserKey,
1414
};
1515
use bitwarden_error::bitwarden_error;
1616
use schemars::JsonSchema;
@@ -611,6 +611,25 @@ pub fn make_user_signing_keys_for_enrollment(
611611
})
612612
}
613613

614+
/// Gets a set of new wrapped account keys for a user, given a new user key.
615+
///
616+
/// In the current implementation, it just re-encrypts any existing keys. This function expects a
617+
/// user to be a v2 user; that is, they have a signing
618+
pub fn get_v2_rotated_account_keys(
619+
client: &Client,
620+
user_key: String,
621+
) -> Result<RotateUserKeysResponse, CryptoError> {
622+
let key_store = client.internal.get_key_store();
623+
let ctx = key_store.context();
624+
625+
bitwarden_crypto::get_v2_rotated_account_keys(
626+
SymmetricCryptoKey::try_from(user_key)?,
627+
AsymmetricKeyId::UserPrivateKey,
628+
SigningKeyId::UserSigningKey,
629+
&ctx,
630+
)
631+
}
632+
614633
#[cfg(test)]
615634
mod tests {
616635
use std::num::NonZeroU32;

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

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
use bitwarden_crypto::CryptoError;
1+
use bitwarden_crypto::{CryptoError, RotateUserKeysResponse};
22
#[cfg(feature = "internal")]
33
use bitwarden_crypto::{EncString, UnsignedSharedKey};
44
#[cfg(feature = "wasm")]
@@ -18,7 +18,8 @@ use crate::key_management::crypto::{
1818
};
1919
use crate::{
2020
client::encryption_settings::EncryptionSettingsError,
21-
key_management::crypto::CryptoClientError, Client,
21+
key_management::crypto::{get_v2_rotated_account_keys, CryptoClientError},
22+
Client,
2223
};
2324

2425
/// A client for the crypto operations.
@@ -69,6 +70,14 @@ impl CryptoClient {
6970
) -> Result<MakeUserSigningKeysResponse, CryptoError> {
7071
make_user_signing_keys_for_enrollment(&self.client)
7172
}
73+
74+
/// Creates a rotated set of account keys for the current state
75+
pub fn get_v2_rotated_account_keys(
76+
&self,
77+
user_key: String,
78+
) -> Result<RotateUserKeysResponse, CryptoError> {
79+
get_v2_rotated_account_keys(&self.client, user_key)
80+
}
7281
}
7382

7483
impl CryptoClient {
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
use base64::{engine::general_purpose::STANDARD, Engine};
2+
use schemars::JsonSchema;
3+
use serde::{Deserialize, Serialize};
4+
#[cfg(feature = "wasm")]
5+
use tsify_next::Tsify;
6+
7+
use crate::{
8+
CoseSerializable, CryptoError, EncString, KeyEncryptable, KeyStoreContext, SymmetricCryptoKey,
9+
};
10+
11+
/// Rotated set of account keys
12+
#[derive(Serialize, Deserialize, Debug, JsonSchema)]
13+
#[serde(rename_all = "camelCase", deny_unknown_fields)]
14+
#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
15+
#[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))]
16+
pub struct RotateUserKeysResponse {
17+
/// The verifying key
18+
verifying_key: String,
19+
/// Signing key, encrypted with a symmetric key (user key, org key)
20+
signing_key: EncString,
21+
/// The user's public key, signed by the signing key
22+
signed_public_key: String,
23+
// The user's public key, without signature
24+
public_key: String,
25+
// The user's private key, encrypted with the user key
26+
private_key: EncString,
27+
}
28+
29+
/// Re-encrypts the user's keys with the provided symmetric key for a v2 user.
30+
pub fn get_v2_rotated_account_keys<Ids: crate::KeyIds>(
31+
new_user_key: SymmetricCryptoKey,
32+
current_user_private_key_id: Ids::Asymmetric,
33+
current_user_signing_key_id: Ids::Signing,
34+
ctx: &KeyStoreContext<Ids>,
35+
) -> Result<RotateUserKeysResponse, CryptoError> {
36+
let signing_key = ctx.get_signing_key(current_user_signing_key_id)?;
37+
let private_key = ctx.get_asymmetric_key(current_user_private_key_id)?;
38+
let signed_public_key: Vec<u8> = ctx
39+
.make_signed_public_key(current_user_private_key_id, current_user_signing_key_id)?
40+
.into();
41+
42+
Ok(RotateUserKeysResponse {
43+
verifying_key: STANDARD.encode(signing_key.to_verifying_key().to_cose()),
44+
signing_key: signing_key.to_cose().encrypt_with_key(&new_user_key)?,
45+
signed_public_key: STANDARD.encode(&signed_public_key),
46+
public_key: STANDARD.encode(private_key.to_public_key().to_der()?),
47+
private_key: private_key.to_der()?.encrypt_with_key(&new_user_key)?,
48+
})
49+
}

crates/bitwarden-crypto/src/lib.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ pub use error::CryptoError;
2020
pub(crate) use error::Result;
2121
mod fingerprint;
2222
pub use fingerprint::fingerprint;
23+
mod key_rotation;
24+
pub use key_rotation::*;
2325
mod keys;
2426
pub use keys::*;
2527
mod rsa;

crates/bitwarden-crypto/src/store/context.rs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -335,7 +335,10 @@ impl<Ids: KeyIds> KeyStoreContext<'_, Ids> {
335335
.ok_or_else(|| crate::CryptoError::MissingKeyId(format!("{key_id:?}")))
336336
}
337337

338-
fn get_asymmetric_key(&self, key_id: Ids::Asymmetric) -> Result<&AsymmetricCryptoKey> {
338+
pub(crate) fn get_asymmetric_key(
339+
&self,
340+
key_id: Ids::Asymmetric,
341+
) -> Result<&AsymmetricCryptoKey> {
339342
if key_id.is_local() {
340343
self.local_asymmetric_keys.get(key_id)
341344
} else {
@@ -344,7 +347,7 @@ impl<Ids: KeyIds> KeyStoreContext<'_, Ids> {
344347
.ok_or_else(|| crate::CryptoError::MissingKeyId(format!("{key_id:?}")))
345348
}
346349

347-
fn get_signing_key(&self, key_id: Ids::Signing) -> Result<&SigningKey> {
350+
pub(crate) fn get_signing_key(&self, key_id: Ids::Signing) -> Result<&SigningKey> {
348351
if key_id.is_local() {
349352
self.local_signing_keys.get(key_id)
350353
} else {

0 commit comments

Comments
 (0)