Compare commits

...

3 Commits

Author SHA1 Message Date
David Perez
1d624606f1 PM-24182: Fix crash in Android 13 (#5594) 2025-07-25 16:53:35 -05:00
Patrick Honkonen
e1d921e013 🍒 [BRE-831] migrate secrets akv (#5347) (#5595)
Co-authored-by: Andy Pixley <3723676+pixman20@users.noreply.github.com>
2025-07-25 17:22:58 -04:00
David Perez
a721744a6b 🍒 PM-23666: Construct unique SDK client for Authentocator Sync feature (#5528) 2025-07-14 16:39:56 -05:00
14 changed files with 632 additions and 290 deletions

View File

@@ -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' }}

View File

@@ -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

View File

@@ -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

View File

@@ -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:

View File

@@ -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)

View File

@@ -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 }}

View File

@@ -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 }}

View File

@@ -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() },
)
}
}

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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(

View File

@@ -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"
}

View File

@@ -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) {