From 83f9061474f54cbb4923f907d3bd83396ad6a0c1 Mon Sep 17 00:00:00 2001
From: Andy Pixley <3723676+pixman20@users.noreply.github.com>
Date: Mon, 21 Jul 2025 15:54:28 -0400
Subject: [PATCH 001/179] [BRE-831] migrate secrets akv (#15158)
---
.github/workflows/build-browser-target.yml | 5 +
.github/workflows/build-browser.yml | 60 ++++++--
.github/workflows/build-cli-target.yml | 5 +
.github/workflows/build-cli.yml | 61 ++++++--
.github/workflows/build-desktop-target.yml | 5 +
.github/workflows/build-desktop.yml | 136 ++++++++++++++----
.github/workflows/build-web-target.yml | 6 +
.github/workflows/build-web.yml | 60 +++++---
.github/workflows/chromatic.yml | 26 +++-
.github/workflows/crowdin-pull.yml | 41 ++++--
.github/workflows/deploy-web.yml | 77 ++++++----
.github/workflows/lint-crowdin-config.yml | 22 ++-
.github/workflows/publish-cli.yml | 53 +++++--
.github/workflows/publish-desktop.yml | 50 +++++--
.github/workflows/publish-web.yml | 30 +++-
.github/workflows/release-desktop-beta.yml | 112 ++++++++++++---
.github/workflows/repository-management.yml | 54 ++++++-
.../retrieve-current-desktop-rollout.yml | 13 +-
.github/workflows/staged-rollout-desktop.yml | 13 +-
.github/workflows/version-auto-bump.yml | 24 +++-
20 files changed, 680 insertions(+), 173 deletions(-)
diff --git a/.github/workflows/build-browser-target.yml b/.github/workflows/build-browser-target.yml
index a2ae48d419b..ef3beef4b8b 100644
--- a/.github/workflows/build-browser-target.yml
+++ b/.github/workflows/build-browser-target.yml
@@ -28,6 +28,8 @@ jobs:
check-run:
name: Check PR run
uses: bitwarden/gh-actions/.github/workflows/check-run.yml@main
+ permissions:
+ contents: read
run-workflow:
name: Build Browser
@@ -35,4 +37,7 @@ jobs:
if: ${{ github.event.pull_request.head.repo.full_name != github.repository }}
uses: ./.github/workflows/build-browser.yml
secrets: inherit
+ permissions:
+ contents: read
+ id-token: write
diff --git a/.github/workflows/build-browser.yml b/.github/workflows/build-browser.yml
index 40b03d9e753..bd7d70e8543 100644
--- a/.github/workflows/build-browser.yml
+++ b/.github/workflows/build-browser.yml
@@ -41,7 +41,8 @@ defaults:
run:
shell: bash
-permissions: {}
+permissions:
+ contents: read
jobs:
setup:
@@ -77,10 +78,8 @@ jobs:
- name: Check secrets
id: check-secrets
- env:
- AZURE_KV_CI_SERVICE_PRINCIPAL: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }}
run: |
- has_secrets=${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL != '' }}
+ has_secrets=${{ secrets.AZURE_CLIENT_ID != '' }}
echo "has_secrets=$has_secrets" >> $GITHUB_OUTPUT
@@ -302,6 +301,9 @@ jobs:
build-safari:
name: Build Safari
runs-on: macos-13
+ permissions:
+ contents: read
+ id-token: write
needs:
- setup
- locales-test
@@ -327,10 +329,19 @@ jobs:
node --version
npm --version
- - name: Login to Azure
- uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0
+ - name: Log in to Azure
+ uses: bitwarden/gh-actions/azure-login@main
with:
- creds: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }}
+ subscription_id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
+ tenant_id: ${{ secrets.AZURE_TENANT_ID }}
+ client_id: ${{ secrets.AZURE_CLIENT_ID }}
+
+ - name: Get Azure Key Vault secrets
+ id: get-kv-secrets
+ uses: bitwarden/gh-actions/get-keyvault-secrets@main
+ with:
+ keyvault: gh-clients
+ secrets: "KEYCHAIN-PASSWORD"
- name: Download Provisioning Profiles secrets
env:
@@ -366,9 +377,12 @@ jobs:
az keyvault secret show --id https://bitwarden-ci.vault.azure.net/certificates/macdev-cert |
jq -r .value | base64 -d > $HOME/certificates/macdev-cert.p12
+ - name: Log out from Azure
+ uses: bitwarden/gh-actions/azure-logout@main
+
- name: Set up keychain
env:
- KEYCHAIN_PASSWORD: ${{ secrets.KEYCHAIN_PASSWORD }}
+ KEYCHAIN_PASSWORD: ${{ steps.get-kv-secrets.outputs.KEYCHAIN-PASSWORD }}
run: |
security create-keychain -p $KEYCHAIN_PASSWORD build.keychain
security default-keychain -s build.keychain
@@ -440,6 +454,10 @@ jobs:
name: Crowdin Push
if: github.event_name != 'pull_request_target' && github.ref == 'refs/heads/main'
runs-on: ubuntu-22.04
+ permissions:
+ contents: write
+ pull-requests: write
+ id-token: write
needs:
- build
- build-safari
@@ -449,10 +467,12 @@ jobs:
with:
ref: ${{ github.event.pull_request.head.sha }}
- - name: Login to Azure
- uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0
+ - name: Log in to Azure
+ uses: bitwarden/gh-actions/azure-login@main
with:
- creds: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }}
+ subscription_id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
+ tenant_id: ${{ secrets.AZURE_TENANT_ID }}
+ client_id: ${{ secrets.AZURE_CLIENT_ID }}
- name: Retrieve secrets
id: retrieve-secrets
@@ -461,6 +481,9 @@ jobs:
keyvault: "bitwarden-ci"
secrets: "crowdin-api-token"
+ - name: Log out from Azure
+ uses: bitwarden/gh-actions/azure-logout@main
+
- name: Upload Sources
uses: crowdin/github-action@f214c8723025f41fc55b2ad26e67b60b80b1885d # v2.7.1
env:
@@ -478,6 +501,9 @@ jobs:
name: Check for failures
if: always()
runs-on: ubuntu-22.04
+ permissions:
+ contents: read
+ id-token: write
needs:
- setup
- locales-test
@@ -493,11 +519,13 @@ jobs:
&& contains(needs.*.result, 'failure')
run: exit 1
- - name: Login to Azure - Prod Subscription
- uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0
+ - name: Log in to Azure
if: failure()
+ uses: bitwarden/gh-actions/azure-login@main
with:
- creds: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }}
+ subscription_id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
+ tenant_id: ${{ secrets.AZURE_TENANT_ID }}
+ client_id: ${{ secrets.AZURE_CLIENT_ID }}
- name: Retrieve secrets
id: retrieve-secrets
@@ -507,6 +535,10 @@ jobs:
keyvault: "bitwarden-ci"
secrets: "devops-alerts-slack-webhook-url"
+ - name: Log out from Azure
+ if: failure()
+ uses: bitwarden/gh-actions/azure-logout@main
+
- name: Notify Slack on failure
uses: act10ns/slack@44541246747a30eb3102d87f7a4cc5471b0ffb7d # v2.1.0
if: failure()
diff --git a/.github/workflows/build-cli-target.yml b/.github/workflows/build-cli-target.yml
index 6b493d4e6d9..54865ddaddd 100644
--- a/.github/workflows/build-cli-target.yml
+++ b/.github/workflows/build-cli-target.yml
@@ -28,6 +28,8 @@ jobs:
check-run:
name: Check PR run
uses: bitwarden/gh-actions/.github/workflows/check-run.yml@main
+ permissions:
+ contents: read
run-workflow:
name: Build CLI
@@ -35,4 +37,7 @@ jobs:
if: ${{ github.event.pull_request.head.repo.full_name != github.repository }}
uses: ./.github/workflows/build-cli.yml
secrets: inherit
+ permissions:
+ contents: read
+ id-token: write
diff --git a/.github/workflows/build-cli.yml b/.github/workflows/build-cli.yml
index ac314a4c33a..b31b22b926e 100644
--- a/.github/workflows/build-cli.yml
+++ b/.github/workflows/build-cli.yml
@@ -78,10 +78,8 @@ jobs:
- name: Check secrets
id: check-secrets
- env:
- AZURE_KV_CI_SERVICE_PRINCIPAL: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }}
run: |
- has_secrets=${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL != '' }}
+ has_secrets=${{ secrets.AZURE_CLIENT_ID != '' }}
echo "has_secrets=$has_secrets" >> $GITHUB_OUTPUT
@@ -108,6 +106,10 @@ jobs:
_NODE_VERSION: ${{ needs.setup.outputs.node_version }}
_WIN_PKG_FETCH_VERSION: 20.11.1
_WIN_PKG_VERSION: 3.5
+ permissions:
+ contents: read
+ id-token: write
+
steps:
- name: Check out repo
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
@@ -156,9 +158,11 @@ jobs:
- name: Login to Azure
if: ${{ matrix.os.base == 'mac' && needs.setup.outputs.has_secrets == 'true' }}
- uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0
+ uses: bitwarden/gh-actions/azure-login@main
with:
- creds: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }}
+ subscription_id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
+ tenant_id: ${{ secrets.AZURE_TENANT_ID }}
+ client_id: ${{ secrets.AZURE_CLIENT_ID }}
- name: Get certificates
if: ${{ matrix.os.base == 'mac' && needs.setup.outputs.has_secrets == 'true' }}
@@ -168,10 +172,21 @@ jobs:
az keyvault secret show --id https://bitwarden-ci.vault.azure.net/certificates/devid-app-cert |
jq -r .value | base64 -d > $HOME/certificates/devid-app-cert.p12
+ - name: Get Azure Key Vault secrets
+ id: get-kv-secrets
+ if: ${{ matrix.os.base == 'mac' && needs.setup.outputs.has_secrets == 'true' }}
+ uses: bitwarden/gh-actions/get-keyvault-secrets@main
+ with:
+ keyvault: gh-clients
+ secrets: "KEYCHAIN-PASSWORD,APP-STORE-CONNECT-AUTH-KEY,APP-STORE-CONNECT-TEAM-ISSUER"
+
+ - name: Log out from Azure
+ uses: bitwarden/gh-actions/azure-logout@main
+
- name: Set up keychain
if: ${{ matrix.os.base == 'mac' && needs.setup.outputs.has_secrets == 'true' }}
env:
- KEYCHAIN_PASSWORD: ${{ secrets.KEYCHAIN_PASSWORD }}
+ KEYCHAIN_PASSWORD: ${{ steps.get-kv-secrets.outputs.KEYCHAIN-PASSWORD }}
run: |
security create-keychain -p $KEYCHAIN_PASSWORD build.keychain
security default-keychain -s build.keychain
@@ -199,13 +214,13 @@ jobs:
run: |
mkdir ~/private_keys
cat << EOF > ~/private_keys/AuthKey_6TV9MKN3GP.p8
- ${{ secrets.APP_STORE_CONNECT_AUTH_KEY }}
+ ${{ steps.get-kv-secrets.outputs.APP-STORE-CONNECT-AUTH-KEY }}
EOF
- name: Notarize app
if: ${{ matrix.os.base == 'mac' && needs.setup.outputs.has_secrets == 'true' }}
env:
- APP_STORE_CONNECT_TEAM_ISSUER: ${{ secrets.APP_STORE_CONNECT_TEAM_ISSUER }}
+ APP_STORE_CONNECT_TEAM_ISSUER: ${{ steps.get-kv-secrets.outputs.APP-STORE-CONNECT-TEAM-ISSUER }}
APP_STORE_CONNECT_AUTH_KEY: 6TV9MKN3GP
APP_STORE_CONNECT_AUTH_KEY_PATH: ~/private_keys/AuthKey_6TV9MKN3GP.p8
run: |
@@ -261,6 +276,9 @@ jobs:
{ build_prefix: "bit", artifact_prefix: "", readable: "commercial license" }
]
runs-on: windows-2022
+ permissions:
+ contents: read
+ id-token: write
needs: setup
env:
_PACKAGE_VERSION: ${{ needs.setup.outputs.package_version }}
@@ -344,11 +362,13 @@ jobs:
ResourceHacker -open version-info.rc -save version-info.res -action compile
ResourceHacker -open %WIN_PKG_BUILT% -save %WIN_PKG_BUILT% -action addoverwrite -resource version-info.res
- - name: Login to Azure
+ - name: Log in to Azure
if: ${{ needs.setup.outputs.has_secrets == 'true' }}
- uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0
+ uses: bitwarden/gh-actions/azure-login@main
with:
- creds: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }}
+ subscription_id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
+ tenant_id: ${{ secrets.AZURE_TENANT_ID }}
+ client_id: ${{ secrets.AZURE_CLIENT_ID }}
- name: Retrieve secrets
id: retrieve-secrets
@@ -362,6 +382,10 @@ jobs:
code-signing-client-secret,
code-signing-cert-name"
+ - name: Log out from Azure
+ if: ${{ needs.setup.outputs.has_secrets == 'true' }}
+ uses: bitwarden/gh-actions/azure-logout@main
+
- name: Install
run: npm ci
working-directory: ./
@@ -520,6 +544,9 @@ jobs:
name: Check for failures
if: always()
runs-on: ubuntu-24.04
+ permissions:
+ contents: read
+ id-token: write
needs:
- setup
- cli
@@ -534,11 +561,13 @@ jobs:
&& contains(needs.*.result, 'failure')
run: exit 1
- - name: Login to Azure - Prod Subscription
- uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0
+ - name: Log in to Azure
if: failure()
+ uses: bitwarden/gh-actions/azure-login@main
with:
- creds: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }}
+ subscription_id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
+ tenant_id: ${{ secrets.AZURE_TENANT_ID }}
+ client_id: ${{ secrets.AZURE_CLIENT_ID }}
- name: Retrieve secrets
id: retrieve-secrets
@@ -548,6 +577,10 @@ jobs:
keyvault: "bitwarden-ci"
secrets: "devops-alerts-slack-webhook-url"
+ - name: Log out from Azure
+ if: failure()
+ uses: bitwarden/gh-actions/azure-logout@main
+
- name: Notify Slack on failure
uses: act10ns/slack@44541246747a30eb3102d87f7a4cc5471b0ffb7d # v2.1.0
if: failure()
diff --git a/.github/workflows/build-desktop-target.yml b/.github/workflows/build-desktop-target.yml
index fa21b3fe5d9..31ac819a3e6 100644
--- a/.github/workflows/build-desktop-target.yml
+++ b/.github/workflows/build-desktop-target.yml
@@ -28,6 +28,8 @@ jobs:
check-run:
name: Check PR run
uses: bitwarden/gh-actions/.github/workflows/check-run.yml@main
+ permissions:
+ contents: read
run-workflow:
name: Build Desktop
@@ -35,4 +37,7 @@ jobs:
if: ${{ github.event.pull_request.head.repo.full_name != github.repository }}
uses: ./.github/workflows/build-desktop.yml
secrets: inherit
+ permissions:
+ contents: read
+ id-token: write
diff --git a/.github/workflows/build-desktop.yml b/.github/workflows/build-desktop.yml
index a022fe7fd0f..366d439fb45 100644
--- a/.github/workflows/build-desktop.yml
+++ b/.github/workflows/build-desktop.yml
@@ -147,10 +147,8 @@ jobs:
- name: Check secrets
id: check-secrets
- env:
- AZURE_KV_CI_SERVICE_PRINCIPAL: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }}
run: |
- has_secrets=${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL != '' }}
+ has_secrets=${{ secrets.AZURE_CLIENT_ID != '' }}
echo "has_secrets=$has_secrets" >> $GITHUB_OUTPUT
linux:
@@ -404,6 +402,9 @@ jobs:
runs-on: windows-2022
needs:
- setup
+ permissions:
+ contents: read
+ id-token: write
defaults:
run:
shell: pwsh
@@ -438,11 +439,13 @@ jobs:
choco --version
rustup show
- - name: Login to Azure
+ - name: Log in to Azure
if: ${{ needs.setup.outputs.has_secrets == 'true' }}
- uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0
+ uses: bitwarden/gh-actions/azure-login@main
with:
- creds: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }}
+ subscription_id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
+ tenant_id: ${{ secrets.AZURE_TENANT_ID }}
+ client_id: ${{ secrets.AZURE_CLIENT_ID }}
- name: Retrieve secrets
id: retrieve-secrets
@@ -456,6 +459,10 @@ jobs:
code-signing-client-secret,
code-signing-cert-name"
+ - name: Log out from Azure
+ if: ${{ needs.setup.outputs.has_secrets == 'true' }}
+ uses: bitwarden/gh-actions/azure-logout@main
+
- name: Install Node dependencies
run: npm ci
working-directory: ./
@@ -655,6 +662,9 @@ jobs:
runs-on: macos-13
needs:
- setup
+ permissions:
+ contents: read
+ id-token: write
env:
_PACKAGE_VERSION: ${{ needs.setup.outputs.package_version }}
_NODE_VERSION: ${{ needs.setup.outputs.node_version }}
@@ -700,11 +710,21 @@ jobs:
path: apps/browser/dist/Safari
key: ${{ runner.os }}-${{ github.run_id }}-safari-extension
- - name: Login to Azure
+ - name: Log in to Azure
if: ${{ needs.setup.outputs.has_secrets == 'true' }}
- uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0
+ uses: bitwarden/gh-actions/azure-login@main
with:
- creds: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }}
+ subscription_id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
+ tenant_id: ${{ secrets.AZURE_TENANT_ID }}
+ client_id: ${{ secrets.AZURE_CLIENT_ID }}
+
+ - name: Get Azure Key Vault secrets
+ id: get-kv-secrets
+ if: ${{ needs.setup.outputs.has_secrets == 'true' }}
+ uses: bitwarden/gh-actions/get-keyvault-secrets@main
+ with:
+ keyvault: gh-clients
+ secrets: "KEYCHAIN-PASSWORD"
- name: Download Provisioning Profiles secrets
if: ${{ needs.setup.outputs.has_secrets == 'true' }}
@@ -747,10 +767,14 @@ jobs:
az keyvault secret show --id https://bitwarden-ci.vault.azure.net/certificates/macdev-cert |
jq -r .value | base64 -d > $HOME/certificates/macdev-cert.p12
+ - name: Log out from Azure
+ if: ${{ needs.setup.outputs.has_secrets == 'true' }}
+ uses: bitwarden/gh-actions/azure-logout@main
+
- name: Set up keychain
if: ${{ needs.setup.outputs.has_secrets == 'true' }}
env:
- KEYCHAIN_PASSWORD: ${{ secrets.KEYCHAIN_PASSWORD }}
+ KEYCHAIN_PASSWORD: ${{ steps.get-kv-secrets.outputs.KEYCHAIN-PASSWORD }}
run: |
security create-keychain -p $KEYCHAIN_PASSWORD build.keychain
security default-keychain -s build.keychain
@@ -850,6 +874,10 @@ jobs:
if: ${{ needs.setup.outputs.has_secrets == 'true' }}
uses: ./.github/workflows/build-browser.yml
secrets: inherit
+ permissions:
+ contents: write
+ pull-requests: write
+ id-token: write
macos-package-github:
@@ -860,6 +888,9 @@ jobs:
- browser-build
- macos-build
- setup
+ permissions:
+ contents: read
+ id-token: write
env:
_PACKAGE_VERSION: ${{ needs.setup.outputs.package_version }}
_NODE_VERSION: ${{ needs.setup.outputs.node_version }}
@@ -905,10 +936,19 @@ jobs:
path: apps/browser/dist/Safari
key: ${{ runner.os }}-${{ github.run_id }}-safari-extension
- - name: Login to Azure
- uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0
+ - name: Log in to Azure
+ uses: bitwarden/gh-actions/azure-login@main
with:
- creds: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }}
+ subscription_id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
+ tenant_id: ${{ secrets.AZURE_TENANT_ID }}
+ client_id: ${{ secrets.AZURE_CLIENT_ID }}
+
+ - name: Get Azure Key Vault secrets
+ id: get-kv-secrets
+ uses: bitwarden/gh-actions/get-keyvault-secrets@main
+ with:
+ keyvault: gh-clients
+ secrets: "KEYCHAIN-PASSWORD,APP-STORE-CONNECT-AUTH-KEY,APP-STORE-CONNECT-TEAM-ISSUER"
- name: Download Provisioning Profiles secrets
env:
@@ -949,9 +989,12 @@ jobs:
az keyvault secret show --id https://bitwarden-ci.vault.azure.net/certificates/macdev-cert |
jq -r .value | base64 -d > $HOME/certificates/macdev-cert.p12
+ - name: Log out from Azure
+ uses: bitwarden/gh-actions/azure-logout@main
+
- name: Set up keychain
env:
- KEYCHAIN_PASSWORD: ${{ secrets.KEYCHAIN_PASSWORD }}
+ KEYCHAIN_PASSWORD: ${{ steps.get-kv-secrets.outputs.KEYCHAIN-PASSWORD }}
run: |
security create-keychain -p $KEYCHAIN_PASSWORD build.keychain
security default-keychain -s build.keychain
@@ -1055,12 +1098,12 @@ jobs:
run: |
mkdir ~/private_keys
cat << EOF > ~/private_keys/AuthKey_6TV9MKN3GP.p8
- ${{ secrets.APP_STORE_CONNECT_AUTH_KEY }}
+ ${{ steps.get-kv-secrets.outputs.APP-STORE-CONNECT-AUTH-KEY }}
EOF
- name: Build application (dist)
env:
- APP_STORE_CONNECT_TEAM_ISSUER: ${{ secrets.APP_STORE_CONNECT_TEAM_ISSUER }}
+ APP_STORE_CONNECT_TEAM_ISSUER: ${{ steps.get-kv-secrets.outputs.APP-STORE-CONNECT-TEAM-ISSUER }}
APP_STORE_CONNECT_AUTH_KEY: 6TV9MKN3GP
APP_STORE_CONNECT_AUTH_KEY_PATH: ~/private_keys/AuthKey_6TV9MKN3GP.p8
CSC_FOR_PULL_REQUEST: true
@@ -1103,6 +1146,9 @@ jobs:
- browser-build
- macos-build
- setup
+ permissions:
+ contents: read
+ id-token: write
env:
_PACKAGE_VERSION: ${{ needs.setup.outputs.package_version }}
_NODE_VERSION: ${{ needs.setup.outputs.node_version }}
@@ -1148,10 +1194,19 @@ jobs:
path: apps/browser/dist/Safari
key: ${{ runner.os }}-${{ github.run_id }}-safari-extension
- - name: Login to Azure
- uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0
+ - name: Log in to Azure
+ uses: bitwarden/gh-actions/azure-login@main
with:
- creds: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }}
+ subscription_id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
+ tenant_id: ${{ secrets.AZURE_TENANT_ID }}
+ client_id: ${{ secrets.AZURE_CLIENT_ID }}
+
+ - name: Get Azure Key Vault secrets
+ id: get-kv-secrets
+ uses: bitwarden/gh-actions/get-keyvault-secrets@main
+ with:
+ keyvault: gh-clients
+ secrets: "KEYCHAIN-PASSWORD,APP-STORE-CONNECT-AUTH-KEY,APP-STORE-CONNECT-TEAM-ISSUER"
- name: Retrieve Slack secret
id: retrieve-slack-secret
@@ -1199,9 +1254,12 @@ jobs:
az keyvault secret show --id https://bitwarden-ci.vault.azure.net/certificates/macdev-cert |
jq -r .value | base64 -d > $HOME/certificates/macdev-cert.p12
+ - name: Log out from Azure
+ uses: bitwarden/gh-actions/azure-logout@main
+
- name: Set up keychain
env:
- KEYCHAIN_PASSWORD: ${{ secrets.KEYCHAIN_PASSWORD }}
+ KEYCHAIN_PASSWORD: ${{ steps.get-kv-secrets.outputs.KEYCHAIN-PASSWORD }}
run: |
security create-keychain -p $KEYCHAIN_PASSWORD build.keychain
security default-keychain -s build.keychain
@@ -1305,12 +1363,12 @@ jobs:
run: |
mkdir ~/private_keys
cat << EOF > ~/private_keys/AuthKey_6TV9MKN3GP.p8
- ${{ secrets.APP_STORE_CONNECT_AUTH_KEY }}
+ ${{ steps.get-kv-secrets.outputs.APP-STORE-CONNECT-AUTH-KEY }}
EOF
- name: Build application for App Store
env:
- APP_STORE_CONNECT_TEAM_ISSUER: ${{ secrets.APP_STORE_CONNECT_TEAM_ISSUER }}
+ APP_STORE_CONNECT_TEAM_ISSUER: ${{ steps.get-kv-secrets.outputs.APP-STORE-CONNECT-TEAM-ISSUER }}
APP_STORE_CONNECT_AUTH_KEY: 6TV9MKN3GP
APP_STORE_CONNECT_AUTH_KEY_PATH: ~/private_keys/AuthKey_6TV9MKN3GP.p8
CSC_FOR_PULL_REQUEST: true
@@ -1334,7 +1392,7 @@ jobs:
cat << EOF > ~/secrets/appstoreconnect-fastlane.json
{
- "issuer_id": "${{ secrets.APP_STORE_CONNECT_TEAM_ISSUER }}",
+ "issuer_id": "${{ steps.get-kv-secrets.outputs.APP-STORE-CONNECT-TEAM-ISSUER }}",
"key_id": "6TV9MKN3GP",
"key": "$KEY_WITHOUT_NEWLINES"
}
@@ -1346,7 +1404,7 @@ jobs:
github.event_name != 'pull_request_target'
&& (inputs.testflight_distribute || github.ref == 'refs/heads/main' || github.ref == 'refs/heads/rc' || github.ref == 'refs/heads/hotfix-rc-desktop')
env:
- APP_STORE_CONNECT_TEAM_ISSUER: ${{ secrets.APP_STORE_CONNECT_TEAM_ISSUER }}
+ APP_STORE_CONNECT_TEAM_ISSUER: ${{ steps.get-kv-secrets.outputs.APP-STORE-CONNECT-TEAM-ISSUER }}
APP_STORE_CONNECT_AUTH_KEY: 6TV9MKN3GP
BRANCH: ${{ github.ref }}
run: |
@@ -1396,6 +1454,10 @@ jobs:
- windows
- macos-package-github
- macos-package-mas
+ permissions:
+ contents: write
+ pull-requests: write
+ id-token: write
runs-on: ubuntu-22.04
steps:
- name: Check out repo
@@ -1403,10 +1465,12 @@ jobs:
with:
ref: ${{ github.event.pull_request.head.sha }}
- - name: Login to Azure
- uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0
+ - name: Log in to Azure
+ uses: bitwarden/gh-actions/azure-login@main
with:
- creds: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }}
+ subscription_id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
+ tenant_id: ${{ secrets.AZURE_TENANT_ID }}
+ client_id: ${{ secrets.AZURE_CLIENT_ID }}
- name: Retrieve secrets
id: retrieve-secrets
@@ -1415,6 +1479,9 @@ jobs:
keyvault: "bitwarden-ci"
secrets: "crowdin-api-token"
+ - name: Log out from Azure
+ uses: bitwarden/gh-actions/azure-logout@main
+
- name: Upload Sources
uses: crowdin/github-action@f214c8723025f41fc55b2ad26e67b60b80b1885d # v2.7.1
env:
@@ -1442,6 +1509,9 @@ jobs:
- macos-package-github
- macos-package-mas
- crowdin-push
+ permissions:
+ contents: read
+ id-token: write
steps:
- name: Check if any job failed
if: |
@@ -1450,11 +1520,13 @@ jobs:
&& contains(needs.*.result, 'failure')
run: exit 1
- - name: Login to Azure - Prod Subscription
- uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0
+ - name: Log in to Azure
if: failure()
+ uses: bitwarden/gh-actions/azure-login@main
with:
- creds: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }}
+ subscription_id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
+ tenant_id: ${{ secrets.AZURE_TENANT_ID }}
+ client_id: ${{ secrets.AZURE_CLIENT_ID }}
- name: Retrieve secrets
id: retrieve-secrets
@@ -1464,6 +1536,9 @@ jobs:
keyvault: "bitwarden-ci"
secrets: "devops-alerts-slack-webhook-url"
+ - name: Log out from Azure
+ uses: bitwarden/gh-actions/azure-logout@main
+
- name: Notify Slack on failure
uses: act10ns/slack@44541246747a30eb3102d87f7a4cc5471b0ffb7d # v2.1.0
if: failure()
@@ -1471,3 +1546,4 @@ jobs:
SLACK_WEBHOOK_URL: ${{ steps.retrieve-secrets.outputs.devops-alerts-slack-webhook-url }}
with:
status: ${{ job.status }}
+
diff --git a/.github/workflows/build-web-target.yml b/.github/workflows/build-web-target.yml
index ca10e6d46f2..b1055885400 100644
--- a/.github/workflows/build-web-target.yml
+++ b/.github/workflows/build-web-target.yml
@@ -27,6 +27,8 @@ jobs:
check-run:
name: Check PR run
uses: bitwarden/gh-actions/.github/workflows/check-run.yml@main
+ permissions:
+ contents: read
run-workflow:
name: Build Web
@@ -34,4 +36,8 @@ jobs:
if: ${{ github.event.pull_request.head.repo.full_name != github.repository }}
uses: ./.github/workflows/build-web.yml
secrets: inherit
+ permissions:
+ contents: read
+ id-token: write
+ security-events: write
diff --git a/.github/workflows/build-web.yml b/.github/workflows/build-web.yml
index 745376b46d8..b4163d161cf 100644
--- a/.github/workflows/build-web.yml
+++ b/.github/workflows/build-web.yml
@@ -51,7 +51,8 @@ env:
_AZ_REGISTRY: bitwardenprod.azurecr.io
_GITHUB_PR_REPO_NAME: ${{ github.event.pull_request.head.repo.full_name }}
-permissions: {}
+permissions:
+ contents: read
jobs:
setup:
@@ -80,10 +81,8 @@ jobs:
- name: Check secrets
id: check-secrets
- env:
- AZURE_KV_CI_SERVICE_PRINCIPAL: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }}
run: |
- has_secrets=${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL != '' }}
+ has_secrets=${{ secrets.AZURE_CLIENT_ID != '' }}
echo "has_secrets=$has_secrets" >> $GITHUB_OUTPUT
@@ -204,11 +203,13 @@ jobs:
uses: docker/setup-buildx-action@f95db51fddba0c2d1ec667646a06c2ce06100226 # v3.0.0
########## ACRs ##########
- - name: Login to Prod Azure
+ - name: Log in to Azure
if: ${{ needs.setup.outputs.has_secrets == 'true' }}
- uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0
+ uses: bitwarden/gh-actions/azure-login@main
with:
- creds: ${{ secrets.AZURE_PROD_KV_CREDENTIALS }}
+ subscription_id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
+ tenant_id: ${{ secrets.AZURE_TENANT_ID }}
+ client_id: ${{ secrets.AZURE_CLIENT_ID }}
- name: Log into Prod container registry
if: ${{ needs.setup.outputs.has_secrets == 'true' }}
@@ -328,11 +329,19 @@ jobs:
- name: Log out of Docker
run: docker logout $_AZ_REGISTRY
+ - name: Log out from Azure
+ if: ${{ needs.setup.outputs.has_secrets == 'true' }}
+ uses: bitwarden/gh-actions/azure-logout@main
+
crowdin-push:
name: Crowdin Push
if: github.event_name != 'pull_request_target' && github.ref == 'refs/heads/main'
needs: build-containers
+ permissions:
+ contents: write
+ pull-requests: write
+ id-token: write
runs-on: ubuntu-24.04
steps:
- name: Check out repo
@@ -340,10 +349,12 @@ jobs:
with:
ref: ${{ github.event.pull_request.head.sha }}
- - name: Login to Azure
- uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0
+ - name: Log in to Azure
+ uses: bitwarden/gh-actions/azure-login@main
with:
- creds: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }}
+ subscription_id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
+ tenant_id: ${{ secrets.AZURE_TENANT_ID }}
+ client_id: ${{ secrets.AZURE_CLIENT_ID }}
- name: Retrieve secrets
id: retrieve-secrets
@@ -352,6 +363,9 @@ jobs:
keyvault: "bitwarden-ci"
secrets: "crowdin-api-token"
+ - name: Log out from Azure
+ uses: bitwarden/gh-actions/azure-logout@main
+
- name: Upload Sources
uses: crowdin/github-action@f214c8723025f41fc55b2ad26e67b60b80b1885d # v2.7.1
env:
@@ -370,11 +384,15 @@ jobs:
if: github.event_name != 'pull_request_target' && github.ref == 'refs/heads/main'
runs-on: ubuntu-24.04
needs: build-containers
+ permissions:
+ id-token: write
steps:
- - name: Login to Azure - CI Subscription
- uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0
+ - name: Log in to Azure
+ uses: bitwarden/gh-actions/azure-login@main
with:
- creds: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }}
+ subscription_id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
+ tenant_id: ${{ secrets.AZURE_TENANT_ID }}
+ client_id: ${{ secrets.AZURE_CLIENT_ID }}
- name: Retrieve github PAT secrets
id: retrieve-secret-pat
@@ -383,6 +401,9 @@ jobs:
keyvault: "bitwarden-ci"
secrets: "github-pat-bitwarden-devops-bot-repo-scope"
+ - name: Log out from Azure
+ uses: bitwarden/gh-actions/azure-logout@main
+
- name: Trigger web vault deploy using GitHub Run ID
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1
with:
@@ -409,6 +430,8 @@ jobs:
- build-containers
- crowdin-push
- trigger-web-vault-deploy
+ permissions:
+ id-token: write
steps:
- name: Check if any job failed
if: |
@@ -417,11 +440,13 @@ jobs:
&& contains(needs.*.result, 'failure')
run: exit 1
- - name: Login to Azure - Prod Subscription
- uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0
+ - name: Log in to Azure
+ uses: bitwarden/gh-actions/azure-login@main
if: failure()
with:
- creds: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }}
+ subscription_id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
+ tenant_id: ${{ secrets.AZURE_TENANT_ID }}
+ client_id: ${{ secrets.AZURE_CLIENT_ID }}
- name: Retrieve secrets
id: retrieve-secrets
@@ -431,6 +456,9 @@ jobs:
keyvault: "bitwarden-ci"
secrets: "devops-alerts-slack-webhook-url"
+ - name: Log out from Azure
+ uses: bitwarden/gh-actions/azure-logout@main
+
- name: Notify Slack on failure
uses: act10ns/slack@44541246747a30eb3102d87f7a4cc5471b0ffb7d # v2.1.0
if: failure()
diff --git a/.github/workflows/chromatic.yml b/.github/workflows/chromatic.yml
index 78733bc5a8b..4ee39305f84 100644
--- a/.github/workflows/chromatic.yml
+++ b/.github/workflows/chromatic.yml
@@ -15,6 +15,8 @@ jobs:
check-run:
name: Check PR run
uses: bitwarden/gh-actions/.github/workflows/check-run.yml@main
+ permissions:
+ contents: read
chromatic:
name: Chromatic
@@ -23,6 +25,7 @@ jobs:
permissions:
contents: read
pull-requests: write
+ id-token: write
steps:
- name: Check out repo
@@ -30,13 +33,13 @@ jobs:
with:
ref: ${{ github.event.pull_request.head.sha }}
fetch-depth: 0
-
+
- name: Get changed files
id: get-changed-files-for-chromatic
uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3.0.2
with:
filters: |
- storyFiles:
+ storyFiles:
- "apps/!(cli)/**"
- "bitwarden_license/bit-web/src/app/**"
- "libs/!(eslint)/**"
@@ -74,11 +77,28 @@ jobs:
if: steps.get-changed-files-for-chromatic.outputs.storyFiles == 'true'
run: npm run build-storybook:ci
+ - name: Log in to Azure
+ uses: bitwarden/gh-actions/azure-login@main
+ with:
+ subscription_id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
+ tenant_id: ${{ secrets.AZURE_TENANT_ID }}
+ client_id: ${{ secrets.AZURE_CLIENT_ID }}
+
+ - name: Get Azure Key Vault secrets
+ id: get-kv-secrets
+ uses: bitwarden/gh-actions/get-keyvault-secrets@main
+ with:
+ keyvault: gh-clients
+ secrets: "CHROMATIC-PROJECT-TOKEN"
+
+ - name: Log out from Azure
+ uses: bitwarden/gh-actions/azure-logout@main
+
- name: Publish to Chromatic
uses: chromaui/action@e8cc4c31775280b175a3c440076c00d19a9014d7 # v11.28.2
with:
token: ${{ secrets.GITHUB_TOKEN }}
- projectToken: ${{ secrets.CHROMATIC_PROJECT_TOKEN }}
+ projectToken: ${{ steps.get-kv-secrets.outputs.CHROMATIC-PROJECT-TOKEN }}
storybookBuildDir: ./storybook-static
exitOnceUploaded: true
onlyChanged: true
diff --git a/.github/workflows/crowdin-pull.yml b/.github/workflows/crowdin-pull.yml
index 2fc035ec038..0b891203855 100644
--- a/.github/workflows/crowdin-pull.yml
+++ b/.github/workflows/crowdin-pull.yml
@@ -10,6 +10,9 @@ jobs:
crowdin-sync:
name: Autosync
runs-on: ubuntu-24.04
+ permissions:
+ contents: read
+ id-token: write
strategy:
fail-fast: false
matrix:
@@ -21,22 +24,19 @@ jobs:
- app_name: web
crowdin_project_id: "308189"
steps:
- - name: Generate GH App token
- uses: actions/create-github-app-token@30bf6253fa41bdc8d1501d202ad15287582246b4 # v2.0.3
- id: app-token
+ - name: Log in to Azure
+ uses: bitwarden/gh-actions/azure-login@main
with:
- app-id: ${{ secrets.BW_GHAPP_ID }}
- private-key: ${{ secrets.BW_GHAPP_KEY }}
+ subscription_id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
+ tenant_id: ${{ secrets.AZURE_TENANT_ID }}
+ client_id: ${{ secrets.AZURE_CLIENT_ID }}
- - name: Checkout repo
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
+ - name: Get Azure Key Vault secrets
+ id: get-kv-secrets
+ uses: bitwarden/gh-actions/get-keyvault-secrets@main
with:
- token: ${{ steps.app-token.outputs.token }}
-
- - name: Login to Azure
- uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0
- with:
- creds: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }}
+ keyvault: gh-org-bitwarden
+ secrets: "BW-GHAPP-ID,BW-GHAPP-KEY"
- name: Retrieve secrets
id: retrieve-secrets
@@ -45,6 +45,21 @@ jobs:
keyvault: "bitwarden-ci"
secrets: "crowdin-api-token, github-gpg-private-key, github-gpg-private-key-passphrase"
+ - name: Log out from Azure
+ uses: bitwarden/gh-actions/azure-logout@main
+
+ - name: Generate GH App token
+ uses: actions/create-github-app-token@30bf6253fa41bdc8d1501d202ad15287582246b4 # v2.0.3
+ id: app-token
+ with:
+ app-id: ${{ steps.get-kv-secrets.outputs.BW-GHAPP-ID }}
+ private-key: ${{ steps.get-kv-secrets.outputs.BW-GHAPP-KEY }}
+
+ - name: Checkout repo
+ uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
+ with:
+ token: ${{ steps.app-token.outputs.token }}
+
- name: Download translations
uses: bitwarden/gh-actions/crowdin@main
env:
diff --git a/.github/workflows/deploy-web.yml b/.github/workflows/deploy-web.yml
index 1cde8dd636a..3ffe18d1729 100644
--- a/.github/workflows/deploy-web.yml
+++ b/.github/workflows/deploy-web.yml
@@ -66,8 +66,9 @@ jobs:
environment_url: ${{ steps.config.outputs.environment_url }}
environment_name: ${{ steps.config.outputs.environment_name }}
environment_artifact: ${{ steps.config.outputs.environment_artifact }}
- azure_login_creds: ${{ steps.config.outputs.azure_login_creds }}
- retrive_secrets_keyvault: ${{ steps.config.outputs.retrive_secrets_keyvault }}
+ azure_login_client_key_name: ${{ steps.config.outputs.azure_login_client_key_name }}
+ azure_login_subscription_id_key_name: ${{ steps.config.outputs.azure_login_subscription_id_key_name }}
+ retrieve_secrets_keyvault: ${{ steps.config.outputs.retrieve_secrets_keyvault }}
sync_utility: ${{ steps.config.outputs.sync_utility }}
sync_delete_destination_files: ${{ steps.config.outputs.sync_delete_destination_files }}
slack_channel_name: ${{ steps.config.outputs.slack_channel_name }}
@@ -81,40 +82,45 @@ jobs:
case ${{ inputs.environment }} in
"USQA")
- echo "azure_login_creds=AZURE_KV_US_QA_SERVICE_PRINCIPAL" >> $GITHUB_OUTPUT
- echo "retrive_secrets_keyvault=bw-webvault-rlktusqa-kv" >> $GITHUB_OUTPUT
+ echo "azure_login_client_key_name=AZURE_CLIENT_ID_USQA" >> $GITHUB_OUTPUT
+ echo "azure_login_subscription_id_key_name=AZURE_SUBSCRIPTION_ID_USQA" >> $GITHUB_OUTPUT
+ echo "retrieve_secrets_keyvault=bw-webvault-rlktusqa-kv" >> $GITHUB_OUTPUT
echo "environment_artifact=web-*-cloud-QA.zip" >> $GITHUB_OUTPUT
echo "environment_name=Web Vault - US QA Cloud" >> $GITHUB_OUTPUT
echo "environment_url=http://vault.$ENV_NAME_LOWER.bitwarden.pw" >> $GITHUB_OUTPUT
echo "slack_channel_name=alerts-deploy-qa" >> $GITHUB_OUTPUT
;;
"EUQA")
- echo "azure_login_creds=AZURE_KV_EU_QA_SERVICE_PRINCIPAL" >> $GITHUB_OUTPUT
- echo "retrive_secrets_keyvault=webvaulteu-westeurope-qa" >> $GITHUB_OUTPUT
+ echo "azure_login_client_key_name=AZURE_CLIENT_ID_EUQA" >> $GITHUB_OUTPUT
+ echo "azure_login_subscription_id_key_name=AZURE_SUBSCRIPTION_ID_EUQA" >> $GITHUB_OUTPUT
+ echo "retrieve_secrets_keyvault=webvaulteu-westeurope-qa" >> $GITHUB_OUTPUT
echo "environment_artifact=web-*-cloud-euqa.zip" >> $GITHUB_OUTPUT
echo "environment_name=Web Vault - EU QA Cloud" >> $GITHUB_OUTPUT
echo "environment_url=http://vault.$ENV_NAME_LOWER.bitwarden.pw" >> $GITHUB_OUTPUT
echo "slack_channel_name=alerts-deploy-qa" >> $GITHUB_OUTPUT
;;
"USPROD")
- echo "azure_login_creds=AZURE_KV_US_PROD_SERVICE_PRINCIPAL" >> $GITHUB_OUTPUT
- echo "retrive_secrets_keyvault=bw-webvault-klrt-kv" >> $GITHUB_OUTPUT
+ echo "azure_login_client_key_name=AZURE_CLIENT_ID_USPROD" >> $GITHUB_OUTPUT
+ echo "azure_login_subscription_id_key_name=AZURE_SUBSCRIPTION_ID_USPROD" >> $GITHUB_OUTPUT
+ echo "retrieve_secrets_keyvault=bw-webvault-klrt-kv" >> $GITHUB_OUTPUT
echo "environment_artifact=web-*-cloud-COMMERCIAL.zip" >> $GITHUB_OUTPUT
echo "environment_name=Web Vault - US Production Cloud" >> $GITHUB_OUTPUT
echo "environment_url=http://vault.bitwarden.com" >> $GITHUB_OUTPUT
echo "slack_channel_name=alerts-deploy-prd" >> $GITHUB_OUTPUT
;;
"EUPROD")
- echo "azure_login_creds=AZURE_KV_EU_PRD_SERVICE_PRINCIPAL" >> $GITHUB_OUTPUT
- echo "retrive_secrets_keyvault=webvault-westeurope-prod" >> $GITHUB_OUTPUT
+ echo "azure_login_client_key_name=AZURE_CLIENT_ID_EUPROD" >> $GITHUB_OUTPUT
+ echo "azure_login_subscription_id_key_name=AZURE_SUBSCRIPTION_ID_EUPROD" >> $GITHUB_OUTPUT
+ echo "retrieve_secrets_keyvault=webvault-westeurope-prod" >> $GITHUB_OUTPUT
echo "environment_artifact=web-*-cloud-euprd.zip" >> $GITHUB_OUTPUT
echo "environment_name=Web Vault - EU Production Cloud" >> $GITHUB_OUTPUT
echo "environment_url=http://vault.bitwarden.eu" >> $GITHUB_OUTPUT
echo "slack_channel_name=alerts-deploy-prd" >> $GITHUB_OUTPUT
;;
"USDEV")
- echo "azure_login_creds=AZURE_KV_US_DEV_SERVICE_PRINCIPAL" >> $GITHUB_OUTPUT
- echo "retrive_secrets_keyvault=webvault-eastus-dev" >> $GITHUB_OUTPUT
+ echo "azure_login_client_key_name=AZURE_CLIENT_ID_USDEV" >> $GITHUB_OUTPUT
+ echo "azure_login_subscription_id_key_name=AZURE_SUBSCRIPTION_ID_USDEV" >> $GITHUB_OUTPUT
+ echo "retrieve_secrets_keyvault=webvault-eastus-dev" >> $GITHUB_OUTPUT
echo "environment_artifact=web-*-cloud-usdev.zip" >> $GITHUB_OUTPUT
echo "environment_name=Web Vault - US Development Cloud" >> $GITHUB_OUTPUT
echo "environment_url=http://vault.$ENV_NAME_LOWER.bitwarden.pw" >> $GITHUB_OUTPUT
@@ -180,6 +186,9 @@ jobs:
name: Check if Web artifact is present
runs-on: ubuntu-22.04
needs: setup
+ permissions:
+ contents: read
+ id-token: write
env:
_ENVIRONMENT_ARTIFACT: ${{ needs.setup.outputs.environment_artifact }}
outputs:
@@ -209,11 +218,13 @@ jobs:
branch: ${{ inputs.branch-or-tag }}
artifacts: ${{ env._ENVIRONMENT_ARTIFACT }}
- - name: Login to Azure
+ - name: Log in to Azure
if: ${{ steps.download-latest-artifacts.outcome == 'failure' }}
- uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0
+ uses: bitwarden/gh-actions/azure-login@main
with:
- creds: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }}
+ subscription_id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
+ tenant_id: ${{ secrets.AZURE_TENANT_ID }}
+ client_id: ${{ secrets.AZURE_CLIENT_ID }}
- name: Retrieve secrets for Build trigger
if: ${{ steps.download-latest-artifacts.outcome == 'failure' }}
@@ -223,6 +234,10 @@ jobs:
keyvault: "bitwarden-ci"
secrets: "github-pat-bitwarden-devops-bot-repo-scope"
+ - name: Log out from Azure
+ if: ${{ steps.download-latest-artifacts.outcome == 'failure' }}
+ uses: bitwarden/gh-actions/azure-logout@main
+
- name: 'Trigger build web for missing branch/tag ${{ inputs.branch-or-tag }}'
if: ${{ steps.download-latest-artifacts.outcome == 'failure' }}
uses: convictional/trigger-workflow-and-wait@f69fa9eedd3c62a599220f4d5745230e237904be # v1.6.5
@@ -277,7 +292,9 @@ jobs:
event: 'start'
commit-sha: ${{ needs.artifact-check.outputs.artifact_build_commit }}
url: https://github.com/bitwarden/clients/actions/runs/${{ github.run_id }}
- AZURE_KV_CI_SERVICE_PRINCIPAL: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }}
+ AZURE_SUBSCRIPTION_ID: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
+ AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }}
+ AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }}
update-summary:
name: Display commit
@@ -302,6 +319,9 @@ jobs:
_ENVIRONMENT_URL: ${{ needs.setup.outputs.environment_url }}
_ENVIRONMENT_NAME: ${{ needs.setup.outputs.environment_name }}
_ENVIRONMENT_ARTIFACT: ${{ needs.setup.outputs.environment_artifact }}
+ permissions:
+ id-token: write
+ deployments: write
steps:
- name: Create GitHub deployment
uses: chrnorm/deployment-action@55729fcebec3d284f60f5bcabbd8376437d696b1 # v2.0.7
@@ -309,23 +329,25 @@ jobs:
with:
token: '${{ secrets.GITHUB_TOKEN }}'
initial-status: 'in_progress'
- environment_url: ${{ env._ENVIRONMENT_URL }}
+ environment-url: ${{ env._ENVIRONMENT_URL }}
environment: ${{ env._ENVIRONMENT_NAME }}
task: 'deploy'
description: 'Deployment from branch/tag: ${{ inputs.branch-or-tag }}'
ref: ${{ needs.artifact-check.outputs.artifact_build_commit }}
- name: Login to Azure
- uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0
+ uses: bitwarden/gh-actions/azure-login@main
with:
- creds: ${{ secrets[needs.setup.outputs.azure_login_creds] }}
+ subscription_id: ${{ secrets[needs.setup.outputs.azure_login_subscription_id_key_name] }}
+ tenant_id: ${{ secrets.AZURE_TENANT_ID }}
+ client_id: ${{ secrets[needs.setup.outputs.azure_login_client_key_name] }}
- name: Retrieve Storage Account connection string for az sync
if: ${{ needs.setup.outputs.sync_utility == 'az-sync' }}
id: retrieve-secrets-az-sync
uses: bitwarden/gh-actions/get-keyvault-secrets@main
with:
- keyvault: ${{ needs.setup.outputs.retrive_secrets_keyvault }}
+ keyvault: ${{ needs.setup.outputs.retrieve_secrets_keyvault }}
secrets: "sa-bitwarden-web-vault-dev-key-temp"
- name: Retrieve Storage Account name and SPN credentials for azcopy
@@ -333,9 +355,12 @@ jobs:
id: retrieve-secrets-azcopy
uses: bitwarden/gh-actions/get-keyvault-secrets@main
with:
- keyvault: ${{ needs.setup.outputs.retrive_secrets_keyvault }}
+ keyvault: ${{ needs.setup.outputs.retrieve_secrets_keyvault }}
secrets: "sa-bitwarden-web-vault-name,sp-bitwarden-web-vault-password,sp-bitwarden-web-vault-appid,sp-bitwarden-web-vault-tenant"
+ - name: Log out from Azure
+ uses: bitwarden/gh-actions/azure-logout@main
+
- name: 'Download latest cloud asset using GitHub Run ID: ${{ inputs.build-web-run-id }}'
if: ${{ inputs.build-web-run-id }}
uses: bitwarden/gh-actions/download-artifacts@main
@@ -397,7 +422,7 @@ jobs:
uses: chrnorm/deployment-status@9a72af4586197112e0491ea843682b5dc280d806 # v2.0.3
with:
token: '${{ secrets.GITHUB_TOKEN }}'
- environment_url: ${{ env._ENVIRONMENT_URL }}
+ environment-url: ${{ env._ENVIRONMENT_URL }}
state: 'success'
deployment-id: ${{ steps.deployment.outputs.deployment_id }}
@@ -406,7 +431,7 @@ jobs:
uses: chrnorm/deployment-status@9a72af4586197112e0491ea843682b5dc280d806 # v2.0.3
with:
token: '${{ secrets.GITHUB_TOKEN }}'
- environment_url: ${{ env._ENVIRONMENT_URL }}
+ environment-url: ${{ env._ENVIRONMENT_URL }}
state: 'failure'
deployment-id: ${{ steps.deployment.outputs.deployment_id }}
@@ -419,6 +444,8 @@ jobs:
- notify-start
- azure-deploy
- artifact-check
+ permissions:
+ id-token: write
steps:
- name: Notify Slack with result
uses: bitwarden/gh-actions/report-deployment-status-to-slack@main
@@ -431,4 +458,6 @@ jobs:
url: https://github.com/bitwarden/clients/actions/runs/${{ github.run_id }}
commit-sha: ${{ needs.artifact-check.outputs.artifact_build_commit }}
update-ts: ${{ needs.notify-start.outputs.ts }}
- AZURE_KV_CI_SERVICE_PRINCIPAL: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }}
+ AZURE_SUBSCRIPTION_ID: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
+ AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }}
+ AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }}
diff --git a/.github/workflows/lint-crowdin-config.yml b/.github/workflows/lint-crowdin-config.yml
index adb5950e3a0..38a3ef59ea7 100644
--- a/.github/workflows/lint-crowdin-config.yml
+++ b/.github/workflows/lint-crowdin-config.yml
@@ -5,12 +5,14 @@ on:
types: [opened, synchronize]
paths:
- '**/crowdin.yml'
-permissions: {}
jobs:
lint-crowdin-config:
name: Lint Crowdin Config ${{ matrix.app.name }}
runs-on: ubuntu-24.04
+ permissions:
+ contents: read
+ id-token: write
strategy:
matrix:
app: [
@@ -22,17 +24,25 @@ jobs:
- name: Check out repo
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
- fetch-depth: 1
- - name: Login to Azure
- uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0
+ fetch-depth: 1
+
+ - name: Log in to Azure
+ uses: bitwarden/gh-actions/azure-login@main
with:
- creds: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }}
+ subscription_id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
+ tenant_id: ${{ secrets.AZURE_TENANT_ID }}
+ client_id: ${{ secrets.AZURE_CLIENT_ID }}
+
- name: Retrieve secrets
id: retrieve-secrets
uses: bitwarden/gh-actions/get-keyvault-secrets@main
with:
keyvault: "bitwarden-ci"
secrets: "crowdin-api-token"
+
+ - name: Log out from Azure
+ uses: bitwarden/gh-actions/azure-logout@main
+
- name: Lint ${{ matrix.app.name }} config
uses: crowdin/github-action@f214c8723025f41fc55b2ad26e67b60b80b1885d # v2.7.1
env:
@@ -42,4 +52,4 @@ jobs:
with:
dryrun_action: true
command: 'config lint'
- command_args: '--verbose -c ${{ matrix.app.config_path }}'
\ No newline at end of file
+ command_args: '--verbose -c ${{ matrix.app.config_path }}'
diff --git a/.github/workflows/publish-cli.yml b/.github/workflows/publish-cli.yml
index d758e6f11c9..efb0f541d70 100644
--- a/.github/workflows/publish-cli.yml
+++ b/.github/workflows/publish-cli.yml
@@ -48,6 +48,10 @@ jobs:
defaults:
run:
working-directory: .
+ permissions:
+ contents: read
+ deployments: write
+
steps:
- name: Branch check
if: ${{ inputs.publish_type != 'Dry Run' }}
@@ -86,6 +90,10 @@ jobs:
name: Deploy Snap
runs-on: ubuntu-22.04
needs: setup
+ permissions:
+ contents: read
+ packages: read
+ id-token: write
if: inputs.snap_publish
env:
_PKG_VERSION: ${{ needs.setup.outputs.release_version }}
@@ -93,10 +101,12 @@ jobs:
- name: Checkout repo
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- - name: Login to Azure
- uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0
+ - name: Log in to Azure
+ uses: bitwarden/gh-actions/azure-login@main
with:
- creds: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }}
+ subscription_id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
+ tenant_id: ${{ secrets.AZURE_TENANT_ID }}
+ client_id: ${{ secrets.AZURE_CLIENT_ID }}
- name: Retrieve secrets
id: retrieve-secrets
@@ -105,6 +115,9 @@ jobs:
keyvault: "bitwarden-ci"
secrets: "snapcraft-store-token"
+ - name: Log out from Azure
+ uses: bitwarden/gh-actions/azure-logout@main
+
- name: Install Snap
uses: samuelmeuli/action-snapcraft@d33c176a9b784876d966f80fb1b461808edc0641 # v2.1.1
@@ -123,6 +136,10 @@ jobs:
name: Deploy Choco
runs-on: windows-2022
needs: setup
+ permissions:
+ contents: read
+ packages: read
+ id-token: write
if: inputs.choco_publish
env:
_PKG_VERSION: ${{ needs.setup.outputs.release_version }}
@@ -130,10 +147,12 @@ jobs:
- name: Checkout repo
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- - name: Login to Azure
- uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0
+ - name: Log in to Azure
+ uses: bitwarden/gh-actions/azure-login@main
with:
- creds: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }}
+ subscription_id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
+ tenant_id: ${{ secrets.AZURE_TENANT_ID }}
+ client_id: ${{ secrets.AZURE_CLIENT_ID }}
- name: Retrieve secrets
id: retrieve-secrets
@@ -142,6 +161,9 @@ jobs:
keyvault: "bitwarden-ci"
secrets: "cli-choco-api-key"
+ - name: Log out from Azure
+ uses: bitwarden/gh-actions/azure-logout@main
+
- name: Setup Chocolatey
run: choco apikey --key $env:CHOCO_API_KEY --source https://push.chocolatey.org/
env:
@@ -163,6 +185,10 @@ jobs:
name: Publish NPM
runs-on: ubuntu-22.04
needs: setup
+ permissions:
+ contents: read
+ packages: read
+ id-token: write
if: inputs.npm_publish
env:
_PKG_VERSION: ${{ needs.setup.outputs.release_version }}
@@ -170,10 +196,12 @@ jobs:
- name: Checkout repo
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- - name: Login to Azure
- uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0
+ - name: Log in to Azure
+ uses: bitwarden/gh-actions/azure-login@main
with:
- creds: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }}
+ subscription_id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
+ tenant_id: ${{ secrets.AZURE_TENANT_ID }}
+ client_id: ${{ secrets.AZURE_CLIENT_ID }}
- name: Retrieve secrets
id: retrieve-secrets
@@ -182,6 +210,9 @@ jobs:
keyvault: "bitwarden-ci"
secrets: "npm-api-key"
+ - name: Log out from Azure
+ uses: bitwarden/gh-actions/azure-logout@main
+
- name: Download and set up artifact
run: |
mkdir -p build
@@ -210,6 +241,10 @@ jobs:
- npm
- snap
- choco
+ permissions:
+ contents: read
+ deployments: write
+
if: ${{ always() && inputs.publish_type != 'Dry Run' }}
steps:
- name: Check if any job failed
diff --git a/.github/workflows/publish-desktop.yml b/.github/workflows/publish-desktop.yml
index ae631165db9..aafc4d25ed4 100644
--- a/.github/workflows/publish-desktop.yml
+++ b/.github/workflows/publish-desktop.yml
@@ -42,6 +42,9 @@ jobs:
release_channel: ${{ steps.release_channel.outputs.channel }}
tag_name: ${{ steps.version.outputs.tag_name }}
deployment_id: ${{ steps.deployment.outputs.deployment_id }}
+ permissions:
+ contents: read
+ deployments: write
steps:
- name: Branch check
if: ${{ inputs.publish_type != 'Dry Run' }}
@@ -106,14 +109,21 @@ jobs:
name: Electron blob publish
runs-on: ubuntu-22.04
needs: setup
+ permissions:
+ contents: read
+ packages: read
+ id-token: write
+ deployments: write
env:
_PKG_VERSION: ${{ needs.setup.outputs.release_version }}
_RELEASE_TAG: ${{ needs.setup.outputs.tag_name }}
steps:
- - name: Login to Azure
- uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0
+ - name: Log in to Azure
+ uses: bitwarden/gh-actions/azure-login@main
with:
- creds: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }}
+ subscription_id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
+ tenant_id: ${{ secrets.AZURE_TENANT_ID }}
+ client_id: ${{ secrets.AZURE_CLIENT_ID }}
- name: Retrieve secrets
id: retrieve-secrets
@@ -124,6 +134,9 @@ jobs:
aws-electron-access-key,
aws-electron-bucket-name"
+ - name: Log out from Azure
+ uses: bitwarden/gh-actions/azure-logout@main
+
- name: Create artifacts directory
run: mkdir -p apps/desktop/artifacts
@@ -176,6 +189,9 @@ jobs:
name: Deploy Snap
runs-on: ubuntu-22.04
needs: setup
+ permissions:
+ contents: read
+ id-token: write
if: inputs.snap_publish
env:
_PKG_VERSION: ${{ needs.setup.outputs.release_version }}
@@ -184,10 +200,12 @@ jobs:
- name: Checkout Repo
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- - name: Login to Azure
- uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0
+ - name: Log in to Azure
+ uses: bitwarden/gh-actions/azure-login@main
with:
- creds: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }}
+ subscription_id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
+ tenant_id: ${{ secrets.AZURE_TENANT_ID }}
+ client_id: ${{ secrets.AZURE_CLIENT_ID }}
- name: Retrieve secrets
id: retrieve-secrets
@@ -196,6 +214,9 @@ jobs:
keyvault: "bitwarden-ci"
secrets: "snapcraft-store-token"
+ - name: Log out from Azure
+ uses: bitwarden/gh-actions/azure-logout@main
+
- name: Install Snap
uses: samuelmeuli/action-snapcraft@d33c176a9b784876d966f80fb1b461808edc0641 # v2.1.1
@@ -220,6 +241,9 @@ jobs:
name: Deploy Choco
runs-on: windows-2022
needs: setup
+ permissions:
+ contents: read
+ id-token: write
if: inputs.choco_publish
env:
_PKG_VERSION: ${{ needs.setup.outputs.release_version }}
@@ -233,10 +257,12 @@ jobs:
dotnet --version
dotnet nuget --version
- - name: Login to Azure
- uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0
+ - name: Log in to Azure
+ uses: bitwarden/gh-actions/azure-login@main
with:
- creds: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }}
+ subscription_id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
+ tenant_id: ${{ secrets.AZURE_TENANT_ID }}
+ client_id: ${{ secrets.AZURE_CLIENT_ID }}
- name: Retrieve secrets
id: retrieve-secrets
@@ -245,6 +271,9 @@ jobs:
keyvault: "bitwarden-ci"
secrets: "cli-choco-api-key"
+ - name: Log out from Azure
+ uses: bitwarden/gh-actions/azure-logout@main
+
- name: Setup Chocolatey
run: choco apikey --key $env:CHOCO_API_KEY --source https://push.chocolatey.org/
env:
@@ -271,6 +300,9 @@ jobs:
- electron-blob
- snap
- choco
+ permissions:
+ contents: read
+ deployments: write
if: ${{ always() && inputs.publish_type != 'Dry Run' }}
steps:
- name: Check if any job failed
diff --git a/.github/workflows/publish-web.yml b/.github/workflows/publish-web.yml
index 69b29086d36..a6f0f1be066 100644
--- a/.github/workflows/publish-web.yml
+++ b/.github/workflows/publish-web.yml
@@ -24,6 +24,8 @@ jobs:
outputs:
release_version: ${{ steps.version.outputs.version }}
tag_version: ${{ steps.version.outputs.tag }}
+ permissions:
+ contents: read
steps:
- name: Checkout repo
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
@@ -52,6 +54,10 @@ jobs:
name: Release self-host docker
runs-on: ubuntu-22.04
needs: setup
+ permissions:
+ id-token: write
+ contents: read
+ deployments: write
env:
_BRANCH_NAME: ${{ github.ref_name }}
_RELEASE_VERSION: ${{ needs.setup.outputs.release_version }}
@@ -69,10 +75,12 @@ jobs:
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
########## ACR ##########
- - name: Login to Azure - PROD Subscription
- uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0
+ - name: Log in to Azure
+ uses: bitwarden/gh-actions/azure-login@main
with:
- creds: ${{ secrets.AZURE_PROD_KV_CREDENTIALS }}
+ subscription_id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
+ tenant_id: ${{ secrets.AZURE_TENANT_ID }}
+ client_id: ${{ secrets.AZURE_CLIENT_ID }}
- name: Login to Azure ACR
run: az acr login -n bitwardenprod
@@ -121,6 +129,9 @@ jobs:
docker push $_AZ_REGISTRY/web-sh:latest
fi
+ - name: Log out from Azure
+ uses: bitwarden/gh-actions/azure-logout@main
+
- name: Update deployment status to Success
if: ${{ inputs.publish_type != 'Dry Run' && success() }}
uses: chrnorm/deployment-status@9a72af4586197112e0491ea843682b5dc280d806 # v2.0.3
@@ -147,11 +158,15 @@ jobs:
runs-on: ubuntu-22.04
needs:
- setup
+ permissions:
+ id-token: write
steps:
- - name: Log in to Azure - CI subscription
- uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0
+ - name: Log in to Azure
+ uses: bitwarden/gh-actions/azure-login@main
with:
- creds: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }}
+ subscription_id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
+ tenant_id: ${{ secrets.AZURE_TENANT_ID }}
+ client_id: ${{ secrets.AZURE_CLIENT_ID }}
- name: Retrieve GitHub PAT secrets
id: retrieve-secret-pat
@@ -160,6 +175,9 @@ jobs:
keyvault: "bitwarden-ci"
secrets: "github-pat-bitwarden-devops-bot-repo-scope"
+ - name: Log out from Azure
+ uses: bitwarden/gh-actions/azure-logout@main
+
- name: Trigger self-host build
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1
with:
diff --git a/.github/workflows/release-desktop-beta.yml b/.github/workflows/release-desktop-beta.yml
index a5e374395d8..e3eb9090cb7 100644
--- a/.github/workflows/release-desktop-beta.yml
+++ b/.github/workflows/release-desktop-beta.yml
@@ -15,6 +15,8 @@ jobs:
setup:
name: Setup
runs-on: ubuntu-22.04
+ permissions:
+ contents: write
outputs:
release_version: ${{ steps.version.outputs.version }}
release_channel: ${{ steps.release_channel.outputs.channel }}
@@ -115,6 +117,8 @@ jobs:
name: Linux Build
runs-on: ubuntu-22.04
needs: setup
+ permissions:
+ contents: read
env:
_PACKAGE_VERSION: ${{ needs.setup.outputs.release_version }}
_NODE_VERSION: ${{ needs.setup.outputs.node_version }}
@@ -204,6 +208,9 @@ jobs:
name: Windows Build
runs-on: windows-2022
needs: setup
+ permissions:
+ contents: read
+ id-token: write
defaults:
run:
shell: pwsh
@@ -237,10 +244,12 @@ jobs:
npm --version
choco --version
- - name: Login to Azure
- uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0
+ - name: Log in to Azure
+ uses: bitwarden/gh-actions/azure-login@main
with:
- creds: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }}
+ subscription_id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
+ tenant_id: ${{ secrets.AZURE_TENANT_ID }}
+ client_id: ${{ secrets.AZURE_CLIENT_ID }}
- name: Retrieve secrets
id: retrieve-secrets
@@ -253,6 +262,9 @@ jobs:
code-signing-client-secret,
code-signing-cert-name"
+ - name: Log out from Azure
+ uses: bitwarden/gh-actions/azure-logout@main
+
- name: Install Node dependencies
run: npm ci
working-directory: ./
@@ -394,6 +406,9 @@ jobs:
name: MacOS Build
runs-on: macos-13
needs: setup
+ permissions:
+ contents: read
+ id-token: write
env:
_PACKAGE_VERSION: ${{ needs.setup.outputs.release_version }}
_NODE_VERSION: ${{ needs.setup.outputs.node_version }}
@@ -438,6 +453,20 @@ jobs:
path: apps/browser/dist/Safari
key: ${{ runner.os }}-${{ github.run_id }}-safari-extension
+ - name: Log in to Azure
+ uses: bitwarden/gh-actions/azure-login@main
+ with:
+ subscription_id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
+ tenant_id: ${{ secrets.AZURE_TENANT_ID }}
+ client_id: ${{ secrets.AZURE_CLIENT_ID }}
+
+ - name: Get Azure Key Vault secrets
+ id: get-kv-secrets
+ uses: bitwarden/gh-actions/get-keyvault-secrets@main
+ with:
+ keyvault: gh-clients
+ secrets: "KEYCHAIN-PASSWORD"
+
- name: Download Provisioning Profiles secrets
env:
ACCOUNT_NAME: bitwardenci
@@ -472,9 +501,12 @@ jobs:
az keyvault secret show --id https://bitwarden-ci.vault.azure.net/certificates/macdev-cert |
jq -r .value | base64 -d > $HOME/certificates/macdev-cert.p12
+ - name: Log out from Azure
+ uses: bitwarden/gh-actions/azure-logout@main
+
- name: Set up keychain
env:
- KEYCHAIN_PASSWORD: ${{ secrets.KEYCHAIN_PASSWORD }}
+ KEYCHAIN_PASSWORD: ${{ steps.get-kv-secrets.outputs.KEYCHAIN-PASSWORD }}
run: |
security create-keychain -p $KEYCHAIN_PASSWORD build.keychain
security default-keychain -s build.keychain
@@ -528,6 +560,10 @@ jobs:
needs:
- setup
- macos-build
+ permissions:
+ contents: read
+ packages: read
+ id-token: write
env:
_PACKAGE_VERSION: ${{ needs.setup.outputs.release_version }}
_NODE_VERSION: ${{ needs.setup.outputs.node_version }}
@@ -572,10 +608,19 @@ jobs:
path: apps/browser/dist/Safari
key: ${{ runner.os }}-${{ github.run_id }}-safari-extension
- - name: Login to Azure
- uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0
+ - name: Log in to Azure
+ uses: bitwarden/gh-actions/azure-login@main
with:
- creds: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }}
+ subscription_id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
+ tenant_id: ${{ secrets.AZURE_TENANT_ID }}
+ client_id: ${{ secrets.AZURE_CLIENT_ID }}
+
+ - name: Get Azure Key Vault secrets
+ id: get-kv-secrets
+ uses: bitwarden/gh-actions/get-keyvault-secrets@main
+ with:
+ keyvault: gh-clients
+ secrets: "KEYCHAIN-PASSWORD,APPLE-ID-USERNAME,APPLE-ID-PASSWORD"
- name: Download Provisioning Profiles secrets
env:
@@ -611,9 +656,12 @@ jobs:
az keyvault secret show --id https://bitwarden-ci.vault.azure.net/certificates/macdev-cert |
jq -r .value | base64 -d > $HOME/certificates/macdev-cert.p12
+ - name: Log out from Azure
+ uses: bitwarden/gh-actions/azure-logout@main
+
- name: Set up keychain
env:
- KEYCHAIN_PASSWORD: ${{ secrets.KEYCHAIN_PASSWORD }}
+ KEYCHAIN_PASSWORD: ${{ steps.get-kv-secrets.outputs.KEYCHAIN-PASSWORD }}
run: |
security create-keychain -p $KEYCHAIN_PASSWORD build.keychain
security default-keychain -s build.keychain
@@ -702,8 +750,8 @@ jobs:
- name: Build application (dist)
env:
- APPLE_ID_USERNAME: ${{ secrets.APPLE_ID_USERNAME }}
- APPLE_ID_PASSWORD: ${{ secrets.APPLE_ID_PASSWORD }}
+ APPLE_ID_USERNAME: ${{ steps.get-kv-secrets.outputs.APPLE-ID-USERNAME }}
+ APPLE_ID_PASSWORD: ${{ steps.get-kv-secrets.outputs.APPLE-ID-PASSWORD }}
run: npm run pack:mac
- name: Upload .zip artifact
@@ -741,6 +789,10 @@ jobs:
needs:
- setup
- macos-build
+ permissions:
+ contents: read
+ packages: read
+ id-token: write
env:
_PACKAGE_VERSION: ${{ needs.setup.outputs.release_version }}
_NODE_VERSION: ${{ needs.setup.outputs.node_version }}
@@ -785,6 +837,20 @@ jobs:
path: apps/browser/dist/Safari
key: ${{ runner.os }}-${{ github.run_id }}-safari-extension
+ - name: Log in to Azure
+ uses: bitwarden/gh-actions/azure-login@main
+ with:
+ subscription_id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
+ tenant_id: ${{ secrets.AZURE_TENANT_ID }}
+ client_id: ${{ secrets.AZURE_CLIENT_ID }}
+
+ - name: Get Azure Key Vault secrets
+ id: get-kv-secrets
+ uses: bitwarden/gh-actions/get-keyvault-secrets@main
+ with:
+ keyvault: gh-clients
+ secrets: "KEYCHAIN-PASSWORD,APPLE-ID-USERNAME,APPLE-ID-PASSWORD"
+
- name: Download Provisioning Profiles secrets
env:
ACCOUNT_NAME: bitwardenci
@@ -819,9 +885,12 @@ jobs:
az keyvault secret show --id https://bitwarden-ci.vault.azure.net/certificates/macdev-cert |
jq -r .value | base64 -d > $HOME/certificates/macdev-cert.p12
+ - name: Log out from Azure
+ uses: bitwarden/gh-actions/azure-logout@main
+
- name: Set up keychain
env:
- KEYCHAIN_PASSWORD: ${{ secrets.KEYCHAIN_PASSWORD }}
+ KEYCHAIN_PASSWORD: ${{ steps.get-kv-secrets.outputs.KEYCHAIN-PASSWORD }}
run: |
security create-keychain -p $KEYCHAIN_PASSWORD build.keychain
security default-keychain -s build.keychain
@@ -911,8 +980,8 @@ jobs:
- name: Build application for App Store
run: npm run pack:mac:mas
env:
- APPLE_ID_USERNAME: ${{ secrets.APPLE_ID_USERNAME }}
- APPLE_ID_PASSWORD: ${{ secrets.APPLE_ID_PASSWORD }}
+ APPLE_ID_USERNAME: ${{ steps.get-kv-secrets.outputs.APPLE-ID-USERNAME }}
+ APPLE_ID_PASSWORD: ${{ steps.get-kv-secrets.outputs.APPLE-ID-PASSWORD }}
- name: Upload .pkg artifact
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
@@ -931,6 +1000,10 @@ jobs:
- macos-build
- macos-package-github
- macos-package-mas
+ permissions:
+ contents: read
+ id-token: write
+ deployments: write
steps:
- name: Create GitHub deployment
uses: chrnorm/deployment-action@55729fcebec3d284f60f5bcabbd8376437d696b1 # v2.0.7
@@ -942,10 +1015,12 @@ jobs:
description: 'Deployment ${{ needs.setup.outputs.release_version }} to channel ${{ needs.setup.outputs.release_channel }} from branch ${{ needs.setup.outputs.branch_name }}'
task: release
- - name: Login to Azure
- uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0
+ - name: Log in to Azure
+ uses: bitwarden/gh-actions/azure-login@main
with:
- creds: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }}
+ subscription_id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
+ tenant_id: ${{ secrets.AZURE_TENANT_ID }}
+ client_id: ${{ secrets.AZURE_CLIENT_ID }}
- name: Retrieve secrets
id: retrieve-secrets
@@ -956,6 +1031,9 @@ jobs:
aws-electron-access-key,
aws-electron-bucket-name"
+ - name: Log out from Azure
+ uses: bitwarden/gh-actions/azure-logout@main
+
- name: Download all artifacts
uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8
with:
@@ -1008,6 +1086,8 @@ jobs:
- macos-package-github
- macos-package-mas
- release
+ permissions:
+ contents: write
steps:
- name: Checkout repo
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
diff --git a/.github/workflows/repository-management.yml b/.github/workflows/repository-management.yml
index d91e0a12afd..ecb8e448a8a 100644
--- a/.github/workflows/repository-management.yml
+++ b/.github/workflows/repository-management.yml
@@ -36,7 +36,9 @@ on:
description: "New version override (leave blank for automatic calculation, example: '2024.1.0')"
required: false
type: string
+
permissions: {}
+
jobs:
setup:
name: Setup
@@ -56,6 +58,7 @@ jobs:
fi
echo "branch=$BRANCH" >> $GITHUB_OUTPUT
+
bump_version:
name: Bump Version
if: ${{ always() }}
@@ -66,6 +69,9 @@ jobs:
version_cli: ${{ steps.set-final-version-output.outputs.version_cli }}
version_desktop: ${{ steps.set-final-version-output.outputs.version_desktop }}
version_web: ${{ steps.set-final-version-output.outputs.version_web }}
+ permissions:
+ id-token: write
+
steps:
- name: Validate version input format
if: ${{ inputs.version_number_override != '' }}
@@ -73,12 +79,29 @@ jobs:
with:
version: ${{ inputs.version_number_override }}
+ - name: Log in to Azure
+ uses: bitwarden/gh-actions/azure-login@main
+ with:
+ subscription_id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
+ tenant_id: ${{ secrets.AZURE_TENANT_ID }}
+ client_id: ${{ secrets.AZURE_CLIENT_ID }}
+
+ - name: Get Azure Key Vault secrets
+ id: get-kv-secrets
+ uses: bitwarden/gh-actions/get-keyvault-secrets@main
+ with:
+ keyvault: gh-org-bitwarden
+ secrets: "BW-GHAPP-ID,BW-GHAPP-KEY"
+
+ - name: Log out from Azure
+ uses: bitwarden/gh-actions/azure-logout@main
+
- name: Generate GH App token
uses: actions/create-github-app-token@30bf6253fa41bdc8d1501d202ad15287582246b4 # v2.0.3
id: app-token
with:
- app-id: ${{ secrets.BW_GHAPP_ID }}
- private-key: ${{ secrets.BW_GHAPP_KEY }}
+ app-id: ${{ steps.get-kv-secrets.outputs.BW-GHAPP-ID }}
+ private-key: ${{ steps.get-kv-secrets.outputs.BW-GHAPP-KEY }}
- name: Check out branch
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
@@ -400,6 +423,7 @@ jobs:
- name: Push changes
if: ${{ steps.version-changed.outputs.changes_to_commit == 'TRUE' }}
run: git push
+
cut_branch:
name: Cut branch
if: ${{ needs.setup.outputs.branch == 'rc' }}
@@ -407,13 +431,33 @@ jobs:
- setup
- bump_version
runs-on: ubuntu-24.04
+ permissions:
+ id-token: write
+
steps:
+ - name: Log in to Azure
+ uses: bitwarden/gh-actions/azure-login@main
+ with:
+ subscription_id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
+ tenant_id: ${{ secrets.AZURE_TENANT_ID }}
+ client_id: ${{ secrets.AZURE_CLIENT_ID }}
+
+ - name: Get Azure Key Vault secrets
+ id: get-kv-secrets
+ uses: bitwarden/gh-actions/get-keyvault-secrets@main
+ with:
+ keyvault: gh-org-bitwarden
+ secrets: "BW-GHAPP-ID,BW-GHAPP-KEY"
+
+ - name: Log out from Azure
+ uses: bitwarden/gh-actions/azure-logout@main
+
- name: Generate GH App token
uses: actions/create-github-app-token@30bf6253fa41bdc8d1501d202ad15287582246b4 # v2.0.3
id: app-token
with:
- app-id: ${{ secrets.BW_GHAPP_ID }}
- private-key: ${{ secrets.BW_GHAPP_KEY }}
+ app-id: ${{ steps.get-kv-secrets.outputs.BW-GHAPP-ID }}
+ private-key: ${{ steps.get-kv-secrets.outputs.BW-GHAPP-KEY }}
- name: Check out target ref
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
@@ -435,4 +479,4 @@ jobs:
BRANCH_NAME: ${{ needs.setup.outputs.branch }}
run: |
git switch --quiet --create $BRANCH_NAME
- git push --quiet --set-upstream origin $BRANCH_NAME
\ No newline at end of file
+ git push --quiet --set-upstream origin $BRANCH_NAME
diff --git a/.github/workflows/retrieve-current-desktop-rollout.yml b/.github/workflows/retrieve-current-desktop-rollout.yml
index 2ab3072f566..c45453ed9d0 100644
--- a/.github/workflows/retrieve-current-desktop-rollout.yml
+++ b/.github/workflows/retrieve-current-desktop-rollout.yml
@@ -11,11 +11,15 @@ jobs:
rollout:
name: Retrieve Rollout Percentage
runs-on: ubuntu-22.04
+ permissions:
+ id-token: write
steps:
- - name: Login to Azure
- uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0
+ - name: Log in to Azure
+ uses: bitwarden/gh-actions/azure-login@main
with:
- creds: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }}
+ subscription_id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
+ tenant_id: ${{ secrets.AZURE_TENANT_ID }}
+ client_id: ${{ secrets.AZURE_CLIENT_ID }}
- name: Retrieve secrets
id: retrieve-secrets
@@ -26,6 +30,9 @@ jobs:
aws-electron-access-key,
aws-electron-bucket-name"
+ - name: Log out from Azure
+ uses: bitwarden/gh-actions/azure-logout@main
+
- name: Download channel update info files from S3
env:
AWS_ACCESS_KEY_ID: ${{ steps.retrieve-secrets.outputs.aws-electron-access-id }}
diff --git a/.github/workflows/staged-rollout-desktop.yml b/.github/workflows/staged-rollout-desktop.yml
index 4ec3af3be97..4adf81100bd 100644
--- a/.github/workflows/staged-rollout-desktop.yml
+++ b/.github/workflows/staged-rollout-desktop.yml
@@ -18,11 +18,15 @@ jobs:
rollout:
name: Update Rollout Percentage
runs-on: ubuntu-22.04
+ permissions:
+ id-token: write
steps:
- - name: Login to Azure
- uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0
+ - name: Log in to Azure
+ uses: bitwarden/gh-actions/azure-login@main
with:
- creds: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }}
+ subscription_id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
+ tenant_id: ${{ secrets.AZURE_TENANT_ID }}
+ client_id: ${{ secrets.AZURE_CLIENT_ID }}
- name: Retrieve secrets
id: retrieve-secrets
@@ -33,6 +37,9 @@ jobs:
aws-electron-access-key,
aws-electron-bucket-name"
+ - name: Log out from Azure
+ uses: bitwarden/gh-actions/azure-logout@main
+
- name: Download channel update info files from S3
env:
AWS_ACCESS_KEY_ID: ${{ steps.retrieve-secrets.outputs.aws-electron-access-id }}
diff --git a/.github/workflows/version-auto-bump.yml b/.github/workflows/version-auto-bump.yml
index e8bd1dde246..3cb5646886a 100644
--- a/.github/workflows/version-auto-bump.yml
+++ b/.github/workflows/version-auto-bump.yml
@@ -9,13 +9,33 @@ jobs:
bump-version:
name: Bump Desktop Version
runs-on: ubuntu-24.04
+ permissions:
+ id-token: write
+ contents: write
steps:
+ - name: Log in to Azure
+ uses: bitwarden/gh-actions/azure-login@main
+ with:
+ subscription_id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
+ tenant_id: ${{ secrets.AZURE_TENANT_ID }}
+ client_id: ${{ secrets.AZURE_CLIENT_ID }}
+
+ - name: Get Azure Key Vault secrets
+ id: get-kv-secrets
+ uses: bitwarden/gh-actions/get-keyvault-secrets@main
+ with:
+ keyvault: gh-org-bitwarden
+ secrets: "BW-GHAPP-ID,BW-GHAPP-KEY"
+
+ - name: Log out from Azure
+ uses: bitwarden/gh-actions/azure-logout@main
+
- name: Generate GH App token
uses: actions/create-github-app-token@30bf6253fa41bdc8d1501d202ad15287582246b4 # v2.0.3
id: app-token
with:
- app-id: ${{ secrets.BW_GHAPP_ID }}
- private-key: ${{ secrets.BW_GHAPP_KEY }}
+ app-id: ${{ steps.get-kv-secrets.outputs.BW-GHAPP-ID }}
+ private-key: ${{ steps.get-kv-secrets.outputs.BW-GHAPP-KEY }}
- name: Check out target ref
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
From 1f20bcecf0b0154ed45c889625e734069113a643 Mon Sep 17 00:00:00 2001
From: Alex Morask <144709477+amorask-bitwarden@users.noreply.github.com>
Date: Mon, 21 Jul 2025 15:06:02 -0500
Subject: [PATCH 002/179] Hide bank account for premium and when non-premium
selects non-US country (#15707)
---
.../change-payment-method-dialog.component.ts | 6 +++-
.../enter-payment-method.component.ts | 31 ++++++++++---------
2 files changed, 21 insertions(+), 16 deletions(-)
diff --git a/apps/web/src/app/billing/payment/components/change-payment-method-dialog.component.ts b/apps/web/src/app/billing/payment/components/change-payment-method-dialog.component.ts
index efd0055fb95..ff5156ba636 100644
--- a/apps/web/src/app/billing/payment/components/change-payment-method-dialog.component.ts
+++ b/apps/web/src/app/billing/payment/components/change-payment-method-dialog.component.ts
@@ -28,7 +28,11 @@ type DialogResult =
{{ "changePaymentMethod" | i18n }}
diff --git a/apps/web/src/app/billing/payment/components/enter-payment-method.component.ts b/apps/web/src/app/billing/payment/components/enter-payment-method.component.ts
index 4f5b2e3b15c..b73c3297e9e 100644
--- a/apps/web/src/app/billing/payment/components/enter-payment-method.component.ts
+++ b/apps/web/src/app/billing/payment/components/enter-payment-method.component.ts
@@ -1,6 +1,6 @@
import { Component, Input, OnInit } from "@angular/core";
import { FormControl, FormGroup, Validators } from "@angular/forms";
-import { BehaviorSubject, startWith, Subject, takeUntil } from "rxjs";
+import { map, Observable, of, startWith, Subject, takeUntil } from "rxjs";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
@@ -48,7 +48,7 @@ type PaymentMethodFormGroup = FormGroup<{
{{ "creditCard" | i18n }}
- @if (showBankAccount) {
+ @if (showBankAccount$ | async) {
@@ -226,20 +226,12 @@ type PaymentMethodFormGroup = FormGroup<{
export class EnterPaymentMethodComponent implements OnInit {
@Input({ required: true }) group!: PaymentMethodFormGroup;
- private showBankAccountSubject = new BehaviorSubject(true);
- showBankAccount$ = this.showBankAccountSubject.asObservable();
- @Input()
- set showBankAccount(value: boolean) {
- this.showBankAccountSubject.next(value);
- }
- get showBankAccount(): boolean {
- return this.showBankAccountSubject.value;
- }
-
- @Input() showPayPal: boolean = true;
- @Input() showAccountCredit: boolean = false;
- @Input() includeBillingAddress: boolean = false;
+ @Input() private showBankAccount = true;
+ @Input() showPayPal = true;
+ @Input() showAccountCredit = false;
+ @Input() includeBillingAddress = false;
+ protected showBankAccount$!: Observable;
protected selectableCountries = selectableCountries;
private destroy$ = new Subject();
@@ -267,7 +259,16 @@ export class EnterPaymentMethodComponent implements OnInit {
}
if (!this.includeBillingAddress) {
+ this.showBankAccount$ = of(this.showBankAccount);
this.group.controls.billingAddress.disable();
+ } else {
+ this.group.controls.billingAddress.patchValue({
+ country: "US",
+ });
+ this.showBankAccount$ = this.group.controls.billingAddress.controls.country.valueChanges.pipe(
+ startWith(this.group.controls.billingAddress.controls.country.value),
+ map((country) => this.showBankAccount && country === "US"),
+ );
}
this.group.controls.type.valueChanges
From 77940116e6818ef5eb4eb8247aab6e75574bf0a3 Mon Sep 17 00:00:00 2001
From: Robyn MacCallum
Date: Mon, 21 Jul 2025 16:15:39 -0400
Subject: [PATCH 003/179] DDG integration files modified workflow (#15665)
* Create alert-ddg-files-modified.yml
* Update alert-ddg-files-modified.yml
* Add encrypted-message-handler.service to alert-ddg-files-modified.yml
* Pin action versions
* Add permissions
* Update alert-ddg-files-modified.yml
* Update alert-ddg-files-modified.yml
* Add parameter to get list of files changed
* Wording update
* Update CODEOWNERS
* Make branch target main
---
.github/CODEOWNERS | 2 +
.../workflows/alert-ddg-files-modified.yml | 50 +++++++++++++++++++
2 files changed, 52 insertions(+)
create mode 100644 .github/workflows/alert-ddg-files-modified.yml
diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS
index ef2e26916e5..7d7fec2a5ea 100644
--- a/.github/CODEOWNERS
+++ b/.github/CODEOWNERS
@@ -138,6 +138,8 @@ apps/desktop/desktop_native/autotype @bitwarden/team-autofill-dev
# DuckDuckGo integration
apps/desktop/native-messaging-test-runner @bitwarden/team-autofill-dev
apps/desktop/src/services/duckduckgo-message-handler.service.ts @bitwarden/team-autofill-dev
+apps/desktop/src/services/encrypted-message-handler.service.ts @bitwarden/team-autofill-dev
+.github/workflows/alert-ddg-files-modified.yml @bitwarden/team-autofill-dev
# SSH Agent
apps/desktop/desktop_native/core/src/ssh_agent @bitwarden/team-autofill-dev @bitwarden/wg-ssh-keys
diff --git a/.github/workflows/alert-ddg-files-modified.yml b/.github/workflows/alert-ddg-files-modified.yml
new file mode 100644
index 00000000000..61bb7f1e8af
--- /dev/null
+++ b/.github/workflows/alert-ddg-files-modified.yml
@@ -0,0 +1,50 @@
+name: DDG File Change Monitor
+
+on:
+ pull_request:
+ branches: [ main ]
+ types: [ opened, synchronize ]
+
+jobs:
+ check-files:
+ name: Check files
+ runs-on: ubuntu-22.04
+ permissions:
+ contents: read
+ pull-requests: write
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
+ with:
+ fetch-depth: 0
+
+ - name: Get changed files
+ id: changed-files
+ uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3.0.2
+ with:
+ list-files: shell
+ filters: |
+ monitored:
+ - 'apps/desktop/native-messaging-test-runner/**'
+ - 'apps/desktop/src/services/duckduckgo-message-handler.service.ts'
+ - 'apps/desktop/src/services/encrypted-message-handler.service.ts'
+
+ - name: Comment on PR if monitored files changed
+ if: steps.changed-files.outputs.monitored == 'true'
+ uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1
+ with:
+ script: |
+ const changedFiles = `${{ steps.changed-files.outputs.monitored_files }}`.split(' ').filter(file => file.trim() !== '');
+
+ const message = `⚠️🦆 **DuckDuckGo Integration files have been modified in this PR:**
+
+ ${changedFiles.map(file => `- \`${file}\``).join('\n')}
+
+ Please run the DuckDuckGo native messaging test runner from this branch using [these instructions](https://contributing.bitwarden.com/getting-started/clients/desktop/native-messaging-test-runner) and ensure it functions properly.`;
+
+ github.rest.issues.createComment({
+ issue_number: context.issue.number,
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ body: message
+ });
From 81ee26733e6040a8d32afda67b5c3db3d8d094e0 Mon Sep 17 00:00:00 2001
From: Andy Pixley <3723676+pixman20@users.noreply.github.com>
Date: Mon, 21 Jul 2025 16:17:39 -0400
Subject: [PATCH 004/179] [BRE-831] Fixing permissions (#15713)
---
.github/workflows/deploy-web.yml | 2 ++
1 file changed, 2 insertions(+)
diff --git a/.github/workflows/deploy-web.yml b/.github/workflows/deploy-web.yml
index 3ffe18d1729..e21f7ae1e79 100644
--- a/.github/workflows/deploy-web.yml
+++ b/.github/workflows/deploy-web.yml
@@ -277,6 +277,8 @@ jobs:
- artifact-check
runs-on: ubuntu-22.04
if: ${{ always() && ( contains( inputs.environment , 'QA' ) || contains( inputs.environment , 'DEV' ) ) }}
+ permissions:
+ id-token: write
outputs:
channel_id: ${{ steps.slack-message.outputs.channel_id }}
ts: ${{ steps.slack-message.outputs.ts }}
From 391f540d1f488354002090823c3054e960b28a15 Mon Sep 17 00:00:00 2001
From: Shane Melton
Date: Mon, 21 Jul 2025 23:27:01 -0700
Subject: [PATCH 005/179] [PM-22136] Implement SDK cipher encryption (#15337)
* [PM-22136] Update sdk cipher view map to support uknown uuid type
* [PM-22136] Add key to CipherView for copying to SdkCipherView for encryption
* [PM-22136] Add fromSdk* helpers to Cipher domain objects
* [PM-22136] Add toSdk* helpers to Cipher View objects
* [PM-22136] Add encrypt() to cipher encryption service
* [PM-22136] Add feature flag
* [PM-22136] Use new SDK encrypt method when feature flag is enabled
* [PM-22136] Filter out null/empty URIs
* [PM-22136] Change default value for cipher view arrays to []. See ADR-0014.
* [PM-22136] Keep encrypted key value on attachment so that it is passed to the SDK
* [PM-22136] Keep encrypted key value on CipherView so that it is passed to the SDK during encryption
* [PM-22136] Update failing attachment test
* [PM-22136] Update failing importer tests due to new default value for arrays
* [PM-22136] Update CipherView.fromJson to handle the prototype of EncString for the cipher key
* [PM-22136] Add tickets for followup work
* [PM-22136] Use new set_fido2_credentials SDK method instead
* [PM-22136] Fix missing prototype when decrypting Fido2Credentials
* [PM-22136] Fix test after sdk change
* [PM-22136] Update @bitwarden/sdk-internal version
* [PM-22136] Fix some strict typing errors
* [PM-23348] Migrate move cipher to org to SDK (#15567)
* [PM-23348] Add moveToOrganization method to cipher-encryption.service.ts
* [PM-23348] Use cipherEncryptionService.moveToOrganization in cipherService shareWithServer and shareManyWithServer methods
* [PM-23348] Update cipherFormService to use the shareWithServer() method instead of encrypt()
* [PM-23348] Fix typo
* [PM-23348] Add missing docs
* [PM-22136] Fix EncString import after merge with main
---
libs/common/src/enums/feature-flag.enum.ts | 2 +
.../abstractions/cipher-encryption.service.ts | 25 +-
.../src/vault/abstractions/cipher.service.ts | 10 +
.../models/api/cipher-permissions.api.ts | 9 +-
.../src/vault/models/data/local.data.ts | 41 +++
.../vault/models/domain/attachment.spec.ts | 1 +
.../src/vault/models/domain/attachment.ts | 21 ++
libs/common/src/vault/models/domain/card.ts | 20 ++
.../src/vault/models/domain/cipher.spec.ts | 188 +++++++++++++-
libs/common/src/vault/models/domain/cipher.ts | 60 ++++-
.../vault/models/domain/fido2-credential.ts | 28 ++
.../src/vault/models/domain/field.spec.ts | 39 ++-
libs/common/src/vault/models/domain/field.ts | 18 ++
.../src/vault/models/domain/identity.ts | 32 +++
.../src/vault/models/domain/login-uri.ts | 13 +
libs/common/src/vault/models/domain/login.ts | 27 ++
.../src/vault/models/domain/password.ts | 16 ++
.../src/vault/models/domain/secure-note.ts | 15 ++
.../common/src/vault/models/domain/ssh-key.ts | 17 ++
.../common/src/vault/models/view/card.view.ts | 10 +-
.../src/vault/models/view/cipher.view.spec.ts | 94 ++++++-
.../src/vault/models/view/cipher.view.ts | 116 +++++++--
.../models/view/fido2-credential.view.ts | 23 +-
.../src/vault/models/view/field.view.ts | 14 +-
.../src/vault/models/view/identity.view.ts | 10 +-
.../src/vault/models/view/login-uri.view.ts | 9 +
.../src/vault/models/view/login.view.ts | 22 +-
.../models/view/password-history.view.spec.ts | 13 +
.../models/view/password-history.view.ts | 10 +
.../src/vault/models/view/secure-note.view.ts | 10 +-
.../src/vault/models/view/ssh-key.view.ts | 11 +
.../src/vault/services/cipher.service.spec.ts | 167 +++++++++++-
.../src/vault/services/cipher.service.ts | 122 +++++++--
.../default-cipher-encryption.service.spec.ts | 245 ++++++++++++++++--
.../default-cipher-encryption.service.ts | 106 +++++++-
libs/importer/src/importers/base-importer.ts | 6 -
.../keeper/keeper-csv-importer.spec.ts | 2 +-
.../keeper/keeper-json-importer.spec.ts | 2 +-
.../services/default-cipher-form.service.ts | 35 ++-
.../cipher-view/cipher-view.component.html | 4 +-
.../password-history-view.component.spec.ts | 11 +-
package-lock.json | 8 +-
package.json | 2 +-
43 files changed, 1485 insertions(+), 149 deletions(-)
diff --git a/libs/common/src/enums/feature-flag.enum.ts b/libs/common/src/enums/feature-flag.enum.ts
index 1af2ab1f0a9..8e9dc6fd35e 100644
--- a/libs/common/src/enums/feature-flag.enum.ts
+++ b/libs/common/src/enums/feature-flag.enum.ts
@@ -54,6 +54,7 @@ export enum FeatureFlag {
PM9111ExtensionPersistAddEditForm = "pm-9111-extension-persist-add-edit-form",
PM19941MigrateCipherDomainToSdk = "pm-19941-migrate-cipher-domain-to-sdk",
PM22134SdkCipherListView = "pm-22134-sdk-cipher-list-view",
+ PM22136_SdkCipherEncryption = "pm-22136-sdk-cipher-encryption",
CipherKeyEncryption = "cipher-key-encryption",
EndUserNotifications = "pm-10609-end-user-notifications",
RemoveCardItemTypePolicy = "pm-16442-remove-card-item-type-policy",
@@ -103,6 +104,7 @@ export const DefaultFeatureFlagValue = {
[FeatureFlag.RemoveCardItemTypePolicy]: FALSE,
[FeatureFlag.PM22134SdkCipherListView]: FALSE,
[FeatureFlag.PM19315EndUserActivationMvp]: FALSE,
+ [FeatureFlag.PM22136_SdkCipherEncryption]: FALSE,
/* Auth */
[FeatureFlag.PM16117_SetInitialPasswordRefactor]: FALSE,
diff --git a/libs/common/src/vault/abstractions/cipher-encryption.service.ts b/libs/common/src/vault/abstractions/cipher-encryption.service.ts
index 6b2a8e8943e..067c63b2110 100644
--- a/libs/common/src/vault/abstractions/cipher-encryption.service.ts
+++ b/libs/common/src/vault/abstractions/cipher-encryption.service.ts
@@ -1,6 +1,7 @@
+import { EncryptionContext } from "@bitwarden/common/vault/abstractions/cipher.service";
import { CipherListView } from "@bitwarden/sdk-internal";
-import { UserId } from "../../types/guid";
+import { UserId, OrganizationId } from "../../types/guid";
import { Cipher } from "../models/domain/cipher";
import { AttachmentView } from "../models/view/attachment.view";
import { CipherView } from "../models/view/cipher.view";
@@ -9,6 +10,28 @@ import { CipherView } from "../models/view/cipher.view";
* Service responsible for encrypting and decrypting ciphers.
*/
export abstract class CipherEncryptionService {
+ /**
+ * Encrypts a cipher using the SDK for the given userId.
+ * @param model The cipher view to encrypt
+ * @param userId The user ID to initialize the SDK client with
+ *
+ * @returns A promise that resolves to the encryption context, or undefined if encryption fails
+ */
+ abstract encrypt(model: CipherView, userId: UserId): Promise;
+
+ /**
+ * Move the cipher to the specified organization by re-encrypting its keys with the organization's key.
+ * The cipher.organizationId will be updated to the new organizationId.
+ * @param model The cipher view to move to the organization
+ * @param organizationId The ID of the organization to move the cipher to
+ * @param userId The user ID to initialize the SDK client with
+ */
+ abstract moveToOrganization(
+ model: CipherView,
+ organizationId: OrganizationId,
+ userId: UserId,
+ ): Promise;
+
/**
* Decrypts a cipher using the SDK for the given userId.
*
diff --git a/libs/common/src/vault/abstractions/cipher.service.ts b/libs/common/src/vault/abstractions/cipher.service.ts
index d1d686a66af..2f186369463 100644
--- a/libs/common/src/vault/abstractions/cipher.service.ts
+++ b/libs/common/src/vault/abstractions/cipher.service.ts
@@ -120,11 +120,21 @@ export abstract class CipherService implements UserKeyRotationDataProvider;
+
+ /**
+ * Move a cipher to an organization by re-encrypting its keys with the organization's key.
+ * @param cipher The cipher to move
+ * @param organizationId The Id of the organization to move the cipher to
+ * @param collectionIds The collection Ids to assign the cipher to in the organization
+ * @param userId The Id of the user performing the operation
+ * @param originalCipher Optional original cipher that will be used to compare/update password history
+ */
abstract shareWithServer(
cipher: CipherView,
organizationId: string,
collectionIds: string[],
userId: UserId,
+ originalCipher?: Cipher,
): Promise;
abstract shareManyWithServer(
ciphers: CipherView[],
diff --git a/libs/common/src/vault/models/api/cipher-permissions.api.ts b/libs/common/src/vault/models/api/cipher-permissions.api.ts
index b7341d39b1d..f9b62c4fc8d 100644
--- a/libs/common/src/vault/models/api/cipher-permissions.api.ts
+++ b/libs/common/src/vault/models/api/cipher-permissions.api.ts
@@ -4,7 +4,7 @@ import { CipherPermissions as SdkCipherPermissions } from "@bitwarden/sdk-intern
import { BaseResponse } from "../../../models/response/base.response";
-export class CipherPermissionsApi extends BaseResponse {
+export class CipherPermissionsApi extends BaseResponse implements SdkCipherPermissions {
delete: boolean = false;
restore: boolean = false;
@@ -35,4 +35,11 @@ export class CipherPermissionsApi extends BaseResponse {
return permissions;
}
+
+ /**
+ * Converts the CipherPermissionsApi to an SdkCipherPermissions
+ */
+ toSdkCipherPermissions(): SdkCipherPermissions {
+ return this;
+ }
}
diff --git a/libs/common/src/vault/models/data/local.data.ts b/libs/common/src/vault/models/data/local.data.ts
index 9ba820a58a2..50a24feba6f 100644
--- a/libs/common/src/vault/models/data/local.data.ts
+++ b/libs/common/src/vault/models/data/local.data.ts
@@ -1,4 +1,45 @@
+import {
+ LocalDataView as SdkLocalDataView,
+ LocalData as SdkLocalData,
+} from "@bitwarden/sdk-internal";
+
export type LocalData = {
lastUsedDate?: number;
lastLaunched?: number;
};
+
+/**
+ * Convert the SdkLocalDataView to LocalData
+ * @param localData
+ */
+export function fromSdkLocalData(
+ localData: SdkLocalDataView | SdkLocalData | undefined,
+): LocalData | undefined {
+ if (localData == null) {
+ return undefined;
+ }
+ return {
+ lastUsedDate: localData.lastUsedDate ? new Date(localData.lastUsedDate).getTime() : undefined,
+ lastLaunched: localData.lastLaunched ? new Date(localData.lastLaunched).getTime() : undefined,
+ };
+}
+
+/**
+ * Convert the LocalData to SdkLocalData
+ * @param localData
+ */
+export function toSdkLocalData(
+ localData: LocalData | undefined,
+): (SdkLocalDataView & SdkLocalData) | undefined {
+ if (localData == null) {
+ return undefined;
+ }
+ return {
+ lastUsedDate: localData.lastUsedDate
+ ? new Date(localData.lastUsedDate).toISOString()
+ : undefined,
+ lastLaunched: localData.lastLaunched
+ ? new Date(localData.lastLaunched).toISOString()
+ : undefined,
+ };
+}
diff --git a/libs/common/src/vault/models/domain/attachment.spec.ts b/libs/common/src/vault/models/domain/attachment.spec.ts
index d2b536b0590..2ea2c3d9a1d 100644
--- a/libs/common/src/vault/models/domain/attachment.spec.ts
+++ b/libs/common/src/vault/models/domain/attachment.spec.ts
@@ -93,6 +93,7 @@ describe("Attachment", () => {
sizeName: "1.1 KB",
fileName: "fileName",
key: expect.any(SymmetricCryptoKey),
+ encryptedKey: attachment.key,
});
});
diff --git a/libs/common/src/vault/models/domain/attachment.ts b/libs/common/src/vault/models/domain/attachment.ts
index abfebffb2e6..638f354c4b8 100644
--- a/libs/common/src/vault/models/domain/attachment.ts
+++ b/libs/common/src/vault/models/domain/attachment.ts
@@ -56,6 +56,7 @@ export class Attachment extends Domain {
if (this.key != null) {
view.key = await this.decryptAttachmentKey(orgId, encKey);
+ view.encryptedKey = this.key; // Keep the encrypted key for the view
}
return view;
@@ -131,4 +132,24 @@ export class Attachment extends Domain {
key: this.key?.toJSON(),
};
}
+
+ /**
+ * Maps an SDK Attachment object to an Attachment
+ * @param obj - The SDK attachment object
+ */
+ static fromSdkAttachment(obj: SdkAttachment): Attachment | undefined {
+ if (!obj) {
+ return undefined;
+ }
+
+ const attachment = new Attachment();
+ attachment.id = obj.id;
+ attachment.url = obj.url;
+ attachment.size = obj.size;
+ attachment.sizeName = obj.sizeName;
+ attachment.fileName = EncString.fromJSON(obj.fileName);
+ attachment.key = EncString.fromJSON(obj.key);
+
+ return attachment;
+ }
}
diff --git a/libs/common/src/vault/models/domain/card.ts b/libs/common/src/vault/models/domain/card.ts
index c78f9dfb719..688053ae93c 100644
--- a/libs/common/src/vault/models/domain/card.ts
+++ b/libs/common/src/vault/models/domain/card.ts
@@ -103,4 +103,24 @@ export class Card extends Domain {
code: this.code?.toJSON(),
};
}
+
+ /**
+ * Maps an SDK Card object to a Card
+ * @param obj - The SDK Card object
+ */
+ static fromSdkCard(obj: SdkCard): Card | undefined {
+ if (obj == null) {
+ return undefined;
+ }
+
+ const card = new Card();
+ card.cardholderName = EncString.fromJSON(obj.cardholderName);
+ card.brand = EncString.fromJSON(obj.brand);
+ card.number = EncString.fromJSON(obj.number);
+ card.expMonth = EncString.fromJSON(obj.expMonth);
+ card.expYear = EncString.fromJSON(obj.expYear);
+ card.code = EncString.fromJSON(obj.code);
+
+ return card;
+ }
}
diff --git a/libs/common/src/vault/models/domain/cipher.spec.ts b/libs/common/src/vault/models/domain/cipher.spec.ts
index 3ea8916a10b..60fff8b510e 100644
--- a/libs/common/src/vault/models/domain/cipher.spec.ts
+++ b/libs/common/src/vault/models/domain/cipher.spec.ts
@@ -10,6 +10,7 @@ import {
UriMatchType,
CipherRepromptType as SdkCipherRepromptType,
LoginLinkedIdType,
+ Cipher as SdkCipher,
} from "@bitwarden/sdk-internal";
import { makeStaticByteArray, mockEnc, mockFromJson } from "../../../../spec/utils";
@@ -206,7 +207,7 @@ describe("Cipher DTO", () => {
it("Convert", () => {
const cipher = new Cipher(cipherData);
- expect(cipher).toEqual({
+ expect(cipher).toMatchObject({
initializerKey: InitializerKey.Cipher,
id: "id",
organizationId: "orgId",
@@ -339,9 +340,9 @@ describe("Cipher DTO", () => {
edit: true,
viewPassword: true,
login: loginView,
- attachments: null,
- fields: null,
- passwordHistory: null,
+ attachments: [],
+ fields: [],
+ passwordHistory: [],
collectionIds: undefined,
revisionDate: new Date("2022-01-31T12:00:00.000Z"),
creationDate: new Date("2022-01-01T12:00:00.000Z"),
@@ -462,9 +463,9 @@ describe("Cipher DTO", () => {
edit: true,
viewPassword: true,
secureNote: { type: 0 },
- attachments: null,
- fields: null,
- passwordHistory: null,
+ attachments: [],
+ fields: [],
+ passwordHistory: [],
collectionIds: undefined,
revisionDate: new Date("2022-01-31T12:00:00.000Z"),
creationDate: new Date("2022-01-01T12:00:00.000Z"),
@@ -603,9 +604,9 @@ describe("Cipher DTO", () => {
edit: true,
viewPassword: true,
card: cardView,
- attachments: null,
- fields: null,
- passwordHistory: null,
+ attachments: [],
+ fields: [],
+ passwordHistory: [],
collectionIds: undefined,
revisionDate: new Date("2022-01-31T12:00:00.000Z"),
creationDate: new Date("2022-01-01T12:00:00.000Z"),
@@ -768,9 +769,9 @@ describe("Cipher DTO", () => {
edit: true,
viewPassword: true,
identity: identityView,
- attachments: null,
- fields: null,
- passwordHistory: null,
+ attachments: [],
+ fields: [],
+ passwordHistory: [],
collectionIds: undefined,
revisionDate: new Date("2022-01-31T12:00:00.000Z"),
creationDate: new Date("2022-01-01T12:00:00.000Z"),
@@ -1001,6 +1002,167 @@ describe("Cipher DTO", () => {
revisionDate: "2022-01-31T12:00:00.000Z",
});
});
+
+ it("should map from SDK Cipher", () => {
+ jest.restoreAllMocks();
+ const sdkCipher: SdkCipher = {
+ id: "id",
+ organizationId: "orgId",
+ folderId: "folderId",
+ collectionIds: [],
+ key: "EncryptedString",
+ name: "EncryptedString",
+ notes: "EncryptedString",
+ type: SdkCipherType.Login,
+ login: {
+ username: "EncryptedString",
+ password: "EncryptedString",
+ passwordRevisionDate: "2022-01-31T12:00:00.000Z",
+ uris: [
+ {
+ uri: "EncryptedString",
+ uriChecksum: "EncryptedString",
+ match: UriMatchType.Domain,
+ },
+ ],
+ totp: "EncryptedString",
+ autofillOnPageLoad: false,
+ fido2Credentials: undefined,
+ },
+ identity: undefined,
+ card: undefined,
+ secureNote: undefined,
+ sshKey: undefined,
+ favorite: false,
+ reprompt: SdkCipherRepromptType.None,
+ organizationUseTotp: true,
+ edit: true,
+ permissions: new CipherPermissionsApi(),
+ viewPassword: true,
+ localData: {
+ lastUsedDate: "2025-04-15T12:00:00.000Z",
+ lastLaunched: "2025-04-15T12:00:00.000Z",
+ },
+ attachments: [
+ {
+ id: "a1",
+ url: "url",
+ size: "1100",
+ sizeName: "1.1 KB",
+ fileName: "file",
+ key: "EncKey",
+ },
+ {
+ id: "a2",
+ url: "url",
+ size: "1100",
+ sizeName: "1.1 KB",
+ fileName: "file",
+ key: "EncKey",
+ },
+ ],
+ fields: [
+ {
+ name: "EncryptedString",
+ value: "EncryptedString",
+ type: FieldType.Linked,
+ linkedId: LoginLinkedIdType.Username,
+ },
+ {
+ name: "EncryptedString",
+ value: "EncryptedString",
+ type: FieldType.Linked,
+ linkedId: LoginLinkedIdType.Password,
+ },
+ ],
+ passwordHistory: [
+ {
+ password: "EncryptedString",
+ lastUsedDate: "2022-01-31T12:00:00.000Z",
+ },
+ ],
+ creationDate: "2022-01-01T12:00:00.000Z",
+ deletedDate: undefined,
+ revisionDate: "2022-01-31T12:00:00.000Z",
+ };
+
+ const lastUsedDate = new Date("2025-04-15T12:00:00.000Z").getTime();
+ const lastLaunched = new Date("2025-04-15T12:00:00.000Z").getTime();
+
+ const cipherData: CipherData = {
+ id: "id",
+ organizationId: "orgId",
+ folderId: "folderId",
+ edit: true,
+ permissions: new CipherPermissionsApi(),
+ collectionIds: [],
+ viewPassword: true,
+ organizationUseTotp: true,
+ favorite: false,
+ revisionDate: "2022-01-31T12:00:00.000Z",
+ type: CipherType.Login,
+ name: "EncryptedString",
+ notes: "EncryptedString",
+ creationDate: "2022-01-01T12:00:00.000Z",
+ deletedDate: null,
+ reprompt: CipherRepromptType.None,
+ key: "EncryptedString",
+ login: {
+ uris: [
+ {
+ uri: "EncryptedString",
+ uriChecksum: "EncryptedString",
+ match: UriMatchStrategy.Domain,
+ },
+ ],
+ username: "EncryptedString",
+ password: "EncryptedString",
+ passwordRevisionDate: "2022-01-31T12:00:00.000Z",
+ totp: "EncryptedString",
+ autofillOnPageLoad: false,
+ },
+ passwordHistory: [
+ { password: "EncryptedString", lastUsedDate: "2022-01-31T12:00:00.000Z" },
+ ],
+ attachments: [
+ {
+ id: "a1",
+ url: "url",
+ size: "1100",
+ sizeName: "1.1 KB",
+ fileName: "file",
+ key: "EncKey",
+ },
+ {
+ id: "a2",
+ url: "url",
+ size: "1100",
+ sizeName: "1.1 KB",
+ fileName: "file",
+ key: "EncKey",
+ },
+ ],
+ fields: [
+ {
+ name: "EncryptedString",
+ value: "EncryptedString",
+ type: FieldType.Linked,
+ linkedId: LoginLinkedId.Username,
+ },
+ {
+ name: "EncryptedString",
+ value: "EncryptedString",
+ type: FieldType.Linked,
+ linkedId: LoginLinkedId.Password,
+ },
+ ],
+ };
+ const expectedCipher = new Cipher(cipherData, { lastUsedDate, lastLaunched });
+
+ const cipher = Cipher.fromSdkCipher(sdkCipher);
+
+ expect(cipher).toEqual(expectedCipher);
+ });
});
});
diff --git a/libs/common/src/vault/models/domain/cipher.ts b/libs/common/src/vault/models/domain/cipher.ts
index 2f64fb82726..2a13cb06d71 100644
--- a/libs/common/src/vault/models/domain/cipher.ts
+++ b/libs/common/src/vault/models/domain/cipher.ts
@@ -2,6 +2,7 @@
// @ts-strict-ignore
import { Jsonify } from "type-fest";
+import { uuidToString } from "@bitwarden/common/platform/abstractions/sdk/sdk.service";
import { Cipher as SdkCipher } from "@bitwarden/sdk-internal";
import { EncString } from "../../../key-management/crypto/models/enc-string";
@@ -14,7 +15,7 @@ import { CipherRepromptType } from "../../enums/cipher-reprompt-type";
import { CipherType } from "../../enums/cipher-type";
import { CipherPermissionsApi } from "../api/cipher-permissions.api";
import { CipherData } from "../data/cipher.data";
-import { LocalData } from "../data/local.data";
+import { LocalData, fromSdkLocalData, toSdkLocalData } from "../data/local.data";
import { AttachmentView } from "../view/attachment.view";
import { CipherView } from "../view/cipher.view";
import { FieldView } from "../view/field.view";
@@ -361,16 +362,7 @@ export class Cipher extends Domain implements Decryptable {
}
: undefined,
viewPassword: this.viewPassword ?? true,
- localData: this.localData
- ? {
- lastUsedDate: this.localData.lastUsedDate
- ? new Date(this.localData.lastUsedDate).toISOString()
- : undefined,
- lastLaunched: this.localData.lastLaunched
- ? new Date(this.localData.lastLaunched).toISOString()
- : undefined,
- }
- : undefined,
+ localData: toSdkLocalData(this.localData),
attachments: this.attachments?.map((a) => a.toSdkAttachment()),
fields: this.fields?.map((f) => f.toSdkField()),
passwordHistory: this.passwordHistory?.map((ph) => ph.toSdkPasswordHistory()),
@@ -408,4 +400,50 @@ export class Cipher extends Domain implements Decryptable {
return sdkCipher;
}
+
+ /**
+ * Maps an SDK Cipher object to a Cipher
+ * @param sdkCipher - The SDK Cipher object
+ */
+ static fromSdkCipher(sdkCipher: SdkCipher | null): Cipher | undefined {
+ if (sdkCipher == null) {
+ return undefined;
+ }
+
+ const cipher = new Cipher();
+
+ cipher.id = sdkCipher.id ? uuidToString(sdkCipher.id) : undefined;
+ cipher.organizationId = sdkCipher.organizationId
+ ? uuidToString(sdkCipher.organizationId)
+ : undefined;
+ cipher.folderId = sdkCipher.folderId ? uuidToString(sdkCipher.folderId) : undefined;
+ cipher.collectionIds = sdkCipher.collectionIds ? sdkCipher.collectionIds.map(uuidToString) : [];
+ cipher.key = EncString.fromJSON(sdkCipher.key);
+ cipher.name = EncString.fromJSON(sdkCipher.name);
+ cipher.notes = EncString.fromJSON(sdkCipher.notes);
+ cipher.type = sdkCipher.type;
+ cipher.favorite = sdkCipher.favorite;
+ cipher.organizationUseTotp = sdkCipher.organizationUseTotp;
+ cipher.edit = sdkCipher.edit;
+ cipher.permissions = CipherPermissionsApi.fromSdkCipherPermissions(sdkCipher.permissions);
+ cipher.viewPassword = sdkCipher.viewPassword;
+ cipher.localData = fromSdkLocalData(sdkCipher.localData);
+ cipher.attachments = sdkCipher.attachments?.map((a) => Attachment.fromSdkAttachment(a)) ?? [];
+ cipher.fields = sdkCipher.fields?.map((f) => Field.fromSdkField(f)) ?? [];
+ cipher.passwordHistory =
+ sdkCipher.passwordHistory?.map((ph) => Password.fromSdkPasswordHistory(ph)) ?? [];
+ cipher.creationDate = new Date(sdkCipher.creationDate);
+ cipher.revisionDate = new Date(sdkCipher.revisionDate);
+ cipher.deletedDate = sdkCipher.deletedDate ? new Date(sdkCipher.deletedDate) : null;
+ cipher.reprompt = sdkCipher.reprompt;
+
+ // Cipher type specific properties
+ cipher.login = Login.fromSdkLogin(sdkCipher.login);
+ cipher.secureNote = SecureNote.fromSdkSecureNote(sdkCipher.secureNote);
+ cipher.card = Card.fromSdkCard(sdkCipher.card);
+ cipher.identity = Identity.fromSdkIdentity(sdkCipher.identity);
+ cipher.sshKey = SshKey.fromSdkSshKey(sdkCipher.sshKey);
+
+ return cipher;
+ }
}
diff --git a/libs/common/src/vault/models/domain/fido2-credential.ts b/libs/common/src/vault/models/domain/fido2-credential.ts
index 508f8a6d5fb..5dbf55b44fc 100644
--- a/libs/common/src/vault/models/domain/fido2-credential.ts
+++ b/libs/common/src/vault/models/domain/fido2-credential.ts
@@ -173,4 +173,32 @@ export class Fido2Credential extends Domain {
creationDate: this.creationDate.toISOString(),
};
}
+
+ /**
+ * Maps an SDK Fido2Credential object to a Fido2Credential
+ * @param obj - The SDK Fido2Credential object
+ */
+ static fromSdkFido2Credential(obj: SdkFido2Credential): Fido2Credential | undefined {
+ if (!obj) {
+ return undefined;
+ }
+
+ const credential = new Fido2Credential();
+
+ credential.credentialId = EncString.fromJSON(obj.credentialId);
+ credential.keyType = EncString.fromJSON(obj.keyType);
+ credential.keyAlgorithm = EncString.fromJSON(obj.keyAlgorithm);
+ credential.keyCurve = EncString.fromJSON(obj.keyCurve);
+ credential.keyValue = EncString.fromJSON(obj.keyValue);
+ credential.rpId = EncString.fromJSON(obj.rpId);
+ credential.userHandle = EncString.fromJSON(obj.userHandle);
+ credential.userName = EncString.fromJSON(obj.userName);
+ credential.counter = EncString.fromJSON(obj.counter);
+ credential.rpName = EncString.fromJSON(obj.rpName);
+ credential.userDisplayName = EncString.fromJSON(obj.userDisplayName);
+ credential.discoverable = EncString.fromJSON(obj.discoverable);
+ credential.creationDate = new Date(obj.creationDate);
+
+ return credential;
+ }
}
diff --git a/libs/common/src/vault/models/domain/field.spec.ts b/libs/common/src/vault/models/domain/field.spec.ts
index 08bc0da84fe..b5e26199e7d 100644
--- a/libs/common/src/vault/models/domain/field.spec.ts
+++ b/libs/common/src/vault/models/domain/field.spec.ts
@@ -1,6 +1,14 @@
+import {
+ Field as SdkField,
+ FieldType,
+ LoginLinkedIdType,
+ CardLinkedIdType,
+ IdentityLinkedIdType,
+} from "@bitwarden/sdk-internal";
+
import { mockEnc, mockFromJson } from "../../../../spec";
import { EncryptedString, EncString } from "../../../key-management/crypto/models/enc-string";
-import { CardLinkedId, FieldType, IdentityLinkedId, LoginLinkedId } from "../../enums";
+import { CardLinkedId, IdentityLinkedId, LoginLinkedId } from "../../enums";
import { FieldData } from "../../models/data/field.data";
import { Field } from "../../models/domain/field";
@@ -103,5 +111,34 @@ describe("Field", () => {
identityField.linkedId = IdentityLinkedId.LicenseNumber;
expect(identityField.toSdkField().linkedId).toBe(415);
});
+
+ it("should map from SDK Field", () => {
+ // Test Login LinkedId
+ const loginField: SdkField = {
+ name: undefined,
+ value: undefined,
+ type: FieldType.Linked,
+ linkedId: LoginLinkedIdType.Username,
+ };
+ expect(Field.fromSdkField(loginField)!.linkedId).toBe(100);
+
+ // Test Card LinkedId
+ const cardField: SdkField = {
+ name: undefined,
+ value: undefined,
+ type: FieldType.Linked,
+ linkedId: CardLinkedIdType.Number,
+ };
+ expect(Field.fromSdkField(cardField)!.linkedId).toBe(305);
+
+ // Test Identity LinkedId
+ const identityFieldSdkField: SdkField = {
+ name: undefined,
+ value: undefined,
+ type: FieldType.Linked,
+ linkedId: IdentityLinkedIdType.LicenseNumber,
+ };
+ expect(Field.fromSdkField(identityFieldSdkField)!.linkedId).toBe(415);
+ });
});
});
diff --git a/libs/common/src/vault/models/domain/field.ts b/libs/common/src/vault/models/domain/field.ts
index d6453932cc7..53756e21046 100644
--- a/libs/common/src/vault/models/domain/field.ts
+++ b/libs/common/src/vault/models/domain/field.ts
@@ -90,4 +90,22 @@ export class Field extends Domain {
linkedId: this.linkedId as unknown as SdkLinkedIdType,
};
}
+
+ /**
+ * Maps SDK Field to Field
+ * @param obj The SDK Field object to map
+ */
+ static fromSdkField(obj: SdkField): Field | undefined {
+ if (!obj) {
+ return undefined;
+ }
+
+ const field = new Field();
+ field.name = EncString.fromJSON(obj.name);
+ field.value = EncString.fromJSON(obj.value);
+ field.type = obj.type;
+ field.linkedId = obj.linkedId;
+
+ return field;
+ }
}
diff --git a/libs/common/src/vault/models/domain/identity.ts b/libs/common/src/vault/models/domain/identity.ts
index 5dc752531be..16e68c72551 100644
--- a/libs/common/src/vault/models/domain/identity.ts
+++ b/libs/common/src/vault/models/domain/identity.ts
@@ -195,4 +195,36 @@ export class Identity extends Domain {
licenseNumber: this.licenseNumber?.toJSON(),
};
}
+
+ /**
+ * Maps an SDK Identity object to an Identity
+ * @param obj - The SDK Identity object
+ */
+ static fromSdkIdentity(obj: SdkIdentity): Identity | undefined {
+ if (obj == null) {
+ return undefined;
+ }
+
+ const identity = new Identity();
+ identity.title = EncString.fromJSON(obj.title);
+ identity.firstName = EncString.fromJSON(obj.firstName);
+ identity.middleName = EncString.fromJSON(obj.middleName);
+ identity.lastName = EncString.fromJSON(obj.lastName);
+ identity.address1 = EncString.fromJSON(obj.address1);
+ identity.address2 = EncString.fromJSON(obj.address2);
+ identity.address3 = EncString.fromJSON(obj.address3);
+ identity.city = EncString.fromJSON(obj.city);
+ identity.state = EncString.fromJSON(obj.state);
+ identity.postalCode = EncString.fromJSON(obj.postalCode);
+ identity.country = EncString.fromJSON(obj.country);
+ identity.company = EncString.fromJSON(obj.company);
+ identity.email = EncString.fromJSON(obj.email);
+ identity.phone = EncString.fromJSON(obj.phone);
+ identity.ssn = EncString.fromJSON(obj.ssn);
+ identity.username = EncString.fromJSON(obj.username);
+ identity.passportNumber = EncString.fromJSON(obj.passportNumber);
+ identity.licenseNumber = EncString.fromJSON(obj.licenseNumber);
+
+ return identity;
+ }
}
diff --git a/libs/common/src/vault/models/domain/login-uri.ts b/libs/common/src/vault/models/domain/login-uri.ts
index e7bc2e8892e..9cfa4951dd8 100644
--- a/libs/common/src/vault/models/domain/login-uri.ts
+++ b/libs/common/src/vault/models/domain/login-uri.ts
@@ -102,4 +102,17 @@ export class LoginUri extends Domain {
match: this.match,
};
}
+
+ static fromSdkLoginUri(obj: SdkLoginUri): LoginUri | undefined {
+ if (obj == null) {
+ return undefined;
+ }
+
+ const view = new LoginUri();
+ view.uri = EncString.fromJSON(obj.uri);
+ view.uriChecksum = obj.uriChecksum ? EncString.fromJSON(obj.uriChecksum) : undefined;
+ view.match = obj.match;
+
+ return view;
+ }
}
diff --git a/libs/common/src/vault/models/domain/login.ts b/libs/common/src/vault/models/domain/login.ts
index 4d77983d4af..93af2269185 100644
--- a/libs/common/src/vault/models/domain/login.ts
+++ b/libs/common/src/vault/models/domain/login.ts
@@ -163,4 +163,31 @@ export class Login extends Domain {
fido2Credentials: this.fido2Credentials?.map((f) => f.toSdkFido2Credential()),
};
}
+
+ /**
+ * Maps an SDK Login object to a Login
+ * @param obj - The SDK Login object
+ */
+ static fromSdkLogin(obj: SdkLogin): Login | undefined {
+ if (!obj) {
+ return undefined;
+ }
+
+ const login = new Login();
+
+ login.uris =
+ obj.uris?.filter((u) => u.uri != null).map((uri) => LoginUri.fromSdkLoginUri(uri)) ?? [];
+ login.username = EncString.fromJSON(obj.username);
+ login.password = EncString.fromJSON(obj.password);
+ login.passwordRevisionDate = obj.passwordRevisionDate
+ ? new Date(obj.passwordRevisionDate)
+ : undefined;
+ login.totp = EncString.fromJSON(obj.totp);
+ login.autofillOnPageLoad = obj.autofillOnPageLoad ?? false;
+ login.fido2Credentials = obj.fido2Credentials?.map((f) =>
+ Fido2Credential.fromSdkFido2Credential(f),
+ );
+
+ return login;
+ }
}
diff --git a/libs/common/src/vault/models/domain/password.ts b/libs/common/src/vault/models/domain/password.ts
index f8aacf765bf..b8a30099454 100644
--- a/libs/common/src/vault/models/domain/password.ts
+++ b/libs/common/src/vault/models/domain/password.ts
@@ -71,4 +71,20 @@ export class Password extends Domain {
lastUsedDate: this.lastUsedDate.toISOString(),
};
}
+
+ /**
+ * Maps an SDK PasswordHistory object to a Password
+ * @param obj - The SDK PasswordHistory object
+ */
+ static fromSdkPasswordHistory(obj: PasswordHistory): Password | undefined {
+ if (!obj) {
+ return undefined;
+ }
+
+ const passwordHistory = new Password();
+ passwordHistory.password = EncString.fromJSON(obj.password);
+ passwordHistory.lastUsedDate = new Date(obj.lastUsedDate);
+
+ return passwordHistory;
+ }
}
diff --git a/libs/common/src/vault/models/domain/secure-note.ts b/libs/common/src/vault/models/domain/secure-note.ts
index ac7977b0e46..1426ff85eab 100644
--- a/libs/common/src/vault/models/domain/secure-note.ts
+++ b/libs/common/src/vault/models/domain/secure-note.ts
@@ -54,4 +54,19 @@ export class SecureNote extends Domain {
type: this.type,
};
}
+
+ /**
+ * Maps an SDK SecureNote object to a SecureNote
+ * @param obj - The SDK SecureNote object
+ */
+ static fromSdkSecureNote(obj: SdkSecureNote): SecureNote | undefined {
+ if (obj == null) {
+ return undefined;
+ }
+
+ const secureNote = new SecureNote();
+ secureNote.type = obj.type;
+
+ return secureNote;
+ }
}
diff --git a/libs/common/src/vault/models/domain/ssh-key.ts b/libs/common/src/vault/models/domain/ssh-key.ts
index c0afcd83fc2..0c8abf76e44 100644
--- a/libs/common/src/vault/models/domain/ssh-key.ts
+++ b/libs/common/src/vault/models/domain/ssh-key.ts
@@ -85,4 +85,21 @@ export class SshKey extends Domain {
fingerprint: this.keyFingerprint.toJSON(),
};
}
+
+ /**
+ * Maps an SDK SshKey object to a SshKey
+ * @param obj - The SDK SshKey object
+ */
+ static fromSdkSshKey(obj: SdkSshKey): SshKey | undefined {
+ if (obj == null) {
+ return undefined;
+ }
+
+ const sshKey = new SshKey();
+ sshKey.privateKey = EncString.fromJSON(obj.privateKey);
+ sshKey.publicKey = EncString.fromJSON(obj.publicKey);
+ sshKey.keyFingerprint = EncString.fromJSON(obj.fingerprint);
+
+ return sshKey;
+ }
}
diff --git a/libs/common/src/vault/models/view/card.view.ts b/libs/common/src/vault/models/view/card.view.ts
index dd7f5d6be57..ed02fa68365 100644
--- a/libs/common/src/vault/models/view/card.view.ts
+++ b/libs/common/src/vault/models/view/card.view.ts
@@ -10,7 +10,7 @@ import { linkedFieldOption } from "../../linked-field-option.decorator";
import { ItemView } from "./item.view";
-export class CardView extends ItemView {
+export class CardView extends ItemView implements SdkCardView {
@linkedFieldOption(LinkedId.CardholderName, { sortPosition: 0 })
cardholderName: string = null;
@linkedFieldOption(LinkedId.ExpMonth, { sortPosition: 3, i18nKey: "expirationMonth" })
@@ -168,4 +168,12 @@ export class CardView extends ItemView {
return cardView;
}
+
+ /**
+ * Converts the CardView to an SDK CardView.
+ * The view implements the SdkView so we can safely return `this`
+ */
+ toSdkCardView(): SdkCardView {
+ return this;
+ }
}
diff --git a/libs/common/src/vault/models/view/cipher.view.spec.ts b/libs/common/src/vault/models/view/cipher.view.spec.ts
index b9d3e42aa62..46cea06979f 100644
--- a/libs/common/src/vault/models/view/cipher.view.spec.ts
+++ b/libs/common/src/vault/models/view/cipher.view.spec.ts
@@ -1,3 +1,7 @@
+import { Jsonify } from "type-fest";
+
+import { EncString } from "@bitwarden/common/key-management/crypto/models/enc-string";
+import { CipherPermissionsApi } from "@bitwarden/common/vault/models/api/cipher-permissions.api";
import {
CipherView as SdkCipherView,
CipherType as SdkCipherType,
@@ -85,6 +89,25 @@ describe("CipherView", () => {
expect(actual).toMatchObject(expected);
});
+
+ it("handle both string and object inputs for the cipher key", () => {
+ const cipherKeyString = "cipherKeyString";
+ const cipherKeyObject = new EncString("cipherKeyObject");
+
+ // Test with string input
+ let actual = CipherView.fromJSON({
+ key: cipherKeyString,
+ });
+ expect(actual.key).toBeInstanceOf(EncString);
+ expect(actual.key?.toJSON()).toBe(cipherKeyString);
+
+ // Test with object input (which can happen when cipher view is stored in an InMemory state provider)
+ actual = CipherView.fromJSON({
+ key: cipherKeyObject,
+ } as Jsonify);
+ expect(actual.key).toBeInstanceOf(EncString);
+ expect(actual.key?.toJSON()).toBe(cipherKeyObject.toJSON());
+ });
});
describe("fromSdkCipherView", () => {
@@ -196,11 +219,80 @@ describe("CipherView", () => {
__fromSdk: true,
},
],
- passwordHistory: null,
+ passwordHistory: [],
creationDate: new Date("2022-01-01T12:00:00.000Z"),
revisionDate: new Date("2022-01-02T12:00:00.000Z"),
deletedDate: null,
});
});
});
+
+ describe("toSdkCipherView", () => {
+ it("maps properties correctly", () => {
+ const cipherView = new CipherView();
+ cipherView.id = "0a54d80c-14aa-4ef8-8c3a-7ea99ce5b602";
+ cipherView.organizationId = "000f2a6e-da5e-4726-87ed-1c5c77322c3c";
+ cipherView.folderId = "41b22db4-8e2a-4ed2-b568-f1186c72922f";
+ cipherView.collectionIds = ["b0473506-3c3c-4260-a734-dfaaf833ab6f"];
+ cipherView.key = new EncString("some-key");
+ cipherView.name = "name";
+ cipherView.notes = "notes";
+ cipherView.type = CipherType.Login;
+ cipherView.favorite = true;
+ cipherView.edit = true;
+ cipherView.viewPassword = false;
+ cipherView.reprompt = CipherRepromptType.None;
+ cipherView.organizationUseTotp = false;
+ cipherView.localData = {
+ lastLaunched: new Date("2022-01-01T12:00:00.000Z").getTime(),
+ lastUsedDate: new Date("2022-01-02T12:00:00.000Z").getTime(),
+ };
+ cipherView.permissions = new CipherPermissionsApi();
+ cipherView.permissions.restore = true;
+ cipherView.permissions.delete = true;
+ cipherView.attachments = [];
+ cipherView.fields = [];
+ cipherView.passwordHistory = [];
+ cipherView.login = new LoginView();
+ cipherView.revisionDate = new Date("2022-01-02T12:00:00.000Z");
+ cipherView.creationDate = new Date("2022-01-02T12:00:00.000Z");
+
+ const sdkCipherView = cipherView.toSdkCipherView();
+
+ expect(sdkCipherView).toMatchObject({
+ id: "0a54d80c-14aa-4ef8-8c3a-7ea99ce5b602",
+ organizationId: "000f2a6e-da5e-4726-87ed-1c5c77322c3c",
+ folderId: "41b22db4-8e2a-4ed2-b568-f1186c72922f",
+ collectionIds: ["b0473506-3c3c-4260-a734-dfaaf833ab6f"],
+ key: "some-key",
+ name: "name",
+ notes: "notes",
+ type: SdkCipherType.Login,
+ favorite: true,
+ edit: true,
+ viewPassword: false,
+ reprompt: SdkCipherRepromptType.None,
+ organizationUseTotp: false,
+ localData: {
+ lastLaunched: "2022-01-01T12:00:00.000Z",
+ lastUsedDate: "2022-01-02T12:00:00.000Z",
+ },
+ permissions: {
+ restore: true,
+ delete: true,
+ },
+ deletedDate: undefined,
+ creationDate: "2022-01-02T12:00:00.000Z",
+ revisionDate: "2022-01-02T12:00:00.000Z",
+ attachments: [],
+ passwordHistory: [],
+ login: undefined,
+ identity: undefined,
+ card: undefined,
+ secureNote: undefined,
+ sshKey: undefined,
+ fields: [],
+ } as SdkCipherView);
+ });
+ });
});
diff --git a/libs/common/src/vault/models/view/cipher.view.ts b/libs/common/src/vault/models/view/cipher.view.ts
index 353fffa8eef..0c41e49c3ab 100644
--- a/libs/common/src/vault/models/view/cipher.view.ts
+++ b/libs/common/src/vault/models/view/cipher.view.ts
@@ -1,5 +1,7 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
+import { EncString } from "@bitwarden/common/key-management/crypto/models/enc-string";
+import { uuidToString, asUuid } from "@bitwarden/common/platform/abstractions/sdk/sdk.service";
import { CipherView as SdkCipherView } from "@bitwarden/sdk-internal";
import { View } from "../../../models/view/view";
@@ -9,7 +11,7 @@ import { DeepJsonify } from "../../../types/deep-jsonify";
import { CipherType, LinkedIdType } from "../../enums";
import { CipherRepromptType } from "../../enums/cipher-reprompt-type";
import { CipherPermissionsApi } from "../api/cipher-permissions.api";
-import { LocalData } from "../data/local.data";
+import { LocalData, toSdkLocalData, fromSdkLocalData } from "../data/local.data";
import { Cipher } from "../domain/cipher";
import { AttachmentView } from "./attachment.view";
@@ -41,14 +43,17 @@ export class CipherView implements View, InitializerMetadata {
card = new CardView();
secureNote = new SecureNoteView();
sshKey = new SshKeyView();
- attachments: AttachmentView[] = null;
- fields: FieldView[] = null;
- passwordHistory: PasswordHistoryView[] = null;
+ attachments: AttachmentView[] = [];
+ fields: FieldView[] = [];
+ passwordHistory: PasswordHistoryView[] = [];
collectionIds: string[] = null;
revisionDate: Date = null;
creationDate: Date = null;
deletedDate: Date = null;
reprompt: CipherRepromptType = CipherRepromptType.None;
+ // We need a copy of the encrypted key so we can pass it to
+ // the SdkCipherView during encryption
+ key?: EncString;
/**
* Flag to indicate if the cipher decryption failed.
@@ -76,6 +81,7 @@ export class CipherView implements View, InitializerMetadata {
this.deletedDate = c.deletedDate;
// Old locally stored ciphers might have reprompt == null. If so set it to None.
this.reprompt = c.reprompt ?? CipherRepromptType.None;
+ this.key = c.key;
}
private get item() {
@@ -194,6 +200,18 @@ export class CipherView implements View, InitializerMetadata {
const attachments = obj.attachments?.map((a: any) => AttachmentView.fromJSON(a));
const fields = obj.fields?.map((f: any) => FieldView.fromJSON(f));
const passwordHistory = obj.passwordHistory?.map((ph: any) => PasswordHistoryView.fromJSON(ph));
+ const permissions = CipherPermissionsApi.fromJSON(obj.permissions);
+ let key: EncString | undefined;
+
+ if (obj.key != null) {
+ if (typeof obj.key === "string") {
+ // If the key is a string, we need to parse it as EncString
+ key = EncString.fromJSON(obj.key);
+ } else if ((obj.key as any) instanceof EncString) {
+ // If the key is already an EncString instance, we can use it directly
+ key = obj.key;
+ }
+ }
Object.assign(view, obj, {
creationDate: creationDate,
@@ -202,6 +220,8 @@ export class CipherView implements View, InitializerMetadata {
attachments: attachments,
fields: fields,
passwordHistory: passwordHistory,
+ permissions: permissions,
+ key: key,
});
switch (obj.type) {
@@ -236,9 +256,9 @@ export class CipherView implements View, InitializerMetadata {
}
const cipherView = new CipherView();
- cipherView.id = obj.id ?? null;
- cipherView.organizationId = obj.organizationId ?? null;
- cipherView.folderId = obj.folderId ?? null;
+ cipherView.id = uuidToString(obj.id) ?? null;
+ cipherView.organizationId = uuidToString(obj.organizationId) ?? null;
+ cipherView.folderId = uuidToString(obj.folderId) ?? null;
cipherView.name = obj.name;
cipherView.notes = obj.notes ?? null;
cipherView.type = obj.type;
@@ -247,26 +267,18 @@ export class CipherView implements View, InitializerMetadata {
cipherView.permissions = CipherPermissionsApi.fromSdkCipherPermissions(obj.permissions);
cipherView.edit = obj.edit;
cipherView.viewPassword = obj.viewPassword;
- cipherView.localData = obj.localData
- ? {
- lastUsedDate: obj.localData.lastUsedDate
- ? new Date(obj.localData.lastUsedDate).getTime()
- : undefined,
- lastLaunched: obj.localData.lastLaunched
- ? new Date(obj.localData.lastLaunched).getTime()
- : undefined,
- }
- : undefined;
+ cipherView.localData = fromSdkLocalData(obj.localData);
cipherView.attachments =
- obj.attachments?.map((a) => AttachmentView.fromSdkAttachmentView(a)) ?? null;
- cipherView.fields = obj.fields?.map((f) => FieldView.fromSdkFieldView(f)) ?? null;
+ obj.attachments?.map((a) => AttachmentView.fromSdkAttachmentView(a)) ?? [];
+ cipherView.fields = obj.fields?.map((f) => FieldView.fromSdkFieldView(f)) ?? [];
cipherView.passwordHistory =
- obj.passwordHistory?.map((ph) => PasswordHistoryView.fromSdkPasswordHistoryView(ph)) ?? null;
- cipherView.collectionIds = obj.collectionIds ?? null;
+ obj.passwordHistory?.map((ph) => PasswordHistoryView.fromSdkPasswordHistoryView(ph)) ?? [];
+ cipherView.collectionIds = obj.collectionIds?.map((i) => uuidToString(i)) ?? [];
cipherView.revisionDate = obj.revisionDate == null ? null : new Date(obj.revisionDate);
cipherView.creationDate = obj.creationDate == null ? null : new Date(obj.creationDate);
cipherView.deletedDate = obj.deletedDate == null ? null : new Date(obj.deletedDate);
cipherView.reprompt = obj.reprompt ?? CipherRepromptType.None;
+ cipherView.key = EncString.fromJSON(obj.key);
switch (obj.type) {
case CipherType.Card:
@@ -290,4 +302,66 @@ export class CipherView implements View, InitializerMetadata {
return cipherView;
}
+
+ /**
+ * Maps CipherView to SdkCipherView
+ *
+ * @returns {SdkCipherView} The SDK cipher view object
+ */
+ toSdkCipherView(): SdkCipherView {
+ const sdkCipherView: SdkCipherView = {
+ id: this.id ? asUuid(this.id) : undefined,
+ organizationId: this.organizationId ? asUuid(this.organizationId) : undefined,
+ folderId: this.folderId ? asUuid(this.folderId) : undefined,
+ name: this.name ?? "",
+ notes: this.notes,
+ type: this.type ?? CipherType.Login,
+ favorite: this.favorite,
+ organizationUseTotp: this.organizationUseTotp,
+ permissions: this.permissions?.toSdkCipherPermissions(),
+ edit: this.edit,
+ viewPassword: this.viewPassword,
+ localData: toSdkLocalData(this.localData),
+ attachments: this.attachments?.map((a) => a.toSdkAttachmentView()),
+ fields: this.fields?.map((f) => f.toSdkFieldView()),
+ passwordHistory: this.passwordHistory?.map((ph) => ph.toSdkPasswordHistoryView()),
+ collectionIds: this.collectionIds?.map((i) => i) ?? [],
+ // Revision and creation dates are non-nullable in SDKCipherView
+ revisionDate: (this.revisionDate ?? new Date()).toISOString(),
+ creationDate: (this.creationDate ?? new Date()).toISOString(),
+ deletedDate: this.deletedDate?.toISOString(),
+ reprompt: this.reprompt ?? CipherRepromptType.None,
+ key: this.key?.toJSON(),
+ // Cipher type specific properties are set in the switch statement below
+ // CipherView initializes each with default constructors (undefined values)
+ // The SDK does not expect those undefined values and will throw exceptions
+ login: undefined,
+ card: undefined,
+ identity: undefined,
+ secureNote: undefined,
+ sshKey: undefined,
+ };
+
+ switch (this.type) {
+ case CipherType.Card:
+ sdkCipherView.card = this.card.toSdkCardView();
+ break;
+ case CipherType.Identity:
+ sdkCipherView.identity = this.identity.toSdkIdentityView();
+ break;
+ case CipherType.Login:
+ sdkCipherView.login = this.login.toSdkLoginView();
+ break;
+ case CipherType.SecureNote:
+ sdkCipherView.secureNote = this.secureNote.toSdkSecureNoteView();
+ break;
+ case CipherType.SshKey:
+ sdkCipherView.sshKey = this.sshKey.toSdkSshKeyView();
+ break;
+ default:
+ break;
+ }
+
+ return sdkCipherView;
+ }
}
diff --git a/libs/common/src/vault/models/view/fido2-credential.view.ts b/libs/common/src/vault/models/view/fido2-credential.view.ts
index bf1d324d22d..410757ebe30 100644
--- a/libs/common/src/vault/models/view/fido2-credential.view.ts
+++ b/libs/common/src/vault/models/view/fido2-credential.view.ts
@@ -2,7 +2,10 @@
// @ts-strict-ignore
import { Jsonify } from "type-fest";
-import { Fido2CredentialView as SdkFido2CredentialView } from "@bitwarden/sdk-internal";
+import {
+ Fido2CredentialView as SdkFido2CredentialView,
+ Fido2CredentialFullView,
+} from "@bitwarden/sdk-internal";
import { ItemView } from "./item.view";
@@ -56,4 +59,22 @@ export class Fido2CredentialView extends ItemView {
return view;
}
+
+ toSdkFido2CredentialFullView(): Fido2CredentialFullView {
+ return {
+ credentialId: this.credentialId,
+ keyType: this.keyType,
+ keyAlgorithm: this.keyAlgorithm,
+ keyCurve: this.keyCurve,
+ keyValue: this.keyValue,
+ rpId: this.rpId,
+ userHandle: this.userHandle,
+ userName: this.userName,
+ counter: this.counter.toString(),
+ rpName: this.rpName,
+ userDisplayName: this.userDisplayName,
+ discoverable: this.discoverable ? "true" : "false",
+ creationDate: this.creationDate?.toISOString(),
+ };
+ }
}
diff --git a/libs/common/src/vault/models/view/field.view.ts b/libs/common/src/vault/models/view/field.view.ts
index 770150f8a63..8c9a923aed2 100644
--- a/libs/common/src/vault/models/view/field.view.ts
+++ b/libs/common/src/vault/models/view/field.view.ts
@@ -2,7 +2,7 @@
// @ts-strict-ignore
import { Jsonify } from "type-fest";
-import { FieldView as SdkFieldView } from "@bitwarden/sdk-internal";
+import { FieldView as SdkFieldView, FieldType as SdkFieldType } from "@bitwarden/sdk-internal";
import { View } from "../../../models/view/view";
import { FieldType, LinkedIdType } from "../../enums";
@@ -50,4 +50,16 @@ export class FieldView implements View {
return view;
}
+
+ /**
+ * Converts the FieldView to an SDK FieldView.
+ */
+ toSdkFieldView(): SdkFieldView {
+ return {
+ name: this.name ?? undefined,
+ value: this.value ?? undefined,
+ type: this.type ?? SdkFieldType.Text,
+ linkedId: this.linkedId ?? undefined,
+ };
+ }
}
diff --git a/libs/common/src/vault/models/view/identity.view.ts b/libs/common/src/vault/models/view/identity.view.ts
index 877940e4aea..2b863dc5e5f 100644
--- a/libs/common/src/vault/models/view/identity.view.ts
+++ b/libs/common/src/vault/models/view/identity.view.ts
@@ -10,7 +10,7 @@ import { linkedFieldOption } from "../../linked-field-option.decorator";
import { ItemView } from "./item.view";
-export class IdentityView extends ItemView {
+export class IdentityView extends ItemView implements SdkIdentityView {
@linkedFieldOption(LinkedId.Title, { sortPosition: 0 })
title: string = null;
@linkedFieldOption(LinkedId.MiddleName, { sortPosition: 2 })
@@ -192,4 +192,12 @@ export class IdentityView extends ItemView {
return identityView;
}
+
+ /**
+ * Converts the IdentityView to an SDK IdentityView.
+ * The view implements the SdkView so we can safely return `this`
+ */
+ toSdkIdentityView(): SdkIdentityView {
+ return this;
+ }
}
diff --git a/libs/common/src/vault/models/view/login-uri.view.ts b/libs/common/src/vault/models/view/login-uri.view.ts
index 43d47aa4a3c..38cd517e542 100644
--- a/libs/common/src/vault/models/view/login-uri.view.ts
+++ b/libs/common/src/vault/models/view/login-uri.view.ts
@@ -129,6 +129,15 @@ export class LoginUriView implements View {
return view;
}
+ /** Converts a LoginUriView object to an SDK LoginUriView object. */
+ toSdkLoginUriView(): SdkLoginUriView {
+ return {
+ uri: this.uri ?? undefined,
+ match: this.match ?? undefined,
+ uriChecksum: undefined, // SDK handles uri checksum generation internally
+ };
+ }
+
matchesUri(
targetUri: string,
equivalentDomains: Set,
diff --git a/libs/common/src/vault/models/view/login.view.ts b/libs/common/src/vault/models/view/login.view.ts
index c6e6ca001e4..d268cf4afaa 100644
--- a/libs/common/src/vault/models/view/login.view.ts
+++ b/libs/common/src/vault/models/view/login.view.ts
@@ -124,10 +124,30 @@ export class LoginView extends ItemView {
obj.passwordRevisionDate == null ? null : new Date(obj.passwordRevisionDate);
loginView.totp = obj.totp ?? null;
loginView.autofillOnPageLoad = obj.autofillOnPageLoad ?? null;
- loginView.uris = obj.uris?.map((uri) => LoginUriView.fromSdkLoginUriView(uri)) || [];
+ loginView.uris =
+ obj.uris
+ ?.filter((uri) => uri.uri != null && uri.uri !== "")
+ .map((uri) => LoginUriView.fromSdkLoginUriView(uri)) || [];
// FIDO2 credentials are not decrypted here, they remain encrypted
loginView.fido2Credentials = null;
return loginView;
}
+
+ /**
+ * Converts the LoginView to an SDK LoginView.
+ *
+ * Note: FIDO2 credentials remain encrypted in the SDK view so they are not included here.
+ */
+ toSdkLoginView(): SdkLoginView {
+ return {
+ username: this.username,
+ password: this.password,
+ passwordRevisionDate: this.passwordRevisionDate?.toISOString(),
+ totp: this.totp,
+ autofillOnPageLoad: this.autofillOnPageLoad ?? undefined,
+ uris: this.uris?.map((uri) => uri.toSdkLoginUriView()),
+ fido2Credentials: undefined, // FIDO2 credentials are handled separately and remain encrypted
+ };
+ }
}
diff --git a/libs/common/src/vault/models/view/password-history.view.spec.ts b/libs/common/src/vault/models/view/password-history.view.spec.ts
index 81894ec7493..512ec8d86d8 100644
--- a/libs/common/src/vault/models/view/password-history.view.spec.ts
+++ b/libs/common/src/vault/models/view/password-history.view.spec.ts
@@ -33,4 +33,17 @@ describe("PasswordHistoryView", () => {
});
});
});
+
+ describe("toSdkPasswordHistoryView", () => {
+ it("should return a SdkPasswordHistoryView", () => {
+ const passwordHistoryView = new PasswordHistoryView();
+ passwordHistoryView.password = "password";
+ passwordHistoryView.lastUsedDate = new Date("2023-10-01T00:00:00.000Z");
+
+ expect(passwordHistoryView.toSdkPasswordHistoryView()).toMatchObject({
+ password: "password",
+ lastUsedDate: "2023-10-01T00:00:00.000Z",
+ });
+ });
+ });
});
diff --git a/libs/common/src/vault/models/view/password-history.view.ts b/libs/common/src/vault/models/view/password-history.view.ts
index 31f05f4cc71..9bd708b19fd 100644
--- a/libs/common/src/vault/models/view/password-history.view.ts
+++ b/libs/common/src/vault/models/view/password-history.view.ts
@@ -41,4 +41,14 @@ export class PasswordHistoryView implements View {
return view;
}
+
+ /**
+ * Converts the PasswordHistoryView to an SDK PasswordHistoryView.
+ */
+ toSdkPasswordHistoryView(): SdkPasswordHistoryView {
+ return {
+ password: this.password ?? "",
+ lastUsedDate: this.lastUsedDate.toISOString(),
+ };
+ }
}
diff --git a/libs/common/src/vault/models/view/secure-note.view.ts b/libs/common/src/vault/models/view/secure-note.view.ts
index 8e7a6b4652d..5e401961869 100644
--- a/libs/common/src/vault/models/view/secure-note.view.ts
+++ b/libs/common/src/vault/models/view/secure-note.view.ts
@@ -9,7 +9,7 @@ import { SecureNote } from "../domain/secure-note";
import { ItemView } from "./item.view";
-export class SecureNoteView extends ItemView {
+export class SecureNoteView extends ItemView implements SdkSecureNoteView {
type: SecureNoteType = null;
constructor(n?: SecureNote) {
@@ -42,4 +42,12 @@ export class SecureNoteView extends ItemView {
return secureNoteView;
}
+
+ /**
+ * Converts the SecureNoteView to an SDK SecureNoteView.
+ * The view implements the SdkView so we can safely return `this`
+ */
+ toSdkSecureNoteView(): SdkSecureNoteView {
+ return this;
+ }
}
diff --git a/libs/common/src/vault/models/view/ssh-key.view.ts b/libs/common/src/vault/models/view/ssh-key.view.ts
index a83793678dc..0547eeb7f8e 100644
--- a/libs/common/src/vault/models/view/ssh-key.view.ts
+++ b/libs/common/src/vault/models/view/ssh-key.view.ts
@@ -63,4 +63,15 @@ export class SshKeyView extends ItemView {
return sshKeyView;
}
+
+ /**
+ * Converts the SshKeyView to an SDK SshKeyView.
+ */
+ toSdkSshKeyView(): SdkSshKeyView {
+ return {
+ privateKey: this.privateKey,
+ publicKey: this.publicKey,
+ fingerprint: this.keyFingerprint,
+ };
+ }
}
diff --git a/libs/common/src/vault/services/cipher.service.spec.ts b/libs/common/src/vault/services/cipher.service.spec.ts
index bf30b78ca63..f027122993d 100644
--- a/libs/common/src/vault/services/cipher.service.spec.ts
+++ b/libs/common/src/vault/services/cipher.service.spec.ts
@@ -1,7 +1,9 @@
import { mock } from "jest-mock-extended";
import { BehaviorSubject, map, of } from "rxjs";
+import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
+import { CipherResponse } from "@bitwarden/common/vault/models/response/cipher.response";
// This import has been flagged as unallowed for this class. It may be involved in a circular dependency loop.
// eslint-disable-next-line no-restricted-imports
import { CipherDecryptionKeys, KeyService } from "@bitwarden/key-management";
@@ -23,7 +25,7 @@ import { Utils } from "../../platform/misc/utils";
import { EncArrayBuffer } from "../../platform/models/domain/enc-array-buffer";
import { SymmetricCryptoKey } from "../../platform/models/domain/symmetric-crypto-key";
import { ContainerService } from "../../platform/services/container.service";
-import { CipherId, UserId } from "../../types/guid";
+import { CipherId, UserId, OrganizationId, CollectionId } from "../../types/guid";
import { CipherKey, OrgKey, UserKey } from "../../types/key";
import { CipherEncryptionService } from "../abstractions/cipher-encryption.service";
import { EncryptionContext } from "../abstractions/cipher.service";
@@ -108,6 +110,7 @@ describe("Cipher Service", () => {
const cipherEncryptionService = mock();
const userId = "TestUserId" as UserId;
+ const orgId = "4ff8c0b2-1d3e-4f8c-9b2d-1d3e4f8c0b2" as OrganizationId;
let cipherService: CipherService;
let encryptionContext: EncryptionContext;
@@ -155,7 +158,9 @@ describe("Cipher Service", () => {
);
configService.checkServerMeetsVersionRequirement$.mockReturnValue(of(false));
- configService.getFeatureFlag.mockResolvedValue(false);
+ configService.getFeatureFlag
+ .calledWith(FeatureFlag.CipherKeyEncryption)
+ .mockResolvedValue(false);
const spy = jest.spyOn(cipherFileUploadService, "upload");
@@ -270,6 +275,55 @@ describe("Cipher Service", () => {
jest.spyOn(cipherService as any, "getAutofillOnPageLoadDefault").mockResolvedValue(true);
});
+ it("should call encrypt method of CipherEncryptionService when feature flag is true", async () => {
+ configService.getFeatureFlag
+ .calledWith(FeatureFlag.PM22136_SdkCipherEncryption)
+ .mockResolvedValue(true);
+ cipherEncryptionService.encrypt.mockResolvedValue(encryptionContext);
+
+ const result = await cipherService.encrypt(cipherView, userId);
+
+ expect(result).toEqual(encryptionContext);
+ expect(cipherEncryptionService.encrypt).toHaveBeenCalledWith(cipherView, userId);
+ });
+
+ it("should call legacy encrypt when feature flag is false", async () => {
+ configService.getFeatureFlag
+ .calledWith(FeatureFlag.PM22136_SdkCipherEncryption)
+ .mockResolvedValue(false);
+
+ jest.spyOn(cipherService as any, "encryptCipher").mockResolvedValue(encryptionContext.cipher);
+
+ const result = await cipherService.encrypt(cipherView, userId);
+
+ expect(result).toEqual(encryptionContext);
+ expect(cipherEncryptionService.encrypt).not.toHaveBeenCalled();
+ });
+
+ it("should call legacy encrypt when keys are provided", async () => {
+ configService.getFeatureFlag
+ .calledWith(FeatureFlag.PM22136_SdkCipherEncryption)
+ .mockResolvedValue(true);
+
+ jest.spyOn(cipherService as any, "encryptCipher").mockResolvedValue(encryptionContext.cipher);
+
+ const encryptKey = new SymmetricCryptoKey(new Uint8Array(32));
+ const decryptKey = new SymmetricCryptoKey(new Uint8Array(32));
+
+ let result = await cipherService.encrypt(cipherView, userId, encryptKey);
+
+ expect(result).toEqual(encryptionContext);
+ expect(cipherEncryptionService.encrypt).not.toHaveBeenCalled();
+
+ result = await cipherService.encrypt(cipherView, userId, undefined, decryptKey);
+ expect(result).toEqual(encryptionContext);
+ expect(cipherEncryptionService.encrypt).not.toHaveBeenCalled();
+
+ result = await cipherService.encrypt(cipherView, userId, encryptKey, decryptKey);
+ expect(result).toEqual(encryptionContext);
+ expect(cipherEncryptionService.encrypt).not.toHaveBeenCalled();
+ });
+
it("should return the encrypting user id", async () => {
keyService.getOrgKey.mockReturnValue(
Promise.resolve(new SymmetricCryptoKey(new Uint8Array(32)) as OrgKey),
@@ -310,7 +364,9 @@ describe("Cipher Service", () => {
});
it("is null when feature flag is false", async () => {
- configService.getFeatureFlag.mockResolvedValue(false);
+ configService.getFeatureFlag
+ .calledWith(FeatureFlag.CipherKeyEncryption)
+ .mockResolvedValue(false);
const { cipher } = await cipherService.encrypt(cipherView, userId);
expect(cipher.key).toBeNull();
@@ -318,7 +374,9 @@ describe("Cipher Service", () => {
describe("when feature flag is true", () => {
beforeEach(() => {
- configService.getFeatureFlag.mockResolvedValue(true);
+ configService.getFeatureFlag
+ .calledWith(FeatureFlag.CipherKeyEncryption)
+ .mockResolvedValue(true);
});
it("is null when the cipher is not viewPassword", async () => {
@@ -348,7 +406,9 @@ describe("Cipher Service", () => {
});
it("is not called when feature flag is false", async () => {
- configService.getFeatureFlag.mockResolvedValue(false);
+ configService.getFeatureFlag
+ .calledWith(FeatureFlag.CipherKeyEncryption)
+ .mockResolvedValue(false);
await cipherService.encrypt(cipherView, userId);
@@ -357,7 +417,9 @@ describe("Cipher Service", () => {
describe("when feature flag is true", () => {
beforeEach(() => {
- configService.getFeatureFlag.mockResolvedValue(true);
+ configService.getFeatureFlag
+ .calledWith(FeatureFlag.CipherKeyEncryption)
+ .mockResolvedValue(true);
});
it("is called when cipher viewPassword is true", async () => {
@@ -401,7 +463,9 @@ describe("Cipher Service", () => {
let encryptedKey: EncString;
beforeEach(() => {
- configService.getFeatureFlag.mockResolvedValue(true);
+ configService.getFeatureFlag
+ .calledWith(FeatureFlag.CipherKeyEncryption)
+ .mockResolvedValue(true);
configService.checkServerMeetsVersionRequirement$.mockReturnValue(of(true));
searchService.indexedEntityId$.mockReturnValue(of(null));
@@ -474,7 +538,9 @@ describe("Cipher Service", () => {
describe("decrypt", () => {
it("should call decrypt method of CipherEncryptionService when feature flag is true", async () => {
- configService.getFeatureFlag.mockResolvedValue(true);
+ configService.getFeatureFlag
+ .calledWith(FeatureFlag.PM19941MigrateCipherDomainToSdk)
+ .mockResolvedValue(true);
cipherEncryptionService.decrypt.mockResolvedValue(new CipherView(encryptionContext.cipher));
const result = await cipherService.decrypt(encryptionContext.cipher, userId);
@@ -488,7 +554,9 @@ describe("Cipher Service", () => {
it("should call legacy decrypt when feature flag is false", async () => {
const mockUserKey = new SymmetricCryptoKey(new Uint8Array(32)) as UserKey;
- configService.getFeatureFlag.mockResolvedValue(false);
+ configService.getFeatureFlag
+ .calledWith(FeatureFlag.PM19941MigrateCipherDomainToSdk)
+ .mockResolvedValue(false);
cipherService.getKeyForCipherKeyDecryption = jest.fn().mockResolvedValue(mockUserKey);
encryptService.decryptToBytes.mockResolvedValue(new Uint8Array(32));
jest
@@ -509,7 +577,9 @@ describe("Cipher Service", () => {
it("should use SDK when feature flag is enabled", async () => {
const cipher = new Cipher(cipherData);
const attachment = new AttachmentView(cipher.attachments![0]);
- configService.getFeatureFlag.mockResolvedValue(true);
+ configService.getFeatureFlag
+ .calledWith(FeatureFlag.PM19941MigrateCipherDomainToSdk)
+ .mockResolvedValue(true);
jest.spyOn(cipherService, "ciphers$").mockReturnValue(of({ [cipher.id]: cipherData }));
cipherEncryptionService.decryptAttachmentContent.mockResolvedValue(mockDecryptedContent);
@@ -534,7 +604,9 @@ describe("Cipher Service", () => {
});
it("should use legacy decryption when feature flag is enabled", async () => {
- configService.getFeatureFlag.mockResolvedValue(false);
+ configService.getFeatureFlag
+ .calledWith(FeatureFlag.PM19941MigrateCipherDomainToSdk)
+ .mockResolvedValue(false);
const cipher = new Cipher(cipherData);
const attachment = new AttachmentView(cipher.attachments![0]);
attachment.key = makeSymmetricCryptoKey(64);
@@ -557,4 +629,77 @@ describe("Cipher Service", () => {
expect(encryptService.decryptFileData).toHaveBeenCalledWith(mockEncBuf, attachment.key);
});
});
+
+ describe("shareWithServer()", () => {
+ it("should use cipherEncryptionService to move the cipher when feature flag enabled", async () => {
+ configService.getFeatureFlag
+ .calledWith(FeatureFlag.PM22136_SdkCipherEncryption)
+ .mockResolvedValue(true);
+
+ apiService.putShareCipher.mockResolvedValue(new CipherResponse(cipherData));
+
+ const expectedCipher = new Cipher(cipherData);
+ expectedCipher.organizationId = orgId;
+ const cipherView = new CipherView(expectedCipher);
+ const collectionIds = ["collection1", "collection2"] as CollectionId[];
+
+ cipherView.organizationId = undefined; // Ensure organizationId is undefined for this test
+
+ cipherEncryptionService.moveToOrganization.mockResolvedValue({
+ cipher: expectedCipher,
+ encryptedFor: userId,
+ });
+
+ await cipherService.shareWithServer(cipherView, orgId, collectionIds, userId);
+
+ // Expect SDK usage
+ expect(cipherEncryptionService.moveToOrganization).toHaveBeenCalledWith(
+ cipherView,
+ orgId,
+ userId,
+ );
+ // Expect collectionIds to be assigned
+ expect(apiService.putShareCipher).toHaveBeenCalledWith(
+ cipherView.id,
+ expect.objectContaining({
+ cipher: expect.objectContaining({ organizationId: orgId }),
+ collectionIds: collectionIds,
+ }),
+ );
+ });
+
+ it("should use legacy encryption when feature flag disabled", async () => {
+ configService.getFeatureFlag
+ .calledWith(FeatureFlag.PM22136_SdkCipherEncryption)
+ .mockResolvedValue(false);
+
+ apiService.putShareCipher.mockResolvedValue(new CipherResponse(cipherData));
+
+ const expectedCipher = new Cipher(cipherData);
+ expectedCipher.organizationId = orgId;
+ const cipherView = new CipherView(expectedCipher);
+ const collectionIds = ["collection1", "collection2"] as CollectionId[];
+
+ cipherView.organizationId = undefined; // Ensure organizationId is undefined for this test
+
+ const oldEncryptSharedSpy = jest
+ .spyOn(cipherService as any, "encryptSharedCipher")
+ .mockResolvedValue({
+ cipher: expectedCipher,
+ encryptedFor: userId,
+ });
+
+ await cipherService.shareWithServer(cipherView, orgId, collectionIds, userId);
+
+ // Expect no SDK usage
+ expect(cipherEncryptionService.moveToOrganization).not.toHaveBeenCalled();
+ expect(oldEncryptSharedSpy).toHaveBeenCalledWith(
+ expect.objectContaining({
+ organizationId: orgId,
+ collectionIds: collectionIds,
+ } as unknown as CipherView),
+ userId,
+ );
+ });
+ });
});
diff --git a/libs/common/src/vault/services/cipher.service.ts b/libs/common/src/vault/services/cipher.service.ts
index 8bef5289a95..1524e4e1b29 100644
--- a/libs/common/src/vault/services/cipher.service.ts
+++ b/libs/common/src/vault/services/cipher.service.ts
@@ -231,13 +231,14 @@ export class CipherService implements CipherServiceAbstraction {
this.clearCipherViewsForUser$.next(userId);
}
- async encrypt(
- model: CipherView,
- userId: UserId,
- keyForCipherEncryption?: SymmetricCryptoKey,
- keyForCipherKeyDecryption?: SymmetricCryptoKey,
- originalCipher: Cipher = null,
- ): Promise {
+ /**
+ * Adjusts the cipher history for the given model by updating its history properties based on the original cipher.
+ * @param model The cipher model to adjust.
+ * @param userId The acting userId
+ * @param originalCipher The original cipher to compare against. If not provided, it will be fetched from the store.
+ * @private
+ */
+ private async adjustCipherHistory(model: CipherView, userId: UserId, originalCipher?: Cipher) {
if (model.id != null) {
if (originalCipher == null) {
originalCipher = await this.get(model.id, userId);
@@ -247,6 +248,25 @@ export class CipherService implements CipherServiceAbstraction {
}
this.adjustPasswordHistoryLength(model);
}
+ }
+
+ async encrypt(
+ model: CipherView,
+ userId: UserId,
+ keyForCipherEncryption?: SymmetricCryptoKey,
+ keyForCipherKeyDecryption?: SymmetricCryptoKey,
+ originalCipher: Cipher = null,
+ ): Promise {
+ await this.adjustCipherHistory(model, userId, originalCipher);
+
+ const sdkEncryptionEnabled =
+ (await this.configService.getFeatureFlag(FeatureFlag.PM22136_SdkCipherEncryption)) &&
+ keyForCipherEncryption == null && // PM-23085 - SDK encryption does not currently support custom keys (e.g. key rotation)
+ keyForCipherKeyDecryption == null; // PM-23348 - Or has explicit methods for re-encrypting ciphers with different keys (e.g. move to org)
+
+ if (sdkEncryptionEnabled) {
+ return await this.cipherEncryptionService.encrypt(model, userId);
+ }
const cipher = new Cipher();
cipher.id = model.id;
@@ -854,22 +874,48 @@ export class CipherService implements CipherServiceAbstraction {
organizationId: string,
collectionIds: string[],
userId: UserId,
+ originalCipher?: Cipher,
): Promise {
- const attachmentPromises: Promise[] = [];
- if (cipher.attachments != null) {
- cipher.attachments.forEach((attachment) => {
- if (attachment.key == null) {
- attachmentPromises.push(
- this.shareAttachmentWithServer(attachment, cipher.id, organizationId),
- );
- }
- });
- }
- await Promise.all(attachmentPromises);
+ const sdkCipherEncryptionEnabled = await this.configService.getFeatureFlag(
+ FeatureFlag.PM22136_SdkCipherEncryption,
+ );
+
+ await this.adjustCipherHistory(cipher, userId, originalCipher);
+
+ let encCipher: EncryptionContext;
+ if (sdkCipherEncryptionEnabled) {
+ // The SDK does not expect the cipher to already have an organizationId. It will result in the wrong
+ // cipher encryption key being used during the move to organization operation.
+ if (cipher.organizationId != null) {
+ throw new Error("Cipher is already associated with an organization.");
+ }
+
+ encCipher = await this.cipherEncryptionService.moveToOrganization(
+ cipher,
+ organizationId as OrganizationId,
+ userId,
+ );
+ encCipher.cipher.collectionIds = collectionIds;
+ } else {
+ // This old attachment logic is safe to remove after it is replaced in PM-22750; which will require fixing
+ // the attachment before sharing.
+ const attachmentPromises: Promise[] = [];
+ if (cipher.attachments != null) {
+ cipher.attachments.forEach((attachment) => {
+ if (attachment.key == null) {
+ attachmentPromises.push(
+ this.shareAttachmentWithServer(attachment, cipher.id, organizationId),
+ );
+ }
+ });
+ }
+ await Promise.all(attachmentPromises);
+
+ cipher.organizationId = organizationId;
+ cipher.collectionIds = collectionIds;
+ encCipher = await this.encryptSharedCipher(cipher, userId);
+ }
- cipher.organizationId = organizationId;
- cipher.collectionIds = collectionIds;
- const encCipher = await this.encryptSharedCipher(cipher, userId);
const request = new CipherShareRequest(encCipher);
const response = await this.apiService.putShareCipher(cipher.id, request);
const data = new CipherData(response, collectionIds);
@@ -883,16 +929,36 @@ export class CipherService implements CipherServiceAbstraction {
collectionIds: string[],
userId: UserId,
) {
+ const sdkCipherEncryptionEnabled = await this.configService.getFeatureFlag(
+ FeatureFlag.PM22136_SdkCipherEncryption,
+ );
const promises: Promise[] = [];
const encCiphers: Cipher[] = [];
for (const cipher of ciphers) {
- cipher.organizationId = organizationId;
- cipher.collectionIds = collectionIds;
- promises.push(
- this.encryptSharedCipher(cipher, userId).then((c) => {
- encCiphers.push(c.cipher);
- }),
- );
+ if (sdkCipherEncryptionEnabled) {
+ // The SDK does not expect the cipher to already have an organizationId. It will result in the wrong
+ // cipher encryption key being used during the move to organization operation.
+ if (cipher.organizationId != null) {
+ throw new Error("Cipher is already associated with an organization.");
+ }
+
+ promises.push(
+ this.cipherEncryptionService
+ .moveToOrganization(cipher, organizationId as OrganizationId, userId)
+ .then((encCipher) => {
+ encCipher.cipher.collectionIds = collectionIds;
+ encCiphers.push(encCipher.cipher);
+ }),
+ );
+ } else {
+ cipher.organizationId = organizationId;
+ cipher.collectionIds = collectionIds;
+ promises.push(
+ this.encryptSharedCipher(cipher, userId).then((c) => {
+ encCiphers.push(c.cipher);
+ }),
+ );
+ }
}
await Promise.all(promises);
const request = new CipherBulkShareRequest(encCiphers, collectionIds, userId);
diff --git a/libs/common/src/vault/services/default-cipher-encryption.service.spec.ts b/libs/common/src/vault/services/default-cipher-encryption.service.spec.ts
index 4d05a5197fb..9e0cf62ed08 100644
--- a/libs/common/src/vault/services/default-cipher-encryption.service.spec.ts
+++ b/libs/common/src/vault/services/default-cipher-encryption.service.spec.ts
@@ -1,20 +1,22 @@
import { mock } from "jest-mock-extended";
import { of } from "rxjs";
+import { Fido2Credential } from "@bitwarden/common/vault/models/domain/fido2-credential";
import {
- Fido2Credential,
+ Fido2Credential as SdkFido2Credential,
Cipher as SdkCipher,
CipherType as SdkCipherType,
CipherView as SdkCipherView,
CipherListView,
AttachmentView as SdkAttachmentView,
+ Fido2CredentialFullView,
} from "@bitwarden/sdk-internal";
import { mockEnc } from "../../../spec";
import { UriMatchStrategy } from "../../models/domain/domain-service";
import { LogService } from "../../platform/abstractions/log.service";
import { SdkService } from "../../platform/abstractions/sdk/sdk.service";
-import { UserId } from "../../types/guid";
+import { UserId, CipherId, OrganizationId } from "../../types/guid";
import { CipherRepromptType, CipherType } from "../enums";
import { CipherPermissionsApi } from "../models/api/cipher-permissions.api";
import { CipherData } from "../models/data/cipher.data";
@@ -25,10 +27,15 @@ import { Fido2CredentialView } from "../models/view/fido2-credential.view";
import { DefaultCipherEncryptionService } from "./default-cipher-encryption.service";
+const cipherId = "bdc4ef23-1116-477e-ae73-247854af58cb" as CipherId;
+const orgId = "c5e9654f-6cc5-44c4-8e09-3d323522668c" as OrganizationId;
+const folderId = "a3e9654f-6cc5-44c4-8e09-3d323522668c";
+const userId = "59fbbb44-8cc8-4279-ab40-afc5f68704f4" as UserId;
+
const cipherData: CipherData = {
- id: "id",
- organizationId: "orgId",
- folderId: "folderId",
+ id: cipherId,
+ organizationId: orgId,
+ folderId: folderId,
edit: true,
viewPassword: true,
organizationUseTotp: true,
@@ -78,13 +85,17 @@ describe("DefaultCipherEncryptionService", () => {
const sdkService = mock();
const logService = mock();
let sdkCipherView: SdkCipherView;
+ let sdkCipher: SdkCipher;
const mockSdkClient = {
vault: jest.fn().mockReturnValue({
ciphers: jest.fn().mockReturnValue({
+ encrypt: jest.fn(),
+ set_fido2_credentials: jest.fn(),
decrypt: jest.fn(),
decrypt_list: jest.fn(),
decrypt_fido2_credentials: jest.fn(),
+ move_to_organization: jest.fn(),
}),
attachments: jest.fn().mockReturnValue({
decrypt_buffer: jest.fn(),
@@ -99,21 +110,25 @@ describe("DefaultCipherEncryptionService", () => {
take: jest.fn().mockReturnValue(mockRef),
};
- const userId = "user-id" as UserId;
-
let cipherObj: Cipher;
+ let cipherViewObj: CipherView;
beforeEach(() => {
sdkService.userClient$ = jest.fn((userId: UserId) => of(mockSdk)) as any;
cipherEncryptionService = new DefaultCipherEncryptionService(sdkService, logService);
cipherObj = new Cipher(cipherData);
+ cipherViewObj = new CipherView(cipherObj);
jest.spyOn(cipherObj, "toSdkCipher").mockImplementation(() => {
return { id: cipherData.id } as SdkCipher;
});
+ jest.spyOn(cipherViewObj, "toSdkCipherView").mockImplementation(() => {
+ return { id: cipherData.id } as SdkCipherView;
+ });
+
sdkCipherView = {
- id: "test-id",
+ id: cipherId as string,
type: SdkCipherType.Login,
name: "test-name",
login: {
@@ -121,16 +136,211 @@ describe("DefaultCipherEncryptionService", () => {
password: "test-password",
},
} as SdkCipherView;
+
+ sdkCipher = {
+ id: cipherId,
+ type: SdkCipherType.Login,
+ name: "encrypted-name",
+ login: {
+ username: "encrypted-username",
+ password: "encrypted-password",
+ },
+ } as unknown as SdkCipher;
});
afterEach(() => {
jest.clearAllMocks();
});
+ describe("encrypt", () => {
+ it("should encrypt a cipher successfully", async () => {
+ const expectedCipher: Cipher = {
+ id: cipherId as string,
+ type: CipherType.Login,
+ name: "encrypted-name",
+ login: {
+ username: "encrypted-username",
+ password: "encrypted-password",
+ },
+ } as unknown as Cipher;
+
+ mockSdkClient.vault().ciphers().encrypt.mockReturnValue({
+ cipher: sdkCipher,
+ encryptedFor: userId,
+ });
+ jest.spyOn(Cipher, "fromSdkCipher").mockReturnValue(expectedCipher);
+
+ const result = await cipherEncryptionService.encrypt(cipherViewObj, userId);
+
+ expect(result).toBeDefined();
+ expect(result!.cipher).toEqual(expectedCipher);
+ expect(result!.encryptedFor).toBe(userId);
+ expect(cipherViewObj.toSdkCipherView).toHaveBeenCalled();
+ expect(mockSdkClient.vault().ciphers().encrypt).toHaveBeenCalledWith({ id: cipherData.id });
+ });
+
+ it("should encrypt FIDO2 credentials if present", async () => {
+ const fidoCredentialView = new Fido2CredentialView();
+ fidoCredentialView.credentialId = "credentialId";
+
+ cipherViewObj.login.fido2Credentials = [fidoCredentialView];
+
+ jest.spyOn(fidoCredentialView, "toSdkFido2CredentialFullView").mockImplementation(
+ () =>
+ ({
+ credentialId: "credentialId",
+ }) as Fido2CredentialFullView,
+ );
+ jest.spyOn(cipherViewObj, "toSdkCipherView").mockImplementation(
+ () =>
+ ({
+ id: cipherId as string,
+ login: {
+ fido2Credentials: undefined,
+ },
+ }) as unknown as SdkCipherView,
+ );
+
+ mockSdkClient
+ .vault()
+ .ciphers()
+ .set_fido2_credentials.mockReturnValue({
+ id: cipherId as string,
+ login: {
+ fido2Credentials: [
+ {
+ credentialId: "encrypted-credentialId",
+ },
+ ],
+ },
+ });
+
+ mockSdkClient.vault().ciphers().encrypt.mockReturnValue({
+ cipher: sdkCipher,
+ encryptedFor: userId,
+ });
+
+ cipherObj.login!.fido2Credentials = [
+ { credentialId: "encrypted-credentialId" } as unknown as Fido2Credential,
+ ];
+
+ jest.spyOn(Cipher, "fromSdkCipher").mockReturnValue(cipherObj);
+
+ const result = await cipherEncryptionService.encrypt(cipherViewObj, userId);
+
+ expect(result).toBeDefined();
+ expect(result!.cipher.login!.fido2Credentials).toHaveLength(1);
+
+ // Ensure set_fido2_credentials was called with correct parameters
+ expect(mockSdkClient.vault().ciphers().set_fido2_credentials).toHaveBeenCalledWith(
+ expect.objectContaining({ id: cipherId }),
+ [{ credentialId: "credentialId" }],
+ );
+
+ // Encrypted fido2 credential should be in the cipher passed to encrypt
+ expect(mockSdkClient.vault().ciphers().encrypt).toHaveBeenCalledWith(
+ expect.objectContaining({
+ id: cipherId,
+ login: { fido2Credentials: [{ credentialId: "encrypted-credentialId" }] },
+ }),
+ );
+ });
+ });
+
+ describe("moveToOrganization", () => {
+ it("should call the sdk method to move a cipher to an organization", async () => {
+ const expectedCipher: Cipher = {
+ id: cipherId as string,
+ type: CipherType.Login,
+ name: "encrypted-name",
+ organizationId: orgId,
+ login: {
+ username: "encrypted-username",
+ password: "encrypted-password",
+ },
+ } as unknown as Cipher;
+
+ mockSdkClient.vault().ciphers().move_to_organization.mockReturnValue({
+ id: cipherId,
+ organizationId: orgId,
+ });
+ mockSdkClient.vault().ciphers().encrypt.mockReturnValue({
+ cipher: sdkCipher,
+ encryptedFor: userId,
+ });
+ jest.spyOn(Cipher, "fromSdkCipher").mockReturnValue(expectedCipher);
+
+ const result = await cipherEncryptionService.moveToOrganization(cipherViewObj, orgId, userId);
+
+ expect(result).toBeDefined();
+ expect(result!.cipher).toEqual(expectedCipher);
+ expect(result!.encryptedFor).toBe(userId);
+ expect(cipherViewObj.toSdkCipherView).toHaveBeenCalled();
+ expect(mockSdkClient.vault().ciphers().move_to_organization).toHaveBeenCalledWith(
+ { id: cipherData.id },
+ orgId,
+ );
+ });
+
+ it("should re-encrypt any fido2 credentials when moving to an organization", async () => {
+ const mockSdkCredentialView = {
+ username: "username",
+ } as unknown as Fido2CredentialFullView;
+ const mockCredentialView = mock();
+ mockCredentialView.toSdkFido2CredentialFullView.mockReturnValue(mockSdkCredentialView);
+ cipherViewObj.login.fido2Credentials = [mockCredentialView];
+ const expectedCipher: Cipher = {
+ id: cipherId as string,
+ type: CipherType.Login,
+ name: "encrypted-name",
+ organizationId: orgId,
+ login: {
+ username: "encrypted-username",
+ password: "encrypted-password",
+ fido2Credentials: [{ username: "encrypted-username" }],
+ },
+ } as unknown as Cipher;
+
+ mockSdkClient
+ .vault()
+ .ciphers()
+ .set_fido2_credentials.mockReturnValue({
+ id: cipherId as string,
+ login: {
+ fido2Credentials: [mockSdkCredentialView],
+ },
+ } as SdkCipherView);
+ mockSdkClient.vault().ciphers().move_to_organization.mockReturnValue({
+ id: cipherId,
+ organizationId: orgId,
+ });
+ mockSdkClient.vault().ciphers().encrypt.mockReturnValue({
+ cipher: sdkCipher,
+ encryptedFor: userId,
+ });
+ jest.spyOn(Cipher, "fromSdkCipher").mockReturnValue(expectedCipher);
+
+ const result = await cipherEncryptionService.moveToOrganization(cipherViewObj, orgId, userId);
+
+ expect(result).toBeDefined();
+ expect(result!.cipher).toEqual(expectedCipher);
+ expect(result!.encryptedFor).toBe(userId);
+ expect(cipherViewObj.toSdkCipherView).toHaveBeenCalled();
+ expect(mockSdkClient.vault().ciphers().set_fido2_credentials).toHaveBeenCalledWith(
+ expect.objectContaining({ id: cipherId }),
+ expect.arrayContaining([mockSdkCredentialView]),
+ );
+ expect(mockSdkClient.vault().ciphers().move_to_organization).toHaveBeenCalledWith(
+ { id: cipherData.id, login: { fido2Credentials: [mockSdkCredentialView] } },
+ orgId,
+ );
+ });
+ });
+
describe("decrypt", () => {
it("should decrypt a cipher successfully", async () => {
const expectedCipherView: CipherView = {
- id: "test-id",
+ id: cipherId as string,
type: CipherType.Login,
name: "test-name",
login: {
@@ -168,12 +378,12 @@ describe("DefaultCipherEncryptionService", () => {
discoverable: mockEnc("true"),
creationDate: new Date("2023-01-01T12:00:00.000Z"),
},
- ] as unknown as Fido2Credential[];
+ ] as unknown as SdkFido2Credential[];
sdkCipherView.login!.fido2Credentials = fido2Credentials;
const expectedCipherView: CipherView = {
- id: "test-id",
+ id: cipherId,
type: CipherType.Login,
name: "test-name",
login: {
@@ -228,13 +438,15 @@ describe("DefaultCipherEncryptionService", () => {
it("should decrypt multiple ciphers successfully", async () => {
const ciphers = [new Cipher(cipherData), new Cipher(cipherData)];
+ const cipherId2 = "bdc4ef23-2222-477e-ae73-247854af58cb" as CipherId;
+
const expectedViews = [
{
- id: "test-id-1",
+ id: cipherId as string,
name: "test-name-1",
} as CipherView,
{
- id: "test-id-2",
+ id: cipherId2 as string,
name: "test-name-2",
} as CipherView,
];
@@ -242,8 +454,11 @@ describe("DefaultCipherEncryptionService", () => {
mockSdkClient
.vault()
.ciphers()
- .decrypt.mockReturnValueOnce({ id: "test-id-1", name: "test-name-1" } as SdkCipherView)
- .mockReturnValueOnce({ id: "test-id-2", name: "test-name-2" } as SdkCipherView);
+ .decrypt.mockReturnValueOnce({
+ id: cipherId,
+ name: "test-name-1",
+ } as unknown as SdkCipherView)
+ .mockReturnValueOnce({ id: cipherId2, name: "test-name-2" } as unknown as SdkCipherView);
jest
.spyOn(CipherView, "fromSdkCipherView")
diff --git a/libs/common/src/vault/services/default-cipher-encryption.service.ts b/libs/common/src/vault/services/default-cipher-encryption.service.ts
index 2c57df6f5bb..3547bafb4c9 100644
--- a/libs/common/src/vault/services/default-cipher-encryption.service.ts
+++ b/libs/common/src/vault/services/default-cipher-encryption.service.ts
@@ -1,10 +1,15 @@
import { EMPTY, catchError, firstValueFrom, map } from "rxjs";
-import { CipherListView } from "@bitwarden/sdk-internal";
+import { EncryptionContext } from "@bitwarden/common/vault/abstractions/cipher.service";
+import {
+ CipherListView,
+ BitwardenClient,
+ CipherView as SdkCipherView,
+} from "@bitwarden/sdk-internal";
import { LogService } from "../../platform/abstractions/log.service";
-import { SdkService } from "../../platform/abstractions/sdk/sdk.service";
-import { UserId } from "../../types/guid";
+import { SdkService, asUuid } from "../../platform/abstractions/sdk/sdk.service";
+import { UserId, OrganizationId } from "../../types/guid";
import { CipherEncryptionService } from "../abstractions/cipher-encryption.service";
import { CipherType } from "../enums";
import { Cipher } from "../models/domain/cipher";
@@ -18,6 +23,67 @@ export class DefaultCipherEncryptionService implements CipherEncryptionService {
private logService: LogService,
) {}
+ async encrypt(model: CipherView, userId: UserId): Promise {
+ return firstValueFrom(
+ this.sdkService.userClient$(userId).pipe(
+ map((sdk) => {
+ if (!sdk) {
+ throw new Error("SDK not available");
+ }
+
+ using ref = sdk.take();
+ const sdkCipherView = this.toSdkCipherView(model, ref.value);
+
+ const encryptionContext = ref.value.vault().ciphers().encrypt(sdkCipherView);
+
+ return {
+ cipher: Cipher.fromSdkCipher(encryptionContext.cipher)!,
+ encryptedFor: asUuid(encryptionContext.encryptedFor),
+ };
+ }),
+ catchError((error: unknown) => {
+ this.logService.error(`Failed to encrypt cipher: ${error}`);
+ return EMPTY;
+ }),
+ ),
+ );
+ }
+
+ async moveToOrganization(
+ model: CipherView,
+ organizationId: OrganizationId,
+ userId: UserId,
+ ): Promise {
+ return firstValueFrom(
+ this.sdkService.userClient$(userId).pipe(
+ map((sdk) => {
+ if (!sdk) {
+ throw new Error("SDK not available");
+ }
+
+ using ref = sdk.take();
+ const sdkCipherView = this.toSdkCipherView(model, ref.value);
+
+ const movedCipherView = ref.value
+ .vault()
+ .ciphers()
+ .move_to_organization(sdkCipherView, asUuid(organizationId));
+
+ const encryptionContext = ref.value.vault().ciphers().encrypt(movedCipherView);
+
+ return {
+ cipher: Cipher.fromSdkCipher(encryptionContext.cipher)!,
+ encryptedFor: asUuid(encryptionContext.encryptedFor),
+ };
+ }),
+ catchError((error: unknown) => {
+ this.logService.error(`Failed to move cipher to organization: ${error}`);
+ return EMPTY;
+ }),
+ ),
+ );
+ }
+
async decrypt(cipher: Cipher, userId: UserId): Promise {
return firstValueFrom(
this.sdkService.userClient$(userId).pipe(
@@ -51,11 +117,8 @@ export class DefaultCipherEncryptionService implements CipherEncryptionService {
clientCipherView.login.fido2Credentials = fido2CredentialViews
.map((f) => {
const view = Fido2CredentialView.fromSdkFido2CredentialView(f)!;
-
- return {
- ...view,
- keyValue: decryptedKeyValue,
- };
+ view.keyValue = decryptedKeyValue;
+ return view;
})
.filter((view): view is Fido2CredentialView => view !== undefined);
}
@@ -104,10 +167,8 @@ export class DefaultCipherEncryptionService implements CipherEncryptionService {
clientCipherView.login.fido2Credentials = fido2CredentialViews
.map((f) => {
const view = Fido2CredentialView.fromSdkFido2CredentialView(f)!;
- return {
- ...view,
- keyValue: decryptedKeyValue,
- };
+ view.keyValue = decryptedKeyValue;
+ return view;
})
.filter((view): view is Fido2CredentialView => view !== undefined);
}
@@ -187,4 +248,25 @@ export class DefaultCipherEncryptionService implements CipherEncryptionService {
),
);
}
+
+ /**
+ * Helper method to convert a CipherView model to an SDK CipherView. Has special handling for Fido2 credentials
+ * that need to be encrypted before being sent to the SDK.
+ * @param model The CipherView model to convert
+ * @param sdk An instance of SDK client
+ * @private
+ */
+ private toSdkCipherView(model: CipherView, sdk: BitwardenClient): SdkCipherView {
+ let sdkCipherView = model.toSdkCipherView();
+
+ if (model.type === CipherType.Login && model.login?.hasFido2Credentials) {
+ // Encrypt Fido2 credentials separately
+ const fido2Credentials = model.login.fido2Credentials?.map((f) =>
+ f.toSdkFido2CredentialFullView(),
+ );
+ sdkCipherView = sdk.vault().ciphers().set_fido2_credentials(sdkCipherView, fido2Credentials);
+ }
+
+ return sdkCipherView;
+ }
}
diff --git a/libs/importer/src/importers/base-importer.ts b/libs/importer/src/importers/base-importer.ts
index 463d61dbbdf..1a97bc5a325 100644
--- a/libs/importer/src/importers/base-importer.ts
+++ b/libs/importer/src/importers/base-importer.ts
@@ -320,12 +320,6 @@ export abstract class BaseImporter {
} else {
cipher.notes = cipher.notes.trim();
}
- if (cipher.fields != null && cipher.fields.length === 0) {
- cipher.fields = null;
- }
- if (cipher.passwordHistory != null && cipher.passwordHistory.length === 0) {
- cipher.passwordHistory = null;
- }
}
protected processKvp(
diff --git a/libs/importer/src/importers/keeper/keeper-csv-importer.spec.ts b/libs/importer/src/importers/keeper/keeper-csv-importer.spec.ts
index d7a4d487bcb..026c501cf5a 100644
--- a/libs/importer/src/importers/keeper/keeper-csv-importer.spec.ts
+++ b/libs/importer/src/importers/keeper/keeper-csv-importer.spec.ts
@@ -66,7 +66,7 @@ describe("Keeper CSV Importer", () => {
expect(result != null).toBe(true);
const cipher = result.ciphers.shift();
- expect(cipher.fields).toBeNull();
+ expect(cipher.fields.length).toBe(0);
const cipher2 = result.ciphers.shift();
expect(cipher2.fields.length).toBe(2);
diff --git a/libs/importer/src/importers/keeper/keeper-json-importer.spec.ts b/libs/importer/src/importers/keeper/keeper-json-importer.spec.ts
index 31169021e0c..22008f3b4c1 100644
--- a/libs/importer/src/importers/keeper/keeper-json-importer.spec.ts
+++ b/libs/importer/src/importers/keeper/keeper-json-importer.spec.ts
@@ -39,7 +39,7 @@ describe("Keeper Json Importer", () => {
expect(cipher3.login.username).toEqual("someUserName");
expect(cipher3.login.password).toEqual("w4k4k1wergf$^&@#*%2");
expect(cipher3.notes).toBeNull();
- expect(cipher3.fields).toBeNull();
+ expect(cipher3.fields.length).toBe(0);
expect(cipher3.login.uris.length).toEqual(1);
const uriView3 = cipher3.login.uris.shift();
expect(uriView3.uri).toEqual("https://example.com");
diff --git a/libs/vault/src/cipher-form/services/default-cipher-form.service.ts b/libs/vault/src/cipher-form/services/default-cipher-form.service.ts
index 5228c85c3f7..d195ff8b00b 100644
--- a/libs/vault/src/cipher-form/services/default-cipher-form.service.ts
+++ b/libs/vault/src/cipher-form/services/default-cipher-form.service.ts
@@ -5,7 +5,7 @@ import { firstValueFrom, map } from "rxjs";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { getUserId } from "@bitwarden/common/auth/services/account.service";
-import { UserId } from "@bitwarden/common/types/guid";
+import { UserId, OrganizationId } from "@bitwarden/common/types/guid";
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
import { CipherType } from "@bitwarden/common/vault/enums";
import { Cipher } from "@bitwarden/common/vault/models/domain/cipher";
@@ -31,21 +31,13 @@ export class DefaultCipherFormService implements CipherFormService {
}
async saveCipher(cipher: CipherView, config: CipherFormConfig): Promise {
- // Passing the original cipher is important here as it is responsible for appending to password history
const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId));
- const encrypted = await this.cipherService.encrypt(
- cipher,
- activeUserId,
- null,
- null,
- config.originalCipher ?? null,
- );
- const encryptedCipher = encrypted.cipher;
let savedCipher: Cipher;
// Creating a new cipher
if (cipher.id == null) {
+ const encrypted = await this.cipherService.encrypt(cipher, activeUserId);
savedCipher = await this.cipherService.createWithServer(encrypted, config.admin);
return await this.cipherService.decrypt(savedCipher, activeUserId);
}
@@ -61,16 +53,37 @@ export class DefaultCipherFormService implements CipherFormService {
// Call shareWithServer if the owner is changing from a user to an organization
if (config.originalCipher.organizationId === null && cipher.organizationId != null) {
+ // shareWithServer expects the cipher to have no organizationId set
+ const organizationId = cipher.organizationId as OrganizationId;
+ cipher.organizationId = null;
+
savedCipher = await this.cipherService.shareWithServer(
cipher,
- cipher.organizationId,
+ organizationId,
cipher.collectionIds,
activeUserId,
+ config.originalCipher,
);
// If the collectionIds are the same, update the cipher normally
} else if (isSetEqual(originalCollectionIds, newCollectionIds)) {
+ const encrypted = await this.cipherService.encrypt(
+ cipher,
+ activeUserId,
+ null,
+ null,
+ config.originalCipher,
+ );
savedCipher = await this.cipherService.updateWithServer(encrypted, config.admin);
} else {
+ const encrypted = await this.cipherService.encrypt(
+ cipher,
+ activeUserId,
+ null,
+ null,
+ config.originalCipher,
+ );
+ const encryptedCipher = encrypted.cipher;
+
// Updating a cipher with collection changes is not supported with a single request currently
// First update the cipher with the original collectionIds
encryptedCipher.collectionIds = config.originalCipher.collectionIds;
diff --git a/libs/vault/src/cipher-view/cipher-view.component.html b/libs/vault/src/cipher-view/cipher-view.component.html
index a2ade0e885c..e65dd62500e 100644
--- a/libs/vault/src/cipher-view/cipher-view.component.html
+++ b/libs/vault/src/cipher-view/cipher-view.component.html
@@ -67,12 +67,12 @@
-
+
-
+
{
});
describe("history", () => {
- const password1 = { password: "bad-password-1", lastUsedDate: new Date("09/13/2004") };
- const password2 = { password: "bad-password-2", lastUsedDate: new Date("02/01/2004") };
+ const password1 = {
+ password: "bad-password-1",
+ lastUsedDate: new Date("09/13/2004"),
+ } as PasswordHistoryView;
+ const password2 = {
+ password: "bad-password-2",
+ lastUsedDate: new Date("02/01/2004"),
+ } as PasswordHistoryView;
beforeEach(async () => {
mockCipher.passwordHistory = [password1, password2];
diff --git a/package-lock.json b/package-lock.json
index e6d4a0b9b89..b9f661f6915 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -23,7 +23,7 @@
"@angular/platform-browser": "19.2.14",
"@angular/platform-browser-dynamic": "19.2.14",
"@angular/router": "19.2.14",
- "@bitwarden/sdk-internal": "0.2.0-main.225",
+ "@bitwarden/sdk-internal": "0.2.0-main.227",
"@electron/fuses": "1.8.0",
"@emotion/css": "11.13.5",
"@koa/multer": "4.0.0",
@@ -4604,9 +4604,9 @@
"link": true
},
"node_modules/@bitwarden/sdk-internal": {
- "version": "0.2.0-main.225",
- "resolved": "https://registry.npmjs.org/@bitwarden/sdk-internal/-/sdk-internal-0.2.0-main.225.tgz",
- "integrity": "sha512-bhSFNX584GPJ9wMBYff1d18/Hfj+o+D4E1l3uDLZNXRI9s7w919AQWqJ0xUy1vh8gpkLJovkf64HQGqs0OiQQA==",
+ "version": "0.2.0-main.227",
+ "resolved": "https://registry.npmjs.org/@bitwarden/sdk-internal/-/sdk-internal-0.2.0-main.227.tgz",
+ "integrity": "sha512-afOsl9jwi1qyX/tF4bYP3EWXgc8oMgnCA0hPPh+AJpn7GgoAPCi+WXaJkbBPwRpxZFKEpwt3oLRNTvtkECvFJw==",
"license": "GPL-3.0",
"dependencies": {
"type-fest": "^4.41.0"
diff --git a/package.json b/package.json
index b7923a92f6f..ac00e674310 100644
--- a/package.json
+++ b/package.json
@@ -158,7 +158,7 @@
"@angular/platform-browser": "19.2.14",
"@angular/platform-browser-dynamic": "19.2.14",
"@angular/router": "19.2.14",
- "@bitwarden/sdk-internal": "0.2.0-main.225",
+ "@bitwarden/sdk-internal": "0.2.0-main.227",
"@electron/fuses": "1.8.0",
"@emotion/css": "11.13.5",
"@koa/multer": "4.0.0",
From b54944da416925c90fabc0479d2cc20b4c3f32da Mon Sep 17 00:00:00 2001
From: Bernd Schoolmann
Date: Tue, 22 Jul 2025 12:35:55 +0200
Subject: [PATCH 006/179] Deprecate encstring's decrypt function (#15703)
---
libs/common/src/key-management/crypto/models/enc-string.ts | 4 ++++
1 file changed, 4 insertions(+)
diff --git a/libs/common/src/key-management/crypto/models/enc-string.ts b/libs/common/src/key-management/crypto/models/enc-string.ts
index 1ff98d1b6b6..3478ced0cf3 100644
--- a/libs/common/src/key-management/crypto/models/enc-string.ts
+++ b/libs/common/src/key-management/crypto/models/enc-string.ts
@@ -153,6 +153,10 @@ export class EncString implements Encrypted {
return EXPECTED_NUM_PARTS_BY_ENCRYPTION_TYPE[encType] === encPieces.length;
}
+ /**
+ * @deprecated - This function is deprecated. Use EncryptService.decryptString instead.
+ * @returns - The decrypted string, or `[error: cannot decrypt]` if decryption fails.
+ */
async decrypt(
orgId: string | null,
key: SymmetricCryptoKey | null = null,
From 481910b82374631454a173816f4b24ba2336437d Mon Sep 17 00:00:00 2001
From: Bernd Schoolmann
Date: Tue, 22 Jul 2025 13:03:04 +0200
Subject: [PATCH 007/179] Fix breaking sdk change and update to 231 (#15617)
---
.../src/platform/services/sdk/default-sdk.service.ts | 1 +
package-lock.json | 8 ++++----
package.json | 2 +-
3 files changed, 6 insertions(+), 5 deletions(-)
diff --git a/libs/common/src/platform/services/sdk/default-sdk.service.ts b/libs/common/src/platform/services/sdk/default-sdk.service.ts
index 1dfdfd207c0..c12fbe2dbb2 100644
--- a/libs/common/src/platform/services/sdk/default-sdk.service.ts
+++ b/libs/common/src/platform/services/sdk/default-sdk.service.ts
@@ -228,6 +228,7 @@ export class DefaultSdkService implements SdkService {
},
privateKey,
signingKey: undefined,
+ securityState: undefined,
});
// We initialize the org crypto even if the org_keys are
diff --git a/package-lock.json b/package-lock.json
index b9f661f6915..e44797997f1 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -23,7 +23,7 @@
"@angular/platform-browser": "19.2.14",
"@angular/platform-browser-dynamic": "19.2.14",
"@angular/router": "19.2.14",
- "@bitwarden/sdk-internal": "0.2.0-main.227",
+ "@bitwarden/sdk-internal": "0.2.0-main.231",
"@electron/fuses": "1.8.0",
"@emotion/css": "11.13.5",
"@koa/multer": "4.0.0",
@@ -4604,9 +4604,9 @@
"link": true
},
"node_modules/@bitwarden/sdk-internal": {
- "version": "0.2.0-main.227",
- "resolved": "https://registry.npmjs.org/@bitwarden/sdk-internal/-/sdk-internal-0.2.0-main.227.tgz",
- "integrity": "sha512-afOsl9jwi1qyX/tF4bYP3EWXgc8oMgnCA0hPPh+AJpn7GgoAPCi+WXaJkbBPwRpxZFKEpwt3oLRNTvtkECvFJw==",
+ "version": "0.2.0-main.231",
+ "resolved": "https://registry.npmjs.org/@bitwarden/sdk-internal/-/sdk-internal-0.2.0-main.231.tgz",
+ "integrity": "sha512-fDKB/RFVvkRPWlhL/qhPAdJDjD1EpFjpEjjpY0v5QNGalh6NCztOr1OcMc4kvipPp4g+epZjs3SPN38K6R+7zw==",
"license": "GPL-3.0",
"dependencies": {
"type-fest": "^4.41.0"
diff --git a/package.json b/package.json
index ac00e674310..089ef3342e9 100644
--- a/package.json
+++ b/package.json
@@ -158,7 +158,7 @@
"@angular/platform-browser": "19.2.14",
"@angular/platform-browser-dynamic": "19.2.14",
"@angular/router": "19.2.14",
- "@bitwarden/sdk-internal": "0.2.0-main.227",
+ "@bitwarden/sdk-internal": "0.2.0-main.231",
"@electron/fuses": "1.8.0",
"@emotion/css": "11.13.5",
"@koa/multer": "4.0.0",
From 2a07b952ef5c633ea4b470f64039cb13c02c0aa4 Mon Sep 17 00:00:00 2001
From: Shane Melton
Date: Tue, 22 Jul 2025 06:32:00 -0700
Subject: [PATCH 008/179] [PM-24000] Convert string date values to Date objects
for CipherExport types (#15715)
---
libs/common/src/models/export/cipher.export.ts | 7 ++++---
libs/common/src/models/export/password-history.export.ts | 2 +-
2 files changed, 5 insertions(+), 4 deletions(-)
diff --git a/libs/common/src/models/export/cipher.export.ts b/libs/common/src/models/export/cipher.export.ts
index ad6d8b7a609..d343621328c 100644
--- a/libs/common/src/models/export/cipher.export.ts
+++ b/libs/common/src/models/export/cipher.export.ts
@@ -53,6 +53,7 @@ export class CipherExport {
view.notes = req.notes;
view.favorite = req.favorite;
view.reprompt = req.reprompt ?? CipherRepromptType.None;
+ view.key = req.key != null ? new EncString(req.key) : null;
if (req.fields != null) {
view.fields = req.fields.map((f) => FieldExport.toView(f));
@@ -80,9 +81,9 @@ export class CipherExport {
view.passwordHistory = req.passwordHistory.map((ph) => PasswordHistoryExport.toView(ph));
}
- view.creationDate = req.creationDate;
- view.revisionDate = req.revisionDate;
- view.deletedDate = req.deletedDate;
+ view.creationDate = req.creationDate ? new Date(req.creationDate) : null;
+ view.revisionDate = req.revisionDate ? new Date(req.revisionDate) : null;
+ view.deletedDate = req.deletedDate ? new Date(req.deletedDate) : null;
return view;
}
diff --git a/libs/common/src/models/export/password-history.export.ts b/libs/common/src/models/export/password-history.export.ts
index e5a44e4e330..f443a2f4ace 100644
--- a/libs/common/src/models/export/password-history.export.ts
+++ b/libs/common/src/models/export/password-history.export.ts
@@ -16,7 +16,7 @@ export class PasswordHistoryExport {
static toView(req: PasswordHistoryExport, view = new PasswordHistoryView()) {
view.password = req.password;
- view.lastUsedDate = req.lastUsedDate;
+ view.lastUsedDate = req.lastUsedDate ? new Date(req.lastUsedDate) : null;
return view;
}
From 5290e0a63beb5ec5f134c996c7e25e3814cbce4f Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E2=9C=A8=20Audrey=20=E2=9C=A8?=
Date: Tue, 22 Jul 2025 09:33:34 -0400
Subject: [PATCH 009/179] [PM-19054] configure send with email otp
authentication via cli (#15360)
---
apps/cli/src/commands/get.command.ts | 10 +-
.../src/tools/send/commands/create.command.ts | 10 +-
.../src/tools/send/commands/edit.command.ts | 24 ++-
apps/cli/src/tools/send/commands/index.ts | 1 +
.../tools/send/commands/template.command.ts | 35 ++++
.../src/tools/send/models/send.response.ts | 3 +
apps/cli/src/tools/send/send.program.ts | 64 +++---
apps/cli/src/tools/send/util.spec.ts | 194 ++++++++++++++++++
apps/cli/src/tools/send/util.ts | 55 +++++
.../src/tools/send/models/data/send.data.ts | 2 +
.../src/tools/send/models/domain/send.spec.ts | 6 +-
.../src/tools/send/models/domain/send.ts | 2 +
.../tools/send/models/request/send.request.ts | 2 +
.../send/models/response/send.response.ts | 2 +
.../src/tools/send/models/view/send.view.ts | 1 +
.../src/tools/send/services/send.service.ts | 7 +-
16 files changed, 369 insertions(+), 49 deletions(-)
create mode 100644 apps/cli/src/tools/send/commands/template.command.ts
create mode 100644 apps/cli/src/tools/send/util.spec.ts
create mode 100644 apps/cli/src/tools/send/util.ts
diff --git a/apps/cli/src/commands/get.command.ts b/apps/cli/src/commands/get.command.ts
index aa2db7c81ab..b20052fbb53 100644
--- a/apps/cli/src/commands/get.command.ts
+++ b/apps/cli/src/commands/get.command.ts
@@ -25,7 +25,6 @@ import { LoginExport } from "@bitwarden/common/models/export/login.export";
import { SecureNoteExport } from "@bitwarden/common/models/export/secure-note.export";
import { ErrorResponse } from "@bitwarden/common/models/response/error.response";
import { Utils } from "@bitwarden/common/platform/misc/utils";
-import { SendType } from "@bitwarden/common/tools/send/enums/send-type";
import { CipherId, OrganizationId, UserId } from "@bitwarden/common/types/guid";
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction";
@@ -44,7 +43,6 @@ import { SelectionReadOnly } from "../admin-console/models/selection-read-only";
import { Response } from "../models/response";
import { StringResponse } from "../models/response/string.response";
import { TemplateResponse } from "../models/response/template.response";
-import { SendResponse } from "../tools/send/models/send.response";
import { CliUtils } from "../utils";
import { CipherResponse } from "../vault/models/cipher.response";
import { FolderResponse } from "../vault/models/folder.response";
@@ -577,11 +575,11 @@ export class GetCommand extends DownloadCommand {
case "org-collection":
template = OrganizationCollectionRequest.template();
break;
- case "send.text":
- template = SendResponse.template(SendType.Text);
- break;
case "send.file":
- template = SendResponse.template(SendType.File);
+ case "send.text":
+ template = Response.badRequest(
+ `Invalid template object. Use \`bw send template ${id}\` instead.`,
+ );
break;
default:
return Response.badRequest("Unknown template object.");
diff --git a/apps/cli/src/tools/send/commands/create.command.ts b/apps/cli/src/tools/send/commands/create.command.ts
index a9264c50126..d4f544d39b7 100644
--- a/apps/cli/src/tools/send/commands/create.command.ts
+++ b/apps/cli/src/tools/send/commands/create.command.ts
@@ -76,9 +76,14 @@ export class SendCreateCommand {
const filePath = req.file?.fileName ?? options.file;
const text = req.text?.text ?? options.text;
const hidden = req.text?.hidden ?? options.hidden;
- const password = req.password ?? options.password;
+ const password = req.password ?? options.password ?? undefined;
+ const emails = req.emails ?? options.emails ?? undefined;
const maxAccessCount = req.maxAccessCount ?? options.maxAccessCount;
+ if (emails !== undefined && password !== undefined) {
+ return Response.badRequest("--password and --emails are mutually exclusive.");
+ }
+
req.key = null;
req.maxAccessCount = maxAccessCount;
@@ -133,6 +138,7 @@ export class SendCreateCommand {
// Add dates from template
encSend.deletionDate = sendView.deletionDate;
encSend.expirationDate = sendView.expirationDate;
+ encSend.emails = emails && emails.join(",");
await this.sendApiService.save([encSend, fileData]);
const newSend = await this.sendService.getFromState(encSend.id);
@@ -151,12 +157,14 @@ class Options {
text: string;
maxAccessCount: number;
password: string;
+ emails: Array;
hidden: boolean;
constructor(passedOptions: Record) {
this.file = passedOptions?.file;
this.text = passedOptions?.text;
this.password = passedOptions?.password;
+ this.emails = passedOptions?.email;
this.hidden = CliUtils.convertBooleanOption(passedOptions?.hidden);
this.maxAccessCount =
passedOptions?.maxAccessCount != null ? parseInt(passedOptions.maxAccessCount, null) : null;
diff --git a/apps/cli/src/tools/send/commands/edit.command.ts b/apps/cli/src/tools/send/commands/edit.command.ts
index ed719b58311..09f89041cc5 100644
--- a/apps/cli/src/tools/send/commands/edit.command.ts
+++ b/apps/cli/src/tools/send/commands/edit.command.ts
@@ -50,11 +50,21 @@ export class SendEditCommand {
const normalizedOptions = new Options(cmdOptions);
req.id = normalizedOptions.itemId || req.id;
-
- if (req.id != null) {
- req.id = req.id.toLowerCase();
+ if (normalizedOptions.emails) {
+ req.emails = normalizedOptions.emails;
+ req.password = undefined;
+ } else if (normalizedOptions.password) {
+ req.emails = undefined;
+ req.password = normalizedOptions.password;
+ } else if (req.password && (typeof req.password !== "string" || req.password === "")) {
+ req.password = undefined;
}
+ if (!req.id) {
+ return Response.error("`itemid` was not provided.");
+ }
+
+ req.id = req.id.toLowerCase();
const send = await this.sendService.getFromState(req.id);
if (send == null) {
@@ -76,10 +86,6 @@ export class SendEditCommand {
let sendView = await send.decrypt();
sendView = SendResponse.toView(req, sendView);
- if (typeof req.password !== "string" || req.password === "") {
- req.password = null;
- }
-
try {
const [encSend, encFileData] = await this.sendService.encrypt(sendView, null, req.password);
// Add dates from template
@@ -97,8 +103,12 @@ export class SendEditCommand {
class Options {
itemId: string;
+ password: string;
+ emails: string[];
constructor(passedOptions: Record) {
this.itemId = passedOptions?.itemId || passedOptions?.itemid;
+ this.password = passedOptions.password;
+ this.emails = passedOptions.email;
}
}
diff --git a/apps/cli/src/tools/send/commands/index.ts b/apps/cli/src/tools/send/commands/index.ts
index 645f5c0d1db..452c228dd9b 100644
--- a/apps/cli/src/tools/send/commands/index.ts
+++ b/apps/cli/src/tools/send/commands/index.ts
@@ -5,3 +5,4 @@ export * from "./get.command";
export * from "./list.command";
export * from "./receive.command";
export * from "./remove-password.command";
+export * from "./template.command";
diff --git a/apps/cli/src/tools/send/commands/template.command.ts b/apps/cli/src/tools/send/commands/template.command.ts
new file mode 100644
index 00000000000..c1c2c97b03d
--- /dev/null
+++ b/apps/cli/src/tools/send/commands/template.command.ts
@@ -0,0 +1,35 @@
+import { SendType } from "@bitwarden/common/tools/send/enums/send-type";
+
+import { Response } from "../../../models/response";
+import { TemplateResponse } from "../../../models/response/template.response";
+import { SendResponse } from "../models/send.response";
+
+export class SendTemplateCommand {
+ constructor() {}
+
+ run(type: string): Response {
+ let template: SendResponse | undefined;
+ let response: Response;
+
+ switch (type) {
+ case "send.text":
+ case "text":
+ template = SendResponse.template(SendType.Text);
+ break;
+ case "send.file":
+ case "file":
+ template = SendResponse.template(SendType.File);
+ break;
+ default:
+ response = Response.badRequest("Unknown template object.");
+ }
+
+ if (template) {
+ response = Response.success(new TemplateResponse(template));
+ }
+
+ response ??= Response.badRequest("An error occurred while retrieving the template.");
+
+ return response;
+ }
+}
diff --git a/apps/cli/src/tools/send/models/send.response.ts b/apps/cli/src/tools/send/models/send.response.ts
index 4d680b5c0a1..a0c1d3f83c6 100644
--- a/apps/cli/src/tools/send/models/send.response.ts
+++ b/apps/cli/src/tools/send/models/send.response.ts
@@ -26,6 +26,7 @@ export class SendResponse implements BaseResponse {
req.deletionDate = this.getStandardDeletionDate(deleteInDays);
req.expirationDate = null;
req.password = null;
+ req.emails = null;
req.disabled = false;
req.hideEmail = false;
return req;
@@ -50,6 +51,7 @@ export class SendResponse implements BaseResponse {
view.deletionDate = send.deletionDate;
view.expirationDate = send.expirationDate;
view.password = send.password;
+ view.emails = send.emails ?? [];
view.disabled = send.disabled;
view.hideEmail = send.hideEmail;
return view;
@@ -87,6 +89,7 @@ export class SendResponse implements BaseResponse {
expirationDate: Date;
password: string;
passwordSet: boolean;
+ emails?: Array;
disabled: boolean;
hideEmail: boolean;
diff --git a/apps/cli/src/tools/send/send.program.ts b/apps/cli/src/tools/send/send.program.ts
index cbeda188a99..650f448e558 100644
--- a/apps/cli/src/tools/send/send.program.ts
+++ b/apps/cli/src/tools/send/send.program.ts
@@ -4,13 +4,12 @@ import * as fs from "fs";
import * as path from "path";
import * as chalk from "chalk";
-import { program, Command, OptionValues } from "commander";
+import { program, Command, Option, OptionValues } from "commander";
import { Utils } from "@bitwarden/common/platform/misc/utils";
import { SendType } from "@bitwarden/common/tools/send/enums/send-type";
import { BaseProgram } from "../../base-program";
-import { GetCommand } from "../../commands/get.command";
import { Response } from "../../models/response";
import { CliUtils } from "../../utils";
@@ -22,10 +21,12 @@ import {
SendListCommand,
SendReceiveCommand,
SendRemovePasswordCommand,
+ SendTemplateCommand,
} from "./commands";
import { SendFileResponse } from "./models/send-file.response";
import { SendTextResponse } from "./models/send-text.response";
import { SendResponse } from "./models/send.response";
+import { parseEmail } from "./util";
const writeLn = CliUtils.writeLn;
@@ -48,6 +49,17 @@ export class SendProgram extends BaseProgram {
"The number of days in the future to set deletion date, defaults to 7",
"7",
)
+ .addOption(
+ new Option(
+ "--password ",
+ "optional password to access this Send. Can also be specified in JSON.",
+ ).conflicts("email"),
+ )
+ .option(
+ "--email ",
+ "optional emails to access this Send. Can also be specified in JSON.",
+ parseEmail,
+ )
.option("-a, --maxAccessCount ", "The amount of max possible accesses.")
.option("--hidden", "Hide in web by default. Valid only if --file is not set.")
.option(
@@ -139,26 +151,9 @@ export class SendProgram extends BaseProgram {
return new Command("template")
.argument("