diff --git a/.github/codecov.yml b/.github/codecov.yml index 3228d009c..eb34abf9c 100644 --- a/.github/codecov.yml +++ b/.github/codecov.yml @@ -1,3 +1,4 @@ ignore: - "crates/sdk-schemas" # Tool - "crates/uniffi-bindgen" # Tool + - "crates/memory-testing" # Testing diff --git a/.github/workflows/build-rust-cross-platform.yml b/.github/workflows/build-rust-cross-platform.yml index 67c41c6f9..0db2d0cde 100644 --- a/.github/workflows/build-rust-cross-platform.yml +++ b/.github/workflows/build-rust-cross-platform.yml @@ -8,6 +8,7 @@ on: - main - rc - hotfix-rc + pull_request: jobs: build_rust: diff --git a/.github/workflows/memory-testing.yml b/.github/workflows/memory-testing.yml new file mode 100644 index 000000000..af5ef6b7c --- /dev/null +++ b/.github/workflows/memory-testing.yml @@ -0,0 +1,43 @@ +--- +name: Test for memory leaks + +on: + pull_request: + paths: + - "crates/bitwarden-crypto/**" + - "crates/memory-testing/**" + push: + paths: + - "crates/bitwarden-crypto/**" + - "crates/memory-testing/**" + branches: + - "main" + - "rc" + - "hotfix-rc" + +jobs: + memory-test: + name: Testing + runs-on: ubuntu-22.04 + + steps: + - name: Check out repo + uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + + - name: Set up gdb + run: | + sudo apt update + sudo apt -y install gdb + + - name: Install rust + uses: dtolnay/rust-toolchain@be73d7920c329f220ce78e0234b8f96b7ae60248 # stable + with: + toolchain: stable + + - name: Cache cargo registry + uses: Swatinem/rust-cache@23bce251a8cd2ffc3c1075eaa2367cf899916d84 # v2.7.3 + with: + key: memtest-cargo + + - name: Test + run: ./crates/memory-testing/run_test.sh no-docker diff --git a/.github/workflows/release-go.yml b/.github/workflows/release-go.yml index 830e5f313..c7a198056 100644 --- a/.github/workflows/release-go.yml +++ b/.github/workflows/release-go.yml @@ -1,4 +1,5 @@ -name: Release Go +name: Release Go SDK +run-name: Release Go SDK ${{ inputs.release_type }} on: workflow_dispatch: @@ -15,6 +16,7 @@ on: env: GO111MODULE: on GO_VERSION: "^1.18" + _KEY_VAULT: "bitwarden-ci" jobs: validate: @@ -27,7 +29,7 @@ jobs: uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 - name: Branch check - if: ${{ github.event.inputs.release_type != 'Dry Run' }} + if: ${{ inputs.release_type != 'Dry Run' }} run: | if [[ "$GITHUB_REF" != "refs/heads/rc" ]] && [[ "$GITHUB_REF" != "refs/heads/hotfix-rc" ]]; then echo "===================================" @@ -47,22 +49,23 @@ jobs: runs-on: ubuntu-22.04 needs: validate env: - _KEY_VAULT: "bitwarden-ci" _BOT_EMAIL: 106330231+bitwarden-devops-bot@users.noreply.github.com _BOT_NAME: bitwarden-devops-bot _PKG_VERSION: ${{ needs.validate.outputs.version }} steps: - name: Checkout SDK repo - uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac # v4.0.0 + uses: actions/checkout@1e31de5234b9f8995739874a8ce0492dc87873e2 # v4.0.0 with: path: sdk - - name: Checkout SDK-Go repo - uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac # v4.0.0 + - name: Download artifacts + uses: bitwarden/gh-actions/download-artifacts@main with: - repository: bitwarden/sm-sdk-go - path: sm-sdk-go - ref: main + workflow: generate_schemas.yml + path: sdk/languages/go/bitwarden_sdk_secrets/lib + workflow_conclusion: success + branch: ${{ inputs.release_type == 'Dry Run' && 'main' || github.ref_name }} + artifacts: schemas.go - name: Login to Azure - Prod Subscription uses: Azure/login@92a5484dfaf04ca78a94597f4f19fea633851fa2 # v1.4.7 @@ -71,11 +74,19 @@ jobs: - name: Retrieve secrets id: retrieve-secrets - uses: bitwarden/gh-actions/get-keyvault-secrets@62d1bf7c3e31c458cc7236b1e69a475d235cd78f + uses: bitwarden/gh-actions/get-keyvault-secrets@4f37134d838f21609c38cb56694d8605f176704c with: keyvault: ${{ env._KEY_VAULT }} secrets: "github-pat-bitwarden-devops-bot-repo-scope" + - name: Checkout SDK-Go repo + uses: actions/checkout@1e31de5234b9f8995739874a8ce0492dc87873e2 # v4.0.0 + with: + repository: bitwarden/sm-sdk-go + path: sm-sdk-go + ref: main + token: ${{ steps.retrieve-secrets.outputs.github-pat-bitwarden-devops-bot-repo-scope }} + - name: Setup Git working-directory: sm-sdk-go run: | @@ -85,7 +96,13 @@ jobs: - name: Update files run: | # Copy files to local sm-sdk-go repo path - cp --verbose -rf sdk/languages/go sm-sdk-go + cp --verbose -rf sdk/languages/go/. sm-sdk-go + + - name: Replace repo name + working-directory: sm-sdk-go + run: | + find . -name '*' -exec \ + gsed -i -e 's/github.com\/bitwarden\/sdk\/languages\/go/github.com\/bitwarden\/sm-sdk-go/g' {} \; - name: Push changes working-directory: sm-sdk-go @@ -93,7 +110,7 @@ jobs: git add . git commit -m "Update Go SDK to ${{ github.sha }}" - if [[ "${{ github.event.inputs.release_type }}" == "Dry Run" ]]; then + if [[ "${{ inputs.release_type }}" == "Dry Run" ]]; then echo "===================================" echo "[!] Dry Run - Skipping push" echo "===================================" @@ -104,7 +121,7 @@ jobs: fi - name: Create release tag on SDK Go repo - if: ${{ github.event.inputs.release_type != 'Dry Run' }} + if: ${{ inputs.release_type != 'Dry Run' }} working-directory: sm-sdk-go run: | # Check if tag exists, set output then exit 0 if true. @@ -134,13 +151,56 @@ jobs: - name: Retrieve secrets id: retrieve-secrets - uses: bitwarden/gh-actions/get-keyvault-secrets@62d1bf7c3e31c458cc7236b1e69a475d235cd78f + uses: bitwarden/gh-actions/get-keyvault-secrets@4f37134d838f21609c38cb56694d8605f176704c with: keyvault: ${{ env._KEY_VAULT }} secrets: "github-pat-bitwarden-devops-bot-repo-scope" + - name: Download x86_64-apple-darwin artifact + uses: bitwarden/gh-actions/download-artifacts@main + with: + workflow: build-rust-cross-platform.yml + workflow_conclusion: success + branch: ${{ inputs.release_type == 'Dry Run' && 'main' || github.ref_name }} + artifacts: libbitwarden_c_files-x86_64-apple-darwin + skip_unpack: true + + - name: Download aarch64-apple-darwin artifact + uses: bitwarden/gh-actions/download-artifacts@main + with: + workflow: build-rust-cross-platform.yml + workflow_conclusion: success + branch: ${{ inputs.release_type == 'Dry Run' && 'main' || github.ref_name }} + artifacts: libbitwarden_c_files-aarch64-apple-darwin + skip_unpack: true + + - name: Download x86_64-unknown-linux-gnu artifact + uses: bitwarden/gh-actions/download-artifacts@main + with: + workflow: build-rust-cross-platform.yml + workflow_conclusion: success + branch: ${{ inputs.release_type == 'Dry Run' && 'main' || github.ref_name }} + artifacts: libbitwarden_c_files-x86_64-unknown-linux-gnu + skip_unpack: true + + - name: Download x86_64-pc-windows-msvc artifact + uses: bitwarden/gh-actions/download-artifacts@main + with: + workflow: build-rust-cross-platform.yml + workflow_conclusion: success + branch: ${{ inputs.release_type == 'Dry Run' && 'main' || github.ref_name }} + artifacts: libbitwarden_c_files-x86_64-pc-windows-msvc + skip_unpack: true + + - name: Rename build artifacts + run: | + mv libbitwarden_c_files-x86_64-apple-darwin.zip libbitwarden_c_files-x86_64-apple-darwin-$_PKG_VERSION.zip + mv libbitwarden_c_files-aarch64-apple-darwin.zip libbitwarden_c_files-aarch64-apple-darwin-$_PKG_VERSION.zip + mv libbitwarden_c_files-x86_64-unknown-linux-gnu.zip libbitwarden_c_files-x86_64-unknown-linux-gnu-$_PKG_VERSION.zip + mv libbitwarden_c_files-x86_64-pc-windows-msvc.zip libbitwarden_c_files-x86_64-pc-windows-msvc-$_PKG_VERSION.zip + - name: Create release - if: ${{ github.event.inputs.release_type != 'Dry Run' }} + if: ${{ inputs.release_type != 'Dry Run' }} uses: ncipollo/release-action@6c75be85e571768fa31b40abf38de58ba0397db5 # v1.13.0 with: tag: v${{ env._PKG_VERSION }} @@ -148,4 +208,9 @@ jobs: body: "" token: ${{ steps.retrieve-secrets.outputs.github-pat-bitwarden-devops-bot-repo-scope }} draft: true - repo: bitwarden/sm-sdk-go + repo: sm-sdk-go + owner: bitwarden + artifacts: "libbitwarden_c_files-x86_64-apple-darwin-${{ env._PKG_VERSION }}.zip, + libbitwarden_c_files-aarch64-apple-darwin-${{ env._PKG_VERSION }}.zip, + libbitwarden_c_files-x86_64-unknown-linux-gnu-${{ env._PKG_VERSION }}.zip, + libbitwarden_c_files-x86_64-pc-windows-msvc-${{ env._PKG_VERSION }}.zip" diff --git a/.gitignore b/.gitignore index 63c5875a3..b13651d19 100644 --- a/.gitignore +++ b/.gitignore @@ -12,7 +12,6 @@ x64/ x86/ build/ bld/ -[Bb]in/ [Oo]bj/ *.wasm diff --git a/Cargo.lock b/Cargo.lock index 7efce8973..b9f383413 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1544,6 +1544,12 @@ version = "0.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bd5256b483761cd23699d0da46cc6fd2ee3be420bbe6d020ae4a091e70b7e9fd" +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + [[package]] name = "hkdf" version = "0.12.4" @@ -1970,6 +1976,18 @@ dependencies = [ "autocfg", ] +[[package]] +name = "memory-testing" +version = "0.1.0" +dependencies = [ + "bitwarden-crypto", + "comfy-table", + "hex", + "serde", + "serde_json", + "zeroize", +] + [[package]] name = "mime" version = "0.3.17" @@ -2003,9 +2021,9 @@ dependencies = [ [[package]] name = "mio" -version = "0.8.10" +version = "0.8.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f3d0b296e374a4e6f3c7b0a1f5a51d748a0d34c85e7dc48fc3fa9a87657fe09" +checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c" dependencies = [ "libc", "log", diff --git a/README.md b/README.md index bab08af9a..cdc52a7cb 100644 --- a/README.md +++ b/README.md @@ -86,5 +86,56 @@ This project uses customized templates which lives in the `support/openapi-templ These templates resolves some outstanding issues we've experienced with the rust generator. But we strive towards modifying the templates as little as possible to ease future upgrades. +## Tests + +Many of the SDK tests are based on encrypted data provided by the other Bitwarden clients. In order +to provide a consistent method of retrieving the data we provide a test account with user keys. + +**Disclaimer:** The server typically encrypts and protects certain fields. In order to allow +accounts to be used on other servers this protection was explicitly removed from these data dumps. + +### `test@bitwarden.com` + +- Email: `test@bitwarden.com` +- Password: `asdfasdfasdf` +- PBKDF2: `600_000` iterations + +```sql +INSERT INTO vault_dev.dbo.[User] ( + Id, Name, Email, EmailVerified, MasterPassword, + MasterPasswordHint, Culture, SecurityStamp, + TwoFactorProviders, TwoFactorRecoveryCode, + EquivalentDomains, ExcludedGlobalEquivalentDomains, + AccountRevisionDate, [Key], PublicKey, + PrivateKey, Premium, PremiumExpirationDate, + Storage, MaxStorageGb, Gateway, GatewayCustomerId, + GatewaySubscriptionId, LicenseKey, + CreationDate, RevisionDate, RenewalReminderDate, + Kdf, KdfIterations, ReferenceData, + ApiKey, ForcePasswordReset, UsesKeyConnector, + FailedLoginCount, LastFailedLoginDate, + AvatarColor, KdfMemory, KdfParallelism, + LastPasswordChangeDate, LastKdfChangeDate, + LastKeyRotationDate, LastEmailChangeDate +) +VALUES + ( + N 'b1fd4bf2-9643-4787-87f3-b0f00189c33b', + N 'Test', N 'test@bitwarden.com', + 0, N 'AQAAAAEAAYagAAAAEJ3ky9F/Zt5sy3/UAHVvBarMR+tBXYOM5IGgXy4/mx82uptgHgItauyCN+UZTvAqiA==', + null, N 'en-US', N 'F3KL7SCJKEXO4LJFVLGZITPEHM7SAVSZ', + null, null, null, null, N '2024-01-07 23:56:48.2600000', + N '2.Q/2PhzcC7GdeiMHhWguYAQ==|GpqzVdr0go0ug5cZh1n+uixeBC3oC90CIe0hd/HWA/pTRDZ8ane4fmsEIcuc8eMKUt55Y2q/fbNzsYu41YTZzzsJUSeqVjT8/iTQtgnNdpo=|dwI+uyvZ1h/iZ03VQ+/wrGEFYVewBUUl/syYgjsNMbE=', + N 'MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA0Ww2chogqCpaAR7Uw448am4b7vDFXiM5kXjFlGfXBlrAdAqTTggEvTDlMNYqPlCo+mBM6iFmTTUY9rpZBvFskMnKvsvpJ47/fehAH2o2e3Ulv/5NFevaVCMCmpkBDtbMbO1A4a3btdRtCP8DsKWMefHauEpaoLxNTLWnOIZVfCMjsSgx2EvULHAZPTtbFwm4+UVKniM4ds4jvOsD85h4jn2aLs/jWJXFfxN8iVSqEqpC2TBvsPdyHb49xQoWWfF0Z6BiNqeNGKEU9Uos1pjL+kzhEzzSpH31PZT/ufJ/oo4+93wrUt57hb6f0jxiXhwd5yQ+9F6wVwpbfkq0IwhjOwIDAQAB', + N '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=', + 0, null, null, null, null, null, null, + null, N '2024-01-07 23:53:38.5900000', + N '2024-01-07 23:53:38.5900000', + null, 0, 600000, N '{"id":null}', N '7gp59kKHt9kMlks0BuNC4IjNXYkljR', + 0, 0, 0, null, null, null, null, null, + null, null, null + ); +``` + [secrets-manager]: https://bitwarden.com/products/secrets-manager/ [bws-help]: https://bitwarden.com/help/secrets-manager-cli/ diff --git a/crates/bitwarden-uniffi/src/auth/mod.rs b/crates/bitwarden-uniffi/src/auth/mod.rs index 34d0de93f..2a451ffdf 100644 --- a/crates/bitwarden-uniffi/src/auth/mod.rs +++ b/crates/bitwarden-uniffi/src/auth/mod.rs @@ -130,7 +130,7 @@ impl ClientAuth { } /// Trust the current device - pub async fn t(&self) -> Result { + pub async fn trust_device(&self) -> Result { Ok(self.0 .0.write().await.auth().trust_device()?) } } diff --git a/crates/bitwarden-uniffi/src/platform/mod.rs b/crates/bitwarden-uniffi/src/platform/mod.rs index 7200b6678..33b14d345 100644 --- a/crates/bitwarden-uniffi/src/platform/mod.rs +++ b/crates/bitwarden-uniffi/src/platform/mod.rs @@ -31,4 +31,10 @@ impl ClientPlatform { .platform() .user_fingerprint(fingerprint_material)?) } + + /// Load feature flags into the client + pub async fn load_flags(&self, flags: std::collections::HashMap) -> Result<()> { + self.0 .0.write().await.load_flags(flags); + Ok(()) + } } diff --git a/crates/bitwarden/CHANGELOG.md b/crates/bitwarden/CHANGELOG.md index ff47c19d8..ee4c6033b 100644 --- a/crates/bitwarden/CHANGELOG.md +++ b/crates/bitwarden/CHANGELOG.md @@ -9,7 +9,8 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ### Changed -- Switched TLS backend to `rustls`, removing the dependency on `OpenSSL`. +- Switched TLS backend to `rustls`, removing the dependency on `OpenSSL`. (#374) +- `client::AccessToken` is now `auth::AccessToken`. (#656) ## [0.4.0] - 2023-12-21 diff --git a/crates/bitwarden/src/client/access_token.rs b/crates/bitwarden/src/auth/access_token.rs similarity index 97% rename from crates/bitwarden/src/client/access_token.rs rename to crates/bitwarden/src/auth/access_token.rs index d7cad2fca..667a78529 100644 --- a/crates/bitwarden/src/client/access_token.rs +++ b/crates/bitwarden/src/auth/access_token.rs @@ -65,12 +65,12 @@ impl FromStr for AccessToken { #[cfg(test)] mod tests { + use super::AccessToken; + #[test] fn can_decode_access_token() { use std::str::FromStr; - use crate::client::AccessToken; - let access_token = "0.ec2c1d46-6a4b-4751-a310-af9601317f2d.C2IgxjjLF7qSshsbwe8JGcbM075YXw:X8vbvA0bduihIDe/qrzIQQ=="; let token = AccessToken::from_str(access_token).unwrap(); @@ -86,8 +86,6 @@ mod tests { fn malformed_tokens() { use std::str::FromStr; - use crate::client::AccessToken; - // Encryption key without base64 padding, we generate it with padding but ignore it when // decoding let t = "0.ec2c1d46-6a4b-4751-a310-af9601317f2d.C2IgxjjLF7qSshsbwe8JGcbM075YXw:X8vbvA0bduihIDe/qrzIQQ"; diff --git a/crates/bitwarden/src/auth/login/access_token.rs b/crates/bitwarden/src/auth/login/access_token.rs index d540b31a8..46c3f2b50 100644 --- a/crates/bitwarden/src/auth/login/access_token.rs +++ b/crates/bitwarden/src/auth/login/access_token.rs @@ -11,9 +11,9 @@ use crate::{ auth::{ api::{request::AccessTokenRequest, response::IdentityTokenResponse}, login::{response::two_factor::TwoFactorProviders, PasswordLoginResponse}, - JWTToken, + AccessToken, JWTToken, }, - client::{AccessToken, LoginMethod, ServiceAccountLoginMethod}, + client::{LoginMethod, ServiceAccountLoginMethod}, error::{Error, Result}, secrets_manager::state::{self, ClientState}, Client, diff --git a/crates/bitwarden/src/auth/mod.rs b/crates/bitwarden/src/auth/mod.rs index 021c97c0f..7918694e2 100644 --- a/crates/bitwarden/src/auth/mod.rs +++ b/crates/bitwarden/src/auth/mod.rs @@ -1,3 +1,4 @@ +mod access_token; pub(super) mod api; pub mod client_auth; mod jwt_token; @@ -5,6 +6,7 @@ pub mod login; #[cfg(feature = "internal")] pub mod password; pub mod renew; +pub use access_token::AccessToken; pub use jwt_token::JWTToken; #[cfg(feature = "internal")] mod register; diff --git a/crates/bitwarden/src/client/client.rs b/crates/bitwarden/src/client/client.rs index 5b2c8b7e0..883720529 100644 --- a/crates/bitwarden/src/client/client.rs +++ b/crates/bitwarden/src/client/client.rs @@ -9,21 +9,24 @@ use chrono::Utc; use reqwest::header::{self, HeaderValue}; use uuid::Uuid; -use super::AccessToken; #[cfg(feature = "secrets")] use crate::auth::login::{AccessTokenLoginRequest, AccessTokenLoginResponse}; -#[cfg(feature = "internal")] -use crate::platform::{ - get_user_api_key, sync, SecretVerificationRequest, SyncRequest, SyncResponse, - UserApiKeyResponse, -}; use crate::{ + auth::AccessToken, client::{ client_settings::{ClientSettings, DeviceType}, encryption_settings::EncryptionSettings, }, error::{Error, Result}, }; +#[cfg(feature = "internal")] +use crate::{ + client::flags::Flags, + platform::{ + get_user_api_key, sync, SecretVerificationRequest, SyncRequest, SyncResponse, + UserApiKeyResponse, + }, +}; #[derive(Debug)] pub(crate) struct ApiConfigurations { @@ -77,6 +80,9 @@ pub struct Client { pub(crate) token_expires_on: Option, pub(crate) login_method: Option, + #[cfg(feature = "internal")] + flags: Flags, + /// Use Client::get_api_configurations() to access this. /// It should only be used directly in renew_token #[doc(hidden)] @@ -138,6 +144,8 @@ impl Client { refresh_token: None, token_expires_on: None, login_method: None, + #[cfg(feature = "internal")] + flags: Flags::default(), __api_configurations: ApiConfigurations { identity, api, @@ -148,6 +156,16 @@ impl Client { } } + #[cfg(feature = "internal")] + pub fn load_flags(&mut self, flags: std::collections::HashMap) { + self.flags = Flags::load_from_map(flags); + } + + #[cfg(feature = "mobile")] + pub(crate) fn get_flags(&self) -> &Flags { + &self.flags + } + pub(crate) async fn get_api_configurations(&mut self) -> &ApiConfigurations { // At the moment we ignore the error result from the token renewal, if it fails, // the token will end up expiring and the next operation is going to fail anyway. diff --git a/crates/bitwarden/src/client/flags.rs b/crates/bitwarden/src/client/flags.rs new file mode 100644 index 000000000..0fc17534b --- /dev/null +++ b/crates/bitwarden/src/client/flags.rs @@ -0,0 +1,43 @@ +#[derive(Debug, Default, Clone, serde::Deserialize)] +pub struct Flags { + #[serde(default, rename = "enableCipherKeyEncryption")] + pub enable_cipher_key_encryption: bool, +} + +impl Flags { + pub fn load_from_map(map: std::collections::HashMap) -> Self { + let map = map + .into_iter() + .map(|(k, v)| (k, serde_json::Value::Bool(v))) + .collect(); + serde_json::from_value(serde_json::Value::Object(map)).expect("Valid map") + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_load_empty_map() { + let map = std::collections::HashMap::new(); + let flags = Flags::load_from_map(map); + assert!(!flags.enable_cipher_key_encryption); + } + + #[test] + fn test_load_valid_map() { + let mut map = std::collections::HashMap::new(); + map.insert("enableCipherKeyEncryption".into(), true); + let flags = Flags::load_from_map(map); + assert!(flags.enable_cipher_key_encryption); + } + + #[test] + fn test_load_invalid_map() { + let mut map = std::collections::HashMap::new(); + map.insert("thisIsNotAFlag".into(), true); + let flags = Flags::load_from_map(map); + assert!(!flags.enable_cipher_key_encryption); + } +} diff --git a/crates/bitwarden/src/client/mod.rs b/crates/bitwarden/src/client/mod.rs index c3719fce2..0c703570f 100644 --- a/crates/bitwarden/src/client/mod.rs +++ b/crates/bitwarden/src/client/mod.rs @@ -1,11 +1,12 @@ //! Bitwarden SDK Client pub(crate) use client::*; -pub(crate) mod access_token; #[allow(clippy::module_inception)] mod client; pub mod client_settings; pub(crate) mod encryption_settings; -pub use access_token::AccessToken; +#[cfg(feature = "internal")] +mod flags; + pub use client::Client; diff --git a/crates/bitwarden/src/mobile/vault/client_ciphers.rs b/crates/bitwarden/src/mobile/vault/client_ciphers.rs index 4e34021ee..c35cf3080 100644 --- a/crates/bitwarden/src/mobile/vault/client_ciphers.rs +++ b/crates/bitwarden/src/mobile/vault/client_ciphers.rs @@ -1,8 +1,8 @@ -use bitwarden_crypto::{Decryptable, Encryptable}; +use bitwarden_crypto::{Decryptable, Encryptable, LocateKey}; use super::client_vault::ClientVault; use crate::{ - error::Result, + error::{Error, Result}, vault::{Cipher, CipherListView, CipherView}, Client, }; @@ -12,9 +12,18 @@ pub struct ClientCiphers<'a> { } impl<'a> ClientCiphers<'a> { - pub async fn encrypt(&self, cipher_view: CipherView) -> Result { + pub async fn encrypt(&self, mut cipher_view: CipherView) -> Result { let enc = self.client.get_encryption_settings()?; + // TODO: Once this flag is removed, the key generation logic should + // be moved directly into the KeyEncryptable implementation + if cipher_view.key.is_none() && self.client.get_flags().enable_cipher_key_encryption { + let key = cipher_view + .locate_key(enc, &None) + .ok_or(Error::VaultLocked)?; + cipher_view.generate_cipher_key(key)?; + } + let cipher = cipher_view.encrypt(enc, &None)?; Ok(cipher) diff --git a/crates/bitwarden/src/secrets_manager/state.rs b/crates/bitwarden/src/secrets_manager/state.rs index 4efa4403b..b2b6f6a8e 100644 --- a/crates/bitwarden/src/secrets_manager/state.rs +++ b/crates/bitwarden/src/secrets_manager/state.rs @@ -4,7 +4,7 @@ use bitwarden_crypto::{EncString, KeyDecryptable, KeyEncryptable}; use serde::{Deserialize, Serialize}; use crate::{ - client::AccessToken, + auth::AccessToken, error::{Error, Result}, }; diff --git a/crates/bitwarden/src/vault/cipher/cipher.rs b/crates/bitwarden/src/vault/cipher/cipher.rs index 9223462a1..24207251f 100644 --- a/crates/bitwarden/src/vault/cipher/cipher.rs +++ b/crates/bitwarden/src/vault/cipher/cipher.rs @@ -289,6 +289,18 @@ impl Cipher { } } +impl CipherView { + pub fn generate_cipher_key(&mut self, key: &SymmetricCryptoKey) -> Result<()> { + let ciphers_key = Cipher::get_cipher_key(key, &self.key)?; + let key = ciphers_key.as_ref().unwrap_or(key); + + let new_key = SymmetricCryptoKey::generate(rand::thread_rng()); + + self.key = Some(new_key.to_vec().encrypt_with_key(key)?); + Ok(()) + } +} + impl KeyDecryptable for Cipher { fn decrypt_with_key(&self, key: &SymmetricCryptoKey) -> Result { let ciphers_key = Cipher::get_cipher_key(key, &self.key)?; @@ -401,3 +413,68 @@ impl From for CipherRepromptType } } } + +#[cfg(test)] +mod tests { + + use super::*; + + #[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, + }), + 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 + let cipher = generate_cipher(); + let no_key_cipher_enc = cipher.encrypt_with_key(&key).unwrap(); + let no_key_cipher_dec: CipherView = no_key_cipher_enc.decrypt_with_key(&key).unwrap(); + assert!(no_key_cipher_dec.key.is_none()); + assert_eq!(no_key_cipher_dec.name, original_cipher.name); + + let mut cipher = generate_cipher(); + cipher.generate_cipher_key(&key).unwrap(); + + // Check that the cipher gets encrypted correctly when it's assigned it's own key + let key_cipher_enc = cipher.encrypt_with_key(&key).unwrap(); + let key_cipher_dec: CipherView = key_cipher_enc.decrypt_with_key(&key).unwrap(); + assert!(key_cipher_dec.key.is_some()); + assert_eq!(key_cipher_dec.name, original_cipher.name); + } +} diff --git a/crates/bws/Cargo.toml b/crates/bws/Cargo.toml index 2a0a13f49..9dc6cb355 100644 --- a/crates/bws/Cargo.toml +++ b/crates/bws/Cargo.toml @@ -11,6 +11,7 @@ description = """ Bitwarden Secrets Manager CLI """ keywords = ["bitwarden", "secrets-manager", "cli"] +exclude = ["Dockerfile*", "entitlements.plist"] [dependencies] bat = { version = "0.24.0", features = [ diff --git a/crates/bws/src/main.rs b/crates/bws/src/main.rs index cb130b52c..6ed78f7b7 100644 --- a/crates/bws/src/main.rs +++ b/crates/bws/src/main.rs @@ -1,8 +1,8 @@ use std::{path::PathBuf, process, str::FromStr}; use bitwarden::{ - auth::login::AccessTokenLoginRequest, - client::{client_settings::ClientSettings, AccessToken}, + auth::{login::AccessTokenLoginRequest, AccessToken}, + client::client_settings::ClientSettings, secrets_manager::{ projects::{ ProjectCreateRequest, ProjectGetRequest, ProjectPutRequest, ProjectsDeleteRequest, diff --git a/crates/memory-testing/.gitignore b/crates/memory-testing/.gitignore new file mode 100644 index 000000000..53752db25 --- /dev/null +++ b/crates/memory-testing/.gitignore @@ -0,0 +1 @@ +output diff --git a/crates/memory-testing/Cargo.toml b/crates/memory-testing/Cargo.toml new file mode 100644 index 000000000..c1ecbbf54 --- /dev/null +++ b/crates/memory-testing/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "memory-testing" +version = "0.1.0" +edition = "2021" +publish = false + +[dependencies] +bitwarden-crypto = { path = "../bitwarden-crypto", version = "=0.1.0" } +comfy-table = "7.1.0" +hex = "0.4.3" +serde = "1.0.196" +serde_json = "1.0.113" +zeroize = "1.7.0" diff --git a/crates/memory-testing/Dockerfile b/crates/memory-testing/Dockerfile new file mode 100644 index 000000000..fdcf5de00 --- /dev/null +++ b/crates/memory-testing/Dockerfile @@ -0,0 +1,26 @@ +############################################### +# Build stage # +############################################### +FROM rust:1.76 AS build + +# Copy required project files +COPY . /app + +# Build project +WORKDIR /app +RUN cargo build -p memory-testing + +############################################### +# App stage # +############################################### +FROM debian:bookworm-slim + +# This specifically needs to run as root to be able to capture core dumps +USER root + +RUN apt-get update && apt-get install -y --no-install-recommends gdb=13.1-3 && apt-get clean && rm -rf /var/lib/apt/lists/* + +# Copy built project from the build stage +COPY --from=build /app/target/debug/memory-testing /app/target/debug/capture-dumps /app/crates/memory-testing/cases.json ./ + +CMD [ "/capture-dumps", "./memory-testing", "/output" ] diff --git a/crates/memory-testing/Dockerfile.dockerignore b/crates/memory-testing/Dockerfile.dockerignore new file mode 100644 index 000000000..50f4b1230 --- /dev/null +++ b/crates/memory-testing/Dockerfile.dockerignore @@ -0,0 +1,4 @@ +* +!crates/* +!Cargo.toml +!Cargo.lock diff --git a/crates/memory-testing/cases.json b/crates/memory-testing/cases.json new file mode 100644 index 000000000..eb85e2aca --- /dev/null +++ b/crates/memory-testing/cases.json @@ -0,0 +1,9 @@ +{ + "symmetric_key": [ + { + "key": "FfhVVP8fmFIZY1WmRszPmRmVCxXNWVcJffPrbkywTPtBNkgfhYGT+D9sVGizYXrPffuj2yoyWqMwF9iF5aMQhQ==", + "decrypted_key_hex": "15f85554ff1f9852196355a646cccf9919950b15cd5957097df3eb6e4cb04cfb", + "decrypted_mac_hex": "4136481f858193f83f6c5468b3617acf7dfba3db2a325aa33017d885e5a31085" + } + ] +} diff --git a/crates/memory-testing/run_test.sh b/crates/memory-testing/run_test.sh new file mode 100755 index 000000000..627e5dacd --- /dev/null +++ b/crates/memory-testing/run_test.sh @@ -0,0 +1,20 @@ +# Move to the root of the repository +cd "$(dirname "$0")" +cd ../../ + +BASE_DIR="./crates/memory-testing" + +mkdir -p $BASE_DIR/output +rm $BASE_DIR/output/* + +cargo build -p memory-testing + +if [ "$1" = "no-docker" ]; then + # This specifically needs to run as root to be able to capture core dumps + sudo ./target/debug/capture-dumps ./target/debug/memory-testing $BASE_DIR +else + docker build -f crates/memory-testing/Dockerfile -t bitwarden/memory-testing . + docker run --rm -it -v $BASE_DIR:/output bitwarden/memory-testing +fi + +./target/debug/analyze-dumps $BASE_DIR diff --git a/crates/memory-testing/src/bin/analyze-dumps.rs b/crates/memory-testing/src/bin/analyze-dumps.rs new file mode 100644 index 000000000..fee72f2e5 --- /dev/null +++ b/crates/memory-testing/src/bin/analyze-dumps.rs @@ -0,0 +1,132 @@ +use std::{env, fmt::Display, io, path::Path, process}; + +use memory_testing::*; + +fn find_subarrays(needle: &[u8], haystack: &[u8]) -> Vec { + let needle_len = needle.len(); + let haystack_len = haystack.len(); + let mut subarrays = vec![]; + + if needle_len == 0 || haystack_len == 0 || needle_len > haystack_len { + return vec![]; + } + + for i in 0..=(haystack_len - needle_len) { + if &haystack[i..i + needle_len] == needle { + subarrays.push(i); + } + } + + subarrays +} + +const OK: &str = "✅"; +const FAIL: &str = "❌"; + +fn comma_sep(nums: &[usize]) -> String { + nums.iter() + .map(ToString::to_string) + .collect::>() + .join(", ") +} + +fn add_row( + table: &mut comfy_table::Table, + name: N, + initial_pos: &[usize], + final_pos: &[usize], + ok_cond: bool, +) -> bool { + table.add_row(vec![ + name.to_string(), + comma_sep(initial_pos), + comma_sep(final_pos), + if ok_cond { + OK.to_string() + } else { + FAIL.to_string() + }, + ]); + !ok_cond +} + +fn main() -> io::Result<()> { + let args: Vec = env::args().collect(); + if args.len() < 2 { + println!("Usage: ./analyze-dumps "); + process::exit(1); + } + let base_dir: &Path = args[1].as_ref(); + + println!("Memory testing script started"); + + let initial_core = std::fs::read(base_dir.join("output/initial_dump.bin"))?; + let final_core = std::fs::read(base_dir.join("output/final_dump.bin"))?; + + let mut error = false; + let mut table = comfy_table::Table::new(); + table.set_header(vec!["Name", "Initial", "Final", "OK"]); + + let cases = memory_testing::load_cases(base_dir); + + let test_string: Vec = TEST_STRING.as_bytes().to_vec(); + let test_initial_pos = find_subarrays(&test_string, &initial_core); + let test_final_pos = find_subarrays(&test_string, &final_core); + + error |= add_row( + &mut table, + "Test String", + &test_initial_pos, + &test_final_pos, + !test_final_pos.is_empty(), + ); + + if test_initial_pos.is_empty() { + println!("ERROR: Test string not found in initial core dump, is the dump valid?"); + error = true; + } + + for (idx, case) in cases.symmetric_key.iter().enumerate() { + let key_part: Vec = hex::decode(&case.decrypted_key_hex).unwrap(); + let mac_part: Vec = hex::decode(&case.decrypted_mac_hex).unwrap(); + let key_in_b64: Vec = case.key.as_bytes().to_vec(); + + let key_initial_pos = find_subarrays(&key_part, &initial_core); + let mac_initial_pos = find_subarrays(&mac_part, &initial_core); + let b64_initial_pos = find_subarrays(&key_in_b64, &initial_core); + + let key_final_pos = find_subarrays(&key_part, &final_core); + let mac_final_pos = find_subarrays(&mac_part, &final_core); + let b64_final_pos = find_subarrays(&key_in_b64, &final_core); + + error |= add_row( + &mut table, + format!("Symm. Key, case {}", idx), + &key_initial_pos, + &key_final_pos, + key_final_pos.is_empty(), + ); + + error |= add_row( + &mut table, + format!("Symm. MAC, case {}", idx), + &mac_initial_pos, + &mac_final_pos, + mac_final_pos.is_empty(), + ); + + // TODO: At the moment we are not zeroizing the base64 key in from_str, so this test is + // ignored + add_row( + &mut table, + format!("Symm. Key in Base64, case {}", idx), + &b64_initial_pos, + &b64_final_pos, + b64_final_pos.is_empty(), + ); + } + + println!("{table}"); + + process::exit(if error { 1 } else { 0 }); +} diff --git a/crates/memory-testing/src/bin/capture-dumps.rs b/crates/memory-testing/src/bin/capture-dumps.rs new file mode 100644 index 000000000..f43905867 --- /dev/null +++ b/crates/memory-testing/src/bin/capture-dumps.rs @@ -0,0 +1,70 @@ +use std::{ + fs, + io::{self, prelude::*}, + path::Path, + process::{Command, Stdio}, + thread::sleep, + time::Duration, +}; + +fn dump_process_to_bytearray(pid: u32, output_dir: &Path, output_name: &Path) -> io::Result { + Command::new("gcore") + .args(["-a", &pid.to_string()]) + .output()?; + + let core_path = format!("core.{}", pid); + let output_path = output_dir.join(output_name); + let len = fs::copy(&core_path, output_path)?; + fs::remove_file(&core_path)?; + Ok(len) +} + +fn main() -> io::Result<()> { + let args: Vec = std::env::args().collect(); + if args.len() < 3 { + println!("Usage: ./capture_dumps "); + std::process::exit(1); + } + + let binary_path = &args[1]; + let base_dir: &Path = args[2].as_ref(); + + println!("Memory dump capture script started"); + + let mut proc = Command::new(binary_path) + .arg(base_dir) + .stdout(Stdio::inherit()) + .stdin(Stdio::piped()) + .spawn()?; + let id = proc.id(); + println!("Started memory testing process with PID: {}", id); + let stdin = proc.stdin.as_mut().expect("Valid stdin"); + + // Wait a bit for it to process + sleep(Duration::from_secs(3)); + + // Dump the process before the variables are freed + let initial_core = + dump_process_to_bytearray(id, &base_dir.join("output"), "initial_dump.bin".as_ref())?; + println!("Initial core dump file size: {}", initial_core); + + stdin.write_all(b".")?; + stdin.flush()?; + + // Wait a bit for it to process + sleep(Duration::from_secs(1)); + + // Dump the process after the variables are freed + let final_core = + dump_process_to_bytearray(id, &base_dir.join("output"), "final_dump.bin".as_ref())?; + println!("Final core dump file size: {}", final_core); + + stdin.write_all(b".")?; + stdin.flush()?; + + // Wait for the process to finish and print the output + let output = proc.wait()?; + println!("Return code: {}", output); + + std::process::exit(output.code().unwrap_or(1)); +} diff --git a/crates/memory-testing/src/lib.rs b/crates/memory-testing/src/lib.rs new file mode 100644 index 000000000..e633756d1 --- /dev/null +++ b/crates/memory-testing/src/lib.rs @@ -0,0 +1,29 @@ +use std::path::Path; + +use zeroize::Zeroize; + +pub const TEST_STRING: &str = "THIS IS USED TO CHECK THAT THE MEMORY IS DUMPED CORRECTLY"; + +pub fn load_cases(base_dir: &Path) -> Cases { + let mut json_str = std::fs::read_to_string(base_dir.join("cases.json")).unwrap(); + let cases: Cases = serde_json::from_str(&json_str).unwrap(); + + // Make sure that we don't leave extra copies of the data in memory + json_str.zeroize(); + cases +} + +// Note: We don't actively zeroize these structs here because we want the code in bitwarden_crypto +// to handle it for us +#[derive(serde::Deserialize)] +pub struct Cases { + pub symmetric_key: Vec, +} + +#[derive(serde::Deserialize)] +pub struct SymmetricKeyCases { + pub key: String, + + pub decrypted_key_hex: String, + pub decrypted_mac_hex: String, +} diff --git a/crates/memory-testing/src/main.rs b/crates/memory-testing/src/main.rs new file mode 100644 index 000000000..96e4ae175 --- /dev/null +++ b/crates/memory-testing/src/main.rs @@ -0,0 +1,44 @@ +use std::{env, io::Read, path::Path, process, str::FromStr}; + +use bitwarden_crypto::SymmetricCryptoKey; + +fn wait_for_dump() { + println!("Waiting for dump..."); + std::io::stdin().read_exact(&mut [1u8]).unwrap(); +} + +fn main() { + let args: Vec = env::args().collect(); + if args.len() < 2 { + println!("Usage: ./memory-testing "); + process::exit(1); + } + let base_dir: &Path = args[1].as_ref(); + + let test_string = String::from(memory_testing::TEST_STRING); + + let cases = memory_testing::load_cases(base_dir); + + let mut symmetric_keys = Vec::new(); + let mut symmetric_keys_as_vecs = Vec::new(); + + for case in &cases.symmetric_key { + let key = SymmetricCryptoKey::from_str(&case.key).unwrap(); + symmetric_keys_as_vecs.push(key.to_vec()); + symmetric_keys.push(key); + } + + // Make a memory dump before the variables are freed + wait_for_dump(); + + // Use all the variables so the compiler doesn't decide to remove them + println!("{test_string} {symmetric_keys:?} {symmetric_keys_as_vecs:?}"); + + drop(symmetric_keys); + drop(symmetric_keys_as_vecs); + + // After the variables are dropped, we want to make another dump + wait_for_dump(); + + println!("Done!") +} diff --git a/languages/go/example/example.go b/languages/go/example/example.go index deb353565..eb4d34612 100644 --- a/languages/go/example/example.go +++ b/languages/go/example/example.go @@ -1,7 +1,9 @@ package main import ( + "encoding/json" "fmt" + "log" "os" sdk "github.com/bitwarden/sdk/languages/go" @@ -87,5 +89,28 @@ func main() { panic(err) } + secretIdentifiers, err := bitwardenClient.Secrets.List(organizationID.String()) + if err != nil { + panic(err) + } + + // Get secrets with a list of IDs + secretIDs := make([]string, len(secretIdentifiers.Data)) + for i, identifier := range secretIdentifiers.Data { + secretIDs[i] = identifier.ID + } + + secrets, err := bitwardenClient.Secrets.GetByIDS(secretIDs) + if err != nil { + log.Fatalf("Error getting secrets: %v", err) + } + + jsonSecrets, err := json.MarshalIndent(secrets, "", " ") + if err != nil { + log.Fatalf("Error marshalling secrets to JSON: %v", err) + } + + fmt.Println(string(jsonSecrets)) + defer bitwardenClient.Close() } diff --git a/languages/go/secrets.go b/languages/go/secrets.go index e6863c459..2c56538f7 100644 --- a/languages/go/secrets.go +++ b/languages/go/secrets.go @@ -4,6 +4,7 @@ type SecretsInterface interface { Create(key, value, note string, organizationID string, projectIDs []string) (*SecretResponse, error) List(organizationID string) (*SecretIdentifiersResponse, error) Get(secretID string) (*SecretResponse, error) + GetByIDS(secretIDs []string) (*SecretsResponse, error) Update(secretID string, key, value, note string, organizationID string, projectIDs []string) (*SecretResponse, error) Delete(secretIDs []string) (*SecretsDeleteResponse, error) } @@ -76,6 +77,22 @@ func (s *Secrets) Get(id string) (*SecretResponse, error) { return &response, nil } +func (s *Secrets) GetByIDS(ids []string) (*SecretsResponse, error) { + command := Command{ + Secrets: &SecretsCommand{ + GetByIDS: &SecretsGetRequest{ + IDS: ids, + }, + }, + } + + var response SecretsResponse + if err := s.executeCommand(command, &response); err != nil { + return nil, err + } + return &response, nil +} + func (s *Secrets) Update(id string, key, value, note string, organizationID string, projectIDs []string) (*SecretResponse, error) { command := Command{ Secrets: &SecretsCommand{ diff --git a/languages/kotlin/doc.md b/languages/kotlin/doc.md index d69e134c4..5dce0dbb4 100644 --- a/languages/kotlin/doc.md +++ b/languages/kotlin/doc.md @@ -46,6 +46,16 @@ Generator operations **Output**: Arc +### `exporters` + +Exporters + +**Arguments**: + +- self: Arc + +**Output**: Arc + ### `auth` Auth operations @@ -138,6 +148,23 @@ password, use the email OTP. **Output**: std::result::Result<,BitwardenError> +### `validate_password_user_key` + +Validate the user password without knowing the password hash + +Used for accounts that we know have master passwords but that have not logged in with a password. +Some example are login with device or TDE. + +This works by comparing the provided password against the encrypted user key. + +**Arguments**: + +- self: +- password: String +- encrypted_user_key: String + +**Output**: std::result::Result + ### `new_auth_request` Initialize a new auth request @@ -160,6 +187,16 @@ Approve an auth request **Output**: std::result::Result +### `trust_device` + +Trust the current device + +**Arguments**: + +- self: + +**Output**: std::result::Result + ## ClientAttachments ### `encrypt_buffer` @@ -494,6 +531,17 @@ Fingerprint using logged in user's public key **Output**: std::result::Result +### `load_flags` + +Load feature flags into the client + +**Arguments**: + +- self: +- flags: std::collections::HashMap + +**Output**: std::result::Result<,BitwardenError> + ## ClientSends ### `encrypt` @@ -1287,6 +1335,37 @@ implementations. + + deviceKey + object + + + + + + + + + + + + + + + + + + + + + + + + + +
KeyTypeDescription
device_keystringThe device's DeviceKey
protected_device_private_keyThe Device Private Key
device_protected_user_keyThe user's symmetric crypto key, encrypted with the Device Key.
+ + ## `InitUserCryptoRequest` diff --git a/languages/ruby/CHANGELOG.md b/languages/ruby/CHANGELOG.md index 91c5ee3c2..0c842ecbb 100644 --- a/languages/ruby/CHANGELOG.md +++ b/languages/ruby/CHANGELOG.md @@ -1,5 +1,6 @@ ## [Unreleased] -## [0.1.0] - 2023-09-19 +## [0.1.0] - 2024-02-23 - Initial release + diff --git a/languages/ruby/examples/example.rb b/languages/ruby/examples/example.rb index 3c8e2045d..d1c7ce455 100644 --- a/languages/ruby/examples/example.rb +++ b/languages/ruby/examples/example.rb @@ -3,6 +3,7 @@ token = ENV['ACCESS_TOKEN'] organization_id = ENV['ORGANIZATION_ID'] +state_path = ENV['STATE_PATH'] # Configuring the URLS is optional, set them to nil to use the default values api_url = ENV['API_URL'] @@ -11,7 +12,7 @@ bitwarden_settings = BitwardenSDKSecrets::BitwardenSettings.new(api_url, identity_url) bw_client = BitwardenSDKSecrets::BitwardenClient.new(bitwarden_settings) -response = bw_client.access_token_login(token) +response = bw_client.access_token_login(token, state_path) puts response # CREATE project