Skip to content

Commit

Permalink
[PM-6764] Move cipher to organization (#695)
Browse files Browse the repository at this point in the history
## Type of change
```
- [ ] Bug fix
- [x] New feature development
- [ ] Tech debt (refactoring, code cleanup, dependency upgrades, etc)
- [ ] Build/deploy pipeline (DevOps)
- [ ] Other
```

## Objective
Added function to allow moving a cipher to an organization, which
requires reencrypting the cipher key if it has one.
  • Loading branch information
dani-garcia authored Apr 9, 2024
1 parent dac4751 commit 1ccf11b
Show file tree
Hide file tree
Showing 7 changed files with 177 additions and 39 deletions.
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions crates/bitwarden-uniffi/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ chrono = { version = ">=0.4.26, <0.5", features = [
env_logger = "0.11.1"
schemars = { version = ">=0.8, <0.9", optional = true }
uniffi = "=0.26.1"
uuid = ">=1.3.3, <2"

[build-dependencies]
uniffi = { version = "=0.26.1", features = ["build"] }
Expand Down
2 changes: 2 additions & 0 deletions crates/bitwarden-uniffi/src/uniffi_support.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
use bitwarden_crypto::{AsymmetricEncString, EncString, SensitiveString};
use uuid::Uuid;

// Forward the type definitions to the main bitwarden crate
type DateTime = chrono::DateTime<chrono::Utc>;
uniffi::ffi_converter_forward!(DateTime, bitwarden::UniFfiTag, crate::UniFfiTag);
uniffi::ffi_converter_forward!(EncString, bitwarden::UniFfiTag, crate::UniFfiTag);
uniffi::ffi_converter_forward!(AsymmetricEncString, bitwarden::UniFfiTag, crate::UniFfiTag);
uniffi::ffi_converter_forward!(SensitiveString, bitwarden::UniFfiTag, crate::UniFfiTag);
uniffi::ffi_converter_forward!(Uuid, bitwarden::UniFfiTag, crate::UniFfiTag);
18 changes: 18 additions & 0 deletions crates/bitwarden-uniffi/src/vault/ciphers.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use std::sync::Arc;

use bitwarden::vault::{Cipher, CipherListView, CipherView};
use uuid::Uuid;

use crate::{Client, Result};

Expand Down Expand Up @@ -47,4 +48,21 @@ impl ClientCiphers {
.decrypt_list(ciphers)
.await?)
}

/// Move a cipher to an organization, reencrypting the cipher key if necessary
pub async fn move_to_organization(
&self,
cipher: CipherView,
organization_id: Uuid,
) -> Result<CipherView> {
Ok(self
.0
.0
.read()
.await
.vault()
.ciphers()
.move_to_organization(cipher, organization_id)
.await?)
}
}
11 changes: 11 additions & 0 deletions crates/bitwarden/src/mobile/vault/client_ciphers.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
use bitwarden_crypto::{Decryptable, Encryptable, LocateKey};
use uuid::Uuid;

use super::client_vault::ClientVault;
use crate::{
Expand Down Expand Up @@ -44,6 +45,16 @@ impl<'a> ClientCiphers<'a> {

Ok(cipher_views)
}

pub async fn move_to_organization(
&self,
mut cipher_view: CipherView,
organization_id: Uuid,
) -> Result<CipherView> {
let enc = self.client.get_encryption_settings()?;
cipher_view.move_to_organization(enc, organization_id)?;
Ok(cipher_view)
}
}

impl<'a> ClientVault<'a> {
Expand Down
157 changes: 119 additions & 38 deletions crates/bitwarden/src/vault/cipher/cipher.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use bitwarden_api_api::models::CipherDetailsResponseModel;
use bitwarden_crypto::{
CryptoError, EncString, KeyContainer, KeyDecryptable, KeyEncryptable, LocateKey,
CryptoError, EncString, KeyContainer, KeyDecryptable, KeyEncryptable, LocateKey, SensitiveVec,
SymmetricCryptoKey,
};
use chrono::{DateTime, Utc};
Expand Down Expand Up @@ -325,6 +325,29 @@ impl CipherView {
uris.retain(|u| u.is_checksum_valid());
}
}

pub fn move_to_organization(
&mut self,
enc: &dyn KeyContainer,
organization_id: Uuid,
) -> Result<()> {
// If the cipher has a key, we need to re-encrypt it with the new organization key
if let Some(cipher_key) = &mut self.key {
let old_key = enc
.get_key(&self.organization_id)
.ok_or(Error::VaultLocked)?;

let new_key = enc
.get_key(&Some(organization_id))
.ok_or(Error::VaultLocked)?;

let dec_cipher_key = SensitiveVec::new(Box::new(cipher_key.decrypt_with_key(old_key)?));
*cipher_key = dec_cipher_key.expose().encrypt_with_key(new_key)?;
}

self.organization_id = Some(organization_id);
Ok(())
}
}

