mirror of
https://github.com/bitwarden/android.git
synced 2026-05-10 05:49:44 -05:00
Compare commits
3 Commits
agalles/20
...
release/ho
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1d624606f1 | ||
|
|
e1d921e013 | ||
|
|
a721744a6b |
25
.github/workflows/build-authenticator.yml
vendored
25
.github/workflows/build-authenticator.yml
vendored
@@ -32,6 +32,7 @@ env:
|
||||
permissions:
|
||||
contents: read
|
||||
packages: read
|
||||
id-token: write
|
||||
|
||||
jobs:
|
||||
build:
|
||||
@@ -122,9 +123,18 @@ jobs:
|
||||
bundle install --jobs 4 --retry 3
|
||||
|
||||
- name: Log in to Azure
|
||||
uses: Azure/login@a457da9ea143d694b1b9c7c869ebb04ebe844ef5 # v2.3.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
|
||||
uses: bitwarden/gh-actions/get-keyvault-secrets@main
|
||||
with:
|
||||
keyvault: gh-android
|
||||
secrets: "BWA-AAB-KEYSTORE-STORE-PASSWORD,BWA-AAB-KEYSTORE-KEY-PASSWORD,BWA-APK-KEYSTORE-STORE-PASSWORD,BWA-APK-KEYSTORE-KEY-PASSWORD"
|
||||
|
||||
- name: Retrieve secrets
|
||||
env:
|
||||
@@ -168,6 +178,9 @@ jobs:
|
||||
az storage blob download --account-name $ACCOUNT_NAME --container-name $CONTAINER_NAME \
|
||||
--name authenticator_play_store-creds.json --file ${{ github.workspace }}/secrets/authenticator_play_store-creds.json --output none
|
||||
|
||||
- name: AZ Logout
|
||||
uses: bitwarden/gh-actions/azure-logout@main
|
||||
|
||||
- name: Verify Play Store credentials
|
||||
if: ${{ inputs.publish-to-play-store }}
|
||||
run: |
|
||||
@@ -222,18 +235,18 @@ jobs:
|
||||
run: |
|
||||
bundle exec fastlane bundleAuthenticatorRelease \
|
||||
storeFile:${{ github.workspace }}/keystores/authenticator_aab-keystore.jks \
|
||||
storePassword:'${{ secrets.BWA_AAB_KEYSTORE_STORE_PASSWORD }}' \
|
||||
storePassword:'${{ steps.get-kv-secrets.outputs.BWA-AAB-KEYSTORE-STORE-PASSWORD }}' \
|
||||
keyAlias:authenticatorupload \
|
||||
keyPassword:'${{ secrets.BWA_AAB_KEYSTORE_KEY_PASSWORD }}'
|
||||
keyPassword:'${{ steps.get-kv-secrets.outputs.BWA-AAB-KEYSTORE-KEY-PASSWORD }}'
|
||||
|
||||
- name: Generate release Play Store APK
|
||||
if: ${{ matrix.variant == 'apk' }}
|
||||
run: |
|
||||
bundle exec fastlane buildAuthenticatorRelease \
|
||||
storeFile:${{ github.workspace }}/keystores/authenticator_apk-keystore.jks \
|
||||
storePassword:'${{ secrets.BWA_APK_KEYSTORE_STORE_PASSWORD }}' \
|
||||
storePassword:'${{ steps.get-kv-secrets.outputs.BWA-APK-KEYSTORE-STORE-PASSWORD }}' \
|
||||
keyAlias:bitwardenauthenticator \
|
||||
keyPassword:'${{ secrets.BWA_APK_KEYSTORE_KEY_PASSWORD }}'
|
||||
keyPassword:'${{ steps.get-kv-secrets.outputs.BWA-APK-KEYSTORE-KEY-PASSWORD }}'
|
||||
|
||||
- name: Upload release Play Store .aab artifact
|
||||
if: ${{ matrix.variant == 'aab' }}
|
||||
|
||||
71
.github/workflows/build.yml
vendored
71
.github/workflows/build.yml
vendored
@@ -33,6 +33,7 @@ env:
|
||||
permissions:
|
||||
contents: read
|
||||
packages: read
|
||||
id-token: write
|
||||
|
||||
jobs:
|
||||
build:
|
||||
@@ -130,9 +131,18 @@ jobs:
|
||||
bundle install --jobs 4 --retry 3
|
||||
|
||||
- name: Log in to Azure
|
||||
uses: Azure/login@a457da9ea143d694b1b9c7c869ebb04ebe844ef5 # v2.3.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
|
||||
uses: bitwarden/gh-actions/get-keyvault-secrets@main
|
||||
with:
|
||||
keyvault: gh-android
|
||||
secrets: "UPLOAD-KEYSTORE-PASSWORD,UPLOAD-BETA-KEYSTORE-PASSWORD,UPLOAD-BETA-KEY-PASSWORD,PLAY-KEYSTORE-PASSWORD,PLAY-BETA-KEYSTORE-PASSWORD,PLAY-BETA-KEY-PASSWORD"
|
||||
|
||||
- name: Retrieve secrets
|
||||
env:
|
||||
@@ -169,6 +179,9 @@ jobs:
|
||||
az storage blob download --account-name $ACCOUNT_NAME --container-name $CONTAINER_NAME \
|
||||
--name app_play_prod_firebase-creds.json --file ${{ github.workspace }}/secrets/app_play_prod_firebase-creds.json --output none
|
||||
|
||||
- name: Log out from Azure
|
||||
uses: bitwarden/gh-actions/azure-logout@main
|
||||
|
||||
- name: Validate Gradle wrapper
|
||||
uses: gradle/actions/wrapper-validation@06832c7b30a0129d7fb559bcc6e43d26f6374244 # v4.3.1
|
||||
|
||||
@@ -216,48 +229,48 @@ jobs:
|
||||
- name: Generate release Play Store bundle
|
||||
if: ${{ matrix.variant == 'prod' && matrix.artifact == 'aab' }}
|
||||
env:
|
||||
UPLOAD_KEYSTORE_PASSWORD: ${{ secrets.UPLOAD_KEYSTORE_PASSWORD }}
|
||||
UPLOAD-KEYSTORE-PASSWORD: ${{ steps.get-kv-secrets.outputs.UPLOAD-KEYSTORE-PASSWORD }}
|
||||
run: |
|
||||
bundle exec fastlane bundlePlayStoreRelease \
|
||||
storeFile:app_upload-keystore.jks \
|
||||
storePassword:${{ env.UPLOAD_KEYSTORE_PASSWORD }} \
|
||||
storePassword:${{ env.UPLOAD-KEYSTORE-PASSWORD }} \
|
||||
keyAlias:upload \
|
||||
keyPassword:${{ env.UPLOAD_KEYSTORE_PASSWORD }}
|
||||
keyPassword:${{ env.UPLOAD-KEYSTORE-PASSWORD }}
|
||||
|
||||
- name: Generate beta Play Store bundle
|
||||
if: ${{ (matrix.variant == 'prod') && (matrix.artifact == 'aab') }}
|
||||
env:
|
||||
UPLOAD_BETA_KEYSTORE_PASSWORD: ${{ secrets.UPLOAD_BETA_KEYSTORE_PASSWORD }}
|
||||
UPLOAD_BETA_KEY_PASSWORD: ${{ secrets.UPLOAD_BETA_KEY_PASSWORD }}
|
||||
UPLOAD-BETA-KEYSTORE-PASSWORD: ${{ steps.get-kv-secrets.outputs.UPLOAD-BETA-KEYSTORE-PASSWORD }}
|
||||
UPLOAD-BETA-KEY-PASSWORD: ${{ steps.get-kv-secrets.outputs.UPLOAD-BETA-KEY-PASSWORD }}
|
||||
run: |
|
||||
bundle exec fastlane bundlePlayStoreBeta \
|
||||
storeFile:app_beta_upload-keystore.jks \
|
||||
storePassword:${{ env.UPLOAD_BETA_KEYSTORE_PASSWORD }} \
|
||||
storePassword:${{ env.UPLOAD-BETA-KEYSTORE-PASSWORD }} \
|
||||
keyAlias:bitwarden-beta-upload \
|
||||
keyPassword:${{ env.UPLOAD_BETA_KEY_PASSWORD }}
|
||||
keyPassword:${{ env.UPLOAD-BETA-KEY-PASSWORD }}
|
||||
|
||||
- name: Generate release Play Store APK
|
||||
if: ${{ (matrix.variant == 'prod') && (matrix.artifact == 'apk') }}
|
||||
env:
|
||||
PLAY_KEYSTORE_PASSWORD: ${{ secrets.PLAY_KEYSTORE_PASSWORD }}
|
||||
PLAY-KEYSTORE-PASSWORD: ${{ steps.get-kv-secrets.outputs.PLAY-KEYSTORE-PASSWORD }}
|
||||
run: |
|
||||
bundle exec fastlane assemblePlayStoreReleaseApk \
|
||||
storeFile:app_play-keystore.jks \
|
||||
storePassword:${{ env.PLAY_KEYSTORE_PASSWORD }} \
|
||||
storePassword:${{ env.PLAY-KEYSTORE-PASSWORD }} \
|
||||
keyAlias:bitwarden \
|
||||
keyPassword:${{ env.PLAY_KEYSTORE_PASSWORD }}
|
||||
keyPassword:${{ env.PLAY-KEYSTORE-PASSWORD }}
|
||||
|
||||
- name: Generate beta Play Store APK
|
||||
if: ${{ (matrix.variant == 'prod') && (matrix.artifact == 'apk') }}
|
||||
env:
|
||||
PLAY_BETA_KEYSTORE_PASSWORD: ${{ secrets.PLAY_BETA_KEYSTORE_PASSWORD }}
|
||||
PLAY_BETA_KEY_PASSWORD: ${{ secrets.PLAY_BETA_KEY_PASSWORD }}
|
||||
PLAY-BETA-KEYSTORE-PASSWORD: ${{ steps.get-kv-secrets.outputs.PLAY-BETA-KEYSTORE-PASSWORD }}
|
||||
PLAY-BETA-KEY-PASSWORD: ${{ steps.get-kv-secrets.outputs.PLAY-BETA-KEY-PASSWORD }}
|
||||
run: |
|
||||
bundle exec fastlane assemblePlayStoreBetaApk \
|
||||
storeFile:app_beta_play-keystore.jks \
|
||||
storePassword:${{ env.PLAY_BETA_KEYSTORE_PASSWORD }} \
|
||||
storePassword:${{ env.PLAY-BETA-KEYSTORE-PASSWORD }} \
|
||||
keyAlias:bitwarden-beta \
|
||||
keyPassword:${{ env.PLAY_BETA_KEY_PASSWORD }}
|
||||
keyPassword:${{ env.PLAY-BETA-KEY-PASSWORD }}
|
||||
|
||||
- name: Generate debug Play Store APKs
|
||||
if: ${{ (matrix.variant != 'prod') && (matrix.artifact == 'apk') }}
|
||||
@@ -429,9 +442,18 @@ jobs:
|
||||
bundle install --jobs 4 --retry 3
|
||||
|
||||
- name: Log in to Azure
|
||||
uses: Azure/login@a457da9ea143d694b1b9c7c869ebb04ebe844ef5 # v2.3.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
|
||||
uses: bitwarden/gh-actions/get-keyvault-secrets@main
|
||||
with:
|
||||
keyvault: gh-android
|
||||
secrets: "FDROID-KEYSTORE-PASSWORD,FDROID-BETA-KEYSTORE-PASSWORD,FDROID-BETA-KEY-PASSWORD"
|
||||
|
||||
- name: Retrieve secrets
|
||||
env:
|
||||
@@ -454,6 +476,9 @@ jobs:
|
||||
az storage blob download --account-name $ACCOUNT_NAME --container-name $CONTAINER_NAME \
|
||||
--name app_fdroid_firebase-creds.json --file ${{ github.workspace }}/secrets/app_fdroid_firebase-creds.json --output none
|
||||
|
||||
- name: Log out from Azure
|
||||
uses: bitwarden/gh-actions/azure-logout@main
|
||||
|
||||
- name: Validate Gradle wrapper
|
||||
uses: gradle/actions/wrapper-validation@06832c7b30a0129d7fb559bcc6e43d26f6374244 # v4.3.1
|
||||
|
||||
@@ -508,7 +533,7 @@ jobs:
|
||||
echo "Version Number: $VERSION_CODE" >> $GITHUB_STEP_SUMMARY
|
||||
- name: Generate F-Droid artifacts
|
||||
env:
|
||||
FDROID_STORE_PASSWORD: ${{ secrets.FDROID_KEYSTORE_PASSWORD }}
|
||||
FDROID_STORE_PASSWORD: ${{ steps.get-kv-secrets.outputs.FDROID-KEYSTORE-PASSWORD }}
|
||||
run: |
|
||||
bundle exec fastlane assembleFDroidReleaseApk \
|
||||
storeFile:app_fdroid-keystore.jks \
|
||||
@@ -518,14 +543,14 @@ jobs:
|
||||
|
||||
- name: Generate F-Droid Beta Artifacts
|
||||
env:
|
||||
FDROID_BETA_KEYSTORE_PASSWORD: ${{ secrets.FDROID_BETA_KEYSTORE_PASSWORD }}
|
||||
FDROID_BETA_KEY_PASSWORD: ${{ secrets.FDROID_BETA_KEY_PASSWORD }}
|
||||
FDROID-BETA-KEYSTORE-PASSWORD: ${{ steps.get-kv-secrets.outputs.FDROID-BETA-KEYSTORE-PASSWORD }}
|
||||
FDROID-BETA-KEY-PASSWORD: ${{ steps.get-kv-secrets.outputs.FDROID-BETA-KEY-PASSWORD }}
|
||||
run: |
|
||||
bundle exec fastlane assembleFDroidBetaApk \
|
||||
storeFile:app_beta_fdroid-keystore.jks \
|
||||
storePassword:"${{ env.FDROID_BETA_KEYSTORE_PASSWORD }}" \
|
||||
storePassword:"${{ env.FDROID-BETA-KEYSTORE-PASSWORD }}" \
|
||||
keyAlias:bitwarden-beta \
|
||||
keyPassword:"${{ env.FDROID_BETA_KEY_PASSWORD }}"
|
||||
keyPassword:"${{ env.FDROID-BETA-KEY-PASSWORD }}"
|
||||
|
||||
- name: Upload F-Droid .apk artifact
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||
|
||||
23
.github/workflows/crowdin-pull.yml
vendored
23
.github/workflows/crowdin-pull.yml
vendored
@@ -13,6 +13,7 @@ jobs:
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
id-token: write
|
||||
strategy:
|
||||
matrix:
|
||||
include:
|
||||
@@ -28,10 +29,19 @@ jobs:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
|
||||
- name: Login to Azure - CI Subscription
|
||||
uses: Azure/login@a457da9ea143d694b1b9c7c869ebb04ebe844ef5 # v2.3.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-org-bitwarden
|
||||
secrets: "BW-GHAPP-ID,BW-GHAPP-KEY"
|
||||
|
||||
- name: Retrieve secrets
|
||||
id: retrieve-secrets
|
||||
@@ -40,12 +50,15 @@ 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@3ff1caaa28b64c9cc276ce0a02e2ff584f3900c5 # v2.0.2
|
||||
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: Download translations for ${{ matrix.name }}
|
||||
uses: crowdin/github-action@b8012bd5491b8aa8578b73ab5b5f5e7c94aaa6e2 # v2.7.0
|
||||
|
||||
10
.github/workflows/crowdin-push.yml
vendored
10
.github/workflows/crowdin-push.yml
vendored
@@ -13,14 +13,17 @@ jobs:
|
||||
runs-on: ubuntu-24.04
|
||||
permissions:
|
||||
contents: read
|
||||
id-token: write
|
||||
steps:
|
||||
- name: Check out repo
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
|
||||
- name: Log in to Azure
|
||||
uses: Azure/login@a457da9ea143d694b1b9c7c869ebb04ebe844ef5 # v2.3.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
|
||||
@@ -40,6 +43,9 @@ jobs:
|
||||
upload_sources: true
|
||||
upload_translations: false
|
||||
|
||||
- name: Log out from Azure
|
||||
uses: bitwarden/gh-actions/azure-logout@main
|
||||
|
||||
- name: Upload sources for Authenticator
|
||||
uses: crowdin/github-action@b8012bd5491b8aa8578b73ab5b5f5e7c94aaa6e2 # v2.7.0
|
||||
env:
|
||||
|
||||
22
.github/workflows/github-release.yml
vendored
22
.github/workflows/github-release.yml
vendored
@@ -21,6 +21,7 @@ jobs:
|
||||
runs-on: ubuntu-24.04
|
||||
permissions:
|
||||
contents: write
|
||||
id-token: write
|
||||
|
||||
steps:
|
||||
- name: Check out repository
|
||||
@@ -115,6 +116,23 @@ jobs:
|
||||
find $ARTIFACTS_PATH -type f
|
||||
fi
|
||||
|
||||
- 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-android
|
||||
secrets: "JIRA-API-EMAIL,JIRA-API-TOKEN"
|
||||
|
||||
- name: Log out from Azure
|
||||
uses: bitwarden/gh-actions/azure-logout@main
|
||||
|
||||
- name: Get product release notes
|
||||
id: get_release_notes
|
||||
env:
|
||||
@@ -122,8 +140,8 @@ jobs:
|
||||
ARTIFACT_RUN_ID: ${{ inputs.artifact-run-id }}
|
||||
_VERSION_NAME: ${{ steps.get_release_info.outputs.version_name }}
|
||||
_RELEASE_TICKET_ID: ${{ inputs.release-ticket-id }}
|
||||
_JIRA_API_EMAIL: ${{ secrets.JIRA_API_EMAIL }}
|
||||
_JIRA_API_TOKEN: ${{ secrets.JIRA_API_TOKEN }}
|
||||
_JIRA_API_EMAIL: ${{ steps.get-kv-secrets.outputs.JIRA-API-EMAIL }}
|
||||
_JIRA_API_TOKEN: ${{ steps.get-kv-secrets.outputs.JIRA-API-TOKEN }}
|
||||
run: |
|
||||
echo "Getting product release notes"
|
||||
product_release_notes=$(python3 .github/scripts/jira-get-release-notes/jira_release_notes.py $_RELEASE_TICKET_ID $_JIRA_API_EMAIL $_JIRA_API_TOKEN)
|
||||
|
||||
45
.github/workflows/scan-ci.yml
vendored
45
.github/workflows/scan-ci.yml
vendored
@@ -13,6 +13,7 @@ jobs:
|
||||
permissions:
|
||||
contents: read
|
||||
security-events: write
|
||||
id-token: write
|
||||
|
||||
steps:
|
||||
- name: Check out repo
|
||||
@@ -20,14 +21,31 @@ jobs:
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- 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: "CHECKMARX-TENANT,CHECKMARX-CLIENT-ID,CHECKMARX-SECRET"
|
||||
|
||||
- name: Log out from Azure
|
||||
uses: bitwarden/gh-actions/azure-logout@main
|
||||
|
||||
- name: Scan with Checkmarx
|
||||
uses: checkmarx/ast-github-action@ef93013c95adc60160bc22060875e90800d3ecfc # 2.3.19
|
||||
with:
|
||||
project_name: ${{ github.repository }}
|
||||
cx_tenant: ${{ secrets.CHECKMARX_TENANT }}
|
||||
cx_tenant: ${{ steps.get-kv-secrets.outputs.CHECKMARX-TENANT }}
|
||||
base_uri: https://ast.checkmarx.net/
|
||||
cx_client_id: ${{ secrets.CHECKMARX_CLIENT_ID }}
|
||||
cx_client_secret: ${{ secrets.CHECKMARX_SECRET }}
|
||||
cx_client_id: ${{ steps.get-kv-secrets.outputs.CHECKMARX-CLIENT-ID }}
|
||||
cx_client_secret: ${{ steps.get-kv-secrets.outputs.CHECKMARX-SECRET }}
|
||||
additional_params: |
|
||||
--report-format sarif \
|
||||
--filter "state=TO_VERIFY;PROPOSED_NOT_EXPLOITABLE;CONFIRMED;URGENT" \
|
||||
@@ -43,17 +61,36 @@ jobs:
|
||||
runs-on: ubuntu-24.04
|
||||
permissions:
|
||||
contents: read
|
||||
id-token: write
|
||||
|
||||
steps:
|
||||
|
||||
- name: Check out repo
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- 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: "SONAR-TOKEN"
|
||||
|
||||
- name: Log out from Azure
|
||||
uses: bitwarden/gh-actions/azure-logout@main
|
||||
|
||||
- name: Scan with SonarCloud
|
||||
uses: sonarsource/sonarqube-scan-action@aa494459d7c39c106cc77b166de8b4250a32bb97 # v5.1.0
|
||||
env:
|
||||
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
|
||||
SONAR_TOKEN: ${{ steps.get-kv-secrets.outputs.SONAR-TOKEN }}
|
||||
with:
|
||||
args: >
|
||||
-Dsonar.organization=${{ github.repository_owner }}
|
||||
|
||||
44
.github/workflows/scan.yml
vendored
44
.github/workflows/scan.yml
vendored
@@ -28,6 +28,7 @@ jobs:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
security-events: write
|
||||
id-token: write
|
||||
|
||||
steps:
|
||||
- name: Check out repo
|
||||
@@ -35,16 +36,33 @@ jobs:
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
|
||||
- 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: "CHECKMARX-TENANT,CHECKMARX-CLIENT-ID,CHECKMARX-SECRET"
|
||||
|
||||
- name: Log out from Azure
|
||||
uses: bitwarden/gh-actions/azure-logout@main
|
||||
|
||||
- name: Scan with Checkmarx
|
||||
uses: checkmarx/ast-github-action@ef93013c95adc60160bc22060875e90800d3ecfc # 2.3.19
|
||||
env:
|
||||
INCREMENTAL: "${{ contains(github.event_name, 'pull_request') && '--sast-incremental' || '' }}"
|
||||
with:
|
||||
project_name: ${{ github.repository }}
|
||||
cx_tenant: ${{ secrets.CHECKMARX_TENANT }}
|
||||
cx_tenant: ${{ steps.get-kv-secrets.outputs.CHECKMARX-TENANT }}
|
||||
base_uri: https://ast.checkmarx.net/
|
||||
cx_client_id: ${{ secrets.CHECKMARX_CLIENT_ID }}
|
||||
cx_client_secret: ${{ secrets.CHECKMARX_SECRET }}
|
||||
cx_client_id: ${{ steps.get-kv-secrets.outputs.CHECKMARX-CLIENT-ID }}
|
||||
cx_client_secret: ${{ steps.get-kv-secrets.outputs.CHECKMARX-SECRET }}
|
||||
additional_params: |
|
||||
--report-format sarif \
|
||||
--filter "state=TO_VERIFY;PROPOSED_NOT_EXPLOITABLE;CONFIRMED;URGENT" \
|
||||
@@ -64,6 +82,7 @@ jobs:
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
id-token: write
|
||||
|
||||
steps:
|
||||
- name: Check out repo
|
||||
@@ -72,10 +91,27 @@ jobs:
|
||||
fetch-depth: 0
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
|
||||
- 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: "SONAR-TOKEN"
|
||||
|
||||
- name: Log out from Azure
|
||||
uses: bitwarden/gh-actions/azure-logout@main
|
||||
|
||||
- name: Scan with SonarCloud
|
||||
uses: sonarsource/sonarqube-scan-action@aa494459d7c39c106cc77b166de8b4250a32bb97 # v5.1.0
|
||||
env:
|
||||
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
|
||||
SONAR_TOKEN: ${{ steps.get-kv-secrets.outputs.SONAR-TOKEN }}
|
||||
with:
|
||||
args: >
|
||||
-Dsonar.organization=${{ github.repository_owner }}
|
||||
|
||||
@@ -1,17 +1,24 @@
|
||||
package com.x8bit.bitwarden.data.platform.repository
|
||||
|
||||
import com.bitwarden.authenticatorbridge.model.SharedAccountData
|
||||
import com.bitwarden.core.InitOrgCryptoRequest
|
||||
import com.bitwarden.core.InitUserCryptoMethod
|
||||
import com.bitwarden.core.InitUserCryptoRequest
|
||||
import com.bitwarden.core.data.util.asSuccess
|
||||
import com.bitwarden.core.data.util.flatMap
|
||||
import com.bitwarden.data.repository.util.toEnvironmentUrlsOrDefault
|
||||
import com.x8bit.bitwarden.data.auth.datasource.disk.AuthDiskSource
|
||||
import com.x8bit.bitwarden.data.auth.datasource.disk.model.AccountJson
|
||||
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
|
||||
import com.x8bit.bitwarden.data.auth.repository.util.toSdkParams
|
||||
import com.x8bit.bitwarden.data.platform.error.MissingPropertyException
|
||||
import com.x8bit.bitwarden.data.platform.repository.util.sanitizeTotpUri
|
||||
import com.x8bit.bitwarden.data.vault.datasource.disk.VaultDiskSource
|
||||
import com.x8bit.bitwarden.data.vault.datasource.sdk.VaultSdkSource
|
||||
import com.x8bit.bitwarden.data.vault.repository.VaultRepository
|
||||
import com.x8bit.bitwarden.data.vault.repository.model.VaultUnlockData
|
||||
import com.x8bit.bitwarden.data.vault.datasource.sdk.ScopedVaultSdkSource
|
||||
import com.x8bit.bitwarden.data.vault.datasource.sdk.model.InitializeCryptoResult
|
||||
import com.x8bit.bitwarden.data.vault.repository.model.VaultUnlockResult
|
||||
import com.x8bit.bitwarden.data.vault.repository.util.statusFor
|
||||
import com.x8bit.bitwarden.data.vault.repository.util.toEncryptedSdkCipher
|
||||
import kotlinx.coroutines.flow.first
|
||||
import com.x8bit.bitwarden.data.vault.repository.util.toVaultUnlockResult
|
||||
|
||||
/**
|
||||
* Default implementation of [AuthenticatorBridgeRepository].
|
||||
@@ -19,9 +26,8 @@ import kotlinx.coroutines.flow.first
|
||||
class AuthenticatorBridgeRepositoryImpl(
|
||||
private val authRepository: AuthRepository,
|
||||
private val authDiskSource: AuthDiskSource,
|
||||
private val vaultRepository: VaultRepository,
|
||||
private val vaultDiskSource: VaultDiskSource,
|
||||
private val vaultSdkSource: VaultSdkSource,
|
||||
private val scopedVaultSdkSource: ScopedVaultSdkSource,
|
||||
) : AuthenticatorBridgeRepository {
|
||||
|
||||
override val authenticatorSyncSymmetricKey: ByteArray?
|
||||
@@ -45,52 +51,41 @@ class AuthenticatorBridgeRepositoryImpl(
|
||||
|
||||
@Suppress("LongMethod")
|
||||
override suspend fun getSharedAccounts(): SharedAccountData {
|
||||
val allAccounts = authRepository.userStateFlow.value?.accounts ?: emptyList()
|
||||
val allAccounts = authDiskSource.userState?.accounts.orEmpty()
|
||||
|
||||
return allAccounts
|
||||
.mapNotNull { account ->
|
||||
val userId = account.userId
|
||||
|
||||
.mapNotNull { (userId, account) ->
|
||||
// Grab the user's authenticator sync unlock key. If it is null,
|
||||
// the user has not enabled authenticator sync.
|
||||
// the user has not enabled authenticator sync and we skip the account.
|
||||
val decryptedUserKey = authDiskSource.getAuthenticatorSyncUnlockKey(userId)
|
||||
?: return@mapNotNull null
|
||||
|
||||
// Wait for any unlocking actions to finish:
|
||||
vaultRepository.vaultUnlockDataStateFlow.first {
|
||||
it.statusFor(userId) != VaultUnlockData.Status.UNLOCKING
|
||||
}
|
||||
|
||||
// Unlock vault if necessary:
|
||||
val isVaultAlreadyUnlocked = vaultRepository.isVaultUnlocked(userId = userId)
|
||||
if (!isVaultAlreadyUnlocked) {
|
||||
val unlockResult = vaultRepository
|
||||
.unlockVaultWithDecryptedUserKey(
|
||||
val vaultUnlockResult = unlockClient(
|
||||
userId = userId,
|
||||
account = account,
|
||||
decryptedUserKey = decryptedUserKey,
|
||||
)
|
||||
when (vaultUnlockResult) {
|
||||
is VaultUnlockResult.AuthenticationError,
|
||||
is VaultUnlockResult.BiometricDecodingError,
|
||||
is VaultUnlockResult.GenericError,
|
||||
is VaultUnlockResult.InvalidStateError,
|
||||
-> {
|
||||
// Not being able to unlock the user's vault with the
|
||||
// decrypted unlock key is an unexpected case, but if it does
|
||||
// happen we omit the account from list of shared accounts
|
||||
// and remove that user's authenticator sync unlock key.
|
||||
// This gives the user a way to potentially re-enable syncing
|
||||
// (going to Account Security and re-enabling the toggle)
|
||||
authDiskSource.storeAuthenticatorSyncUnlockKey(
|
||||
userId = userId,
|
||||
decryptedUserKey = decryptedUserKey,
|
||||
authenticatorSyncUnlockKey = null,
|
||||
)
|
||||
|
||||
when (unlockResult) {
|
||||
is VaultUnlockResult.AuthenticationError,
|
||||
is VaultUnlockResult.BiometricDecodingError,
|
||||
is VaultUnlockResult.GenericError,
|
||||
is VaultUnlockResult.InvalidStateError,
|
||||
-> {
|
||||
// Not being able to unlock the user's vault with the
|
||||
// decrypted unlock key is an unexpected case, but if it does
|
||||
// happen we omit the account from list of shared accounts
|
||||
// and remove that user's authenticator sync unlock key.
|
||||
// This gives the user a way to potentially re-enable syncing
|
||||
// (going to Account Security and re-enabling the toggle)
|
||||
authDiskSource.storeAuthenticatorSyncUnlockKey(
|
||||
userId = userId,
|
||||
authenticatorSyncUnlockKey = null,
|
||||
)
|
||||
return@mapNotNull null
|
||||
}
|
||||
// Proceed
|
||||
VaultUnlockResult.Success -> Unit
|
||||
// Destroy our stand-alone instance of the vault.
|
||||
scopedVaultSdkSource.clearCrypto(userId = userId)
|
||||
return@mapNotNull null
|
||||
}
|
||||
// Proceed
|
||||
VaultUnlockResult.Success -> Unit
|
||||
}
|
||||
|
||||
// Vault is unlocked, query vault disk source for totp logins:
|
||||
@@ -99,7 +94,7 @@ class AuthenticatorBridgeRepositoryImpl(
|
||||
// Filter out any deleted ciphers.
|
||||
.filter { it.deletedDate == null }
|
||||
.mapNotNull {
|
||||
val decryptedCipher = vaultSdkSource
|
||||
val decryptedCipher = scopedVaultSdkSource
|
||||
.decryptCipher(
|
||||
userId = userId,
|
||||
cipher = it.toEncryptedSdkCipher(),
|
||||
@@ -113,19 +108,18 @@ class AuthenticatorBridgeRepositoryImpl(
|
||||
rawTotp.sanitizeTotpUri(cipherName, username)
|
||||
}
|
||||
|
||||
// Lock the user's vault if we unlocked it for this operation:
|
||||
if (!isVaultAlreadyUnlocked) {
|
||||
vaultRepository.lockVault(
|
||||
userId = userId,
|
||||
isUserInitiated = false,
|
||||
)
|
||||
}
|
||||
// Lock and destroy our stand-alone instance of the vault:
|
||||
scopedVaultSdkSource.clearCrypto(userId = userId)
|
||||
|
||||
SharedAccountData.Account(
|
||||
userId = account.userId,
|
||||
name = account.name,
|
||||
email = account.email,
|
||||
environmentLabel = account.environment.label,
|
||||
userId = userId,
|
||||
name = account.profile.name,
|
||||
email = account.profile.email,
|
||||
environmentLabel = account
|
||||
.settings
|
||||
.environmentUrlData
|
||||
.toEnvironmentUrlsOrDefault()
|
||||
.label,
|
||||
totpUris = totpUris,
|
||||
)
|
||||
}
|
||||
@@ -133,4 +127,44 @@ class AuthenticatorBridgeRepositoryImpl(
|
||||
SharedAccountData(it)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun unlockClient(
|
||||
userId: String,
|
||||
account: AccountJson,
|
||||
decryptedUserKey: String,
|
||||
): VaultUnlockResult {
|
||||
val privateKey = authDiskSource
|
||||
.getPrivateKey(userId = userId)
|
||||
?: return VaultUnlockResult.InvalidStateError(MissingPropertyException("Private key"))
|
||||
return scopedVaultSdkSource
|
||||
.initializeCrypto(
|
||||
userId = userId,
|
||||
request = InitUserCryptoRequest(
|
||||
userId = userId,
|
||||
kdfParams = account.profile.toSdkParams(),
|
||||
email = account.profile.email,
|
||||
privateKey = privateKey,
|
||||
method = InitUserCryptoMethod.DecryptedKey(
|
||||
decryptedUserKey = decryptedUserKey,
|
||||
),
|
||||
signingKey = null,
|
||||
),
|
||||
)
|
||||
.flatMap { result ->
|
||||
// Initialize the SDK for organizations if necessary
|
||||
val organizationKeys = authDiskSource.getOrganizationKeys(userId = userId)
|
||||
if (organizationKeys != null && result is InitializeCryptoResult.Success) {
|
||||
scopedVaultSdkSource.initializeOrganizationCrypto(
|
||||
userId = userId,
|
||||
request = InitOrgCryptoRequest(organizationKeys = organizationKeys),
|
||||
)
|
||||
} else {
|
||||
result.asSuccess()
|
||||
}
|
||||
}
|
||||
.fold(
|
||||
onFailure = { VaultUnlockResult.GenericError(error = it) },
|
||||
onSuccess = { it.toVaultUnlockResult() },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,8 +21,8 @@ import com.x8bit.bitwarden.data.platform.repository.EnvironmentRepositoryImpl
|
||||
import com.x8bit.bitwarden.data.platform.repository.SettingsRepository
|
||||
import com.x8bit.bitwarden.data.platform.repository.SettingsRepositoryImpl
|
||||
import com.x8bit.bitwarden.data.vault.datasource.disk.VaultDiskSource
|
||||
import com.x8bit.bitwarden.data.vault.datasource.sdk.ScopedVaultSdkSource
|
||||
import com.x8bit.bitwarden.data.vault.datasource.sdk.VaultSdkSource
|
||||
import com.x8bit.bitwarden.data.vault.repository.VaultRepository
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
@@ -41,15 +41,13 @@ object PlatformRepositoryModule {
|
||||
fun providesAuthenticatorBridgeRepository(
|
||||
authRepository: AuthRepository,
|
||||
authDiskSource: AuthDiskSource,
|
||||
vaultRepository: VaultRepository,
|
||||
vaultDiskSource: VaultDiskSource,
|
||||
vaultSdkSource: VaultSdkSource,
|
||||
scopedVaultSdkSource: ScopedVaultSdkSource,
|
||||
): AuthenticatorBridgeRepository = AuthenticatorBridgeRepositoryImpl(
|
||||
authRepository = authRepository,
|
||||
authDiskSource = authDiskSource,
|
||||
vaultRepository = vaultRepository,
|
||||
vaultDiskSource = vaultDiskSource,
|
||||
vaultSdkSource = vaultSdkSource,
|
||||
scopedVaultSdkSource = scopedVaultSdkSource,
|
||||
)
|
||||
|
||||
@Provides
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
package com.x8bit.bitwarden.data.vault.datasource.sdk
|
||||
|
||||
/**
|
||||
* This is a non-singleton instance of the [VaultSdkSource] that is intentionally separate; this
|
||||
* allows you to temporarily unlock vaults for a given user within its own scope without affecting
|
||||
* the foreground behavior of the app.
|
||||
*
|
||||
* Users of this class must always call [ScopedVaultSdkSource.clearCrypto] when they are done using
|
||||
* the unlocked vault in order to ensure that this instance of the vault is re-locked.
|
||||
*/
|
||||
interface ScopedVaultSdkSource : VaultSdkSource
|
||||
@@ -0,0 +1,32 @@
|
||||
package com.x8bit.bitwarden.data.vault.datasource.sdk
|
||||
|
||||
import com.bitwarden.annotation.OmitFromCoverage
|
||||
import com.bitwarden.core.data.util.asSuccess
|
||||
import com.bitwarden.data.manager.DispatcherManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.FeatureFlagManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.NativeLibraryManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.SdkClientManagerImpl
|
||||
import com.x8bit.bitwarden.data.platform.manager.sdk.SdkRepositoryFactory
|
||||
|
||||
/**
|
||||
* The default instance of the [ScopedVaultSdkSource]. This uses its own instance of the
|
||||
* [SdkClientManagerImpl] to keep it separate from the rest of the app.
|
||||
*/
|
||||
@OmitFromCoverage
|
||||
class ScopedVaultSdkSourceImpl(
|
||||
dispatcherManager: DispatcherManager,
|
||||
featureFlagManager: FeatureFlagManager,
|
||||
sdkRepositoryFactory: SdkRepositoryFactory,
|
||||
vaultSdkSource: VaultSdkSource = VaultSdkSourceImpl(
|
||||
sdkClientManager = SdkClientManagerImpl(
|
||||
// We do not want to have the real NativeLibraryManager used here to avoid
|
||||
// initializing the library twice.
|
||||
nativeLibraryManager = object : NativeLibraryManager {
|
||||
override fun loadLibrary(libraryName: String): Result<Unit> = Unit.asSuccess()
|
||||
},
|
||||
sdkRepoFactory = sdkRepositoryFactory,
|
||||
featureFlagManager = featureFlagManager,
|
||||
),
|
||||
dispatcherManager = dispatcherManager,
|
||||
),
|
||||
) : ScopedVaultSdkSource, VaultSdkSource by vaultSdkSource
|
||||
@@ -3,7 +3,11 @@ package com.x8bit.bitwarden.data.vault.datasource.sdk.di
|
||||
import com.bitwarden.data.manager.DispatcherManager
|
||||
import com.bitwarden.sdk.Fido2CredentialStore
|
||||
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
|
||||
import com.x8bit.bitwarden.data.platform.manager.FeatureFlagManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.SdkClientManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.sdk.SdkRepositoryFactory
|
||||
import com.x8bit.bitwarden.data.vault.datasource.sdk.ScopedVaultSdkSource
|
||||
import com.x8bit.bitwarden.data.vault.datasource.sdk.ScopedVaultSdkSourceImpl
|
||||
import com.x8bit.bitwarden.data.vault.datasource.sdk.VaultSdkSource
|
||||
import com.x8bit.bitwarden.data.vault.datasource.sdk.VaultSdkSourceImpl
|
||||
import com.x8bit.bitwarden.data.vault.datasource.sdk.model.Fido2CredentialStoreImpl
|
||||
@@ -32,6 +36,18 @@ object VaultSdkModule {
|
||||
dispatcherManager = dispatcherManager,
|
||||
)
|
||||
|
||||
@Provides
|
||||
fun providesScopedVaultSdkSource(
|
||||
dispatcherManager: DispatcherManager,
|
||||
featureFlagManager: FeatureFlagManager,
|
||||
sdkRepositoryFactory: SdkRepositoryFactory,
|
||||
): ScopedVaultSdkSource =
|
||||
ScopedVaultSdkSourceImpl(
|
||||
dispatcherManager = dispatcherManager,
|
||||
featureFlagManager = featureFlagManager,
|
||||
sdkRepositoryFactory = sdkRepositoryFactory,
|
||||
)
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun providesFido2CredentialStore(
|
||||
|
||||
@@ -3,29 +3,38 @@ package com.x8bit.bitwarden.data.platform.repository
|
||||
import com.bitwarden.authenticatorbridge.model.SharedAccountData
|
||||
import com.bitwarden.authenticatorbridge.util.generateSecretKey
|
||||
import com.bitwarden.authenticatorbridge.util.toSymmetricEncryptionKeyData
|
||||
import com.bitwarden.core.InitOrgCryptoRequest
|
||||
import com.bitwarden.core.InitUserCryptoMethod
|
||||
import com.bitwarden.core.InitUserCryptoRequest
|
||||
import com.bitwarden.core.data.util.asSuccess
|
||||
import com.bitwarden.crypto.Kdf
|
||||
import com.bitwarden.data.datasource.disk.model.EnvironmentUrlDataJson
|
||||
import com.bitwarden.data.repository.util.toEnvironmentUrlsOrDefault
|
||||
import com.bitwarden.network.model.KdfTypeJson
|
||||
import com.bitwarden.network.model.SyncResponseJson
|
||||
import com.bitwarden.vault.Cipher
|
||||
import com.bitwarden.vault.CipherView
|
||||
import com.x8bit.bitwarden.data.auth.datasource.disk.model.AccountJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.disk.model.AccountTokensJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.disk.model.UserStateJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.disk.util.FakeAuthDiskSource
|
||||
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.UserState
|
||||
import com.x8bit.bitwarden.data.platform.repository.util.sanitizeTotpUri
|
||||
import com.x8bit.bitwarden.data.vault.datasource.disk.VaultDiskSource
|
||||
import com.x8bit.bitwarden.data.vault.datasource.sdk.VaultSdkSource
|
||||
import com.x8bit.bitwarden.data.vault.repository.VaultRepository
|
||||
import com.x8bit.bitwarden.data.vault.repository.model.VaultUnlockData
|
||||
import com.x8bit.bitwarden.data.vault.repository.model.VaultUnlockResult
|
||||
import com.x8bit.bitwarden.data.vault.datasource.sdk.ScopedVaultSdkSource
|
||||
import com.x8bit.bitwarden.data.vault.datasource.sdk.model.InitializeCryptoResult
|
||||
import com.x8bit.bitwarden.data.vault.repository.util.toEncryptedSdkCipher
|
||||
import io.mockk.coEvery
|
||||
import io.mockk.coVerify
|
||||
import io.mockk.confirmVerified
|
||||
import io.mockk.every
|
||||
import io.mockk.just
|
||||
import io.mockk.mockk
|
||||
import io.mockk.mockkStatic
|
||||
import io.mockk.runs
|
||||
import io.mockk.unmockkStatic
|
||||
import io.mockk.verify
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.jupiter.api.AfterEach
|
||||
@@ -39,18 +48,17 @@ import java.time.ZonedDateTime
|
||||
class AuthenticatorBridgeRepositoryTest {
|
||||
|
||||
private val authRepository = mockk<AuthRepository>()
|
||||
private val vaultSdkSource = mockk<VaultSdkSource>()
|
||||
private val scopedVaultSdkSource = mockk<ScopedVaultSdkSource>()
|
||||
private val vaultDiskSource = mockk<VaultDiskSource>()
|
||||
private val vaultRepository = mockk<VaultRepository>()
|
||||
private val fakeAuthDiskSource = FakeAuthDiskSource()
|
||||
|
||||
private val authenticatorBridgeRepository = AuthenticatorBridgeRepositoryImpl(
|
||||
authRepository = authRepository,
|
||||
authDiskSource = fakeAuthDiskSource,
|
||||
vaultRepository = vaultRepository,
|
||||
vaultDiskSource = vaultDiskSource,
|
||||
vaultSdkSource = vaultSdkSource,
|
||||
)
|
||||
private val authenticatorBridgeRepository: AuthenticatorBridgeRepository =
|
||||
AuthenticatorBridgeRepositoryImpl(
|
||||
authRepository = authRepository,
|
||||
authDiskSource = fakeAuthDiskSource,
|
||||
vaultDiskSource = vaultDiskSource,
|
||||
scopedVaultSdkSource = scopedVaultSdkSource,
|
||||
)
|
||||
|
||||
@BeforeEach
|
||||
fun setup() {
|
||||
@@ -66,6 +74,7 @@ class AuthenticatorBridgeRepositoryTest {
|
||||
|
||||
// Setup authRepository to return default USER_STATE:
|
||||
every { authRepository.userStateFlow } returns MutableStateFlow(USER_STATE)
|
||||
fakeAuthDiskSource.userState = USER_STATE_JSON
|
||||
|
||||
// Setup authDiskSource to have each user's authenticator sync unlock key:
|
||||
fakeAuthDiskSource.storeAuthenticatorSyncUnlockKey(
|
||||
@@ -76,30 +85,69 @@ class AuthenticatorBridgeRepositoryTest {
|
||||
userId = USER_2_ID,
|
||||
authenticatorSyncUnlockKey = USER_2_UNLOCK_KEY,
|
||||
)
|
||||
// Setup vaultRepository to not be stuck unlocking:
|
||||
every { vaultRepository.vaultUnlockDataStateFlow } returns MutableStateFlow(
|
||||
listOf(
|
||||
VaultUnlockData(USER_1_ID, VaultUnlockData.Status.UNLOCKED),
|
||||
VaultUnlockData(USER_2_ID, VaultUnlockData.Status.UNLOCKED),
|
||||
),
|
||||
)
|
||||
// Setup vaultRepository to be unlocked for user 1:
|
||||
every { vaultRepository.isVaultUnlocked(USER_1_ID) } returns true
|
||||
// But locked for user 2:
|
||||
every { vaultRepository.isVaultUnlocked(USER_2_ID) } returns false
|
||||
every { vaultRepository.lockVault(USER_2_ID, isUserInitiated = false) } returns Unit
|
||||
fakeAuthDiskSource.storePrivateKey(userId = USER_1_ID, privateKey = USER_1_PRIVATE_KEY)
|
||||
fakeAuthDiskSource.storePrivateKey(userId = USER_2_ID, privateKey = USER_2_PRIVATE_KEY)
|
||||
coEvery {
|
||||
vaultRepository.unlockVaultWithDecryptedUserKey(
|
||||
userId = USER_2_ID,
|
||||
decryptedUserKey = USER_2_UNLOCK_KEY,
|
||||
scopedVaultSdkSource.initializeCrypto(
|
||||
userId = USER_1_ID,
|
||||
request = InitUserCryptoRequest(
|
||||
userId = USER_1_ID,
|
||||
kdfParams = Kdf.Argon2id(iterations = 0U, memory = 0U, parallelism = 0U),
|
||||
email = USER_1_EMAIL,
|
||||
privateKey = USER_1_PRIVATE_KEY,
|
||||
method = InitUserCryptoMethod.DecryptedKey(
|
||||
decryptedUserKey = USER_1_UNLOCK_KEY,
|
||||
),
|
||||
signingKey = null,
|
||||
),
|
||||
)
|
||||
} returns VaultUnlockResult.Success
|
||||
} returns InitializeCryptoResult.Success.asSuccess()
|
||||
coEvery {
|
||||
scopedVaultSdkSource.initializeCrypto(
|
||||
userId = USER_2_ID,
|
||||
request = InitUserCryptoRequest(
|
||||
userId = USER_2_ID,
|
||||
kdfParams = Kdf.Argon2id(iterations = 0U, memory = 0U, parallelism = 0U),
|
||||
email = USER_2_EMAIL,
|
||||
privateKey = USER_2_PRIVATE_KEY,
|
||||
method = InitUserCryptoMethod.DecryptedKey(
|
||||
decryptedUserKey = USER_2_UNLOCK_KEY,
|
||||
),
|
||||
signingKey = null,
|
||||
),
|
||||
)
|
||||
} returns InitializeCryptoResult.Success.asSuccess()
|
||||
fakeAuthDiskSource.storeOrganizationKeys(
|
||||
userId = USER_1_ID,
|
||||
organizationKeys = USER_1_ORG_KEYS,
|
||||
)
|
||||
fakeAuthDiskSource.storeOrganizationKeys(
|
||||
userId = USER_2_ID,
|
||||
organizationKeys = USER_2_ORG_KEYS,
|
||||
)
|
||||
coEvery {
|
||||
scopedVaultSdkSource.initializeOrganizationCrypto(
|
||||
userId = USER_1_ID,
|
||||
request = InitOrgCryptoRequest(organizationKeys = USER_1_ORG_KEYS),
|
||||
)
|
||||
} returns InitializeCryptoResult.Success.asSuccess()
|
||||
coEvery {
|
||||
scopedVaultSdkSource.initializeOrganizationCrypto(
|
||||
userId = USER_2_ID,
|
||||
request = InitOrgCryptoRequest(organizationKeys = USER_2_ORG_KEYS),
|
||||
)
|
||||
} returns InitializeCryptoResult.Success.asSuccess()
|
||||
every { scopedVaultSdkSource.clearCrypto(userId = USER_1_ID) } just runs
|
||||
every { scopedVaultSdkSource.clearCrypto(userId = USER_2_ID) } just runs
|
||||
|
||||
// Add some ciphers to vaultDiskSource for each user,
|
||||
// and setup mock decryption for them:
|
||||
coEvery { vaultDiskSource.getTotpCiphers(USER_1_ID) } returns USER_1_CIPHERS
|
||||
coEvery { vaultDiskSource.getTotpCiphers(USER_2_ID) } returns USER_2_CIPHERS
|
||||
mockkStatic(SyncResponseJson.Cipher::toEncryptedSdkCipher)
|
||||
mockkStatic(
|
||||
SyncResponseJson.Cipher::toEncryptedSdkCipher,
|
||||
EnvironmentUrlDataJson::toEnvironmentUrlsOrDefault,
|
||||
)
|
||||
every {
|
||||
USER_1_TOTP_CIPHER.toEncryptedSdkCipher()
|
||||
} returns USER_1_ENCRYPTED_SDK_TOTP_CIPHER
|
||||
@@ -107,10 +155,10 @@ class AuthenticatorBridgeRepositoryTest {
|
||||
USER_2_TOTP_CIPHER.toEncryptedSdkCipher()
|
||||
} returns USER_2_ENCRYPTED_SDK_TOTP_CIPHER
|
||||
coEvery {
|
||||
vaultSdkSource.decryptCipher(USER_1_ID, USER_1_ENCRYPTED_SDK_TOTP_CIPHER)
|
||||
scopedVaultSdkSource.decryptCipher(USER_1_ID, USER_1_ENCRYPTED_SDK_TOTP_CIPHER)
|
||||
} returns USER_1_DECRYPTED_TOTP_CIPHER.asSuccess()
|
||||
coEvery {
|
||||
vaultSdkSource.decryptCipher(USER_2_ID, USER_2_ENCRYPTED_SDK_TOTP_CIPHER)
|
||||
scopedVaultSdkSource.decryptCipher(USER_2_ID, USER_2_ENCRYPTED_SDK_TOTP_CIPHER)
|
||||
} returns USER_2_DECRYPTED_TOTP_CIPHER.asSuccess()
|
||||
mockkStatic(String::sanitizeTotpUri)
|
||||
every {
|
||||
@@ -120,50 +168,26 @@ class AuthenticatorBridgeRepositoryTest {
|
||||
|
||||
@AfterEach
|
||||
fun teardown() {
|
||||
confirmVerified(authRepository, vaultSdkSource, vaultRepository, vaultDiskSource)
|
||||
unmockkStatic(SyncResponseJson.Cipher::toEncryptedSdkCipher)
|
||||
unmockkStatic(String::sanitizeTotpUri)
|
||||
confirmVerified(authRepository, scopedVaultSdkSource, vaultDiskSource)
|
||||
unmockkStatic(
|
||||
SyncResponseJson.Cipher::toEncryptedSdkCipher,
|
||||
EnvironmentUrlDataJson::toEnvironmentUrlsOrDefault,
|
||||
String::sanitizeTotpUri,
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
@Suppress("MaxLineLength")
|
||||
fun `syncAccounts with user 1 vault unlocked and all data present should send expected shared accounts data`() =
|
||||
runTest {
|
||||
val sharedAccounts = authenticatorBridgeRepository.getSharedAccounts()
|
||||
assertEquals(
|
||||
BOTH_ACCOUNT_SUCCESS,
|
||||
sharedAccounts,
|
||||
)
|
||||
verify { authRepository.userStateFlow }
|
||||
verify { vaultRepository.vaultUnlockDataStateFlow }
|
||||
coVerify { vaultDiskSource.getTotpCiphers(USER_1_ID) }
|
||||
coVerify { vaultDiskSource.getTotpCiphers(USER_2_ID) }
|
||||
verify { vaultRepository.isVaultUnlocked(USER_1_ID) }
|
||||
verify { vaultRepository.isVaultUnlocked(USER_2_ID) }
|
||||
coVerify {
|
||||
vaultRepository.unlockVaultWithDecryptedUserKey(
|
||||
userId = USER_2_ID,
|
||||
decryptedUserKey = USER_2_UNLOCK_KEY,
|
||||
)
|
||||
}
|
||||
verify { vaultRepository.lockVault(USER_2_ID, isUserInitiated = false) }
|
||||
coVerify { vaultSdkSource.decryptCipher(USER_1_ID, USER_1_ENCRYPTED_SDK_TOTP_CIPHER) }
|
||||
coVerify { vaultSdkSource.decryptCipher(USER_2_ID, USER_2_ENCRYPTED_SDK_TOTP_CIPHER) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `syncAccounts when userStateFlow is null should return an empty list`() = runTest {
|
||||
every { authRepository.userStateFlow } returns MutableStateFlow(null)
|
||||
fun `getSharedAccounts when userStateFlow is null should return an empty list`() = runTest {
|
||||
fakeAuthDiskSource.userState = null
|
||||
|
||||
val sharedData = authenticatorBridgeRepository.getSharedAccounts()
|
||||
|
||||
assertTrue(sharedData.accounts.isEmpty())
|
||||
verify { authRepository.userStateFlow }
|
||||
}
|
||||
|
||||
@Test
|
||||
@Suppress("MaxLineLength")
|
||||
fun `syncAccounts when there is no authenticator sync unlock key for user 1 should omit user 1 from list`() =
|
||||
fun `getSharedAccounts when there is no authenticator sync unlock key for user 1 should omit user 1 from list`() =
|
||||
runTest {
|
||||
fakeAuthDiskSource.storeAuthenticatorSyncUnlockKey(
|
||||
userId = USER_1_ID,
|
||||
@@ -175,138 +199,162 @@ class AuthenticatorBridgeRepositoryTest {
|
||||
authenticatorBridgeRepository.getSharedAccounts(),
|
||||
)
|
||||
|
||||
verify { authRepository.userStateFlow }
|
||||
verify { vaultRepository.isVaultUnlocked(USER_2_ID) }
|
||||
coVerify {
|
||||
vaultRepository.unlockVaultWithDecryptedUserKey(
|
||||
coVerify(exactly = 1) {
|
||||
scopedVaultSdkSource.initializeCrypto(
|
||||
userId = USER_2_ID,
|
||||
decryptedUserKey = USER_2_UNLOCK_KEY,
|
||||
request = InitUserCryptoRequest(
|
||||
userId = USER_2_ID,
|
||||
kdfParams = Kdf.Argon2id(iterations = 0U, memory = 0U, parallelism = 0U),
|
||||
email = USER_2_EMAIL,
|
||||
privateKey = USER_2_PRIVATE_KEY,
|
||||
method = InitUserCryptoMethod.DecryptedKey(
|
||||
decryptedUserKey = USER_2_UNLOCK_KEY,
|
||||
),
|
||||
signingKey = null,
|
||||
),
|
||||
)
|
||||
scopedVaultSdkSource.initializeOrganizationCrypto(
|
||||
userId = USER_2_ID,
|
||||
request = InitOrgCryptoRequest(organizationKeys = USER_2_ORG_KEYS),
|
||||
)
|
||||
vaultDiskSource.getTotpCiphers(userId = USER_2_ID)
|
||||
scopedVaultSdkSource.decryptCipher(
|
||||
userId = USER_2_ID,
|
||||
cipher = USER_2_ENCRYPTED_SDK_TOTP_CIPHER,
|
||||
)
|
||||
}
|
||||
verify { vaultRepository.vaultUnlockDataStateFlow }
|
||||
verify { vaultRepository.lockVault(USER_2_ID, isUserInitiated = false) }
|
||||
coVerify { vaultDiskSource.getTotpCiphers(USER_2_ID) }
|
||||
coVerify { vaultSdkSource.decryptCipher(USER_2_ID, USER_2_ENCRYPTED_SDK_TOTP_CIPHER) }
|
||||
verify(exactly = 1) {
|
||||
scopedVaultSdkSource.clearCrypto(userId = USER_2_ID)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@Suppress("MaxLineLength")
|
||||
fun `syncAccounts when vault is locked for both users should unlock and re-lock vault for both users and filter out deleted ciphers`() =
|
||||
fun `getSharedAccounts should unlock and re-lock vault for both users and filter out deleted ciphers`() =
|
||||
runTest {
|
||||
every { vaultRepository.isVaultUnlocked(USER_1_ID) } returns false
|
||||
coEvery {
|
||||
vaultRepository.unlockVaultWithDecryptedUserKey(USER_1_ID, USER_1_UNLOCK_KEY)
|
||||
} returns VaultUnlockResult.Success
|
||||
every { vaultRepository.lockVault(USER_1_ID, isUserInitiated = false) } returns Unit
|
||||
|
||||
val sharedAccounts = authenticatorBridgeRepository.getSharedAccounts()
|
||||
assertEquals(
|
||||
BOTH_ACCOUNT_SUCCESS,
|
||||
sharedAccounts,
|
||||
authenticatorBridgeRepository.getSharedAccounts(),
|
||||
)
|
||||
verify { vaultRepository.vaultUnlockDataStateFlow }
|
||||
coVerify { vaultDiskSource.getTotpCiphers(USER_1_ID) }
|
||||
verify { vaultRepository.isVaultUnlocked(USER_1_ID) }
|
||||
coVerify { vaultSdkSource.decryptCipher(USER_1_ID, USER_1_ENCRYPTED_SDK_TOTP_CIPHER) }
|
||||
verify { authRepository.userStateFlow }
|
||||
coVerify {
|
||||
vaultRepository.unlockVaultWithDecryptedUserKey(
|
||||
|
||||
coVerify(exactly = 1) {
|
||||
scopedVaultSdkSource.initializeCrypto(
|
||||
userId = USER_1_ID,
|
||||
decryptedUserKey = USER_1_UNLOCK_KEY,
|
||||
request = InitUserCryptoRequest(
|
||||
userId = USER_1_ID,
|
||||
kdfParams = Kdf.Argon2id(iterations = 0U, memory = 0U, parallelism = 0U),
|
||||
email = USER_1_EMAIL,
|
||||
privateKey = USER_1_PRIVATE_KEY,
|
||||
method = InitUserCryptoMethod.DecryptedKey(
|
||||
decryptedUserKey = USER_1_UNLOCK_KEY,
|
||||
),
|
||||
signingKey = null,
|
||||
),
|
||||
)
|
||||
}
|
||||
verify { vaultRepository.lockVault(USER_1_ID, isUserInitiated = false) }
|
||||
verify { vaultRepository.isVaultUnlocked(USER_2_ID) }
|
||||
coVerify {
|
||||
vaultRepository.unlockVaultWithDecryptedUserKey(
|
||||
scopedVaultSdkSource.initializeOrganizationCrypto(
|
||||
userId = USER_1_ID,
|
||||
request = InitOrgCryptoRequest(organizationKeys = USER_1_ORG_KEYS),
|
||||
)
|
||||
vaultDiskSource.getTotpCiphers(userId = USER_1_ID)
|
||||
scopedVaultSdkSource.decryptCipher(
|
||||
userId = USER_1_ID,
|
||||
cipher = USER_1_ENCRYPTED_SDK_TOTP_CIPHER,
|
||||
)
|
||||
scopedVaultSdkSource.initializeCrypto(
|
||||
userId = USER_2_ID,
|
||||
decryptedUserKey = USER_2_UNLOCK_KEY,
|
||||
request = InitUserCryptoRequest(
|
||||
userId = USER_2_ID,
|
||||
kdfParams = Kdf.Argon2id(iterations = 0U, memory = 0U, parallelism = 0U),
|
||||
email = USER_2_EMAIL,
|
||||
privateKey = USER_2_PRIVATE_KEY,
|
||||
method = InitUserCryptoMethod.DecryptedKey(
|
||||
decryptedUserKey = USER_2_UNLOCK_KEY,
|
||||
),
|
||||
signingKey = null,
|
||||
),
|
||||
)
|
||||
scopedVaultSdkSource.initializeOrganizationCrypto(
|
||||
userId = USER_2_ID,
|
||||
request = InitOrgCryptoRequest(organizationKeys = USER_2_ORG_KEYS),
|
||||
)
|
||||
vaultDiskSource.getTotpCiphers(userId = USER_2_ID)
|
||||
scopedVaultSdkSource.decryptCipher(
|
||||
userId = USER_2_ID,
|
||||
cipher = USER_2_ENCRYPTED_SDK_TOTP_CIPHER,
|
||||
)
|
||||
}
|
||||
verify { vaultRepository.vaultUnlockDataStateFlow }
|
||||
verify { vaultRepository.lockVault(USER_2_ID, isUserInitiated = false) }
|
||||
coVerify { vaultDiskSource.getTotpCiphers(USER_2_ID) }
|
||||
coVerify { vaultSdkSource.decryptCipher(USER_2_ID, USER_2_ENCRYPTED_SDK_TOTP_CIPHER) }
|
||||
verify(exactly = 1) {
|
||||
scopedVaultSdkSource.clearCrypto(userId = USER_1_ID)
|
||||
scopedVaultSdkSource.clearCrypto(userId = USER_2_ID)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@Suppress("MaxLineLength")
|
||||
fun `syncAccounts when for user 1 vault is locked and unlock fails should reset authenticator sync unlock key and omit user from the list`() =
|
||||
fun `getSharedAccounts when for user 1 vault fails to unlock should reset authenticator sync unlock key and omit user from the list`() =
|
||||
runTest {
|
||||
every { vaultRepository.isVaultUnlocked(USER_1_ID) } returns false
|
||||
val error = Throwable("Fail")
|
||||
coEvery {
|
||||
vaultRepository.unlockVaultWithDecryptedUserKey(USER_1_ID, USER_1_UNLOCK_KEY)
|
||||
} returns VaultUnlockResult.InvalidStateError(error = error)
|
||||
|
||||
val sharedAccounts = authenticatorBridgeRepository.getSharedAccounts()
|
||||
assertEquals(SharedAccountData(listOf(USER_2_SHARED_ACCOUNT)), sharedAccounts)
|
||||
assertNull(fakeAuthDiskSource.getAuthenticatorSyncUnlockKey(USER_1_ID))
|
||||
verify { vaultRepository.vaultUnlockDataStateFlow }
|
||||
verify { vaultRepository.isVaultUnlocked(USER_1_ID) }
|
||||
verify { authRepository.userStateFlow }
|
||||
coVerify {
|
||||
vaultRepository.unlockVaultWithDecryptedUserKey(
|
||||
scopedVaultSdkSource.initializeCrypto(
|
||||
userId = USER_1_ID,
|
||||
decryptedUserKey = USER_1_UNLOCK_KEY,
|
||||
request = InitUserCryptoRequest(
|
||||
userId = USER_1_ID,
|
||||
kdfParams = Kdf.Argon2id(iterations = 0U, memory = 0U, parallelism = 0U),
|
||||
email = USER_1_EMAIL,
|
||||
privateKey = USER_1_PRIVATE_KEY,
|
||||
method = InitUserCryptoMethod.DecryptedKey(
|
||||
decryptedUserKey = USER_1_UNLOCK_KEY,
|
||||
),
|
||||
signingKey = null,
|
||||
),
|
||||
)
|
||||
}
|
||||
verify { vaultRepository.isVaultUnlocked(USER_2_ID) }
|
||||
coVerify {
|
||||
vaultRepository.unlockVaultWithDecryptedUserKey(
|
||||
userId = USER_2_ID,
|
||||
decryptedUserKey = USER_2_UNLOCK_KEY,
|
||||
)
|
||||
}
|
||||
verify { vaultRepository.vaultUnlockDataStateFlow }
|
||||
verify { vaultRepository.lockVault(USER_2_ID, isUserInitiated = false) }
|
||||
coVerify { vaultDiskSource.getTotpCiphers(USER_2_ID) }
|
||||
coVerify { vaultSdkSource.decryptCipher(USER_2_ID, USER_2_ENCRYPTED_SDK_TOTP_CIPHER) }
|
||||
}
|
||||
|
||||
@Test
|
||||
@Suppress("MaxLineLength")
|
||||
fun `syncAccounts when when the vault repository never leaves unlocking state should never callback`() =
|
||||
runTest {
|
||||
val vaultUnlockStateFlow = MutableStateFlow(
|
||||
listOf(
|
||||
VaultUnlockData(USER_1_ID, VaultUnlockData.Status.UNLOCKING),
|
||||
VaultUnlockData(USER_2_ID, VaultUnlockData.Status.UNLOCKED),
|
||||
),
|
||||
)
|
||||
every { vaultRepository.vaultUnlockDataStateFlow } returns vaultUnlockStateFlow
|
||||
val deferred = async {
|
||||
val sharedAccounts = authenticatorBridgeRepository.getSharedAccounts()
|
||||
assertEquals(BOTH_ACCOUNT_SUCCESS, sharedAccounts)
|
||||
}
|
||||
|
||||
// None of these calls should happen until after user 1's vault state is not UNLOCKING:
|
||||
verify(exactly = 0) { vaultRepository.isVaultUnlocked(userId = USER_1_ID) }
|
||||
coVerify(exactly = 0) { vaultDiskSource.getTotpCiphers(USER_1_ID) }
|
||||
|
||||
// Then move out of UNLOCKING state, and things should proceed as normal:
|
||||
vaultUnlockStateFlow.value = listOf(
|
||||
VaultUnlockData(USER_1_ID, VaultUnlockData.Status.UNLOCKED),
|
||||
VaultUnlockData(USER_2_ID, VaultUnlockData.Status.UNLOCKED),
|
||||
} returns InitializeCryptoResult.AuthenticationError(error = Throwable()).asSuccess()
|
||||
assertEquals(
|
||||
SharedAccountData(listOf(USER_2_SHARED_ACCOUNT)),
|
||||
authenticatorBridgeRepository.getSharedAccounts(),
|
||||
)
|
||||
|
||||
deferred.await()
|
||||
|
||||
verify { authRepository.userStateFlow }
|
||||
coVerify { vaultDiskSource.getTotpCiphers(USER_1_ID) }
|
||||
coVerify { vaultDiskSource.getTotpCiphers(USER_2_ID) }
|
||||
verify { vaultRepository.isVaultUnlocked(USER_1_ID) }
|
||||
verify { vaultRepository.isVaultUnlocked(USER_2_ID) }
|
||||
verify { vaultRepository.vaultUnlockDataStateFlow }
|
||||
coVerify {
|
||||
vaultRepository.unlockVaultWithDecryptedUserKey(
|
||||
assertNull(fakeAuthDiskSource.getAuthenticatorSyncUnlockKey(USER_1_ID))
|
||||
coVerify(exactly = 1) {
|
||||
scopedVaultSdkSource.initializeCrypto(
|
||||
userId = USER_1_ID,
|
||||
request = InitUserCryptoRequest(
|
||||
userId = USER_1_ID,
|
||||
kdfParams = Kdf.Argon2id(iterations = 0U, memory = 0U, parallelism = 0U),
|
||||
email = USER_1_EMAIL,
|
||||
privateKey = USER_1_PRIVATE_KEY,
|
||||
method = InitUserCryptoMethod.DecryptedKey(
|
||||
decryptedUserKey = USER_1_UNLOCK_KEY,
|
||||
),
|
||||
signingKey = null,
|
||||
),
|
||||
)
|
||||
scopedVaultSdkSource.initializeCrypto(
|
||||
userId = USER_2_ID,
|
||||
decryptedUserKey = USER_2_UNLOCK_KEY,
|
||||
request = InitUserCryptoRequest(
|
||||
userId = USER_2_ID,
|
||||
kdfParams = Kdf.Argon2id(iterations = 0U, memory = 0U, parallelism = 0U),
|
||||
email = USER_2_EMAIL,
|
||||
privateKey = USER_2_PRIVATE_KEY,
|
||||
method = InitUserCryptoMethod.DecryptedKey(
|
||||
decryptedUserKey = USER_2_UNLOCK_KEY,
|
||||
),
|
||||
signingKey = null,
|
||||
),
|
||||
)
|
||||
scopedVaultSdkSource.initializeOrganizationCrypto(
|
||||
userId = USER_2_ID,
|
||||
request = InitOrgCryptoRequest(organizationKeys = USER_2_ORG_KEYS),
|
||||
)
|
||||
vaultDiskSource.getTotpCiphers(userId = USER_2_ID)
|
||||
scopedVaultSdkSource.decryptCipher(
|
||||
userId = USER_2_ID,
|
||||
cipher = USER_2_ENCRYPTED_SDK_TOTP_CIPHER,
|
||||
)
|
||||
}
|
||||
verify { vaultRepository.lockVault(USER_2_ID, isUserInitiated = false) }
|
||||
coVerify { vaultSdkSource.decryptCipher(USER_1_ID, USER_1_ENCRYPTED_SDK_TOTP_CIPHER) }
|
||||
coVerify { vaultSdkSource.decryptCipher(USER_2_ID, USER_2_ENCRYPTED_SDK_TOTP_CIPHER) }
|
||||
verify(exactly = 1) {
|
||||
scopedVaultSdkSource.clearCrypto(userId = USER_1_ID)
|
||||
scopedVaultSdkSource.clearCrypto(userId = USER_2_ID)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -362,20 +410,79 @@ private val SYMMETRIC_KEY = generateSecretKey()
|
||||
private const val USER_1_ID = "user1Id"
|
||||
private const val USER_2_ID = "user2Id"
|
||||
|
||||
private const val USER_1_EMAIL = "john@doe.com"
|
||||
private const val USER_2_EMAIL = "jane@doe.com"
|
||||
|
||||
private const val USER_1_PRIVATE_KEY = "user1PrivateKey"
|
||||
private const val USER_2_PRIVATE_KEY = "user2PrivateKey"
|
||||
|
||||
private const val USER_1_UNLOCK_KEY = "user1UnlockKey"
|
||||
private const val USER_2_UNLOCK_KEY = "user2UnlockKey"
|
||||
|
||||
private val USER_1_ORG_KEYS = mapOf("test_1" to "test_1_data")
|
||||
private val USER_2_ORG_KEYS = mapOf("test_2" to "test_2_data")
|
||||
|
||||
private val ACCOUNT_JSON_1 = AccountJson(
|
||||
profile = mockk {
|
||||
every { userId } returns USER_1_ID
|
||||
every { name } returns "John Doe"
|
||||
every { email } returns USER_1_EMAIL
|
||||
every { kdfType } returns KdfTypeJson.ARGON2_ID
|
||||
every { kdfIterations } returns 0
|
||||
every { kdfMemory } returns 0
|
||||
every { kdfParallelism } returns 0
|
||||
},
|
||||
tokens = AccountTokensJson(
|
||||
accessToken = "accessToken1",
|
||||
refreshToken = "refreshToken1",
|
||||
),
|
||||
settings = AccountJson.Settings(
|
||||
environmentUrlData = EnvironmentUrlDataJson(
|
||||
base = "https://vault.bitwarden.com",
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
private val ACCOUNT_JSON_2 = AccountJson(
|
||||
profile = mockk {
|
||||
every { userId } returns USER_2_ID
|
||||
every { name } returns "Jane Doe"
|
||||
every { email } returns USER_2_EMAIL
|
||||
every { kdfType } returns KdfTypeJson.ARGON2_ID
|
||||
every { kdfIterations } returns 0
|
||||
every { kdfMemory } returns 0
|
||||
every { kdfParallelism } returns 0
|
||||
},
|
||||
tokens = AccountTokensJson(
|
||||
accessToken = "accessToken2",
|
||||
refreshToken = "refreshToken2",
|
||||
),
|
||||
settings = AccountJson.Settings(
|
||||
environmentUrlData = EnvironmentUrlDataJson(
|
||||
base = "https://vault.bitwarden.com",
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
private val USER_STATE_JSON = UserStateJson(
|
||||
activeUserId = USER_1_ID,
|
||||
accounts = mapOf(
|
||||
USER_1_ID to ACCOUNT_JSON_1,
|
||||
USER_2_ID to ACCOUNT_JSON_2,
|
||||
),
|
||||
)
|
||||
|
||||
private val ACCOUNT_1 = mockk<UserState.Account> {
|
||||
every { userId } returns USER_1_ID
|
||||
every { name } returns "John Doe"
|
||||
every { email } returns "john@doe.com"
|
||||
every { email } returns USER_1_EMAIL
|
||||
every { environment.label } returns "bitwarden.com"
|
||||
}
|
||||
|
||||
private val ACCOUNT_2 = mockk<UserState.Account> {
|
||||
every { userId } returns USER_2_ID
|
||||
every { name } returns "Jane Doe"
|
||||
every { email } returns "Jane@doe.com"
|
||||
every { email } returns USER_2_EMAIL
|
||||
every { environment.label } returns "bitwarden.com"
|
||||
}
|
||||
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
package com.bitwarden.ui.platform.util
|
||||
|
||||
import android.os.Build
|
||||
import android.os.Parcel
|
||||
import android.os.Parcelable
|
||||
import android.util.Base64
|
||||
import androidx.core.os.ParcelCompat
|
||||
import com.bitwarden.annotation.OmitFromCoverage
|
||||
import kotlinx.serialization.ExperimentalSerializationApi
|
||||
import kotlinx.serialization.InternalSerializationApi
|
||||
@@ -105,7 +105,7 @@ open class ParcelableRouteSerializer<T : Parcelable>(
|
||||
}
|
||||
}
|
||||
encodedString
|
||||
?.toParcelable<T>()
|
||||
?.toParcelable()
|
||||
?: throw IllegalStateException("Invalid decoding for ${kClass.qualifiedName}.")
|
||||
}
|
||||
|
||||
@@ -137,15 +137,11 @@ open class ParcelableRouteSerializer<T : Parcelable>(
|
||||
}
|
||||
val value = try {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
parcel.readParcelable(
|
||||
ParcelableRouteSerializer::class.java.classLoader,
|
||||
kClass.java,
|
||||
)
|
||||
} else {
|
||||
@Suppress("DEPRECATION")
|
||||
parcel.readParcelable(ParcelableRouteSerializer::class.java.classLoader)
|
||||
} as T?
|
||||
ParcelCompat.readParcelable(
|
||||
parcel,
|
||||
ParcelableRouteSerializer::class.java.classLoader,
|
||||
kClass.java,
|
||||
) as T?
|
||||
} catch (_: IllegalArgumentException) {
|
||||
null
|
||||
} catch (_: IllegalStateException) {
|
||||
|
||||
Reference in New Issue
Block a user