From 878cd4a4d3a27c05e3306cf23d5a37920d313400 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Garc=C3=ADa?= Date: Wed, 31 Jan 2024 13:49:07 +0100 Subject: [PATCH 1/5] Implement password update support (#566) ## Type of change ``` - [ ] Bug fix - [x] New feature development - [ ] Tech debt (refactoring, code cleanup, dependency upgrades, etc) - [ ] Build/deploy pipeline (DevOps) - [ ] Other ``` ## Objective Support re-encrypting the user key with the new password. --- crates/bitwarden-uniffi/src/crypto.rs | 15 ++- crates/bitwarden/src/mobile/client_crypto.rs | 11 +- crates/bitwarden/src/mobile/crypto.rs | 120 +++++++++++++++++++ languages/kotlin/doc.md | 12 ++ 4 files changed, 156 insertions(+), 2 deletions(-) diff --git a/crates/bitwarden-uniffi/src/crypto.rs b/crates/bitwarden-uniffi/src/crypto.rs index 92afcbb87..2d847b33d 100644 --- a/crates/bitwarden-uniffi/src/crypto.rs +++ b/crates/bitwarden-uniffi/src/crypto.rs @@ -1,7 +1,7 @@ use std::sync::Arc; use bitwarden::mobile::crypto::{ - DerivePinKeyResponse, InitOrgCryptoRequest, InitUserCryptoRequest, + DerivePinKeyResponse, InitOrgCryptoRequest, InitUserCryptoRequest, UpdatePasswordResponse, }; use bitwarden_crypto::EncString; @@ -51,6 +51,19 @@ impl ClientCrypto { .await?) } + /// Update the user's password, which will re-encrypt the user's encryption key with the new + /// password. This returns the new encrypted user key and the new password hash. + pub async fn update_password(&self, new_password: String) -> Result { + Ok(self + .0 + .0 + .write() + .await + .crypto() + .update_password(new_password) + .await?) + } + /// Generates a PIN protected user key from the provided PIN. The result can be stored and later /// used to initialize another client instance by using the PIN and the PIN key with /// `initialize_user_crypto`. diff --git a/crates/bitwarden/src/mobile/client_crypto.rs b/crates/bitwarden/src/mobile/client_crypto.rs index 6f8887002..f6ea3346b 100644 --- a/crates/bitwarden/src/mobile/client_crypto.rs +++ b/crates/bitwarden/src/mobile/client_crypto.rs @@ -7,7 +7,8 @@ use crate::{ error::Result, mobile::crypto::{ derive_pin_key, derive_pin_user_key, get_user_encryption_key, initialize_org_crypto, - initialize_user_crypto, DerivePinKeyResponse, InitOrgCryptoRequest, InitUserCryptoRequest, + initialize_user_crypto, update_password, DerivePinKeyResponse, InitOrgCryptoRequest, + InitUserCryptoRequest, UpdatePasswordResponse, }, }; @@ -31,6 +32,14 @@ impl<'a> ClientCrypto<'a> { get_user_encryption_key(self.client).await } + #[cfg(feature = "internal")] + pub async fn update_password( + &mut self, + new_password: String, + ) -> Result { + update_password(self.client, new_password) + } + #[cfg(feature = "internal")] pub async fn derive_pin_key(&mut self, pin: String) -> Result { derive_pin_key(self.client, pin) diff --git a/crates/bitwarden/src/mobile/crypto.rs b/crates/bitwarden/src/mobile/crypto.rs index bd71af2be..9b431b4b4 100644 --- a/crates/bitwarden/src/mobile/crypto.rs +++ b/crates/bitwarden/src/mobile/crypto.rs @@ -125,6 +125,53 @@ pub async fn get_user_encryption_key(client: &mut Client) -> Result { Ok(user_key.to_base64()) } +#[cfg(feature = "internal")] +#[derive(Serialize, Deserialize, Debug, JsonSchema)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +#[cfg_attr(feature = "mobile", derive(uniffi::Record))] +pub struct UpdatePasswordResponse { + /// Hash of the new password + password_hash: String, + /// User key, encrypted with the new password + new_key: EncString, +} + +pub fn update_password( + client: &mut Client, + new_password: String, +) -> Result { + let user_key = client + .get_encryption_settings()? + .get_key(&None) + .ok_or(Error::VaultLocked)?; + + let login_method = client + .login_method + .as_ref() + .ok_or(Error::NotAuthenticated)?; + + // Derive a new master key from password + let new_master_key = match login_method { + LoginMethod::User( + UserLoginMethod::Username { email, kdf, .. } + | UserLoginMethod::ApiKey { email, kdf, .. }, + ) => MasterKey::derive(new_password.as_bytes(), email.as_bytes(), kdf)?, + _ => return Err(Error::NotAuthenticated), + }; + + let new_key = new_master_key.encrypt_user_key(user_key)?; + + let password_hash = new_master_key.derive_master_key_hash( + new_password.as_bytes(), + bitwarden_crypto::HashPurpose::ServerAuthorization, + )?; + + Ok(UpdatePasswordResponse { + password_hash, + new_key, + }) +} + #[cfg(feature = "internal")] #[derive(Serialize, Deserialize, Debug, JsonSchema)] #[serde(rename_all = "camelCase", deny_unknown_fields)] @@ -194,6 +241,79 @@ mod tests { use super::*; use crate::{client::Kdf, Client}; + #[tokio::test] + async fn test_update_password() { + let mut client = Client::new(None); + + let priv_key = "2.kmLY8NJVuiKBFJtNd/ZFpA==|qOodlRXER+9ogCe3yOibRHmUcSNvjSKhdDuztLlucs10jLiNoVVVAc+9KfNErLSpx5wmUF1hBOJM8zwVPjgQTrmnNf/wuDpwiaCxNYb/0v4FygPy7ccAHK94xP1lfqq7U9+tv+/yiZSwgcT+xF0wFpoxQeNdNRFzPTuD9o4134n8bzacD9DV/WjcrXfRjbBCzzuUGj1e78+A7BWN7/5IWLz87KWk8G7O/W4+8PtEzlwkru6Wd1xO19GYU18oArCWCNoegSmcGn7w7NDEXlwD403oY8Oa7ylnbqGE28PVJx+HLPNIdSC6YKXeIOMnVs7Mctd/wXC93zGxAWD6ooTCzHSPVV50zKJmWIG2cVVUS7j35H3rGDtUHLI+ASXMEux9REZB8CdVOZMzp2wYeiOpggebJy6MKOZqPT1R3X0fqF2dHtRFPXrNsVr1Qt6bS9qTyO4ag1/BCvXF3P1uJEsI812BFAne3cYHy5bIOxuozPfipJrTb5WH35bxhElqwT3y/o/6JWOGg3HLDun31YmiZ2HScAsUAcEkA4hhoTNnqy4O2s3yVbCcR7jF7NLsbQc0MDTbnjxTdI4VnqUIn8s2c9hIJy/j80pmO9Bjxp+LQ9a2hUkfHgFhgHxZUVaeGVth8zG2kkgGdrp5VHhxMVFfvB26Ka6q6qE/UcS2lONSv+4T8niVRJz57qwctj8MNOkA3PTEfe/DP/LKMefke31YfT0xogHsLhDkx+mS8FCc01HReTjKLktk/Jh9mXwC5oKwueWWwlxI935ecn+3I2kAuOfMsgPLkoEBlwgiREC1pM7VVX1x8WmzIQVQTHd4iwnX96QewYckGRfNYWz/zwvWnjWlfcg8kRSe+68EHOGeRtC5r27fWLqRc0HNcjwpgHkI/b6czerCe8+07TWql4keJxJxhBYj3iOH7r9ZS8ck51XnOb8tGL1isimAJXodYGzakwktqHAD7MZhS+P02O+6jrg7d+yPC2ZCuS/3TOplYOCHQIhnZtR87PXTUwr83zfOwAwCyv6KP84JUQ45+DItrXLap7nOVZKQ5QxYIlbThAO6eima6Zu5XHfqGPMNWv0bLf5+vAjIa5np5DJrSwz9no/hj6CUh0iyI+SJq4RGI60lKtypMvF6MR3nHLEHOycRUQbZIyTHWl4QQLdHzuwN9lv10ouTEvNr6sFflAX2yb6w3hlCo7oBytH3rJekjb3IIOzBpeTPIejxzVlh0N9OT5MZdh4sNKYHUoWJ8mnfjdM+L4j5Q2Kgk/XiGDgEebkUxiEOQUdVpePF5uSCE+TPav/9FIRGXGiFn6NJMaU7aBsDTFBLloffFLYDpd8/bTwoSvifkj7buwLYM+h/qcnfdy5FWau1cKav+Blq/ZC0qBpo658RTC8ZtseAFDgXoQZuksM10hpP9bzD04Bx30xTGX81QbaSTNwSEEVrOtIhbDrj9OI43KH4O6zLzK+t30QxAv5zjk10RZ4+5SAdYndIlld9Y62opCfPDzRy3ubdve4ZEchpIKWTQvIxq3T5ogOhGaWBVYnkMtM2GVqvWV//46gET5SH/MdcwhACUcZ9kCpMnWH9CyyUwYvTT3UlNyV+DlS27LMPvaw7tx7qa+GfNCoCBd8S4esZpQYK/WReiS8=|pc7qpD42wxyXemdNPuwxbh8iIaryrBPu8f/DGwYdHTw="; + + let kdf = Kdf::PBKDF2 { + iterations: 100_000.try_into().unwrap(), + }; + + initialize_user_crypto( + &mut client, + InitUserCryptoRequest { + kdf_params: kdf.clone(), + email: "test@bitwarden.com".into(), + private_key: priv_key.to_owned(), + method: InitUserCryptoMethod::Password { + password: "asdfasdfasdf".into(), + user_key: "2.u2HDQ/nH2J7f5tYHctZx6Q==|NnUKODz8TPycWJA5svexe1wJIz2VexvLbZh2RDfhj5VI3wP8ZkR0Vicvdv7oJRyLI1GyaZDBCf9CTBunRTYUk39DbZl42Rb+Xmzds02EQhc=|rwuo5wgqvTJf3rgwOUfabUyzqhguMYb3sGBjOYqjevc=".into(), + }, + }, + ) + .await + .unwrap(); + + let new_password_response = update_password(&mut client, "123412341234".into()).unwrap(); + + let mut client2 = Client::new(None); + + initialize_user_crypto( + &mut client2, + InitUserCryptoRequest { + kdf_params: kdf.clone(), + email: "test@bitwarden.com".into(), + private_key: priv_key.to_owned(), + method: InitUserCryptoMethod::Password { + password: "123412341234".into(), + user_key: new_password_response.new_key.to_string(), + }, + }, + ) + .await + .unwrap(); + + let new_hash = client2 + .kdf() + .hash_password( + "test@bitwarden.com".into(), + "123412341234".into(), + kdf.clone(), + bitwarden_crypto::HashPurpose::ServerAuthorization, + ) + .await + .unwrap(); + + assert_eq!(new_hash, new_password_response.password_hash); + + assert_eq!( + client + .get_encryption_settings() + .unwrap() + .get_key(&None) + .unwrap() + .to_base64(), + client2 + .get_encryption_settings() + .unwrap() + .get_key(&None) + .unwrap() + .to_base64() + ); + } + #[tokio::test] async fn test_initialize_user_crypto_pin() { let mut client = Client::new(None); diff --git a/languages/kotlin/doc.md b/languages/kotlin/doc.md index fd98e463e..f85222738 100644 --- a/languages/kotlin/doc.md +++ b/languages/kotlin/doc.md @@ -311,6 +311,18 @@ as it can be used to decrypt all of the user's data **Output**: std::result::Result +### `update_password` + +Update the user's password, which will re-encrypt the user's encryption key with the new +password. This returns the new encrypted user key and the new password hash. + +**Arguments**: + +- self: +- new_password: String + +**Output**: std::result::Result + ### `derive_pin_key` Generates a PIN protected user key from the provided PIN. The result can be stored and later used to From 8b468ec34a770f8857408382b8a71624a9176ad1 Mon Sep 17 00:00:00 2001 From: Oscar Hinton Date: Wed, 31 Jan 2024 14:20:04 +0100 Subject: [PATCH 2/5] Fix device keys incorrectly being treated as strings (#564) Device keys are generated from random u8 vectors. We incorrectly attempted to parse these as strings when decrypting them in the last trust device step. Added a complete test which confirms the whole device approval process. --- Cargo.lock | 1 + crates/bitwarden/Cargo.toml | 1 + crates/bitwarden/src/auth/auth_request.rs | 86 ++++++++++++++++++++--- 3 files changed, 80 insertions(+), 8 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index c4e1fe3c2..8ebf52136 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -373,6 +373,7 @@ dependencies = [ "uniffi", "uuid", "wiremock", + "zeroize", "zxcvbn", ] diff --git a/crates/bitwarden/Cargo.toml b/crates/bitwarden/Cargo.toml index 1f666e21c..b52e2f8f3 100644 --- a/crates/bitwarden/Cargo.toml +++ b/crates/bitwarden/Cargo.toml @@ -78,3 +78,4 @@ reqwest = { version = "*", features = [ rand_chacha = "0.3.1" tokio = { version = "1.35.1", features = ["rt", "macros"] } wiremock = "0.5.22" +zeroize = { version = ">=1.7.0, <2.0", features = ["derive", "aarch64"] } diff --git a/crates/bitwarden/src/auth/auth_request.rs b/crates/bitwarden/src/auth/auth_request.rs index facdc8f82..98fe4996a 100644 --- a/crates/bitwarden/src/auth/auth_request.rs +++ b/crates/bitwarden/src/auth/auth_request.rs @@ -58,9 +58,9 @@ pub(crate) fn auth_request_decrypt_user_key( user_key: AsymmetricEncString, ) -> Result { let key = AsymmetricCryptoKey::from_der(&STANDARD.decode(private_key)?)?; - let key: String = user_key.decrypt_with_key(&key)?; + let mut key: Vec = user_key.decrypt_with_key(&key)?; - Ok(key.parse()?) + Ok(SymmetricCryptoKey::try_from(key.as_mut_slice())?) } /// Approve an auth request. @@ -83,20 +83,25 @@ pub(crate) fn approve_auth_request( #[test] fn test_auth_request() { + use zeroize::Zeroizing; + let request = new_auth_request("test@bitwarden.com").unwrap(); - let secret = - "w2LO+nwV4oxwswVYCxlOfRUseXfvU03VzvKQHrqeklPgiMZrspUe6sOBToCnDn9Ay0tuCBn8ykVVRb7PWhub2Q=="; + let secret: &[u8] = &[ + 111, 32, 97, 169, 4, 241, 174, 74, 239, 206, 113, 86, 174, 68, 216, 238, 52, 85, 156, 27, + 134, 149, 54, 55, 91, 147, 45, 130, 131, 237, 51, 31, 191, 106, 155, 14, 160, 82, 47, 40, + 96, 31, 114, 127, 212, 187, 167, 110, 205, 116, 198, 243, 218, 72, 137, 53, 248, 43, 255, + 67, 35, 61, 245, 93, + ]; let private_key = AsymmetricCryptoKey::from_der(&STANDARD.decode(&request.private_key).unwrap()).unwrap(); - let encrypted = - AsymmetricEncString::encrypt_rsa2048_oaep_sha1(secret.as_bytes(), &private_key).unwrap(); + let encrypted = AsymmetricEncString::encrypt_rsa2048_oaep_sha1(secret, &private_key).unwrap(); let decrypted = auth_request_decrypt_user_key(request.private_key, encrypted).unwrap(); - assert_eq!(decrypted.to_base64(), secret); + assert_eq!(decrypted.to_vec(), Zeroizing::new(secret.to_owned())); } #[cfg(test)] @@ -106,7 +111,10 @@ mod tests { use bitwarden_crypto::Kdf; use super::*; - use crate::client::{LoginMethod, UserLoginMethod}; + use crate::{ + client::{LoginMethod, UserLoginMethod}, + mobile::crypto::{InitUserCryptoMethod, InitUserCryptoRequest}, + }; #[test] fn test_approve() { @@ -134,4 +142,66 @@ mod tests { approve_auth_request(&mut client, public_key.to_owned()).unwrap(); } + + #[tokio::test] + async fn test_device_login() { + let kdf = Kdf::PBKDF2 { + iterations: NonZeroU32::new(600_000).unwrap(), + }; + let email = "test@bitwarden.com"; + + let user_key = "2.Q/2PhzcC7GdeiMHhWguYAQ==|GpqzVdr0go0ug5cZh1n+uixeBC3oC90CIe0hd/HWA/pTRDZ8ane4fmsEIcuc8eMKUt55Y2q/fbNzsYu41YTZzzsJUSeqVjT8/iTQtgnNdpo=|dwI+uyvZ1h/iZ03VQ+/wrGEFYVewBUUl/syYgjsNMbE=".parse().unwrap(); + let private_key = "2.yN7l00BOlUE0Sb0M//Q53w==|EwKG/BduQRQ33Izqc/ogoBROIoI5dmgrxSo82sgzgAMIBt3A2FZ9vPRMY+GWT85JiqytDitGR3TqwnFUBhKUpRRAq4x7rA6A1arHrFp5Tp1p21O3SfjtvB3quiOKbqWk6ZaU1Np9HwqwAecddFcB0YyBEiRX3VwF2pgpAdiPbSMuvo2qIgyob0CUoC/h4Bz1be7Qa7B0Xw9/fMKkB1LpOm925lzqosyMQM62YpMGkjMsbZz0uPopu32fxzDWSPr+kekNNyLt9InGhTpxLmq1go/pXR2uw5dfpXc5yuta7DB0EGBwnQ8Vl5HPdDooqOTD9I1jE0mRyuBpWTTI3FRnu3JUh3rIyGBJhUmHqGZvw2CKdqHCIrQeQkkEYqOeJRJVdBjhv5KGJifqT3BFRwX/YFJIChAQpebNQKXe/0kPivWokHWwXlDB7S7mBZzhaAPidZvnuIhalE2qmTypDwHy22FyqV58T8MGGMchcASDi/QXI6kcdpJzPXSeU9o+NC68QDlOIrMVxKFeE7w7PvVmAaxEo0YwmuAzzKy9QpdlK0aab/xEi8V4iXj4hGepqAvHkXIQd+r3FNeiLfllkb61p6WTjr5urcmDQMR94/wYoilpG5OlybHdbhsYHvIzYoLrC7fzl630gcO6t4nM24vdB6Ymg9BVpEgKRAxSbE62Tqacxqnz9AcmgItb48NiR/He3n3ydGjPYuKk/ihZMgEwAEZvSlNxYONSbYrIGDtOY+8Nbt6KiH3l06wjZW8tcmFeVlWv+tWotnTY9IqlAfvNVTjtsobqtQnvsiDjdEVtNy/s2ci5TH+NdZluca2OVEr91Wayxh70kpM6ib4UGbfdmGgCo74gtKvKSJU0rTHakQ5L9JlaSDD5FamBRyI0qfL43Ad9qOUZ8DaffDCyuaVyuqk7cz9HwmEmvWU3VQ+5t06n/5kRDXttcw8w+3qClEEdGo1KeENcnXCB32dQe3tDTFpuAIMLqwXs6FhpawfZ5kPYvLPczGWaqftIs/RXJ/EltGc0ugw2dmTLpoQhCqrcKEBDoYVk0LDZKsnzitOGdi9mOWse7Se8798ib1UsHFUjGzISEt6upestxOeupSTOh0v4+AjXbDzRUyogHww3V+Bqg71bkcMxtB+WM+pn1XNbVTyl9NR040nhP7KEf6e9ruXAtmrBC2ah5cFEpLIot77VFZ9ilLuitSz+7T8n1yAh1IEG6xxXxninAZIzi2qGbH69O5RSpOJuJTv17zTLJQIIc781JwQ2TTwTGnx5wZLbffhCasowJKd2EVcyMJyhz6ru0PvXWJ4hUdkARJs3Xu8dus9a86N8Xk6aAPzBDqzYb1vyFIfBxP0oO8xFHgd30Cgmz8UrSE3qeWRrF8ftrI6xQnFjHBGWD/JWSvd6YMcQED0aVuQkuNW9ST/DzQThPzRfPUoiL10yAmV7Ytu4fR3x2sF0Yfi87YhHFuCMpV/DsqxmUizyiJuD938eRcH8hzR/VO53Qo3UIsqOLcyXtTv6THjSlTopQ+JOLOnHm1w8dzYbLN44OG44rRsbihMUQp+wUZ6bsI8rrOnm9WErzkbQFbrfAINdoCiNa6cimYIjvvnMTaFWNymqY1vZxGztQiMiHiHYwTfwHTXrb9j0uPM=|09J28iXv9oWzYtzK2LBT6Yht4IT4MijEkk0fwFdrVQ4="; + + // Initialize an existing client which is unlocked + let mut existing_device = Client::new(None); + existing_device.set_login_method(LoginMethod::User(UserLoginMethod::Username { + client_id: "123".to_owned(), + email: email.to_owned(), + kdf: kdf.clone(), + })); + + existing_device + .initialize_user_crypto("asdfasdfasdf", user_key, private_key.parse().unwrap()) + .unwrap(); + + // Initialize a new device which will request to be logged in + let mut new_device = Client::new(None); + + // Initialize an auth request, and approve it on the existing device + let auth_req = new_auth_request(email).unwrap(); + let approved_req = approve_auth_request(&mut existing_device, auth_req.public_key).unwrap(); + + // Unlock the vault using the approved request + new_device + .crypto() + .initialize_user_crypto(InitUserCryptoRequest { + kdf_params: kdf, + email: email.to_owned(), + private_key: private_key.to_owned(), + method: InitUserCryptoMethod::AuthRequest { + request_private_key: auth_req.private_key, + protected_user_key: approved_req, + }, + }) + .await + .unwrap(); + + // We can validate that the vault is unlocked correctly by confirming the user key is the + // same + assert_eq!( + existing_device + .get_encryption_settings() + .unwrap() + .get_key(&None) + .unwrap() + .to_base64(), + new_device + .get_encryption_settings() + .unwrap() + .get_key(&None) + .unwrap() + .to_base64() + ); + } } From 4a2089cd9b885f51025d255f6afcc78ebf03b13e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Ch=C4=99ci=C5=84ski?= Date: Wed, 31 Jan 2024 16:31:30 +0100 Subject: [PATCH 3/5] Add workflow linter (#567) ## Type of change ``` - [ ] Bug fix - [ ] New feature development - [ ] Tech debt (refactoring, code cleanup, dependency upgrades, etc) - [x] Build/deploy pipeline (DevOps) - [ ] Other ``` ## Objective ## Code changes - **.github/workflows/workflow-linter.yml:** Add workflow linter ## Before you submit - Please add **unit tests** where it makes sense to do so --- .github/workflows/workflow-linter.yml | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 .github/workflows/workflow-linter.yml diff --git a/.github/workflows/workflow-linter.yml b/.github/workflows/workflow-linter.yml new file mode 100644 index 000000000..24f10f1e4 --- /dev/null +++ b/.github/workflows/workflow-linter.yml @@ -0,0 +1,12 @@ +--- +name: Workflow linter + +on: + pull_request: + paths: + - .github/workflows/** + +jobs: + call-workflow: + name: Lint + uses: bitwarden/gh-actions/.github/workflows/workflow-linter.yml@main From 8c6b21562bc5bb5b862a0116f74d375265382ea9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Ch=C4=99ci=C5=84ski?= Date: Wed, 31 Jan 2024 16:33:18 +0100 Subject: [PATCH 4/5] [DEVOPS-1655] Build publish workflow for python sdk (#558) ## Type of change ``` - [ ] Bug fix - [ ] New feature development - [ ] Tech debt (refactoring, code cleanup, dependency upgrades, etc) - [x] Build/deploy pipeline (DevOps) - [ ] Other ``` ## Objective Create publish workflow for Python SDK to publish it to pypi. ## Code changes - **.github/workflows/publish-python.yml:** Add steps to publish python SDK to pypi ## Before you submit - Please add **unit tests** where it makes sense to do so --------- Co-authored-by: MtnBurrit0 <77340197+mimartin12@users.noreply.github.com> --- .github/workflows/publish-python.yml | 94 ++++++++++++++++++++++++++-- .github/workflows/version-bump.yml | 8 +++ 2 files changed, 98 insertions(+), 4 deletions(-) diff --git a/.github/workflows/publish-python.yml b/.github/workflows/publish-python.yml index bccde1aa2..780195161 100644 --- a/.github/workflows/publish-python.yml +++ b/.github/workflows/publish-python.yml @@ -1,13 +1,99 @@ --- name: Publish Python SDK +run-name: Publish Python SDK ${{ inputs.release_type }} on: workflow_dispatch: + inputs: + release_type: + description: "Release Options" + required: true + default: "Release" + type: choice + options: + - Release + - Dry Run + +defaults: + run: + shell: bash jobs: - stub: - name: Stub + setup: + name: Setup runs-on: ubuntu-22.04 steps: - - name: Stub - run: echo "Stub" + - name: Checkout repo + uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + + - name: Branch check + if: ${{ github.event.inputs.release_type != 'Dry Run' }} + run: | + if [[ "$GITHUB_REF" != "refs/heads/rc" ]] && [[ "$GITHUB_REF" != "refs/heads/hotfix-rc" ]]; then + echo "===================================" + echo "[!] Can only release from the 'rc' or 'hotfix-rc' branches" + echo "===================================" + exit 1 + fi + + publish: + name: Publish + runs-on: ubuntu-22.04 + needs: setup + steps: + - name: Install Python + uses: actions/setup-python@0a5c61591373683505ea898e09a3ea4f39ef2b9c # v5.0.0 + with: + python-version: "3.9" + + - name: Install twine + run: pip install twine + + - name: Download artifacts + uses: dawidd6/action-download-artifact@e7466d1a7587ed14867642c2ca74b5bcc1e19a2d # v3.0.0 + with: + workflow: build-python-wheels.yml + path: ${{ github.workspace }}/target/wheels/dist + workflow_conclusion: success + branch: ${{ github.event.inputs.release_type == 'Dry Run' && 'main' || github.ref_name }} + name: bitwarden_sdk(.*) + name_is_regexp: true + + - name: Move files + working-directory: ${{ github.workspace }}/target/wheels/dist + run: | + find . -maxdepth 2 -type f -print0 | xargs -0 mv -t . + rm -rf */ + + - name: Login to Azure + uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0 + with: + creds: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }} + + - name: Retrieve pypi api token + id: retrieve-secret + uses: bitwarden/gh-actions/get-keyvault-secrets@main + with: + keyvault: "bitwarden-ci" + secrets: "pypi-api-token, + pypi-test-api-token" + + - name: Check + working-directory: ${{ github.workspace }}/target/wheels + run: twine check dist/* + + - name: Publish + if: ${{ github.event.inputs.release_type != 'Dry Run' }} + working-directory: ${{ github.workspace }}/target/wheels + env: + TWINE_USERNAME: __token__ + TWINE_PASSWORD: ${{ steps.retrieve-secret.outputs.pypi-api-token }} + run: twine upload --repository pypi dist/* + + - name: Dry Run - Publish + if: ${{ github.event.inputs.release_type == 'Dry Run' }} + working-directory: ${{ github.workspace }}/target/wheels + env: + TWINE_USERNAME: __token__ + TWINE_PASSWORD: ${{ steps.retrieve-secret.outputs.pypi-test-api-token }} + run: twine upload --repository testpypi dist/* diff --git a/.github/workflows/version-bump.yml b/.github/workflows/version-bump.yml index c3055f097..e40f68a8e 100644 --- a/.github/workflows/version-bump.yml +++ b/.github/workflows/version-bump.yml @@ -18,6 +18,7 @@ on: - bitwarden-json - cli - napi + - python-sdk version_number: description: "New version (example: '2024.1.0')" required: true @@ -142,6 +143,13 @@ jobs: if: ${{ inputs.project == 'bitwarden-json' }} run: cargo-set-version set-version -p bitwarden-json ${{ inputs.version_number }} + ### python + - name: Bump python-sdk Version + if: ${{ inputs.project == 'python-sdk' }} + run: | + sed -i 's/version = "[0-9]\.[0-9]\.[0-9]"/version = "${{ inputs.version_number }}"/' ./languages/python/pyproject.toml + sed -i 's/__version__ = "[0-9]\.[0-9]\.[0-9]"/__version__ = "${{ inputs.version_number }}"/' ./languages/python/bitwarden_sdk/__init__.py + ############################ # VERSION BUMP SECTION END # ############################ From 872a05b7ba7c9ce267b64dc91b2ec53c4e4e7068 Mon Sep 17 00:00:00 2001 From: tangowithfoxtrot <5676771+tangowithfoxtrot@users.noreply.github.com> Date: Wed, 31 Jan 2024 08:51:16 -0800 Subject: [PATCH 5/5] Update Python README (#563) ## Type of change Fix errors in README and update the Python package name in preparation for publishing to PyPI. - [ ] Bug fix - [ ] New feature development - [ ] Tech debt (refactoring, code cleanup, dependency upgrades, etc) - [ ] Build/deploy pipeline (DevOps) - [x] Other ## Before you submit - Please add **unit tests** where it makes sense to do so --- languages/python/README.md | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/languages/python/README.md b/languages/python/README.md index e77e7a8eb..f41fd8a12 100644 --- a/languages/python/README.md +++ b/languages/python/README.md @@ -2,7 +2,7 @@ ## Requirements - Python 3 -- `maturin` (install with `pip install maturin[patchelf]`) +- `maturin` (install with `pip install maturin`) - `npm` ## Build @@ -15,12 +15,15 @@ cd languages/python/ maturin develop ``` -You can now import `BitwardenClient` in your Python code. +You can now import `BitwardenClient` in your Python code with: +```python +from bitwarden_sdk import BitwardenClient +``` # Use without building locally ```bash -pip install BitwardenClient +pip install bitwarden-sdk ``` # Run