1use bitwarden_core::key_management::KeySlotIds;
2use bitwarden_crypto::{CryptoError, KeyStore};
3use bitwarden_error::bitwarden_error;
4use thiserror::Error;
5
6use super::{
7 cipher::{Cipher, CipherType, CipherView, EncryptMode, StrictDecrypt},
8 cipher_client::should_use_blob_encryption,
9};
10
11#[bitwarden_error(flat)]
13#[derive(Debug, Error)]
14pub enum ChangeLoginPasswordError {
15 #[error("Cannot change the password of a partial cipher")]
17 PartialCipher,
18 #[error("Cannot change the login password of a cipher that is not a login")]
20 NotLogin,
21 #[error(transparent)]
23 Crypto(#[from] CryptoError),
24}
25
26impl Cipher {
27 pub fn change_login_password(
30 self,
31 key_store: &KeyStore<KeySlotIds>,
32 new_password: &str,
33 ) -> Result<Cipher, ChangeLoginPasswordError> {
34 if self.partial_data.is_some() {
36 return Err(ChangeLoginPasswordError::PartialCipher);
37 }
38 if self.r#type != CipherType::Login {
39 return Err(ChangeLoginPasswordError::NotLogin);
40 }
41
42 let mut view: CipherView = key_store.decrypt(&StrictDecrypt(self))?;
44 if view.login.is_none() {
45 return Err(ChangeLoginPasswordError::NotLogin);
46 }
47
48 let original = view.clone();
49 if let Some(login) = view.login.as_mut() {
50 login.password = Some(new_password.to_owned());
51 }
52 view.update_password_history(&original);
53
54 let use_blob = should_use_blob_encryption(&key_store.context(), view.organization_id);
57 let mode = if use_blob {
58 EncryptMode::Blob(view)
59 } else {
60 EncryptMode::Legacy(view)
61 };
62 Ok(key_store.encrypt(mode)?)
63 }
64}
65
66#[cfg(test)]
67mod tests {
68 use bitwarden_core::key_management::{
69 BLOB_SECURITY_VERSION, SymmetricKeySlotId, create_test_crypto_with_user_key,
70 };
71 use bitwarden_crypto::{Decryptable, SymmetricCryptoKey, SymmetricKeyAlgorithm};
72
73 use super::*;
74 use crate::{
75 LoginUriView, LoginView, PasswordHistoryView,
76 cipher::{CipherRepromptType, blob::try_parse_blob},
77 };
78
79 fn key_store() -> KeyStore<KeySlotIds> {
80 create_test_crypto_with_user_key(SymmetricCryptoKey::make(
81 SymmetricKeyAlgorithm::Aes256CbcHmac,
82 ))
83 }
84
85 fn login_view() -> CipherView {
86 CipherView {
87 partial: false,
88 r#type: CipherType::Login,
89 login: Some(LoginView {
90 username: Some("user".to_string()),
91 password: Some("old-password".to_string()),
92 password_revision_date: None,
93 uris: Some(vec![LoginUriView {
94 uri: Some("https://example.com".to_string()),
95 r#match: None,
96 uri_checksum: None,
97 }]),
98 totp: None,
99 autofill_on_page_load: None,
100 fido2_credentials: None,
101 }),
102 id: Some("fd411a1a-fec8-4070-985d-0e6560860e69".parse().unwrap()),
103 organization_id: None,
104 folder_id: None,
105 collection_ids: vec![],
106 key: None,
107 name: "My login".to_string(),
108 notes: None,
109 identity: None,
110 card: None,
111 secure_note: None,
112 ssh_key: None,
113 bank_account: None,
114 drivers_license: None,
115 passport: None,
116 favorite: false,
117 reprompt: CipherRepromptType::None,
118 organization_use_totp: true,
119 edit: true,
120 permissions: None,
121 view_password: true,
122 local_data: None,
123 attachments: None,
124 attachment_decryption_failures: None,
125 fields: None,
126 password_history: None,
127 creation_date: "2024-01-30T17:55:36.150Z".parse().unwrap(),
128 deleted_date: None,
129 revision_date: "2024-01-30T17:55:36.150Z".parse().unwrap(),
130 archived_date: None,
131 }
132 }
133
134 fn encrypt(key_store: &KeyStore<KeySlotIds>, view: CipherView) -> Cipher {
135 key_store.encrypt(EncryptMode::Legacy(view)).unwrap()
136 }
137
138 #[test]
139 fn changes_password_and_keeps_other_fields() {
140 let key_store = key_store();
141 let cipher = encrypt(&key_store, login_view());
142
143 let changed = cipher
144 .change_login_password(&key_store, "new-password")
145 .unwrap();
146
147 let view: CipherView = key_store.decrypt(&changed).unwrap();
148 let login = view.login.unwrap();
149 assert_eq!(login.password.as_deref(), Some("new-password"));
150 assert_eq!(login.username.as_deref(), Some("user"));
151 assert_eq!(
152 login.uris.unwrap()[0].uri.as_deref(),
153 Some("https://example.com")
154 );
155 assert_eq!(view.name, "My login");
156 }
157
158 #[test]
159 fn records_old_password_in_history() {
160 let key_store = key_store();
161 let mut view = login_view();
162 view.password_history = Some(vec![PasswordHistoryView {
163 password: "older-password".to_string(),
164 last_used_date: "2024-01-01T00:00:00Z".parse().unwrap(),
165 }]);
166 let cipher = encrypt(&key_store, view);
167
168 let changed = cipher
169 .change_login_password(&key_store, "new-password")
170 .unwrap();
171
172 let view: CipherView = key_store.decrypt(&changed).unwrap();
173 let history = view.password_history.unwrap();
174 assert_eq!(history.len(), 2);
175 assert_eq!(history[0].password, "old-password");
176 assert_eq!(history[1].password, "older-password");
177 assert!(view.login.unwrap().password_revision_date.is_some());
178 }
179
180 #[test]
181 fn keeps_legacy_format_below_blob_version() {
182 let key_store = key_store();
183 let cipher = encrypt(&key_store, login_view());
184
185 let changed = cipher
186 .change_login_password(&key_store, "new-password")
187 .unwrap();
188
189 assert!(try_parse_blob(&changed).is_none());
190 }
191
192 #[test]
193 fn uses_blob_format_at_blob_version() {
194 let key_store = key_store();
195 key_store.set_security_state_version(BLOB_SECURITY_VERSION);
196 let cipher = key_store.encrypt(EncryptMode::Blob(login_view())).unwrap();
197
198 let changed = cipher
199 .change_login_password(&key_store, "new-password")
200 .unwrap();
201
202 assert!(try_parse_blob(&changed).is_some());
203 let view: CipherView = key_store.decrypt(&changed).unwrap();
204 assert_eq!(
205 view.login.unwrap().password.as_deref(),
206 Some("new-password")
207 );
208 assert_eq!(view.password_history.unwrap()[0].password, "old-password");
209 }
210
211 #[test]
212 fn keeps_the_item_key() {
213 let key_store = key_store();
214 let cipher = encrypt(&key_store, login_view());
215 let original_key = cipher.key.clone().expect("item key is generated");
216
217 let changed = cipher
218 .change_login_password(&key_store, "new-password")
219 .unwrap();
220
221 let mut ctx = key_store.context();
223 let item_key = ctx
224 .unwrap_symmetric_key(SymmetricKeySlotId::User, &original_key)
225 .unwrap();
226 let name: String = changed.name.unwrap().decrypt(&mut ctx, item_key).unwrap();
227 assert_eq!(name, "My login");
228 }
229
230 #[test]
231 fn refuses_non_login_cipher() {
232 let key_store = key_store();
233 let mut view = login_view();
234 view.r#type = CipherType::SecureNote;
235 view.login = None;
236 let cipher = encrypt(&key_store, view);
237
238 let result = cipher.change_login_password(&key_store, "new-password");
239
240 assert!(matches!(result, Err(ChangeLoginPasswordError::NotLogin)));
241 }
242
243 #[test]
244 fn refuses_partial_cipher() {
245 let key_store = key_store();
246 let mut cipher = encrypt(&key_store, login_view());
247 cipher.partial_data = Some("{}".to_string());
248
249 let result = cipher.change_login_password(&key_store, "new-password");
250
251 assert!(matches!(
252 result,
253 Err(ChangeLoginPasswordError::PartialCipher)
254 ));
255 }
256
257 #[test]
258 fn refuses_cipher_with_undecryptable_field() {
259 let key_store = key_store();
260 let other_store = create_test_crypto_with_user_key(SymmetricCryptoKey::make(
261 SymmetricKeyAlgorithm::Aes256CbcHmac,
262 ));
263 let mut cipher = encrypt(&key_store, login_view());
264 let foreign = encrypt(&other_store, login_view());
266 cipher.login.as_mut().unwrap().password = foreign.login.unwrap().password;
267
268 let result = cipher.change_login_password(&key_store, "new-password");
269
270 assert!(matches!(result, Err(ChangeLoginPasswordError::Crypto(_))));
271 }
272}