impl KeyDecryptable<SymmetricCryptoKey, CipherListView> for Cipher {
Expand Down Expand Up @@ -443,49 +466,51 @@ impl From<bitwarden_api_api::models::CipherRepromptType> for CipherRepromptType
#[cfg(test)]
mod tests {

use std::collections::HashMap;

use super::*;

fn generate_cipher() -> CipherView {
CipherView {
r#type: CipherType::Login,
login: Some(login::LoginView {
username: Some("test_username".to_string()),
password: Some("test_password".to_string()),
password_revision_date: None,
uris: None,
totp: None,
autofill_on_page_load: None,
fido2_credentials: None,
}),
id: "fd411a1a-fec8-4070-985d-0e6560860e69".parse().ok(),
organization_id: None,
folder_id: None,
collection_ids: vec![],
key: None,
name: "My test login".to_string(),
notes: None,
identity: None,
card: None,
secure_note: None,
favorite: false,
reprompt: CipherRepromptType::None,
organization_use_totp: true,
edit: true,
view_password: true,
local_data: None,
attachments: None,
fields: None,
password_history: None,
creation_date: "2024-01-30T17:55:36.150Z".parse().unwrap(),
deleted_date: None,
revision_date: "2024-01-30T17:55:36.150Z".parse().unwrap(),
}
}

#[test]
fn test_generate_cipher_key() {
let key = SymmetricCryptoKey::generate(rand::thread_rng());

fn generate_cipher() -> CipherView {
CipherView {
r#type: CipherType::Login,
login: Some(login::LoginView {
username: Some("test_username".to_string()),
password: Some("test_password".to_string()),
password_revision_date: None,
uris: None,
totp: None,
autofill_on_page_load: None,
fido2_credentials: None,
}),
id: "fd411a1a-fec8-4070-985d-0e6560860e69".parse().ok(),
organization_id: None,
folder_id: None,
collection_ids: vec![],
key: None,
name: "My test login".to_string(),
notes: None,
identity: None,
card: None,
secure_note: None,
favorite: false,
reprompt: CipherRepromptType::None,
organization_use_totp: true,
edit: true,
view_password: true,
local_data: None,
attachments: None,
fields: None,
password_history: None,
creation_date: "2024-01-30T17:55:36.150Z".parse().unwrap(),
deleted_date: None,
revision_date: "2024-01-30T17:55:36.150Z".parse().unwrap(),
}
}

let original_cipher = generate_cipher();

// Check that the cipher gets encrypted correctly without it's own key
Expand All @@ -504,4 +529,60 @@ mod tests {
assert!(key_cipher_dec.key.is_some());
assert_eq!(key_cipher_dec.name, original_cipher.name);
}

struct MockKeyContainer(HashMap<Option<Uuid>, SymmetricCryptoKey>);
impl KeyContainer for MockKeyContainer {
fn get_key<'a>(&'a self, org_id: &Option<Uuid>) -> Option<&'a SymmetricCryptoKey> {
self.0.get(org_id)
}
}

#[test]
fn test_move_user_cipher_to_org() {
let org = uuid::Uuid::new_v4();

let enc = MockKeyContainer(HashMap::from([
(None, SymmetricCryptoKey::generate(rand::thread_rng())),
(Some(org), SymmetricCryptoKey::generate(rand::thread_rng())),
]));

// Create a cipher with a user key
let mut cipher = generate_cipher();
cipher
.generate_cipher_key(enc.get_key(&None).unwrap())
.unwrap();

cipher.move_to_organization(&enc, org).unwrap();
assert_eq!(cipher.organization_id, Some(org));

// Check that the cipher can be encrypted/decrypted with the new org key
let org_key = enc.get_key(&Some(org)).unwrap();
let cipher_enc = cipher.encrypt_with_key(org_key).unwrap();
let cipher_dec: CipherView = cipher_enc.decrypt_with_key(org_key).unwrap();

assert_eq!(cipher_dec.name, "My test login");
}

#[test]
fn test_move_user_cipher_to_org_manually() {
let org = uuid::Uuid::new_v4();

let enc = MockKeyContainer(HashMap::from([
(None, SymmetricCryptoKey::generate(rand::thread_rng())),
(Some(org), SymmetricCryptoKey::generate(rand::thread_rng())),
]));

// Create a cipher with a user key
let mut cipher = generate_cipher();
cipher
.generate_cipher_key(enc.get_key(&None).unwrap())
.unwrap();

cipher.organization_id = Some(org);

// Check that the cipher can not be encrypted, as the
// cipher key is tied to the user key and not the org key
let org_key = enc.get_key(&Some(org)).unwrap();
assert!(cipher.encrypt_with_key(org_key).is_err());
}
}
26 changes: 25 additions & 1 deletion languages/kotlin/doc.md
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,18 @@ Generate keys needed for registration process

**Output**: std::result::Result<RegisterKeyResponse,BitwardenError>

### `make_register_tde_keys`

Generate keys needed for TDE process

**Arguments**:

- self:
- org_public_key: String
- remember_device:

**Output**: std::result::Result<RegisterTdeKeyResponse,BitwardenError>

### `validate_password`

Validate the user password
Expand Down Expand Up @@ -288,6 +300,18 @@ Decrypt cipher list

**Output**: std::result::Result<Vec,BitwardenError>

### `move_to_organization`

Move a cipher to an organization, reencrypting the cipher key if necessary

**Arguments**:

- self:
- cipher: [CipherView](#cipherview)
- organization_id: Uuid

**Output**: std::result::Result<CipherView,BitwardenError>

## ClientCollections

### `decrypt`
Expand Down Expand Up @@ -346,7 +370,7 @@ as it can be used to decrypt all of the user&#x27;s data

- self:

**Output**: std::result::Result<String,BitwardenError>
**Output**: std::result::Result<SensitiveString,BitwardenError>

### `update_password`

Expand Down

0 comments on commit 1ccf11b

Please sign in to comment.