bitwarden_vault/cipher/cipher_client/admin/
create.rs1use bitwarden_api_api::models::{CipherCreateRequestModel, CipherRequestModel};
2use bitwarden_core::{
3 ApiError, MissingFieldError, NotAuthenticatedError, UserId, key_management::KeySlotIds,
4};
5use bitwarden_crypto::{CryptoError, IdentifyKey, KeyStore};
6use bitwarden_error::bitwarden_error;
7use thiserror::Error;
8#[cfg(feature = "wasm")]
9use wasm_bindgen::prelude::*;
10
11use crate::{
12 Cipher, CipherView, VaultParseError,
13 cipher::{
14 cipher::{EncryptMode, PartialCipher, StrictDecrypt},
15 cipher_client::create::convert_request_to_cipher_view,
16 },
17 cipher_client::{
18 admin::CipherAdminClient, create::CipherCreateRequest, should_use_blob_encryption,
19 },
20};
21
22#[allow(missing_docs)]
23#[bitwarden_error(flat)]
24#[derive(Debug, Error)]
25pub enum CreateCipherAdminError {
26 #[error(transparent)]
27 Crypto(#[from] CryptoError),
28 #[error(transparent)]
29 Api(#[from] ApiError),
30 #[error(transparent)]
31 VaultParse(#[from] VaultParseError),
32 #[error(transparent)]
33 MissingField(#[from] MissingFieldError),
34 #[error(transparent)]
35 NotAuthenticated(#[from] NotAuthenticatedError),
36}
37
38async fn create_cipher(
40 view: CipherView,
41 encrypted_for: UserId,
42 api_client: &bitwarden_api_api::apis::ApiClient,
43 key_store: &KeyStore<KeySlotIds>,
44 use_strict_decryption: bool,
45 use_blob: bool,
46) -> Result<CipherView, CreateCipherAdminError> {
47 let collection_ids = view.collection_ids.clone();
48 let folder_id = view.folder_id;
51 let favorite = view.favorite;
52
53 let encrypted_by_key_id = key_store
59 .context()
60 .get_symmetric_key_id(view.key_identifier())
61 .map(|id| id.to_string());
62 let mode = if use_blob {
63 EncryptMode::Blob(view)
64 } else {
65 EncryptMode::Legacy(view)
66 };
67 let cipher: Cipher = key_store.encrypt(mode)?;
68 let mut cipher_request: CipherRequestModel = cipher.try_into()?;
69 cipher_request.encrypted_for = Some(encrypted_for.into());
70 cipher_request.encrypted_by_key_id = encrypted_by_key_id;
71
72 let mut cipher: Cipher = api_client
73 .ciphers_api()
74 .post_admin(Some(CipherCreateRequestModel {
75 collection_ids: Some(collection_ids.iter().cloned().map(Into::into).collect()),
76 cipher: Box::new(cipher_request),
77 }))
78 .await?
79 .merge_with_cipher(None)?;
80
81 cipher.collection_ids = collection_ids;
82 cipher.folder_id = folder_id;
83 cipher.favorite = favorite;
84 cipher.edit = true;
85 cipher.view_password = true;
86
87 Ok(if use_strict_decryption {
88 key_store.decrypt(&StrictDecrypt(cipher))?
89 } else {
90 key_store.decrypt(&cipher)?
91 })
92}
93
94#[allow(deprecated)]
95#[cfg_attr(feature = "wasm", wasm_bindgen)]
96impl CipherAdminClient {
97 pub async fn create(
100 &self,
101 request: CipherCreateRequest,
102 ) -> Result<CipherView, CreateCipherAdminError> {
103 let key_store = self.client.internal.get_key_store();
104 let config = self.client.internal.get_api_configurations();
105
106 let user_id = self
107 .client
108 .internal
109 .get_user_id()
110 .ok_or(NotAuthenticatedError)?;
111
112 let mut view: CipherView = convert_request_to_cipher_view(request);
113
114 if self.client.flags().get().await.enable_cipher_key_encryption {
117 let key = view.key_identifier();
118 view.generate_cipher_key(&mut key_store.context(), key)?;
119 }
120
121 let use_blob = should_use_blob_encryption(&key_store.context(), view.organization_id);
122
123 create_cipher(
124 view,
125 user_id,
126 &config.api_client,
127 key_store,
128 self.is_strict_decrypt().await,
129 use_blob,
130 )
131 .await
132 }
133}
134
135#[cfg(test)]
136mod tests {
137 use bitwarden_api_api::models::CipherMiniResponseModel;
138 use bitwarden_core::{OrganizationId, key_management::SymmetricKeySlotId};
139 use bitwarden_crypto::{SymmetricCryptoKey, SymmetricKeyAlgorithm};
140 use chrono::Utc;
141
142 use super::*;
143 use crate::{CipherRepromptType, CipherViewType, LoginView};
144
145 const TEST_CIPHER_ID: &str = "5faa9684-c793-4a2d-8a12-b33900187097";
146 const TEST_COLLECTION_ID: &str = "73546b86-8802-4449-ad2a-69ea981b4ffd";
147 const TEST_USER_ID: &str = "550e8400-e29b-41d4-a716-446655440000";
148 const TEST_ORG_ID: &str = "1bc9ac1e-f5aa-45f2-94bf-b181009709b8";
149
150 #[tokio::test]
151 async fn test_create_org_cipher() {
152 let api_client = bitwarden_api_api::apis::ApiClient::new_mocked(|mock| {
153 mock.ciphers_api
154 .expect_post_admin()
155 .returning(move |request| {
156 let request = request.unwrap();
157
158 Ok(CipherMiniResponseModel {
159 id: Some(TEST_CIPHER_ID.try_into().unwrap()),
160 organization_id: request
161 .cipher
162 .organization_id
163 .and_then(|id| id.parse().ok()),
164 name: Some(request.cipher.name.clone()),
165 r#type: request.cipher.r#type,
166 creation_date: Some(
167 Utc::now().to_rfc3339_opts(chrono::SecondsFormat::Secs, true),
168 ),
169 revision_date: Some(
170 Utc::now().to_rfc3339_opts(chrono::SecondsFormat::Secs, true),
171 ),
172 ..Default::default()
173 })
174 });
175 });
176
177 let store: KeyStore<KeySlotIds> = KeyStore::default();
178 #[allow(deprecated)]
179 let _ = store.context_mut().set_symmetric_key(
180 SymmetricKeySlotId::User,
181 SymmetricCryptoKey::make(SymmetricKeyAlgorithm::Aes256CbcHmac),
182 );
183 #[allow(deprecated)]
184 let _ = store.context_mut().set_symmetric_key(
185 SymmetricKeySlotId::Organization(TEST_ORG_ID.parse::<OrganizationId>().unwrap()),
186 SymmetricCryptoKey::make(SymmetricKeyAlgorithm::Aes256CbcHmac),
187 );
188
189 let test_folder_id: crate::FolderId =
190 "a4e13cc0-1234-5678-abcd-b181009709b8".parse().unwrap();
191 let test_collection_id: bitwarden_collections::collection::CollectionId =
192 TEST_COLLECTION_ID.parse().unwrap();
193
194 let view: CipherView = convert_request_to_cipher_view(CipherCreateRequest {
195 organization_id: Some(TEST_ORG_ID.parse().unwrap()),
196 collection_ids: vec![test_collection_id],
197 folder_id: Some(test_folder_id),
198 name: "Test Cipher".into(),
199 notes: None,
200 favorite: true,
201 reprompt: CipherRepromptType::None,
202 r#type: CipherViewType::Login(LoginView {
203 username: None,
204 password: None,
205 password_revision_date: None,
206 uris: None,
207 totp: None,
208 autofill_on_page_load: None,
209 fido2_credentials: None,
210 }),
211 fields: vec![],
212 archived_date: None,
213 });
214
215 let response = create_cipher(
216 view.clone(),
217 TEST_USER_ID.parse().unwrap(),
218 &api_client,
219 &store,
220 false,
221 false,
222 )
223 .await
224 .unwrap();
225
226 assert_eq!(response.id, Some(TEST_CIPHER_ID.parse().unwrap()));
227 assert_eq!(response.organization_id, view.organization_id);
228 assert_eq!(response.collection_ids, view.collection_ids);
230 assert_eq!(response.folder_id, view.folder_id);
231 assert_eq!(response.favorite, view.favorite);
232 assert!(response.edit, "edit should be true after admin create");
233 assert!(
234 response.view_password,
235 "view_password should be true after admin create"
236 );
237 }
238}