Compare commits

..

6 Commits

Author SHA1 Message Date
André Bispo
947eec9348 [PM-11304] Lint
(cherry picked from commit a0e3ae0cc2)
2024-11-13 11:28:25 +00:00
André Bispo
b8b9cf2950 [PM-11304] lint 2024-11-13 11:27:45 +00:00
André Bispo
3954f53af8 [PM-11304] Update show add cipher fab button logic
(cherry picked from commit 3dc76578d3)
2024-11-13 11:24:41 +00:00
André Bispo
4a22627dba [PM-11304] Select owner and collection if creating cipher from an org folder
(cherry picked from commit 9213ad2882)
2024-11-13 11:24:31 +00:00
André Bispo
1a188cc73a [PM-11304] Enable fab for collection vault listings
(cherry picked from commit 96f118e1e4)
2024-11-13 11:24:21 +00:00
André Bispo
d0d212791d [PM-11304] Add collection Id to navigation params 2024-11-13 11:24:00 +00:00
765 changed files with 18246 additions and 48014 deletions

4
.github/CODEOWNERS vendored
View File

@@ -5,10 +5,10 @@
# https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/about-code-owners
# Default file owners.
* @bitwarden/team-android @brian-livefront @david-livefront @dseverns-livefront @ahaisting-livefront @phil-livefront
* @bitwarden/team-android @brian-livefront @david-livefront @dseverns-livefront @ahaisting-livefront
# Actions and workflow changes.
.github/ @bitwarden/dept-development-mobile
.github/workflows @bitwarden/dept-development-mobile
# Auth
# app/src/main/java/com/x8bit/bitwarden/data/auth @bitwarden/team-auth-dev

View File

@@ -15,5 +15,3 @@ contact_links:
- name: Security Issues
url: https://hackerone.com/bitwarden
about: We use HackerOne to manage security disclosures.
- name: Report mobile autofill failure
url: https://docs.google.com/forms/d/e/1FAIpQLScMopHyN7KGJs8hW562VTzbIGL4KcFnx0wJcsW0GYE1BnPiGA/viewform

View File

@@ -1,13 +0,0 @@
name: Build Authenticator
on:
workflow_dispatch:
jobs:
placeholder:
name: Placeholder Job
runs-on: ubuntu-24.04
steps:
- name: Placeholder Step
run: echo "placeholder workflow"

View File

@@ -40,10 +40,10 @@ jobs:
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Validate Gradle wrapper
uses: gradle/actions/wrapper-validation@0bdd871935719febd78681f197cd39af5b6e16a6 # v4.2.2
uses: gradle/actions/wrapper-validation@d156388eb19639ec20ade50009f3d199ce1e2808 # v4.1.0
- name: Cache Gradle files
uses: actions/cache@1bd1e32a3bdc45362d1e726936510720a7c30a57 # v4.2.0
uses: actions/cache@6849a6489940f00c2f30c0fb92c6274307ccb58a # v4.1.2
with:
path: |
~/.gradle/caches
@@ -53,7 +53,7 @@ jobs:
${{ runner.os }}-gradle-v2-
- name: Cache build output
uses: actions/cache@1bd1e32a3bdc45362d1e726936510720a7c30a57 # v4.2.0
uses: actions/cache@6849a6489940f00c2f30c0fb92c6274307ccb58a # v4.1.2
with:
path: |
${{ github.workspace }}/build-cache
@@ -62,13 +62,13 @@ jobs:
${{ runner.os }}-build-
- name: Configure JDK
uses: actions/setup-java@7a6d8a8234af8eb26422e24e3006232cccaa061b # v4.6.0
uses: actions/setup-java@8df1039502a15bceb9433410b1a100fbe190c53b # v4.5.0
with:
distribution: "temurin"
java-version: ${{ env.JAVA_VERSION }}
- name: Configure Ruby
uses: ruby/setup-ruby@28c4deda893d5a96a6b2d958c5b47fc18d65c9d3 # v1.213.0
uses: ruby/setup-ruby@a2bbe5b1b236842c1cb7dd11e8e3b51e0a616acc # v1.202.0
with:
bundler-cache: true
@@ -85,7 +85,7 @@ jobs:
run: bundle exec fastlane assembleDebugApks
- name: Upload test reports on failure
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
if: failure()
with:
name: test-reports
@@ -106,7 +106,7 @@ jobs:
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Configure Ruby
uses: ruby/setup-ruby@28c4deda893d5a96a6b2d958c5b47fc18d65c9d3 # v1.213.0
uses: ruby/setup-ruby@a2bbe5b1b236842c1cb7dd11e8e3b51e0a616acc # v1.202.0
with:
bundler-cache: true
@@ -157,10 +157,10 @@ jobs:
--name app_play_prod_firebase-creds.json --file ${{ github.workspace }}/secrets/app_play_prod_firebase-creds.json --output none
- name: Validate Gradle wrapper
uses: gradle/actions/wrapper-validation@0bdd871935719febd78681f197cd39af5b6e16a6 # v4.2.2
uses: gradle/actions/wrapper-validation@d156388eb19639ec20ade50009f3d199ce1e2808 # v4.1.0
- name: Cache Gradle files
uses: actions/cache@1bd1e32a3bdc45362d1e726936510720a7c30a57 # v4.2.0
uses: actions/cache@6849a6489940f00c2f30c0fb92c6274307ccb58a # v4.1.2
with:
path: |
~/.gradle/caches
@@ -170,7 +170,7 @@ jobs:
${{ runner.os }}-gradle-v2-
- name: Cache build output
uses: actions/cache@1bd1e32a3bdc45362d1e726936510720a7c30a57 # v4.2.0
uses: actions/cache@6849a6489940f00c2f30c0fb92c6274307ccb58a # v4.1.2
with:
path: |
${{ github.workspace }}/build-cache
@@ -179,20 +179,11 @@ jobs:
${{ runner.os }}-build-
- name: Configure JDK
uses: actions/setup-java@7a6d8a8234af8eb26422e24e3006232cccaa061b # v4.6.0
uses: actions/setup-java@8df1039502a15bceb9433410b1a100fbe190c53b # v4.5.0
with:
distribution: "temurin"
java-version: ${{ env.JAVA_VERSION }}
- name: Update app CI Build info
run: |
./scripts/update_app_ci_build_info.sh \
$GITHUB_REPOSITORY \
$GITHUB_REF_NAME \
$GITHUB_SHA \
$GITHUB_RUN_ID \
$GITHUB_RUN_ATTEMPT
- name: Increment version
run: |
DEFAULT_VERSION_CODE=$((11000+$GITHUB_RUN_NUMBER))
@@ -253,78 +244,78 @@ jobs:
- name: Upload release Play Store .aab artifact
if: ${{ (matrix.variant == 'prod') && (matrix.artifact == 'aab') }}
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
with:
name: com.x8bit.bitwarden.aab
path: app/build/outputs/bundle/standardRelease/com.x8bit.bitwarden.aab
path: app/build/outputs/bundle/standardRelease/com.x8bit.bitwarden-standard-release.aab
if-no-files-found: error
- name: Upload beta Play Store .aab artifact
if: ${{ (matrix.variant == 'prod') && (matrix.artifact == 'aab') }}
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
with:
name: com.x8bit.bitwarden.beta.aab
path: app/build/outputs/bundle/standardBeta/com.x8bit.bitwarden.beta.aab
path: app/build/outputs/bundle/standardBeta/com.x8bit.bitwarden-standard-beta.aab
if-no-files-found: error
- name: Upload release .apk artifact
if: ${{ (matrix.variant == 'prod') && (matrix.artifact == 'apk') }}
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
with:
name: com.x8bit.bitwarden.apk
path: app/build/outputs/apk/standard/release/com.x8bit.bitwarden.apk
path: app/build/outputs/apk/standard/release/com.x8bit.bitwarden-standard-release.apk
if-no-files-found: error
- name: Upload beta .apk artifact
if: ${{ (matrix.variant == 'prod') && (matrix.artifact == 'apk') }}
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
with:
name: com.x8bit.bitwarden.beta.apk
path: app/build/outputs/apk/standard/beta/com.x8bit.bitwarden.beta.apk
path: app/build/outputs/apk/standard/beta/com.x8bit.bitwarden-standard-beta.apk
if-no-files-found: error
# When building variants other than 'prod'
- name: Upload debug .apk artifact
if: ${{ (matrix.variant != 'prod') && (matrix.artifact == 'apk') }}
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
with:
name: com.x8bit.bitwarden.${{ matrix.variant }}.apk
path: app/build/outputs/apk/standard/debug/com.x8bit.bitwarden.dev.apk
path: app/build/outputs/apk/standard/debug/com.x8bit.bitwarden-standard-debug.apk
if-no-files-found: error
- name: Create checksum for release .apk artifact
if: ${{ (matrix.variant == 'prod') && (matrix.artifact == 'apk') }}
run: |
sha256sum "app/build/outputs/apk/standard/release/com.x8bit.bitwarden.apk" \
sha256sum "app/build/outputs/apk/standard/release/com.x8bit.bitwarden-standard-release.apk" \
> ./com.x8bit.bitwarden.apk-sha256.txt
- name: Create checksum for beta .apk artifact
if: ${{ (matrix.variant == 'prod') && (matrix.artifact == 'apk') }}
run: |
sha256sum "app/build/outputs/apk/standard/beta/com.x8bit.bitwarden.beta.apk" \
sha256sum "app/build/outputs/apk/standard/beta/com.x8bit.bitwarden-standard-beta.apk" \
> ./com.x8bit.bitwarden.beta.apk-sha256.txt
- name: Create checksum for release .aab artifact
if: ${{ (matrix.variant == 'prod') && (matrix.artifact == 'aab') }}
run: |
sha256sum "app/build/outputs/bundle/standardRelease/com.x8bit.bitwarden.aab" \
sha256sum "app/build/outputs/bundle/standardRelease/com.x8bit.bitwarden-standard-release.aab" \
> ./com.x8bit.bitwarden.aab-sha256.txt
- name: Create checksum for beta .aab artifact
if: ${{ (matrix.variant == 'prod') && (matrix.artifact == 'aab') }}
run: |
sha256sum "app/build/outputs/bundle/standardBeta/com.x8bit.bitwarden.beta.aab" \
sha256sum "app/build/outputs/bundle/standardBeta/com.x8bit.bitwarden-standard-beta.aab" \
> ./com.x8bit.bitwarden.beta.aab-sha256.txt
- name: Create checksum for Debug .apk artifact
if: ${{ (matrix.variant != 'prod') && (matrix.artifact == 'apk') }}
run: |
sha256sum "app/build/outputs/apk/standard/debug/com.x8bit.bitwarden.dev.apk" \
sha256sum "app/build/outputs/apk/standard/debug/com.x8bit.bitwarden-standard-debug.apk" \
> ./com.x8bit.bitwarden.${{ matrix.variant }}.apk-sha256.txt
- name: Upload .apk SHA file for release
if: ${{ (matrix.variant == 'prod') && (matrix.artifact == 'apk') }}
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
with:
name: com.x8bit.bitwarden.apk-sha256.txt
path: ./com.x8bit.bitwarden.apk-sha256.txt
@@ -332,7 +323,7 @@ jobs:
- name: Upload .apk SHA file for beta
if: ${{ (matrix.variant == 'prod') && (matrix.artifact == 'apk') }}
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
with:
name: com.x8bit.bitwarden.beta.apk-sha256.txt
path: ./com.x8bit.bitwarden.beta.apk-sha256.txt
@@ -340,7 +331,7 @@ jobs:
- name: Upload .aab SHA file for release
if: ${{ (matrix.variant == 'prod') && (matrix.artifact == 'aab') }}
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
with:
name: com.x8bit.bitwarden.aab-sha256.txt
path: ./com.x8bit.bitwarden.aab-sha256.txt
@@ -348,7 +339,7 @@ jobs:
- name: Upload .aab SHA file for beta
if: ${{ (matrix.variant == 'prod') && (matrix.artifact == 'aab') }}
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
with:
name: com.x8bit.bitwarden.beta.aab-sha256.txt
path: ./com.x8bit.bitwarden.beta.aab-sha256.txt
@@ -356,7 +347,7 @@ jobs:
- name: Upload .apk SHA file for debug
if: ${{ (matrix.variant != 'prod') && (matrix.artifact == 'apk') }}
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
with:
name: com.x8bit.bitwarden.${{ matrix.variant }}.apk-sha256.txt
path: ./com.x8bit.bitwarden.${{ matrix.variant }}.apk-sha256.txt
@@ -405,7 +396,7 @@ jobs:
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Configure Ruby
uses: ruby/setup-ruby@28c4deda893d5a96a6b2d958c5b47fc18d65c9d3 # v1.213.0
uses: ruby/setup-ruby@a2bbe5b1b236842c1cb7dd11e8e3b51e0a616acc # v1.202.0
with:
bundler-cache: true
@@ -442,10 +433,10 @@ jobs:
--name app_fdroid_firebase-creds.json --file ${{ github.workspace }}/secrets/app_fdroid_firebase-creds.json --output none
- name: Validate Gradle wrapper
uses: gradle/actions/wrapper-validation@0bdd871935719febd78681f197cd39af5b6e16a6 # v4.2.2
uses: gradle/actions/wrapper-validation@d156388eb19639ec20ade50009f3d199ce1e2808 # v4.1.0
- name: Cache Gradle files
uses: actions/cache@1bd1e32a3bdc45362d1e726936510720a7c30a57 # v4.2.0
uses: actions/cache@6849a6489940f00c2f30c0fb92c6274307ccb58a # v4.1.2
with:
path: |
~/.gradle/caches
@@ -455,7 +446,7 @@ jobs:
${{ runner.os }}-gradle-v2-
- name: Cache build output
uses: actions/cache@1bd1e32a3bdc45362d1e726936510720a7c30a57 # v4.2.0
uses: actions/cache@6849a6489940f00c2f30c0fb92c6274307ccb58a # v4.1.2
with:
path: |
${{ github.workspace }}/build-cache
@@ -464,35 +455,19 @@ jobs:
${{ runner.os }}-build-
- name: Configure JDK
uses: actions/setup-java@7a6d8a8234af8eb26422e24e3006232cccaa061b # v4.6.0
uses: actions/setup-java@8df1039502a15bceb9433410b1a100fbe190c53b # v4.5.0
with:
distribution: "temurin"
java-version: ${{ env.JAVA_VERSION }}
- name: Update app CI Build info
run: |
./scripts/update_app_ci_build_info.sh \
$GITHUB_REPOSITORY \
$GITHUB_REF_NAME \
$GITHUB_SHA \
$GITHUB_RUN_ID \
$GITHUB_RUN_ATTEMPT
# Start from 11000 to prevent collisions with mobile build version codes
- name: Increment version
run: |
DEFAULT_VERSION_CODE=$((11000+$GITHUB_RUN_NUMBER))
VERSION_CODE="${{ inputs.version-code || '$DEFAULT_VERSION_CODE' }}"
bundle exec fastlane setBuildVersionInfo \
versionCode:$VERSION_CODE \
versionCode:${{ inputs.version-code || '$DEFAULT_VERSION_CODE' }} \
versionName:${{ inputs.version-name || '' }}
regex='versionName = "([^"]+)"'
if [[ "$(cat app/build.gradle.kts)" =~ $regex ]]; then
VERSION_NAME="${BASH_REMATCH[1]}"
fi
echo "Version Name: ${VERSION_NAME}" >> $GITHUB_STEP_SUMMARY
echo "Version Number: $VERSION_CODE" >> $GITHUB_STEP_SUMMARY
- name: Generate F-Droid artifacts
env:
FDROID_STORE_PASSWORD: ${{ secrets.FDROID_KEYSTORE_PASSWORD }}
@@ -515,49 +490,49 @@ jobs:
keyPassword:"${{ env.FDROID_BETA_KEY_PASSWORD }}"
- name: Upload F-Droid .apk artifact
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
with:
name: com.x8bit.bitwarden-fdroid.apk
path: app/build/outputs/apk/fdroid/release/com.x8bit.bitwarden-fdroid.apk
path: app/build/outputs/apk/fdroid/release/com.x8bit.bitwarden-fdroid-release.apk
if-no-files-found: error
- name: Create checksum for F-Droid artifact
run: |
sha256sum "app/build/outputs/apk/fdroid/release/com.x8bit.bitwarden-fdroid.apk" \
sha256sum "app/build/outputs/apk/fdroid/release/com.x8bit.bitwarden-fdroid-release.apk" \
> ./com.x8bit.bitwarden-fdroid.apk-sha256.txt
- name: Upload F-Droid SHA file
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
with:
name: com.x8bit.bitwarden-fdroid.apk-sha256.txt
path: ./com.x8bit.bitwarden-fdroid.apk-sha256.txt
if-no-files-found: error
- name: Upload F-Droid Beta .apk artifact
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
with:
name: com.x8bit.bitwarden.beta-fdroid.apk
path: app/build/outputs/apk/fdroid/beta/com.x8bit.bitwarden.beta-fdroid.apk
path: app/build/outputs/apk/fdroid/beta/com.x8bit.bitwarden-fdroid-beta.apk
if-no-files-found: error
- name: Create checksum for F-Droid Beta artifact
run: |
sha256sum "app/build/outputs/apk/fdroid/beta/com.x8bit.bitwarden.beta-fdroid.apk" \
sha256sum "app/build/outputs/apk/fdroid/beta/com.x8bit.bitwarden-fdroid-beta.apk" \
> ./com.x8bit.bitwarden.beta-fdroid.apk-sha256.txt
- name: Upload F-Droid Beta SHA file
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
with:
name: com.x8bit.bitwarden.beta-fdroid.apk-sha256.txt
path: ./com.x8bit.bitwarden.beta-fdroid.apk-sha256.txt
if-no-files-found: error
- name: Install Firebase app distribution plugin
if: ${{ inputs.distribute-to-firebase || github.event_name == 'push' }}
if: ${{ inputs.distribute_to_firebase || github.event_name == 'push' }}
run: bundle exec fastlane add_plugin firebase_app_distribution
- name: Publish release F-Droid artifacts to Firebase
if: ${{ inputs.distribute-to-firebase || github.event_name == 'push' }}
if: ${{ inputs.distribute_to_firebase || github.event_name == 'push' }}
env:
APP_FDROID_FIREBASE_CREDS_PATH: ${{ github.workspace }}/secrets/app_fdroid_firebase-creds.json
run: |

View File

@@ -1,13 +0,0 @@
name: Crowdin Sync - Authenticator
on:
workflow_dispatch:
jobs:
placeholder:
name: Placeholder Job
runs-on: ubuntu-24.04
steps:
- name: Placeholder Step
run: echo "placeholder workflow"

View File

@@ -2,7 +2,7 @@ name: Crowdin Sync
on:
workflow_dispatch:
inputs: {}
inputs: { }
schedule:
- cron: '0 0 * * 5'
@@ -28,17 +28,10 @@ jobs:
keyvault: "bitwarden-ci"
secrets: "crowdin-api-token, github-gpg-private-key, github-gpg-private-key-passphrase"
- name: Generate GH App token
uses: actions/create-github-app-token@c1a285145b9d317df6ced56c09f525b5c2b6f755 # v1.11.1
id: app-token
with:
app-id: ${{ secrets.BW_GHAPP_ID }}
private-key: ${{ secrets.BW_GHAPP_KEY }}
- name: Download translations
uses: crowdin/github-action@d1632879d4d4da358f2d040f79fa094571c9a649 # v2.5.1
uses: crowdin/github-action@2d540f18b0a416b1fbf2ee5be35841bd380fc1da # v2.3.0
env:
GITHUB_TOKEN: ${{ steps.app-token.outputs.token }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
CROWDIN_API_TOKEN: ${{ steps.retrieve-secrets.outputs.crowdin-api-token }}
with:
config: crowdin.yml

View File

@@ -1,13 +0,0 @@
name: Crowdin Push - Authenticator
on:
workflow_dispatch:
jobs:
placeholder:
name: Placeholder Job
runs-on: ubuntu-24.04
steps:
- name: Placeholder Step
run: echo "placeholder workflow"

View File

@@ -23,13 +23,13 @@ jobs:
- name: Retrieve secrets
id: retrieve-secrets
uses: bitwarden/gh-actions/get-keyvault-secrets@main
uses: bitwarden/gh-actions/get-keyvault-secrets@2bd1450c2cdb2a8ac886232b8589696f22794229 # v0.2.0
with:
keyvault: "bitwarden-ci"
secrets: "crowdin-api-token"
- name: Upload sources
uses: crowdin/github-action@d1632879d4d4da358f2d040f79fa094571c9a649 # v2.5.1
uses: crowdin/github-action@2d540f18b0a416b1fbf2ee5be35841bd380fc1da # v2.3.0
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
CROWDIN_API_TOKEN: ${{ steps.retrieve-secrets.outputs.crowdin-api-token }}

View File

@@ -1,129 +0,0 @@
name: Create GitHub Release
on:
workflow_dispatch:
inputs:
version-name:
description: 'Version Name - E.g. "2024.11.1"'
required: true
type: string
version-number:
description: 'Version Number - E.g. "123456"'
required: true
type: string
artifact-run-id:
description: 'GitHub Action Run ID containing artifacts'
required: true
type: string
draft:
description: 'Create as draft release'
type: boolean
default: true
prerelease:
description: 'Mark as pre-release'
type: boolean
default: true
make-latest:
description: 'Set as the latest release'
type: boolean
branch-protection-type:
description: 'Branch protection type'
type: choice
options:
- Branch Name
- GitHub API
default: Branch Name
env:
ARTIFACTS_PATH: artifacts
jobs:
create-release:
name: Create GitHub Release
runs-on: ubuntu-24.04
permissions:
contents: write
actions: read
steps:
- name: Check out repository
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
fetch-depth: 0
- name: Get branch from workflow run
id: get_release_branch
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
ARTIFACT_RUN_ID: ${{ inputs.artifact-run-id }}
BRANCH_PROTECTION_TYPE: ${{ inputs.branch-protection-type }}
run: |
release_branch=$(gh run view $ARTIFACT_RUN_ID --json headBranch -q .headBranch)
case "$BRANCH_PROTECTION_TYPE" in
"Branch Name")
if [[ "$release_branch" != "main" && ! "$release_branch" =~ ^release/ ]]; then
echo "::error::Branch '$release_branch' is not 'main' or a release branch starting with 'release/'. Releases must be created from protected branches."
exit 1
fi
;;
"GitHub API")
#NOTE requires token with "administration:read" scope
if ! gh api "repos/${{ github.repository }}/branches/$release_branch/protection" | grep -q "required_status_checks"; then
echo "::error::Branch '$release_branch' is not protected. Releases must be created from protected branches. If that's not correct, confirm if the github token user has the 'administration:read' scope."
exit 1
fi
;;
*)
echo "::error::Unsupported branch protection type: $BRANCH_PROTECTION_TYPE"
exit 1
;;
esac
echo "release_branch=$release_branch" >> $GITHUB_OUTPUT
- name: Download artifacts
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
ARTIFACT_RUN_ID: ${{ inputs.artifact-run-id }}
run: |
gh run download $ARTIFACT_RUN_ID -D $ARTIFACTS_PATH
file_count=$(find $ARTIFACTS_PATH -type f | wc -l)
echo "Downloaded $file_count file(s)."
if [ "$file_count" -gt 0 ]; then
echo "Downloaded files:"
find $ARTIFACTS_PATH -type f
fi
- name: Create Release
id: create_release
uses: softprops/action-gh-release@c95fe1489396fe8a9eb87c0abf8aa5b2ef267fda # v2.2.1
with:
tag_name: "v${{ inputs.version-name }}"
name: "${{ inputs.version-name }} (${{ inputs.version-number }})"
prerelease: ${{ inputs.prerelease }}
draft: ${{ inputs.draft }}
make_latest: ${{ inputs.make-latest }}
target_commitish: ${{ steps.get_release_branch.outputs.release_branch }}
generate_release_notes: true
files: |
artifacts/**/*
- name: Update Release Description
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
RELEASE_ID: ${{ steps.create_release.outputs.id }}
RELEASE_URL: ${{ steps.create_release.outputs.url }}
ARTIFACT_RUN_ID: ${{ inputs.artifact-run-id }}
run: |
# Get current release body
current_body=$(gh api /repos/${{ github.repository }}/releases/$RELEASE_ID --jq .body)
# Append build source to the end
updated_body="${current_body}
**Builds Source:** https://github.com/${{ github.repository }}/actions/runs/$ARTIFACT_RUN_ID"
# Update release
gh api --method PATCH /repos/${{ github.repository }}/releases/$RELEASE_ID \
-f body="$updated_body"
echo "# :rocket: Release ready at:" >> $GITHUB_STEP_SUMMARY
echo "$RELEASE_URL" >> $GITHUB_STEP_SUMMARY

View File

@@ -1,58 +0,0 @@
name: Cut Release Branch
on:
workflow_dispatch:
inputs:
release_type:
description: 'Release Type'
required: true
type: choice
options:
- RC
- Hotfix
jobs:
create-release-branch:
name: Create Release Branch
runs-on: ubuntu-24.04
permissions:
contents: write
steps:
- name: Check out repository
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
fetch-depth: 0
- name: Create RC Branch
if: inputs.release_type == 'RC'
env:
RC_PREFIX_DATE: "true" # replace with input if needed
run: |
if [ "$RC_PREFIX_DATE" = "true" ]; then
current_date=$(date +'%Y.%m')
branch_name="release/${current_date}-rc${{ github.run_number }}"
else
branch_name="release/rc${{ github.run_number }}"
fi
git switch main
git switch -c $branch_name
git push origin $branch_name
echo "# :cherry_blossom: RC branch: ${branch_name}" >> $GITHUB_STEP_SUMMARY
- name: Create Hotfix Branch
if: inputs.release_type == 'Hotfix'
run: |
latest_tag=$(git tag -l --sort=-creatordate | head -n 1)
if [ -z "$latest_tag" ]; then
echo "::error::No tags found in the repository"
exit 1
fi
branch_name="release/hotfix-${latest_tag}"
echo "🌿 branch name: $branch_name"
if git show-ref --verify --quiet "refs/remotes/origin/$branch_name"; then
echo "# :fire: :warning: Hotfix branch already exists: ${branch_name}" >> $GITHUB_STEP_SUMMARY
exit 0
fi
git switch -c $branch_name $latest_tag
git push origin $branch_name
echo "# :fire: Hotfix branch: ${branch_name}" >> $GITHUB_STEP_SUMMARY

View File

@@ -1,13 +0,0 @@
name: Scan Authenticator
on:
workflow_dispatch:
jobs:
placeholder:
name: Placeholder Job
runs-on: ubuntu-24.04
steps:
- name: Placeholder Step
run: echo "placeholder workflow"

View File

@@ -1,61 +0,0 @@
name: Scan Protected Branches On Push
on:
workflow_dispatch:
push:
branches:
- "main"
jobs:
sast:
name: SAST scan
runs-on: ubuntu-24.04
permissions:
contents: read
security-events: write
steps:
- name: Check out repo
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
fetch-depth: 0
- name: Scan with Checkmarx
uses: checkmarx/ast-github-action@184bf2f64f55d1c93fd6636d539edf274703e434 # 2.0.41
with:
project_name: ${{ github.repository }}
cx_tenant: ${{ secrets.CHECKMARX_TENANT }}
base_uri: https://ast.checkmarx.net/
cx_client_id: ${{ secrets.CHECKMARX_CLIENT_ID }}
cx_client_secret: ${{ secrets.CHECKMARX_SECRET }}
additional_params: |
--report-format sarif \
--filter "state=TO_VERIFY;PROPOSED_NOT_EXPLOITABLE;CONFIRMED;URGENT" \
--output-path .
- name: Upload Checkmarx results to GitHub
uses: github/codeql-action/upload-sarif@b6a472f63d85b9c78a3ac5e89422239fc15e9b3c # v3.28.1
with:
sarif_file: cx_result.sarif
quality:
name: Quality scan
runs-on: ubuntu-24.04
permissions:
contents: read
steps:
- name: Check out repo
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
fetch-depth: 0
- name: Scan with SonarCloud
uses: sonarsource/sonarqube-scan-action@bfd4e558cda28cda6b5defafb9232d191be8c203 # v4.2.1
env:
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
with:
args: >
-Dsonar.organization=${{ github.repository_owner }}
-Dsonar.projectKey=${{ github.repository_owner }}_${{ github.event.repository.name }}
-Dsonar.pullrequest.key=${{ github.event.pull_request.number }}

View File

@@ -1,9 +1,16 @@
name: Scan Pull Requests
name: Scan
on:
workflow_dispatch:
push:
branches:
- "main"
- "rc"
- "hotfix-rc"
pull_request_target:
types: [opened, synchronize]
merge_group:
types: [checks_requested]
jobs:
check-run:
@@ -26,7 +33,7 @@ jobs:
ref: ${{ github.event.pull_request.head.sha }}
- name: Scan with Checkmarx
uses: checkmarx/ast-github-action@184bf2f64f55d1c93fd6636d539edf274703e434 # 2.0.41
uses: checkmarx/ast-github-action@03a90e7253dadd7e2fff55f5dfbce647b39040a1 # 2.0.37
env:
INCREMENTAL: "${{ contains(github.event_name, 'pull_request') && '--sast-incremental' || '' }}"
with:
@@ -41,7 +48,7 @@ jobs:
--output-path . ${{ env.INCREMENTAL }}
- name: Upload Checkmarx results to GitHub
uses: github/codeql-action/upload-sarif@b6a472f63d85b9c78a3ac5e89422239fc15e9b3c # v3.28.1
uses: github/codeql-action/upload-sarif@9278e421667d5d90a2839487a482448c4ec7df4d # v3.27.2
with:
sarif_file: cx_result.sarif
@@ -61,11 +68,11 @@ jobs:
ref: ${{ github.event.pull_request.head.sha }}
- name: Scan with SonarCloud
uses: sonarsource/sonarqube-scan-action@bfd4e558cda28cda6b5defafb9232d191be8c203 # v4.2.1
uses: sonarsource/sonarcloud-github-action@383f7e52eae3ab0510c3cb0e7d9d150bbaeab838 # v3.1.0
env:
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
args: >
-Dsonar.organization=${{ github.repository_owner }}
-Dsonar.projectKey=${{ github.repository_owner }}_${{ github.event.repository.name }}
-Dsonar.pullrequest.key=${{ github.event.pull_request.number }}

View File

@@ -1,13 +0,0 @@
name: Test Authenticator
on:
workflow_dispatch:
jobs:
placeholder:
name: Placeholder Job
runs-on: ubuntu-24.04
steps:
- name: Placeholder Step
run: echo "placeholder workflow"

View File

@@ -6,33 +6,42 @@ on:
- "main"
- "rc"
- "hotfix-rc"
pull_request:
pull_request_target:
types: [opened, synchronize]
merge_group:
type: [checks_requested]
workflow_dispatch:
env:
_JAVA_VERSION: 17
_GITHUB_ACTION_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}/attempts/${{ github.run_attempt }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
JAVA_VERSION: 17
jobs:
check-run:
name: Check PR run
uses: bitwarden/gh-actions/.github/workflows/check-run.yml@main
test:
name: Test
runs-on: ubuntu-24.04
needs: check-run
permissions:
contents: read
issues: write
packages: read
pull-requests: write
steps:
- name: Check out repo
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
ref: ${{ github.event.pull_request.head.sha }}
- name: Validate Gradle wrapper
uses: gradle/actions/wrapper-validation@0bdd871935719febd78681f197cd39af5b6e16a6 # v4.2.2
uses: gradle/actions/wrapper-validation@d156388eb19639ec20ade50009f3d199ce1e2808 # v4.1.0
- name: Cache Gradle files
uses: actions/cache@1bd1e32a3bdc45362d1e726936510720a7c30a57 # v4.2.0
uses: actions/cache@6849a6489940f00c2f30c0fb92c6274307ccb58a # v4.1.2
with:
path: |
~/.gradle/caches
@@ -42,7 +51,7 @@ jobs:
${{ runner.os }}-gradle-v2-
- name: Cache build output
uses: actions/cache@1bd1e32a3bdc45362d1e726936510720a7c30a57 # v4.2.0
uses: actions/cache@6849a6489940f00c2f30c0fb92c6274307ccb58a # v4.1.2
with:
path: |
${{ github.workspace }}/build-cache
@@ -51,15 +60,15 @@ jobs:
${{ runner.os }}-build-
- name: Configure Ruby
uses: ruby/setup-ruby@28c4deda893d5a96a6b2d958c5b47fc18d65c9d3 # v1.213.0
uses: ruby/setup-ruby@a2bbe5b1b236842c1cb7dd11e8e3b51e0a616acc # v1.202.0
with:
bundler-cache: true
- name: Configure JDK
uses: actions/setup-java@7a6d8a8234af8eb26422e24e3006232cccaa061b # v4.6.0
uses: actions/setup-java@8df1039502a15bceb9433410b1a100fbe190c53b # v4.5.0
with:
distribution: "temurin"
java-version: ${{ env._JAVA_VERSION }}
java-version: ${{ env.JAVA_VERSION }}
- name: Install Fastlane
run: |
@@ -68,58 +77,19 @@ jobs:
bundle install --jobs 4 --retry 3
- name: Build and test
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # Used in settings.gradle.kts to download the SDK from GitHub Maven Packages
run: |
bundle exec fastlane check
- name: Upload test reports
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
if: always()
with:
name: test-reports
path: |
app/build/reports/tests/
app/build/reports/kover/reportStandardDebug.xml
report:
name: Process Test Reports
needs: test
runs-on: ubuntu-24.04
permissions:
contents: read
issues: write
pull-requests: write
if: success()
steps:
- name: Download test artifacts
uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8
if: github.event_name == 'push' || github.event_name == 'pull_request'
- name: Upload test reports on failure
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
if: failure()
with:
name: test-reports
path: app/build/reports/tests/
- name: Upload to codecov.io
id: upload-to-codecov
uses: codecov/codecov-action@1e68e06f1dbfde0e4cefc87efeba9e4643565303 # v5.1.2
if: github.event_name == 'push' || github.event_name == 'pull_request'
continue-on-error: true
uses: codecov/codecov-action@b9fd7d16f6d7d1b5d2bec1a2887e65ceed900238 # v4.6.0
with:
os: linux
files: kover/reportStandardDebug.xml
fail_ci_if_error: true
- name: Comment PR if tests failed
if: steps.upload-to-codecov.outcome == 'failure' && (github.event_name == 'push' || github.event_name == 'pull_request')
file: app/build/reports/kover/reportStandardDebug.xml
env:
PR_NUMBER: ${{ github.event.number }}
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
RUN_ACTOR: ${{ github.triggering_actor }}
run: |
echo "> [!WARNING]" >> $GITHUB_STEP_SUMMARY
echo "> Uploading code coverage report failed. Please check the \"Upload to codecov.io\" step of \"Process Test Reports\" job for more details." >> $GITHUB_STEP_SUMMARY
if [ ! -z "$PR_NUMBER" ]; then
message=$'> [!WARNING]\n> @'$RUN_ACTOR' Uploading code coverage report failed. Please check the "Upload to codecov.io" step of [Process Test Reports job]('$_GITHUB_ACTION_RUN_URL') for more details.'
gh pr comment --repo $GITHUB_REPOSITORY $PR_NUMBER --body "$message"
fi
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}

View File

@@ -10,20 +10,20 @@ GEM
artifactory (3.0.17)
atomos (0.1.3)
aws-eventstream (1.3.0)
aws-partitions (1.1040.0)
aws-sdk-core (3.216.0)
aws-partitions (1.1003.0)
aws-sdk-core (3.212.0)
aws-eventstream (~> 1, >= 1.3.0)
aws-partitions (~> 1, >= 1.992.0)
aws-sigv4 (~> 1.9)
jmespath (~> 1, >= 1.6.1)
aws-sdk-kms (1.97.0)
aws-sdk-core (~> 3, >= 3.216.0)
aws-sdk-kms (1.95.0)
aws-sdk-core (~> 3, >= 3.210.0)
aws-sigv4 (~> 1.5)
aws-sdk-s3 (1.178.0)
aws-sdk-core (~> 3, >= 3.216.0)
aws-sdk-s3 (1.170.0)
aws-sdk-core (~> 3, >= 3.210.0)
aws-sdk-kms (~> 1)
aws-sigv4 (~> 1.5)
aws-sigv4 (1.11.0)
aws-sigv4 (1.10.1)
aws-eventstream (~> 1, >= 1.0.2)
babosa (1.0.4)
base64 (0.2.0)
@@ -32,7 +32,7 @@ GEM
colored2 (3.1.2)
commander (4.6.0)
highline (~> 2.0.0)
date (3.4.1)
date (3.4.0)
declarative (0.0.20)
digest-crc (0.6.5)
rake (>= 12.0.0, < 14.0.0)
@@ -59,8 +59,8 @@ GEM
faraday-em_synchrony (1.0.0)
faraday-excon (1.1.0)
faraday-httpclient (1.0.1)
faraday-multipart (1.1.0)
multipart-post (~> 2.0)
faraday-multipart (1.0.4)
multipart-post (~> 2)
faraday-net_http (1.0.2)
faraday-net_http_persistent (1.2.0)
faraday-patron (1.0.0)
@@ -68,8 +68,8 @@ GEM
faraday-retry (1.0.3)
faraday_middleware (1.2.1)
faraday (~> 1.0)
fastimage (2.4.0)
fastlane (2.226.0)
fastimage (2.3.1)
fastlane (2.225.0)
CFPropertyList (>= 2.3, < 4.0.0)
addressable (>= 2.8, < 3.0.0)
artifactory (~> 3.0)
@@ -109,9 +109,9 @@ GEM
tty-spinner (>= 0.8.0, < 1.0.0)
word_wrap (~> 1.0.0)
xcodeproj (>= 1.13.0, < 2.0.0)
xcpretty (~> 0.4.0)
xcpretty (~> 0.3.0)
xcpretty-travis-formatter (>= 0.0.3, < 2.0.0)
fastlane-plugin-firebase_app_distribution (0.10.0)
fastlane-plugin-firebase_app_distribution (0.9.1)
google-apis-firebaseappdistribution_v1 (~> 0.3.0)
google-apis-firebaseappdistribution_v1alpha (~> 0.2.0)
fastlane-sirp (1.0.0)
@@ -158,12 +158,12 @@ GEM
os (>= 0.9, < 2.0)
signet (>= 0.16, < 2.a)
highline (2.0.3)
http-cookie (1.0.8)
http-cookie (1.0.7)
domain_name (~> 0.5)
httpclient (2.8.3)
jmespath (1.6.2)
json (2.9.1)
jwt (2.10.1)
json (2.8.1)
jwt (2.9.3)
base64
mini_magick (4.13.2)
mini_mime (1.1.5)
@@ -174,7 +174,7 @@ GEM
nkf (0.2.0)
optparse (0.6.0)
os (1.1.4)
plist (3.7.2)
plist (3.7.1)
public_suffix (6.0.1)
rake (13.2.1)
representable (3.2.0)
@@ -182,10 +182,10 @@ GEM
trailblazer-option (>= 0.1.1, < 0.2.0)
uber (< 0.2.0)
retriable (3.1.2)
rexml (3.4.0)
rouge (3.28.0)
rexml (3.3.9)
rouge (2.0.7)
ruby2_keywords (0.0.5)
rubyzip (2.4.1)
rubyzip (2.3.2)
security (0.1.5)
signet (0.19.0)
addressable (~> 2.8)
@@ -216,8 +216,8 @@ GEM
colored2 (~> 3.1)
nanaimo (~> 0.4.0)
rexml (>= 3.3.6, < 4.0)
xcpretty (0.4.0)
rouge (~> 3.28.0)
xcpretty (0.3.0)
rouge (~> 2.0.7)
xcpretty-travis-formatter (1.0.1)
xcpretty (~> 0.2, >= 0.0.7)

View File

@@ -9,7 +9,7 @@
## Compatibility
- **Minimum SDK**: 29
- **Target SDK**: 35
- **Target SDK**: 34
- **Device Types Supported**: Phone and Tablet
- **Orientations Supported**: Portrait and Landscape
@@ -132,11 +132,6 @@ The following is a list of all third-party dependencies included as part of the
- https://github.com/firebase/firebase-android-sdk
- Purpose: SDK for crash and non-fatal error reporting. (**NOTE:** This dependency is not included in builds distributed via F-Droid.)
- License: Apache 2.0
- **Google Play Reviews**
- https://developer.android.com/reference/com/google/android/play/core/release-notes
- Purpose: On standard builds provide an interface to add a review for the password manager application in Google Play.
- License: Apache 2.0
- **Glide**
- https://github.com/bumptech/glide

View File

@@ -1,9 +1,6 @@
import com.android.build.gradle.internal.api.BaseVariantOutputImpl
import com.android.utils.cxx.io.removeExtensionIfPresent
import com.google.firebase.crashlytics.buildtools.gradle.tasks.InjectMappingFileIdTask
import com.google.firebase.crashlytics.buildtools.gradle.tasks.UploadMappingFileTask
import com.google.gms.googleservices.GoogleServicesTask
import dagger.hilt.android.plugin.util.capitalize
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
import java.io.FileInputStream
import java.util.Properties
@@ -35,16 +32,6 @@ val userProperties = Properties().apply {
}
}
/**
* Loads CI-specific build properties that are not checked into source control.
*/
val ciProperties = Properties().apply {
val ciPropsFile = File(rootDir, "ci.properties")
if (ciPropsFile.exists()) {
FileInputStream(ciPropsFile).use { load(it) }
}
}
android {
namespace = "com.x8bit.bitwarden"
compileSdk = libs.versions.compileSdk.get().toInt()
@@ -64,12 +51,6 @@ android {
}
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
buildConfigField(
type = "String",
name = "CI_INFO",
value = "${ciProperties.getOrDefault("ci.info", "\"local\"")}"
)
}
androidResources {
@@ -134,39 +115,6 @@ android {
}
}
applicationVariants.all {
val bundlesDir = "${layout.buildDirectory.get()}/outputs/bundle"
outputs
.mapNotNull { it as? BaseVariantOutputImpl }
.forEach { output ->
val fileNameWithoutExtension = when (flavorName) {
"fdroid" -> "$applicationId-$flavorName"
"standard" -> "$applicationId"
else -> output.outputFileName.removeExtensionIfPresent(".apk")
}
// Set the APK output filename.
output.outputFileName = "$fileNameWithoutExtension.apk"
val variantName = name
val renameTaskName = "rename${variantName.capitalize()}AabFiles"
tasks.register(renameTaskName) {
group = "build"
description = "Renames the bundle files for $variantName variant"
doLast {
renameFile(
"$bundlesDir/$variantName/$namespace-$flavorName-${buildType.name}.aab",
"$fileNameWithoutExtension.aab",
)
}
}
// Force renaming task to execute after the variant is built.
tasks
.getByName("bundle${variantName.capitalize()}")
.finalizedBy(renameTaskName)
}
}
compileOptions {
sourceCompatibility(libs.versions.jvmTarget.get())
targetCompatibility(libs.versions.jvmTarget.get())
@@ -268,7 +216,6 @@ dependencies {
standardImplementation(libs.google.firebase.cloud.messaging)
standardImplementation(platform(libs.google.firebase.bom))
standardImplementation(libs.google.firebase.crashlytics)
standardImplementation(libs.google.play.review)
testImplementation(libs.androidx.compose.ui.test)
testImplementation(libs.google.hilt.android.testing)
@@ -325,7 +272,6 @@ kover {
"*_*Factory\$*",
"*.Hilt_*",
"*_HiltModules",
"*_HiltModules*",
"*_HiltModules\$*",
"*_Impl",
"*_Impl\$*",
@@ -352,10 +298,6 @@ tasks {
dependsOn("detekt")
}
getByName("sonar") {
dependsOn("check")
}
withType<io.gitlab.arturbosch.detekt.Detekt>().configureEach {
jvmTarget = libs.versions.jvmTarget.get()
}
@@ -368,16 +310,15 @@ tasks {
maxHeapSize = "2g"
maxParallelForks = Runtime.getRuntime().availableProcessors()
jvmArgs = jvmArgs.orEmpty() + "-XX:+UseParallelGC"
android.sourceSets["main"].res.srcDirs("src/test/res")
}
}
afterEvaluate {
// Disable Fdroid-specific tasks that we want to exclude
val fdroidTasksToDisable = tasks.withType<GoogleServicesTask>() +
val tasks = tasks.withType<GoogleServicesTask>() +
tasks.withType<InjectMappingFileIdTask>() +
tasks.withType<UploadMappingFileTask>()
fdroidTasksToDisable
tasks
.filter { it.name.contains("Fdroid") }
.forEach { it.enabled = false }
}
@@ -394,17 +335,8 @@ sonar {
}
}
private fun renameFile(path: String, newName: String) {
val originalFile = File(path)
if (!originalFile.exists()) {
println("File $originalFile does not exist!")
return
}
val newFile = File(originalFile.parentFile, newName)
if (originalFile.renameTo(newFile)) {
println("Renamed $originalFile to $newFile")
} else {
throw RuntimeException("Failed to rename $originalFile to $newFile")
tasks {
getByName("sonar") {
dependsOn("check")
}
}

View File

@@ -1,256 +0,0 @@
{
"formatVersion": 1,
"database": {
"version": 6,
"identityHash": "ee158c483edfe5102504670f3d9845d4",
"entities": [
{
"tableName": "ciphers",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `user_id` TEXT NOT NULL, `cipher_type` TEXT NOT NULL, `cipher_json` TEXT NOT NULL, PRIMARY KEY(`id`))",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "userId",
"columnName": "user_id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "cipherType",
"columnName": "cipher_type",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "cipherJson",
"columnName": "cipher_json",
"affinity": "TEXT",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"id"
]
},
"indices": [
{
"name": "index_ciphers_user_id",
"unique": false,
"columnNames": [
"user_id"
],
"orders": [],
"createSql": "CREATE INDEX IF NOT EXISTS `index_ciphers_user_id` ON `${TABLE_NAME}` (`user_id`)"
}
],
"foreignKeys": []
},
{
"tableName": "collections",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `user_id` TEXT NOT NULL, `organization_id` TEXT NOT NULL, `should_hide_passwords` INTEGER NOT NULL, `name` TEXT NOT NULL, `external_id` TEXT, `read_only` INTEGER NOT NULL, `manage` INTEGER, PRIMARY KEY(`id`))",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "userId",
"columnName": "user_id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "organizationId",
"columnName": "organization_id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "shouldHidePasswords",
"columnName": "should_hide_passwords",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "name",
"columnName": "name",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "externalId",
"columnName": "external_id",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "isReadOnly",
"columnName": "read_only",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "canManage",
"columnName": "manage",
"affinity": "INTEGER",
"notNull": false
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"id"
]
},
"indices": [
{
"name": "index_collections_user_id",
"unique": false,
"columnNames": [
"user_id"
],
"orders": [],
"createSql": "CREATE INDEX IF NOT EXISTS `index_collections_user_id` ON `${TABLE_NAME}` (`user_id`)"
}
],
"foreignKeys": []
},
{
"tableName": "domains",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`user_id` TEXT NOT NULL, `domains_json` TEXT, PRIMARY KEY(`user_id`))",
"fields": [
{
"fieldPath": "userId",
"columnName": "user_id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "domainsJson",
"columnName": "domains_json",
"affinity": "TEXT",
"notNull": false
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"user_id"
]
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "folders",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `user_id` TEXT NOT NULL, `name` TEXT, `revision_date` INTEGER NOT NULL, PRIMARY KEY(`id`))",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "userId",
"columnName": "user_id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "name",
"columnName": "name",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "revisionDate",
"columnName": "revision_date",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"id"
]
},
"indices": [
{
"name": "index_folders_user_id",
"unique": false,
"columnNames": [
"user_id"
],
"orders": [],
"createSql": "CREATE INDEX IF NOT EXISTS `index_folders_user_id` ON `${TABLE_NAME}` (`user_id`)"
}
],
"foreignKeys": []
},
{
"tableName": "sends",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `user_id` TEXT NOT NULL, `send_type` TEXT NOT NULL, `send_json` TEXT NOT NULL, PRIMARY KEY(`id`))",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "userId",
"columnName": "user_id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "sendType",
"columnName": "send_type",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "sendJson",
"columnName": "send_json",
"affinity": "TEXT",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"id"
]
},
"indices": [
{
"name": "index_sends_user_id",
"unique": false,
"columnNames": [
"user_id"
],
"orders": [],
"createSql": "CREATE INDEX IF NOT EXISTS `index_sends_user_id` ON `${TABLE_NAME}` (`user_id`)"
}
],
"foreignKeys": []
}
],
"views": [],
"setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'ee158c483edfe5102504670f3d9845d4')"
]
}
}

View File

@@ -1,12 +0,0 @@
package com.x8bit.bitwarden.ui.platform.manager.review
import android.app.Activity
/**
* No-op implementation of [AppReviewManager] for F-Droid builds.
*/
class AppReviewManagerImpl(
activity: Activity,
) : AppReviewManager {
override fun promptForReview() = Unit
}

View File

@@ -15,7 +15,7 @@
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission android:name="android.permission.READ_USER_DICTIONARY"/>
<!-- Protect access to AuthenticatorBridgeService using this custom permission.
Note that each build type uses a different value for knownCerts.
@@ -320,11 +320,6 @@
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.HOME" />
</intent>
<!-- To Query Chrome Beta: -->
<package android:name="com.chrome.beta" />
<!-- To Query Chrome Stable: -->
<package android:name="com.android.chrome" />
</queries>
</manifest>

View File

@@ -1,31 +1,5 @@
{
"apps": [
{
"type": "android",
"info": {
"package_name": "io.github.forkmaintainers.iceraven",
"signatures": [
{
"build": "release",
"cert_fingerprint_sha256": "9C:0D:22:37:9F:48:7B:70:A4:F9:F8:BE:C0:17:3C:F9:1A:16:44:F0:8F:93:38:5B:5B:78:2C:E3:76:60:BA:81"
}
]
}
},
{
"type": "android",
"info": {
"package_name": "net.quetta.browser",
"signatures": [
{
"build": "release",
"cert_fingerprint_sha256": "BE:FE:E7:31:12:6A:A5:6E:7E:FD:AE:AF:5E:F3:FA:EA:44:1C:19:CC:E0:CA:EC:42:6B:65:BB:F8:2C:59:46:80"
}
]
}
},
{
"type": "android",
"info": {
@@ -50,30 +24,6 @@
]
}
},
{
"type": "android",
"info": {
"package_name": "org.ironfoxoss.ironfox",
"signatures": [
{
"build": "release",
"cert_fingerprint_sha256": "C5:E2:91:B5:A5:71:F9:C8:CD:9A:97:99:C2:C9:4E:02:EC:97:03:94:88:93:F2:CA:75:6D:67:B9:42:04:F9:04"
}
]
}
},
{
"type": "android",
"info": {
"package_name": "org.mozilla.fenix",
"signatures": [
{
"build": "release",
"cert_fingerprint_sha256": "50:04:77:90:88:E7:F9:88:D5:BC:5C:C5:F8:79:8F:EB:F4:F8:CD:08:4A:1B:2A:46:EF:D4:C8:EE:4A:EA:F2:11"
}
]
}
},
{
"type": "android",
"info": {
@@ -85,6 +35,46 @@
}
]
}
},
{
"type": "android",
"info": {
"package_name": "us.spotco.fennec_dos",
"signatures": [
{
"build": "release",
"cert_fingerprint_sha256": "26:0E:0A:49:67:8C:78:B7:0C:02:D6:53:7A:DD:3B:6D:C0:A1:71:71:BB:DE:8C:E7:5F:D4:02:6A:8A:3E:18:D2"
},
{
"build": "release",
"cert_fingerprint_sha256": "FF:81:F5:BE:56:39:65:94:EE:E7:0F:EF:28:32:25:6E:15:21:41:22:E2:BA:9C:ED:D2:60:05:FF:D4:BC:AA:A8"
}
]
}
},
{
"type": "android",
"info": {
"package_name": "us.spotco.mulch",
"signatures": [
{
"build": "release",
"cert_fingerprint_sha256": "26:0E:0A:49:67:8C:78:B7:0C:02:D6:53:7A:DD:3B:6D:C0:A1:71:71:BB:DE:8C:E7:5F:D4:02:6A:8A:3E:18:D2"
}
]
}
},
{
"type": "android",
"info": {
"package_name": "io.github.forkmaintainers.iceraven",
"signatures": [
{
"build": "release",
"cert_fingerprint_sha256": "9C:0D:22:37:9F:48:7B:70:A4:F9:F8:BE:C0:17:3C:F9:1A:16:44:F0:8F:93:38:5B:5B:78:2C:E3:76:60:BA:81"
}
]
}
}
]
}

View File

@@ -4,8 +4,8 @@ import android.app.Application
import com.x8bit.bitwarden.data.auth.manager.AuthRequestNotificationManager
import com.x8bit.bitwarden.data.platform.annotation.OmitFromCoverage
import com.x8bit.bitwarden.data.platform.manager.LogsManager
import com.x8bit.bitwarden.data.platform.manager.NetworkConfigManager
import com.x8bit.bitwarden.data.platform.manager.event.OrganizationEventManager
import com.x8bit.bitwarden.data.platform.manager.network.NetworkConfigManager
import com.x8bit.bitwarden.data.platform.manager.restriction.RestrictionManager
import dagger.hilt.android.HiltAndroidApp
import javax.inject.Inject

View File

@@ -11,16 +11,15 @@ import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.app.AppCompatDelegate
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.core.os.LocaleListCompat
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.navigation.compose.rememberNavController
import com.x8bit.bitwarden.data.autofill.accessibility.manager.AccessibilityActivityManager
import com.x8bit.bitwarden.data.autofill.accessibility.manager.AccessibilityCompletionManager
import com.x8bit.bitwarden.data.autofill.manager.AutofillActivityManager
import com.x8bit.bitwarden.data.autofill.manager.AutofillCompletionManager
import com.x8bit.bitwarden.data.platform.annotation.OmitFromCoverage
import com.x8bit.bitwarden.data.platform.manager.util.ObserveScreenDataEffect
import com.x8bit.bitwarden.data.platform.repository.SettingsRepository
import com.x8bit.bitwarden.ui.platform.base.util.EventsEffect
import com.x8bit.bitwarden.ui.platform.composition.LocalManagerProvider
@@ -40,6 +39,9 @@ class MainActivity : AppCompatActivity() {
private val mainViewModel: MainViewModel by viewModels()
@Inject
lateinit var accessibilityActivityManager: AccessibilityActivityManager
@Inject
lateinit var autofillActivityManager: AutofillActivityManager
@@ -55,7 +57,6 @@ class MainActivity : AppCompatActivity() {
@Inject
lateinit var debugLaunchManager: DebugMenuLaunchManager
@Suppress("LongMethod")
override fun onCreate(savedInstanceState: Bundle?) {
var shouldShowSplashScreen = true
installSplashScreen().setKeepOnScreenCondition { shouldShowSplashScreen }
@@ -69,14 +70,13 @@ class MainActivity : AppCompatActivity() {
)
}
// Within the app the language and theme will change dynamically and will be managed by the
// OS, but we need to ensure we properly set the values when upgrading from older versions
// that handle this differently or when the activity restarts.
// Within the app the language will change dynamically and will be managed
// by the OS, but we need to ensure we properly set the language when
// upgrading from older versions that handle this differently.
settingsRepository.appLanguage.localeName?.let { localeName ->
val localeList = LocaleListCompat.forLanguageTags(localeName)
AppCompatDelegate.setApplicationLocales(localeList)
}
AppCompatDelegate.setDefaultNightMode(settingsRepository.appTheme.osValue)
setContent {
val state by mainViewModel.stateFlow.collectAsStateWithLifecycle()
val navController = rememberNavController()
@@ -98,29 +98,10 @@ class MainActivity : AppCompatActivity() {
)
.show()
}
is MainEvent.UpdateAppLocale -> {
AppCompatDelegate.setApplicationLocales(
LocaleListCompat.forLanguageTags(event.localeName),
)
}
is MainEvent.UpdateAppTheme -> {
AppCompatDelegate.setDefaultNightMode(event.osTheme)
}
}
}
updateScreenCapture(isScreenCaptureAllowed = state.isScreenCaptureAllowed)
LocalManagerProvider {
ObserveScreenDataEffect(
onDataUpdate = remember(mainViewModel) {
{
mainViewModel.trySendAction(
MainAction.ResumeScreenDataReceived(it),
)
}
},
)
BitwardenTheme(theme = state.theme) {
RootNavScreen(
onSplashScreenRemoved = { shouldShowSplashScreen = false },

View File

@@ -13,15 +13,13 @@ import com.x8bit.bitwarden.data.auth.util.getPasswordlessRequestDataIntentOrNull
import com.x8bit.bitwarden.data.autofill.accessibility.manager.AccessibilitySelectionManager
import com.x8bit.bitwarden.data.autofill.fido2.manager.Fido2CredentialManager
import com.x8bit.bitwarden.data.autofill.fido2.util.getFido2AssertionRequestOrNull
import com.x8bit.bitwarden.data.autofill.fido2.util.getFido2CreateCredentialRequestOrNull
import com.x8bit.bitwarden.data.autofill.fido2.util.getFido2CredentialRequestOrNull
import com.x8bit.bitwarden.data.autofill.fido2.util.getFido2GetCredentialsRequestOrNull
import com.x8bit.bitwarden.data.autofill.manager.AutofillSelectionManager
import com.x8bit.bitwarden.data.autofill.util.getAutofillSaveItemOrNull
import com.x8bit.bitwarden.data.autofill.util.getAutofillSelectionDataOrNull
import com.x8bit.bitwarden.data.platform.manager.AppResumeManager
import com.x8bit.bitwarden.data.platform.manager.SpecialCircumstanceManager
import com.x8bit.bitwarden.data.platform.manager.garbage.GarbageCollectionManager
import com.x8bit.bitwarden.data.platform.manager.model.AppResumeScreenData
import com.x8bit.bitwarden.data.platform.manager.model.CompleteRegistrationData
import com.x8bit.bitwarden.data.platform.manager.model.SpecialCircumstance
import com.x8bit.bitwarden.data.platform.repository.EnvironmentRepository
@@ -73,7 +71,6 @@ class MainViewModel @Inject constructor(
private val authRepository: AuthRepository,
private val environmentRepository: EnvironmentRepository,
private val savedStateHandle: SavedStateHandle,
private val appResumeManager: AppResumeManager,
private val clock: Clock,
) : BaseViewModel<MainState, MainEvent, MainAction>(
initialState = MainState(
@@ -111,11 +108,6 @@ class MainViewModel @Inject constructor(
.appThemeStateFlow
.onEach { trySendAction(MainAction.Internal.ThemeUpdate(it)) }
.launchIn(viewModelScope)
settingsRepository
.appLanguageStateFlow
.map { MainEvent.UpdateAppLocale(it.localeName) }
.onEach(::sendEvent)
.launchIn(viewModelScope)
settingsRepository
.isScreenCaptureAllowedStateFlow
@@ -188,14 +180,6 @@ class MainViewModel @Inject constructor(
is MainAction.ReceiveFirstIntent -> handleFirstIntentReceived(action)
is MainAction.ReceiveNewIntent -> handleNewIntentReceived(action)
MainAction.OpenDebugMenu -> handleOpenDebugMenu()
is MainAction.ResumeScreenDataReceived -> handleAppResumeDataUpdated(action)
}
}
private fun handleAppResumeDataUpdated(action: MainAction.ResumeScreenDataReceived) {
when (val data = action.screenResumeData) {
null -> appResumeManager.clearResumeScreen()
else -> appResumeManager.setResumeScreen(data)
}
}
@@ -227,7 +211,6 @@ class MainViewModel @Inject constructor(
private fun handleAppThemeUpdated(action: MainAction.Internal.ThemeUpdate) {
mutableStateFlow.update { it.copy(theme = action.theme) }
sendEvent(MainEvent.UpdateAppTheme(osTheme = action.theme.osValue))
}
private fun handleVaultUnlockStateChange() {
@@ -274,7 +257,7 @@ class MainViewModel @Inject constructor(
val hasGeneratorShortcut = intent.isPasswordGeneratorShortcut
val hasVaultShortcut = intent.isMyVaultShortcut
val hasAccountSecurityShortcut = intent.isAccountSecurityShortcut
val fido2CreateCredentialRequestData = intent.getFido2CreateCredentialRequestOrNull()
val fido2CredentialRequestData = intent.getFido2CredentialRequestOrNull()
val completeRegistrationData = intent.getCompleteRegistrationDataIntentOrNull()
val fido2CredentialAssertionRequest = intent.getFido2AssertionRequestOrNull()
val fido2GetCredentialsRequest = intent.getFido2GetCredentialsRequestOrNull()
@@ -335,31 +318,25 @@ class MainViewModel @Inject constructor(
)
}
fido2CreateCredentialRequestData != null -> {
fido2CredentialRequestData != null -> {
// Set the user's verification status when a new FIDO 2 request is received to force
// explicit verification if the user's vault is unlocked when the request is
// received.
fido2CreateCredentialRequestData.isUserVerified
?.let { isVerified -> fido2CredentialManager.isUserVerified = isVerified }
fido2CredentialManager.isUserVerified = false
specialCircumstanceManager.specialCircumstance =
SpecialCircumstance.Fido2Save(
fido2CreateCredentialRequest = fido2CreateCredentialRequestData,
fido2CredentialRequest = fido2CredentialRequestData,
)
// Switch accounts if the selected user is not the active user.
if (authRepository.activeUserId != null &&
authRepository.activeUserId != fido2CreateCredentialRequestData.userId
authRepository.activeUserId != fido2CredentialRequestData.userId
) {
authRepository.switchAccount(fido2CreateCredentialRequestData.userId)
authRepository.switchAccount(fido2CredentialRequestData.userId)
}
}
fido2CredentialAssertionRequest != null -> {
// If device biometric verification was performed as part of single-tap
// authentication, set the user's verification state to the device result.
// Otherwise, retain the verification state as-is.
fido2CredentialAssertionRequest.isUserVerified
?.let { isVerified -> fido2CredentialManager.isUserVerified = isVerified }
specialCircumstanceManager.specialCircumstance =
SpecialCircumstance.Fido2Assertion(
fido2AssertionRequest = fido2CredentialAssertionRequest,
@@ -466,11 +443,6 @@ sealed class MainAction {
*/
data object OpenDebugMenu : MainAction()
/**
* Receive event to save the app resume screen
*/
data class ResumeScreenDataReceived(val screenResumeData: AppResumeScreenData?) : MainAction()
/**
* Actions for internal use by the ViewModel.
*/
@@ -546,18 +518,4 @@ sealed class MainEvent {
* Show a toast with the given [message].
*/
data class ShowToast(val message: Text) : MainEvent()
/**
* Indicates that the app language has been updated.
*/
data class UpdateAppLocale(
val localeName: String?,
) : MainEvent()
/**
* Indicates that the app theme has been updated.
*/
data class UpdateAppTheme(
val osTheme: Int,
) : MainEvent()
}

View File

@@ -1,13 +1,11 @@
package com.x8bit.bitwarden.data.auth.datasource.disk
import com.x8bit.bitwarden.data.auth.datasource.disk.model.AccountTokensJson
import com.x8bit.bitwarden.data.auth.datasource.disk.model.NewDeviceNoticeState
import com.x8bit.bitwarden.data.auth.datasource.disk.model.OnboardingStatus
import com.x8bit.bitwarden.data.auth.datasource.disk.model.PendingAuthRequestJson
import com.x8bit.bitwarden.data.auth.datasource.disk.model.UserStateJson
import com.x8bit.bitwarden.data.vault.datasource.network.model.SyncResponseJson
import kotlinx.coroutines.flow.Flow
import java.time.Instant
/**
* Primary access point for disk information.
@@ -173,16 +171,6 @@ interface AuthDiskSource {
pendingAuthRequest: PendingAuthRequestJson?,
)
/**
* Gets the biometrics initialization vector for the given [userId].
*/
fun getUserBiometricInitVector(userId: String): ByteArray?
/**
* Stores the biometrics initialization vector for the given [userId].
*/
fun storeUserBiometricInitVector(userId: String, iv: ByteArray?)
/**
* Gets the biometrics key for the given [userId].
*/
@@ -340,27 +328,7 @@ interface AuthDiskSource {
fun storeShowImportLogins(userId: String, showImportLogins: Boolean?)
/**
* Emits updates that track [getShowImportLogins]. This will replay the last known value.
* Emits updates that track [getShowImportLogins]. This will replay the last known value,
*/
fun getShowImportLoginsFlow(userId: String): Flow<Boolean?>
/**
* Gets the new device notice state for the given [userId].
*/
fun getNewDeviceNoticeState(userId: String): NewDeviceNoticeState
/**
* Stores the new device notice state for the given [userId].
*/
fun storeNewDeviceNoticeState(userId: String, newState: NewDeviceNoticeState?)
/**
* Gets the last lock timestamp for the given [userId].
*/
fun getLastLockTimestamp(userId: String): Instant?
/**
* Stores the last lock timestamp for the given [userId].
*/
fun storeLastLockTimestamp(userId: String, lastLockTimestamp: Instant?)
}

View File

@@ -2,8 +2,6 @@ package com.x8bit.bitwarden.data.auth.datasource.disk
import android.content.SharedPreferences
import com.x8bit.bitwarden.data.auth.datasource.disk.model.AccountTokensJson
import com.x8bit.bitwarden.data.auth.datasource.disk.model.NewDeviceNoticeDisplayStatus
import com.x8bit.bitwarden.data.auth.datasource.disk.model.NewDeviceNoticeState
import com.x8bit.bitwarden.data.auth.datasource.disk.model.OnboardingStatus
import com.x8bit.bitwarden.data.auth.datasource.disk.model.PendingAuthRequestJson
import com.x8bit.bitwarden.data.auth.datasource.disk.model.UserStateJson
@@ -15,15 +13,14 @@ import com.x8bit.bitwarden.data.vault.datasource.network.model.SyncResponseJson
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.onSubscription
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import java.time.Instant
import java.util.UUID
// These keys should be encrypted
private const val ACCOUNT_TOKENS_KEY = "accountTokens"
private const val AUTHENTICATOR_SYNC_SYMMETRIC_KEY = "authenticatorSyncSymmetric"
private const val AUTHENTICATOR_SYNC_UNLOCK_KEY = "authenticatorSyncUnlock"
private const val BIOMETRICS_INIT_VECTOR_KEY = "biometricInitializationVector"
private const val BIOMETRICS_UNLOCK_KEY = "userKeyBiometricUnlock"
private const val USER_AUTO_UNLOCK_KEY_KEY = "userKeyAutoUnlock"
private const val DEVICE_KEY_KEY = "deviceKey"
@@ -49,8 +46,6 @@ private const val TDE_LOGIN_COMPLETE = "tdeLoginComplete"
private const val USES_KEY_CONNECTOR = "usesKeyConnector"
private const val ONBOARDING_STATUS_KEY = "onboardingStatus"
private const val SHOW_IMPORT_LOGINS_KEY = "showImportLogins"
private const val NEW_DEVICE_NOTICE_STATE = "newDeviceNoticeState"
private const val LAST_LOCK_TIMESTAMP = "lastLockTimestamp"
/**
* Primary implementation of [AuthDiskSource].
@@ -147,7 +142,6 @@ class AuthDiskSourceImpl(
storePrivateKey(userId = userId, privateKey = null)
storeOrganizationKeys(userId = userId, organizationKeys = null)
storeOrganizations(userId = userId, organizations = null)
storeUserBiometricInitVector(userId = userId, iv = null)
storeUserBiometricUnlockKey(userId = userId, biometricsKey = null)
storeMasterPasswordHash(userId = userId, passwordHash = null)
storePolicies(userId = userId, policies = null)
@@ -156,7 +150,6 @@ class AuthDiskSourceImpl(
storeIsTdeLoginComplete(userId = userId, isTdeLoginComplete = null)
storeAuthenticatorSyncUnlockKey(userId = userId, authenticatorSyncUnlockKey = null)
storeShowImportLogins(userId = userId, showImportLogins = null)
storeLastLockTimestamp(userId = userId, lastLockTimestamp = null)
// Do not remove the DeviceKey or PendingAuthRequest on logout, these are persisted
// indefinitely unless the TDE flow explicitly removes them.
@@ -284,17 +277,6 @@ class AuthDiskSourceImpl(
)
}
override fun getUserBiometricInitVector(userId: String): ByteArray? =
getEncryptedString(key = BIOMETRICS_INIT_VECTOR_KEY.appendIdentifier(userId))
?.toByteArray(Charsets.ISO_8859_1)
override fun storeUserBiometricInitVector(userId: String, iv: ByteArray?) {
putEncryptedString(
key = BIOMETRICS_INIT_VECTOR_KEY.appendIdentifier(userId),
value = iv?.toString(Charsets.ISO_8859_1),
)
}
override fun getUserBiometricUnlockKey(userId: String): String? =
getEncryptedString(key = BIOMETRICS_UNLOCK_KEY.appendIdentifier(userId))
@@ -489,35 +471,6 @@ class AuthDiskSourceImpl(
getMutableShowImportLoginsFlow(userId)
.onSubscription { emit(getShowImportLogins(userId)) }
override fun getNewDeviceNoticeState(userId: String): NewDeviceNoticeState {
return getString(key = NEW_DEVICE_NOTICE_STATE.appendIdentifier(userId))?.let {
json.decodeFromStringOrNull(it)
} ?: NewDeviceNoticeState(
displayStatus = NewDeviceNoticeDisplayStatus.HAS_NOT_SEEN,
lastSeenDate = null,
)
}
override fun storeNewDeviceNoticeState(userId: String, newState: NewDeviceNoticeState?) {
putString(
key = NEW_DEVICE_NOTICE_STATE.appendIdentifier(userId),
value = newState?.let { json.encodeToString(it) },
)
}
override fun getLastLockTimestamp(userId: String): Instant? {
return getLong(key = LAST_LOCK_TIMESTAMP.appendIdentifier(userId))?.let {
Instant.ofEpochMilli(it)
}
}
override fun storeLastLockTimestamp(userId: String, lastLockTimestamp: Instant?) {
putLong(
key = LAST_LOCK_TIMESTAMP.appendIdentifier(userId),
value = lastLockTimestamp?.toEpochMilli(),
)
}
private fun generateAndStoreUniqueAppId(): String =
UUID
.randomUUID()

View File

@@ -2,12 +2,8 @@ package com.x8bit.bitwarden.data.auth.datasource.disk.model
import com.x8bit.bitwarden.data.auth.datasource.network.model.KdfTypeJson
import com.x8bit.bitwarden.data.auth.datasource.network.model.UserDecryptionOptionsJson
import kotlinx.serialization.Contextual
import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.JsonNames
import java.time.ZonedDateTime
/**
* Represents the current account information for a given user.
@@ -37,7 +33,6 @@ data class AccountJson(
* @property userId The ID of the user.
* @property email The user's email address.
* @property isEmailVerified Whether or not the user's email is verified.
* @property isTwoFactorEnabled If the profile has two factor authentication enabled.
* @property name The user's name (if applicable).
* @property stamp The account's security stamp (if applicable).
* @property organizationId The ID of the associated organization (if applicable).
@@ -49,9 +44,7 @@ data class AccountJson(
* @property kdfMemory The amount of memory to use when calculating a password hash (MB).
* @property kdfParallelism The number of threads to use when calculating a password hash.
* @property userDecryptionOptions The options available to a user for decryption.
* @property creationDate The creation date of the account.
*/
@OptIn(ExperimentalSerializationApi::class)
@Serializable
data class Profile(
@SerialName("userId")
@@ -63,9 +56,6 @@ data class AccountJson(
@SerialName("emailVerified")
val isEmailVerified: Boolean?,
@SerialName("isTwoFactorEnabled")
val isTwoFactorEnabled: Boolean?,
@SerialName("name")
val name: String?,
@@ -96,13 +86,8 @@ data class AccountJson(
@SerialName("kdfParallelism")
val kdfParallelism: Int?,
@SerialName("userDecryptionOptions")
@JsonNames("accountDecryptionOptions")
@SerialName("accountDecryptionOptions")
val userDecryptionOptions: UserDecryptionOptionsJson?,
@SerialName("creationDate")
@Contextual
val creationDate: ZonedDateTime?,
)
/**

View File

@@ -7,7 +7,6 @@ import kotlinx.serialization.Serializable
* Represents URLs for various Bitwarden domains.
*
* @property base The overall base URL.
* @property keyUri A Uri containing the alias and host of the key used for mutual TLS.
* @property api Separate base URL for the "/api" domain (if applicable).
* @property identity Separate base URL for the "/identity" domain (if applicable).
* @property icon Separate base URL for the icon domain (if applicable).
@@ -20,9 +19,6 @@ data class EnvironmentUrlDataJson(
@SerialName("base")
val base: String,
@SerialName("keyUri")
val keyUri: String? = null,
@SerialName("api")
val api: String? = null,
@@ -55,7 +51,6 @@ data class EnvironmentUrlDataJson(
*/
val DEFAULT_LEGACY_US: EnvironmentUrlDataJson = EnvironmentUrlDataJson(
base = "https://vault.bitwarden.com",
keyUri = null,
api = "https://api.bitwarden.com",
identity = "https://identity.bitwarden.com",
icon = "https://icons.bitwarden.net",
@@ -76,7 +71,6 @@ data class EnvironmentUrlDataJson(
*/
val DEFAULT_LEGACY_EU: EnvironmentUrlDataJson = EnvironmentUrlDataJson(
base = "https://vault.bitwarden.eu",
keyUri = null,
api = "https://api.bitwarden.eu",
identity = "https://identity.bitwarden.eu",
icon = "https://icons.bitwarden.eu",

View File

@@ -1,60 +0,0 @@
package com.x8bit.bitwarden.data.auth.datasource.disk.model
import kotlinx.serialization.Contextual
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import java.time.ZonedDateTime
/**
* Describes the current display status of the new device notice screen.
*/
@Serializable
enum class NewDeviceNoticeDisplayStatus {
/**
* The user has seen the screen and indicated they can access their email.
*/
@SerialName("canAccessEmail")
CAN_ACCESS_EMAIL,
/**
* The user has indicated they can access their email
* as specified by the Permanent mode of the notice.
*/
@SerialName("canAccessEmailPermanent")
CAN_ACCESS_EMAIL_PERMANENT,
/**
* The user has not seen the screen.
*/
@SerialName("hasNotSeen")
HAS_NOT_SEEN,
/**
* The user has seen the screen and selected "remind me later".
*/
@SerialName("hasSeen")
HAS_SEEN,
}
/**
* The state of the new device notice screen.
*/
@Suppress("MagicNumber")
@Serializable
data class NewDeviceNoticeState(
@SerialName("displayStatus")
val displayStatus: NewDeviceNoticeDisplayStatus,
@SerialName("lastSeenDate")
@Contextual
val lastSeenDate: ZonedDateTime?,
) {
/**
* Whether the [lastSeenDate] is at least 7 days old.
*/
val shouldDisplayNoticeIfSeen = lastSeenDate
?.isBefore(
ZonedDateTime.now().minusDays(7),
)
?: false
}

View File

@@ -3,7 +3,6 @@ package com.x8bit.bitwarden.data.auth.datasource.network.api
import com.x8bit.bitwarden.data.auth.datasource.network.model.KeyConnectorKeyRequestJson
import com.x8bit.bitwarden.data.auth.datasource.network.model.PasswordHintRequestJson
import com.x8bit.bitwarden.data.auth.datasource.network.model.ResendEmailRequestJson
import com.x8bit.bitwarden.data.auth.datasource.network.model.ResendNewDeviceOtpRequestJson
import com.x8bit.bitwarden.data.platform.datasource.network.model.NetworkResult
import com.x8bit.bitwarden.data.platform.datasource.network.util.HEADER_KEY_AUTHORIZATION
import retrofit2.http.Body
@@ -29,9 +28,4 @@ interface UnauthenticatedAccountsApi {
@Body body: KeyConnectorKeyRequestJson,
@Header(HEADER_KEY_AUTHORIZATION) bearerToken: String,
): NetworkResult<Unit>
@POST("/accounts/resend-new-device-otp")
suspend fun resendNewDeviceOtp(
@Body body: ResendNewDeviceOtpRequestJson,
): NetworkResult<Unit>
}

View File

@@ -47,13 +47,12 @@ interface UnauthenticatedIdentityApi {
@Field(value = "twoFactorProvider") twoFactorMethod: String?,
@Field(value = "twoFactorRemember") twoFactorRemember: String?,
@Field(value = "authRequest") authRequestId: String?,
@Field(value = "newDeviceOtp") newDeviceOtp: String?,
): NetworkResult<GetTokenResponseJson.Success>
@GET("/sso/prevalidate")
suspend fun prevalidateSso(
@Query("domainHint") organizationIdentifier: String,
): NetworkResult<PrevalidateSsoResponseJson.Success>
): NetworkResult<PrevalidateSsoResponseJson>
/**
* This call needs to be synchronous so we need it to return a [Call] directly. The identity

View File

@@ -21,7 +21,5 @@ enum class AuthRequestTypeJson {
}
@Keep
private class AuthRequestTypeSerializer : BaseEnumeratedIntSerializer<AuthRequestTypeJson>(
className = "AuthRequestTypeJson",
values = AuthRequestTypeJson.entries.toTypedArray(),
)
private class AuthRequestTypeSerializer :
BaseEnumeratedIntSerializer<AuthRequestTypeJson>(AuthRequestTypeJson.entries.toTypedArray())

View File

@@ -107,28 +107,6 @@ sealed class GetTokenResponseJson {
val errorMessage: String?
get() = errorModel?.errorMessage ?: legacyErrorModel?.errorMessage
/**
* The type of invalid responses that can be received.
*/
sealed class InvalidType {
/**
* Represents an invalid response indicating that a new device verification is required.
*/
data object NewDeviceVerification : InvalidType()
/**
* Represents generic invalid response
*/
data object GenericInvalid : InvalidType()
}
val invalidType: InvalidType
get() = if (errorMessage?.lowercase() == "new device verification required") {
InvalidType.NewDeviceVerification
} else {
InvalidType.GenericInvalid
}
/**
* The error body of an invalid request containing a message.
*/

View File

@@ -18,7 +18,5 @@ enum class KdfTypeJson {
}
@Keep
private class KdfTypeSerializer : BaseEnumeratedIntSerializer<KdfTypeJson>(
className = "KdfTypeJson",
values = KdfTypeJson.entries.toTypedArray(),
)
private class KdfTypeSerializer :
BaseEnumeratedIntSerializer<KdfTypeJson>(KdfTypeJson.entries.toTypedArray())

View File

@@ -1,19 +1,15 @@
package com.x8bit.bitwarden.data.auth.datasource.network.model
import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.JsonNames
/**
* Decryption options related to a user's key connector.
*
* @property keyConnectorUrl URL to the user's key connector.
*/
@OptIn(ExperimentalSerializationApi::class)
@Serializable
data class KeyConnectorUserDecryptionOptionsJson(
@SerialName("keyConnectorUrl")
@JsonNames("KeyConnectorUrl")
@SerialName("KeyConnectorUrl")
val keyConnectorUrl: String,
)

View File

@@ -7,20 +7,6 @@ import kotlinx.serialization.Serializable
* Response body from the SSO prevalidate request.
*/
@Serializable
sealed class PrevalidateSsoResponseJson {
/**
* Models json body of a successful response.
*/
@Serializable
data class Success(
@SerialName("token") val token: String?,
) : PrevalidateSsoResponseJson()
/**
* Models json body of an error response.
*/
@Serializable
data class Error(
@SerialName("message") val message: String?,
) : PrevalidateSsoResponseJson()
}
data class PrevalidateSsoResponseJson(
@SerialName("token") val token: String?,
)

View File

@@ -1,20 +0,0 @@
package com.x8bit.bitwarden.data.auth.datasource.network.model
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
/**
* Hold the information necessary to resend the email with the
* new device verification code.
*
* @property email The user's email address.
* @property passwordHash The master password hash
*/
@Serializable
data class ResendNewDeviceOtpRequestJson(
@SerialName("Email")
val email: String,
@SerialName("MasterPasswordHash")
val passwordHash: String?,
)

View File

@@ -1,9 +1,7 @@
package com.x8bit.bitwarden.data.auth.datasource.network.model
import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.JsonNames
/**
* Decryption options related to a user's trusted device.
@@ -15,26 +13,20 @@ import kotlinx.serialization.json.JsonNames
* @property hasManageResetPasswordPermission Whether or not the user has manage reset password
* permission.
*/
@OptIn(ExperimentalSerializationApi::class)
@Serializable
data class TrustedDeviceUserDecryptionOptionsJson(
@SerialName("encryptedPrivateKey")
@JsonNames("EncryptedPrivateKey")
@SerialName("EncryptedPrivateKey")
val encryptedPrivateKey: String?,
@SerialName("encryptedUserKey")
@JsonNames("EncryptedUserKey")
@SerialName("EncryptedUserKey")
val encryptedUserKey: String?,
@SerialName("hasAdminApproval")
@JsonNames("HasAdminApproval")
@SerialName("HasAdminApproval")
val hasAdminApproval: Boolean,
@SerialName("hasLoginApprovingDevice")
@JsonNames("HasLoginApprovingDevice")
@SerialName("HasLoginApprovingDevice")
val hasLoginApprovingDevice: Boolean,
@SerialName("hasManageResetPasswordPermission")
@JsonNames("HasManageResetPasswordPermission")
@SerialName("HasManageResetPasswordPermission")
val hasManageResetPasswordPermission: Boolean,
)

View File

@@ -39,7 +39,5 @@ enum class TwoFactorAuthMethod(val value: UInt) {
}
@Keep
private class TwoFactorAuthMethodSerializer : BaseEnumeratedIntSerializer<TwoFactorAuthMethod>(
className = "TwoFactorAuthMethod",
values = TwoFactorAuthMethod.entries.toTypedArray(),
)
private class TwoFactorAuthMethodSerializer :
BaseEnumeratedIntSerializer<TwoFactorAuthMethod>(TwoFactorAuthMethod.entries.toTypedArray())

View File

@@ -1,9 +1,7 @@
package com.x8bit.bitwarden.data.auth.datasource.network.model
import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.JsonNames
/**
* The options available to a user for decryption.
@@ -14,18 +12,14 @@ import kotlinx.serialization.json.JsonNames
* device.
* @property keyConnectorUserDecryptionOptions Decryption options related to a user's key connector.
*/
@OptIn(ExperimentalSerializationApi::class)
@Serializable
data class UserDecryptionOptionsJson(
@SerialName("hasMasterPassword")
@JsonNames("HasMasterPassword")
@SerialName("HasMasterPassword")
val hasMasterPassword: Boolean,
@SerialName("trustedDeviceOption")
@JsonNames("TrustedDeviceOption")
@SerialName("TrustedDeviceOption")
val trustedDeviceUserDecryptionOptions: TrustedDeviceUserDecryptionOptionsJson?,
@SerialName("keyConnectorOption")
@JsonNames("KeyConnectorOption")
@SerialName("KeyConnectorOption")
val keyConnectorUserDecryptionOptions: KeyConnectorUserDecryptionOptionsJson?,
)

View File

@@ -5,7 +5,6 @@ import com.x8bit.bitwarden.data.auth.datasource.network.model.KeyConnectorKeyReq
import com.x8bit.bitwarden.data.auth.datasource.network.model.KeyConnectorMasterKeyResponseJson
import com.x8bit.bitwarden.data.auth.datasource.network.model.PasswordHintResponseJson
import com.x8bit.bitwarden.data.auth.datasource.network.model.ResendEmailRequestJson
import com.x8bit.bitwarden.data.auth.datasource.network.model.ResendNewDeviceOtpRequestJson
import com.x8bit.bitwarden.data.auth.datasource.network.model.ResetPasswordRequestJson
import com.x8bit.bitwarden.data.auth.datasource.network.model.SetPasswordRequestJson
@@ -53,11 +52,6 @@ interface AccountsService {
*/
suspend fun resendVerificationCodeEmail(body: ResendEmailRequestJson): Result<Unit>
/**
* Resend the email with the verification code for new devices
*/
suspend fun resendNewDeviceOtp(body: ResendNewDeviceOtpRequestJson): Result<Unit>
/**
* Reset the password.
*/

View File

@@ -13,7 +13,6 @@ import com.x8bit.bitwarden.data.auth.datasource.network.model.KeyConnectorMaster
import com.x8bit.bitwarden.data.auth.datasource.network.model.PasswordHintRequestJson
import com.x8bit.bitwarden.data.auth.datasource.network.model.PasswordHintResponseJson
import com.x8bit.bitwarden.data.auth.datasource.network.model.ResendEmailRequestJson
import com.x8bit.bitwarden.data.auth.datasource.network.model.ResendNewDeviceOtpRequestJson
import com.x8bit.bitwarden.data.auth.datasource.network.model.ResetPasswordRequestJson
import com.x8bit.bitwarden.data.auth.datasource.network.model.SetPasswordRequestJson
import com.x8bit.bitwarden.data.auth.datasource.network.model.VerifyOtpRequestJson
@@ -115,11 +114,6 @@ class AccountsServiceImpl(
.resendVerificationCodeEmail(body = body)
.toResult()
override suspend fun resendNewDeviceOtp(body: ResendNewDeviceOtpRequestJson): Result<Unit> =
unauthenticatedAccountsApi
.resendNewDeviceOtp(body = body)
.toResult()
override suspend fun resetPassword(body: ResetPasswordRequestJson): Result<Unit> =
if (body.currentPasswordHash == null) {
authenticatedAccountsApi

View File

@@ -9,7 +9,6 @@ import com.x8bit.bitwarden.data.auth.datasource.network.model.RegisterFinishRequ
import com.x8bit.bitwarden.data.auth.datasource.network.model.RegisterRequestJson
import com.x8bit.bitwarden.data.auth.datasource.network.model.RegisterResponseJson
import com.x8bit.bitwarden.data.auth.datasource.network.model.SendVerificationEmailRequestJson
import com.x8bit.bitwarden.data.auth.datasource.network.model.SendVerificationEmailResponseJson
import com.x8bit.bitwarden.data.auth.datasource.network.model.TwoFactorDataModel
import com.x8bit.bitwarden.data.auth.datasource.network.model.VerifyEmailTokenRequestJson
import com.x8bit.bitwarden.data.auth.datasource.network.model.VerifyEmailTokenResponseJson
@@ -46,7 +45,6 @@ interface IdentityService {
authModel: IdentityTokenAuthModel,
captchaToken: String?,
twoFactorData: TwoFactorDataModel? = null,
newDeviceOtp: String? = null,
): Result<GetTokenResponseJson>
/**
@@ -70,7 +68,7 @@ interface IdentityService {
*/
suspend fun sendVerificationEmail(
body: SendVerificationEmailRequestJson,
): Result<SendVerificationEmailResponseJson>
): Result<String?>
/**
* Register a new account to Bitwarden using email verification flow.

View File

@@ -11,7 +11,6 @@ import com.x8bit.bitwarden.data.auth.datasource.network.model.RegisterFinishRequ
import com.x8bit.bitwarden.data.auth.datasource.network.model.RegisterRequestJson
import com.x8bit.bitwarden.data.auth.datasource.network.model.RegisterResponseJson
import com.x8bit.bitwarden.data.auth.datasource.network.model.SendVerificationEmailRequestJson
import com.x8bit.bitwarden.data.auth.datasource.network.model.SendVerificationEmailResponseJson
import com.x8bit.bitwarden.data.auth.datasource.network.model.TwoFactorDataModel
import com.x8bit.bitwarden.data.auth.datasource.network.model.VerifyEmailTokenRequestJson
import com.x8bit.bitwarden.data.auth.datasource.network.model.VerifyEmailTokenResponseJson
@@ -60,7 +59,6 @@ class IdentityServiceImpl(
authModel: IdentityTokenAuthModel,
captchaToken: String?,
twoFactorData: TwoFactorDataModel?,
newDeviceOtp: String?,
): Result<GetTokenResponseJson> = unauthenticatedIdentityApi
.getToken(
scope = "api offline_access",
@@ -80,28 +78,22 @@ class IdentityServiceImpl(
twoFactorRemember = twoFactorData?.remember?.let { if (it) "1" else "0 " },
captchaResponse = captchaToken,
authRequestId = authModel.authRequestId,
newDeviceOtp = newDeviceOtp,
)
.toResult()
.recoverCatching { throwable ->
val bitwardenError = throwable.toBitwardenError()
bitwardenError
.parseErrorBodyOrNull<GetTokenResponseJson.CaptchaRequired>(
code = 400,
json = json,
)
?: bitwardenError.parseErrorBodyOrNull<GetTokenResponseJson.TwoFactorRequired>(
code = 400,
json = json,
)
?: bitwardenError.parseErrorBodyOrNull<GetTokenResponseJson.Invalid>(
code = 400,
json = json,
)
?: throw throwable
bitwardenError.parseErrorBodyOrNull<GetTokenResponseJson.CaptchaRequired>(
code = 400,
json = json,
) ?: bitwardenError.parseErrorBodyOrNull<GetTokenResponseJson.TwoFactorRequired>(
code = 400,
json = json,
) ?: bitwardenError.parseErrorBodyOrNull<GetTokenResponseJson.Invalid>(
code = 400,
json = json,
) ?: throw throwable
}
@Suppress("MagicNumber")
override suspend fun prevalidateSso(
organizationIdentifier: String,
): Result<PrevalidateSsoResponseJson> = unauthenticatedIdentityApi
@@ -109,15 +101,6 @@ class IdentityServiceImpl(
organizationIdentifier = organizationIdentifier,
)
.toResult()
.recoverCatching { throwable ->
val bitwardenError = throwable.toBitwardenError()
bitwardenError
.parseErrorBodyOrNull<PrevalidateSsoResponseJson.Error>(
code = 400,
json = json,
)
?: throw throwable
}
override fun refreshTokenSynchronously(
refreshToken: String,
@@ -147,26 +130,15 @@ class IdentityServiceImpl(
?: throw throwable
}
@Suppress("MagicNumber")
override suspend fun sendVerificationEmail(
body: SendVerificationEmailRequestJson,
): Result<SendVerificationEmailResponseJson> {
): Result<String?> {
return unauthenticatedIdentityApi
.sendVerificationEmail(body = body)
.toResult()
.map { SendVerificationEmailResponseJson.Success(it?.content) }
.recoverCatching { throwable ->
throwable
.toBitwardenError()
.parseErrorBodyOrNull<SendVerificationEmailResponseJson.Invalid>(
code = 400,
json = json,
)
?: throw throwable
}
.map { it?.content }
}
@Suppress("MagicNumber")
override suspend fun verifyEmailRegistrationToken(
body: VerifyEmailTokenRequestJson,
): Result<VerifyEmailTokenResponseJson> = unauthenticatedIdentityApi

View File

@@ -8,7 +8,7 @@ import com.bitwarden.core.RegisterKeyResponse
import com.bitwarden.core.RegisterTdeKeyResponse
import com.bitwarden.crypto.HashPurpose
import com.bitwarden.crypto.Kdf
import com.bitwarden.sdk.AuthClient
import com.bitwarden.sdk.ClientAuth
import com.x8bit.bitwarden.data.auth.datasource.sdk.model.PasswordStrength
import com.x8bit.bitwarden.data.auth.datasource.sdk.util.toPasswordStrengthOrNull
import com.x8bit.bitwarden.data.auth.datasource.sdk.util.toUByte
@@ -17,7 +17,7 @@ import com.x8bit.bitwarden.data.platform.manager.SdkClientManager
/**
* Primary implementation of [AuthSdkSource] that serves as a convenience wrapper around a
* [AuthClient].
* [ClientAuth].
*/
class AuthSdkSourceImpl(
sdkClientManager: SdkClientManager,

View File

@@ -55,5 +55,5 @@ class TrustedDeviceManagerImpl(
authDiskSource.storeIsTdeLoginComplete(userId = userId, isTdeLoginComplete = true)
}
.also { authDiskSource.storeShouldTrustDevice(userId = userId, shouldTrustDevice = null) }
.map { }
.map { Unit }
}

View File

@@ -1,7 +1,6 @@
package com.x8bit.bitwarden.data.auth.repository
import com.x8bit.bitwarden.data.auth.datasource.disk.model.ForcePasswordResetReason
import com.x8bit.bitwarden.data.auth.datasource.disk.model.NewDeviceNoticeState
import com.x8bit.bitwarden.data.auth.datasource.disk.model.OnboardingStatus
import com.x8bit.bitwarden.data.auth.datasource.network.model.GetTokenResponseJson
import com.x8bit.bitwarden.data.auth.datasource.network.model.TwoFactorDataModel
@@ -230,19 +229,6 @@ interface AuthRepository : AuthenticatorProvider, AuthRequestManager {
organizationIdentifier: String,
): LoginResult
/**
* Repeat the previous login attempt but this time with New Device OTP
* information. Password is included if available to unlock the vault after
* authentication. Updated access token will be reflected in [authStateFlow].
*/
suspend fun login(
email: String,
password: String?,
newDeviceOtp: String,
captchaToken: String?,
orgIdentifier: String?,
): LoginResult
/**
* Log out the current user.
*/
@@ -265,11 +251,6 @@ interface AuthRepository : AuthenticatorProvider, AuthRequestManager {
*/
suspend fun resendVerificationCodeEmail(): ResendEmailResult
/**
* Resend the email with the new device verification code.
*/
suspend fun resendNewDeviceOtp(): ResendEmailResult
/**
* Switches to the account corresponding to the given [userId] if possible.
*/
@@ -380,10 +361,8 @@ interface AuthRepository : AuthenticatorProvider, AuthRequestManager {
/**
* Get the password strength for the given [email] and [password] combo.
* If no value is passed for the [email] will use the active email of the current active
* account via the [userStateFlow].
*/
suspend fun getPasswordStrength(email: String? = null, password: String): PasswordStrengthResult
suspend fun getPasswordStrength(email: String, password: String): PasswordStrengthResult
/**
* Validates the master password for the current logged in user.
@@ -421,20 +400,5 @@ interface AuthRepository : AuthenticatorProvider, AuthRequestManager {
/**
* Update the value of the onboarding status for the user.
*/
fun setOnboardingStatus(status: OnboardingStatus)
/**
* Checks if a new device notice should be displayed.
*/
fun checkUserNeedsNewDeviceTwoFactorNotice(): Boolean
/**
* Gets the new device notice state of active user.
*/
fun getNewDeviceNoticeState(): NewDeviceNoticeState?
/**
* Stores the new device notice state for active user.
*/
fun setNewDeviceNoticeState(newState: NewDeviceNoticeState?)
fun setOnboardingStatus(userId: String, status: OnboardingStatus?)
}

View File

@@ -2,14 +2,13 @@ package com.x8bit.bitwarden.data.auth.repository
import com.bitwarden.core.AuthRequestMethod
import com.bitwarden.core.InitUserCryptoMethod
import com.bitwarden.core.InitUserCryptoRequest
import com.bitwarden.crypto.HashPurpose
import com.bitwarden.crypto.Kdf
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.datasource.disk.model.AccountTokensJson
import com.x8bit.bitwarden.data.auth.datasource.disk.model.ForcePasswordResetReason
import com.x8bit.bitwarden.data.auth.datasource.disk.model.NewDeviceNoticeDisplayStatus
import com.x8bit.bitwarden.data.auth.datasource.disk.model.NewDeviceNoticeState
import com.x8bit.bitwarden.data.auth.datasource.disk.model.OnboardingStatus
import com.x8bit.bitwarden.data.auth.datasource.disk.model.UserStateJson
import com.x8bit.bitwarden.data.auth.datasource.network.model.DeleteAccountResponseJson
@@ -17,16 +16,13 @@ import com.x8bit.bitwarden.data.auth.datasource.network.model.DeviceDataModel
import com.x8bit.bitwarden.data.auth.datasource.network.model.GetTokenResponseJson
import com.x8bit.bitwarden.data.auth.datasource.network.model.IdentityTokenAuthModel
import com.x8bit.bitwarden.data.auth.datasource.network.model.PasswordHintResponseJson
import com.x8bit.bitwarden.data.auth.datasource.network.model.PrevalidateSsoResponseJson
import com.x8bit.bitwarden.data.auth.datasource.network.model.RefreshTokenResponseJson
import com.x8bit.bitwarden.data.auth.datasource.network.model.RegisterFinishRequestJson
import com.x8bit.bitwarden.data.auth.datasource.network.model.RegisterRequestJson
import com.x8bit.bitwarden.data.auth.datasource.network.model.RegisterResponseJson
import com.x8bit.bitwarden.data.auth.datasource.network.model.ResendEmailRequestJson
import com.x8bit.bitwarden.data.auth.datasource.network.model.ResendNewDeviceOtpRequestJson
import com.x8bit.bitwarden.data.auth.datasource.network.model.ResetPasswordRequestJson
import com.x8bit.bitwarden.data.auth.datasource.network.model.SendVerificationEmailRequestJson
import com.x8bit.bitwarden.data.auth.datasource.network.model.SendVerificationEmailResponseJson
import com.x8bit.bitwarden.data.auth.datasource.network.model.SetPasswordRequestJson
import com.x8bit.bitwarden.data.auth.datasource.network.model.TrustedDeviceUserDecryptionOptionsJson
import com.x8bit.bitwarden.data.auth.datasource.network.model.TwoFactorAuthMethod
@@ -98,7 +94,6 @@ import com.x8bit.bitwarden.data.auth.util.KdfParamsConstants.DEFAULT_PBKDF2_ITER
import com.x8bit.bitwarden.data.auth.util.YubiKeyResult
import com.x8bit.bitwarden.data.auth.util.toSdkParams
import com.x8bit.bitwarden.data.platform.datasource.disk.ConfigDiskSource
import com.x8bit.bitwarden.data.platform.datasource.network.util.isSslHandShakeError
import com.x8bit.bitwarden.data.platform.manager.FeatureFlagManager
import com.x8bit.bitwarden.data.platform.manager.FirstTimeActionManager
import com.x8bit.bitwarden.data.platform.manager.LogsManager
@@ -110,7 +105,6 @@ import com.x8bit.bitwarden.data.platform.manager.model.FlagKey
import com.x8bit.bitwarden.data.platform.manager.util.getActivePolicies
import com.x8bit.bitwarden.data.platform.repository.EnvironmentRepository
import com.x8bit.bitwarden.data.platform.repository.SettingsRepository
import com.x8bit.bitwarden.data.platform.repository.model.Environment
import com.x8bit.bitwarden.data.platform.repository.util.bufferedMutableSharedFlow
import com.x8bit.bitwarden.data.platform.repository.util.toEnvironmentUrls
import com.x8bit.bitwarden.data.platform.util.asFailure
@@ -120,6 +114,7 @@ import com.x8bit.bitwarden.data.vault.datasource.network.model.OrganizationType
import com.x8bit.bitwarden.data.vault.datasource.network.model.PolicyTypeJson
import com.x8bit.bitwarden.data.vault.datasource.network.model.SyncResponseJson
import com.x8bit.bitwarden.data.vault.datasource.sdk.VaultSdkSource
import com.x8bit.bitwarden.data.vault.datasource.sdk.model.InitializeCryptoResult
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.VaultUnlockError
@@ -146,7 +141,6 @@ import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.receiveAsFlow
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.flow.update
import java.time.ZonedDateTime
import javax.inject.Singleton
/**
@@ -226,11 +220,6 @@ class AuthRepositoryImpl(
*/
private var resendEmailRequestJson: ResendEmailRequestJson? = null
/**
* The information necessary to resend the verification code email for new devices.
*/
private var resendNewDeviceOtpRequestJson: ResendNewDeviceOtpRequestJson? = null
private var organizationIdentifier: String? = null
/**
@@ -636,12 +625,7 @@ class AuthRepositoryImpl(
)
}
.fold(
onFailure = { throwable ->
when {
throwable.isSslHandShakeError() -> LoginResult.CertificateError
else -> LoginResult.Error(errorMessage = null)
}
},
onFailure = { LoginResult.Error(errorMessage = null) },
onSuccess = { it },
)
@@ -690,26 +674,6 @@ class AuthRepositoryImpl(
}
?: LoginResult.Error(errorMessage = null)
override suspend fun login(
email: String,
password: String?,
newDeviceOtp: String,
captchaToken: String?,
orgIdentifier: String?,
): LoginResult = identityTokenAuthModel
?.let {
loginCommon(
email = email,
password = password,
authModel = it,
newDeviceOtp = newDeviceOtp,
captchaToken = captchaToken ?: twoFactorResponse?.captchaToken,
deviceData = twoFactorDeviceData,
orgIdentifier = orgIdentifier,
)
}
?: LoginResult.Error(errorMessage = null)
override suspend fun login(
email: String,
ssoCode: String,
@@ -791,16 +755,6 @@ class AuthRepositoryImpl(
}
?: ResendEmailResult.Error(message = null)
override suspend fun resendNewDeviceOtp(): ResendEmailResult =
resendNewDeviceOtpRequestJson
?.let { jsonRequest ->
accountsService.resendNewDeviceOtp(body = jsonRequest).fold(
onFailure = { ResendEmailResult.Error(message = it.message) },
onSuccess = { ResendEmailResult.Success },
)
}
?: ResendEmailResult.Error(message = null)
override fun switchAccount(userId: String): SwitchAccountResult {
val currentUserState = authDiskSource.userState ?: return SwitchAccountResult.NoChange
val previousActiveUserId = currentUserState.activeUserId
@@ -1125,7 +1079,6 @@ class AuthRepositoryImpl(
}
is VaultUnlockResult.AuthenticationError,
VaultUnlockResult.BiometricDecodingError,
VaultUnlockResult.InvalidStateError,
VaultUnlockResult.GenericError,
-> {
@@ -1200,21 +1153,13 @@ class AuthRepositoryImpl(
)
.fold(
onSuccess = {
when (it) {
is PrevalidateSsoResponseJson.Error -> {
PrevalidateSsoResult.Failure(message = it.message)
}
is PrevalidateSsoResponseJson.Success -> {
if (it.token.isNullOrBlank()) {
PrevalidateSsoResult.Failure()
} else {
PrevalidateSsoResult.Success(token = it.token)
}
}
if (it.token.isNullOrBlank()) {
PrevalidateSsoResult.Failure
} else {
PrevalidateSsoResult.Success(it.token)
}
},
onFailure = { PrevalidateSsoResult.Failure() },
onFailure = { PrevalidateSsoResult.Failure },
)
override fun setSsoCallbackResult(result: SsoCallbackResult) {
@@ -1241,17 +1186,12 @@ class AuthRepositoryImpl(
)
override suspend fun getPasswordStrength(
email: String?,
email: String,
password: String,
): PasswordStrengthResult =
authSdkSource
.passwordStrength(
email = email
?: userStateFlow
.value
?.activeAccount
?.email
.orEmpty(),
email = email,
password = password,
)
.fold(
@@ -1309,17 +1249,41 @@ class AuthRepositoryImpl(
?.activeAccount
?.profile
?: return ValidatePinResult.Error
val privateKey = authDiskSource
.getPrivateKey(userId = activeAccount.userId)
?: return ValidatePinResult.Error
val pinProtectedUserKey = authDiskSource
.getPinProtectedUserKey(userId = activeAccount.userId)
?: return ValidatePinResult.Error
// HACK: As the SDK doesn't provide a way to directly validate the pin yet, we instead
// try to initialize the user crypto, and if it succeeds then the PIN is correct, otherwise
// the PIN is incorrect.
return vaultSdkSource
.validatePin(
.initializeCrypto(
userId = activeAccount.userId,
pin = pin,
pinProtectedUserKey = pinProtectedUserKey,
request = InitUserCryptoRequest(
kdfParams = activeAccount.toSdkParams(),
email = activeAccount.email,
privateKey = privateKey,
method = InitUserCryptoMethod.Pin(
pin = pin,
pinProtectedUserKey = pinProtectedUserKey,
),
),
)
.fold(
onSuccess = { ValidatePinResult.Success(isValid = it) },
onSuccess = {
when (it) {
InitializeCryptoResult.Success -> {
ValidatePinResult.Success(isValid = true)
}
is InitializeCryptoResult.AuthenticationError -> {
ValidatePinResult.Success(isValid = false)
}
}
},
onFailure = { ValidatePinResult.Error },
)
}
@@ -1344,15 +1308,7 @@ class AuthRepositoryImpl(
)
.fold(
onSuccess = {
when (it) {
is SendVerificationEmailResponseJson.Invalid -> {
SendVerificationEmailResult.Error(it.message)
}
is SendVerificationEmailResponseJson.Success -> {
SendVerificationEmailResult.Success(it.emailVerificationToken)
}
}
SendVerificationEmailResult.Success(it)
},
onFailure = {
SendVerificationEmailResult.Error(null)
@@ -1384,99 +1340,8 @@ class AuthRepositoryImpl(
)
}
override fun setOnboardingStatus(status: OnboardingStatus) {
activeUserId?.let { userId ->
authDiskSource.storeOnboardingStatus(
userId = userId,
onboardingStatus = status,
)
}
}
override fun getNewDeviceNoticeState(): NewDeviceNoticeState? {
return activeUserId?.let { userId ->
authDiskSource.getNewDeviceNoticeState(userId = userId)
}
}
override fun setNewDeviceNoticeState(newState: NewDeviceNoticeState?) {
activeUserId?.let { userId ->
authDiskSource.storeNewDeviceNoticeState(userId = userId, newState = newState)
}
}
override fun checkUserNeedsNewDeviceTwoFactorNotice(): Boolean {
return activeUserId?.let { userId ->
val temporaryFlag = featureFlagManager.getFeatureFlag(FlagKey.NewDeviceTemporaryDismiss)
val permanentFlag = featureFlagManager.getFeatureFlag(FlagKey.NewDevicePermanentDismiss)
// check if feature flags are disabled
if (!temporaryFlag && !permanentFlag) {
return false
}
if (!newDeviceNoticePreConditionsValid()) {
return false
}
val newDeviceNoticeState = authDiskSource.getNewDeviceNoticeState(userId = userId)
return when (newDeviceNoticeState.displayStatus) {
// if the user has already attested email access but permanent flag is enabled,
// the notice needs to appear again
NewDeviceNoticeDisplayStatus.CAN_ACCESS_EMAIL -> permanentFlag
// if the user has already seen but 7 days have already passed,
// the notice needs to appear again
NewDeviceNoticeDisplayStatus.HAS_SEEN ->
newDeviceNoticeState.shouldDisplayNoticeIfSeen
NewDeviceNoticeDisplayStatus.HAS_NOT_SEEN -> true
// the user never needs to see the notice again
NewDeviceNoticeDisplayStatus.CAN_ACCESS_EMAIL_PERMANENT -> false
}
}
?: false
}
/**
* Checks if the preconditions are met for a user to see a new device notice:
* - Must be a Bitwarden cloud user.
* - The account must be at least one week old.
* - Cannot have an active policy requiring SSO to be enabled.
* - Cannot have two-factor authentication enabled.
*/
private fun newDeviceNoticePreConditionsValid(): Boolean {
val checkEnvironment = !featureFlagManager.getFeatureFlag(FlagKey.IgnoreEnvironmentCheck)
val isSelfHosted = environmentRepository.environment.type == Environment.Type.SELF_HOSTED
if (checkEnvironment && isSelfHosted) {
return false
}
val userProfile = authDiskSource.userState?.activeAccount?.profile
val isProfileAtLeastWeekOld = userProfile
?.let {
it.creationDate
?.plusWeeks(1)
?.isBefore(
ZonedDateTime.now(),
)
}
?: false
if (!isProfileAtLeastWeekOld) {
return false
}
val hasTwoFactorEnabled = userProfile
?.isTwoFactorEnabled
?: false
if (hasTwoFactorEnabled) {
return false
}
val hasSSOPolicy =
policyManager.getActivePolicies(type = PolicyTypeJson.REQUIRE_SSO)
.any { p -> p.isEnabled }
return !hasSSOPolicy
override fun setOnboardingStatus(userId: String, status: OnboardingStatus?) {
authDiskSource.storeOnboardingStatus(userId = userId, onboardingStatus = status)
}
@Suppress("CyclomaticComplexMethod")
@@ -1634,7 +1499,6 @@ class AuthRepositoryImpl(
deviceData: DeviceDataModel? = null,
orgIdentifier: String? = null,
captchaToken: String?,
newDeviceOtp: String? = null,
): LoginResult = identityService
.getToken(
uniqueAppId = authDiskSource.uniqueAppId,
@@ -1642,16 +1506,11 @@ class AuthRepositoryImpl(
authModel = authModel,
twoFactorData = twoFactorData ?: getRememberedTwoFactorData(email),
captchaToken = captchaToken,
newDeviceOtp = newDeviceOtp,
)
.fold(
onFailure = { throwable ->
when {
throwable.isSslHandShakeError() -> LoginResult.CertificateError
configDiskSource.serverConfig?.isOfficialBitwardenServer == false -> {
LoginResult.UnofficialServerError
}
onFailure = {
when (configDiskSource.serverConfig?.isOfficialBitwardenServer) {
false -> LoginResult.UnofficialServerError
else -> LoginResult.Error(errorMessage = null)
}
},
@@ -1676,22 +1535,9 @@ class AuthRepositoryImpl(
orgIdentifier = orgIdentifier,
)
is GetTokenResponseJson.Invalid -> {
when (loginResponse.invalidType) {
is GetTokenResponseJson.Invalid.InvalidType.NewDeviceVerification ->
handleLoginCommonNewDeviceVerification(
email = email,
authModel = authModel,
error = loginResponse.errorMessage,
)
is GetTokenResponseJson.Invalid.InvalidType.GenericInvalid -> {
LoginResult.Error(
errorMessage = loginResponse.errorMessage,
)
}
}
}
is GetTokenResponseJson.Invalid -> LoginResult.Error(
errorMessage = loginResponse.errorMessage,
)
}
},
)
@@ -1779,6 +1625,15 @@ class AuthRepositoryImpl(
)
settingsRepository.hasUserLoggedInOrCreatedAccount = true
val shouldSetOnboardingStatus = featureFlagManager.getFeatureFlag(FlagKey.OnboardingFlow) &&
!settingsRepository.getUserHasLoggedInValue(userId = userId)
if (shouldSetOnboardingStatus) {
setOnboardingStatus(
userId = userId,
status = OnboardingStatus.NOT_STARTED,
)
}
authDiskSource.userState = userStateJson
loginResponse.key?.let {
// Only set the value if it's present, since we may have set it already
@@ -1807,7 +1662,6 @@ class AuthRepositoryImpl(
twoFactorResponse = null
resendEmailRequestJson = null
twoFactorDeviceData = null
resendNewDeviceOtpRequestJson = null
settingsRepository.setDefaultsIfNecessary(userId = userId)
settingsRepository.storeUserHasLoggedInValue(userId)
vaultRepository.syncIfNecessary()
@@ -1840,24 +1694,6 @@ class AuthRepositoryImpl(
return LoginResult.TwoFactorRequired
}
/**
* A helper method that processes the
* [GetTokenResponseJson.Invalid.InvalidType.NewDeviceVerification] when logging in.
*/
private fun handleLoginCommonNewDeviceVerification(
email: String,
authModel: IdentityTokenAuthModel,
error: String?,
): LoginResult {
identityTokenAuthModel = authModel
resendNewDeviceOtpRequestJson = ResendNewDeviceOtpRequestJson(
email = email,
passwordHash = authModel.password,
)
return LoginResult.NewDeviceVerification(error)
}
/**
* Attempt to unlock the current user's vault with key connector data.
*/

View File

@@ -28,14 +28,4 @@ sealed class LoginResult {
* There was an error while logging into an unofficial Bitwarden server.
*/
data object UnofficialServerError : LoginResult()
/**
* There was an error in validating the certificate chain for the server
*/
data object CertificateError : LoginResult()
/**
* New device verification is required
*/
data class NewDeviceVerification(val errorMessage: String?) : LoginResult()
}

View File

@@ -9,7 +9,6 @@ import com.x8bit.bitwarden.data.vault.repository.model.VaultUnlockResult
*/
fun VaultUnlockError.toLoginErrorResult(): LoginResult.Error = when (this) {
is VaultUnlockResult.AuthenticationError -> LoginResult.Error(this.message)
VaultUnlockResult.BiometricDecodingError,
VaultUnlockResult.GenericError,
VaultUnlockResult.InvalidStateError,
-> LoginResult.Error(errorMessage = null)

View File

@@ -14,7 +14,5 @@ sealed class PrevalidateSsoResult {
/**
* There was an error in prevalidation.
*/
data class Failure(
val message: String? = null,
) : PrevalidateSsoResult()
data object Failure : PrevalidateSsoResult()
}

View File

@@ -25,7 +25,6 @@ fun GetTokenResponseJson.Success.toUserState(
userId = userId,
email = jwtTokenData.email,
isEmailVerified = jwtTokenData.isEmailVerified,
isTwoFactorEnabled = null,
name = jwtTokenData.name,
stamp = null,
organizationId = null,
@@ -37,7 +36,6 @@ fun GetTokenResponseJson.Success.toUserState(
kdfMemory = this.kdfMemory,
kdfParallelism = this.kdfParallelism,
userDecryptionOptions = this.userDecryptionOptions,
creationDate = null,
),
settings = AccountJson.Settings(
environmentUrlData = environmentUrlData,

View File

@@ -3,7 +3,6 @@ package com.x8bit.bitwarden.data.auth.repository.util
import com.x8bit.bitwarden.data.auth.repository.model.JwtTokenDataJson
import com.x8bit.bitwarden.data.platform.datasource.network.util.base64UrlDecodeOrNull
import kotlinx.serialization.json.Json
import timber.log.Timber
/**
* Internal, generally basic [Json] instance for JWT parsing purposes.
@@ -18,24 +17,17 @@ private val json: Json by lazy {
/**
* Parses a [JwtTokenDataJson] from the given [jwtToken], or `null` if this parsing is not possible.
*/
@Suppress("MagicNumber", "TooGenericExceptionCaught")
@Suppress("MagicNumber")
fun parseJwtTokenDataOrNull(jwtToken: String): JwtTokenDataJson? {
val parts = jwtToken.split(".")
if (parts.size != 3) {
Timber.e(IllegalArgumentException("Incorrect number of parts"), "Invalid JWT Token")
return null
}
if (parts.size != 3) return null
val dataJson = parts[1]
val decodedDataJson = dataJson.base64UrlDecodeOrNull() ?: run {
Timber.e(IllegalArgumentException("Unable to decode"), "Invalid JWT Token")
return null
}
val decodedDataJson = dataJson.base64UrlDecodeOrNull() ?: return null
return try {
json.decodeFromString<JwtTokenDataJson>(decodedDataJson)
} catch (throwable: Throwable) {
Timber.e(throwable, "Failed to decode JwtTokenDataJson")
} catch (_: Throwable) {
null
}
}

View File

@@ -59,8 +59,6 @@ fun UserStateJson.toUpdatedUserStateJson(
avatarColorHex = syncProfile.avatarColor,
stamp = syncProfile.securityStamp,
hasPremium = syncProfile.isPremium || syncProfile.isPremiumFromOrganization,
isTwoFactorEnabled = syncProfile.isTwoFactorEnabled,
creationDate = syncProfile.creationDate,
)
val updatedAccount = account.copy(profile = updatedProfile)
return this

View File

@@ -3,7 +3,6 @@ package com.x8bit.bitwarden.data.autofill.accessibility.di
import android.content.Context
import android.content.pm.PackageManager
import android.os.PowerManager
import android.view.accessibility.AccessibilityManager
import com.x8bit.bitwarden.data.autofill.accessibility.manager.AccessibilityAutofillManager
import com.x8bit.bitwarden.data.autofill.accessibility.manager.AccessibilityAutofillManagerImpl
import com.x8bit.bitwarden.data.autofill.accessibility.manager.AccessibilityCompletionManager
@@ -56,12 +55,8 @@ object AccessibilityModule {
@Singleton
@Provides
fun providesAccessibilityEnabledManager(
accessibilityManager: AccessibilityManager,
): AccessibilityEnabledManager =
AccessibilityEnabledManagerImpl(
accessibilityManager = accessibilityManager,
)
fun providesAccessibilityEnabledManager(): AccessibilityEnabledManager =
AccessibilityEnabledManagerImpl()
@Singleton
@Provides
@@ -115,12 +110,6 @@ object AccessibilityModule {
@ApplicationContext context: Context,
): PackageManager = context.packageManager
@Singleton
@Provides
fun provideAccessibilityManager(
@ApplicationContext context: Context,
): AccessibilityManager = context.getSystemService(AccessibilityManager::class.java)
@Singleton
@Provides
fun providesPowerManager(

View File

@@ -0,0 +1,36 @@
package com.x8bit.bitwarden.data.autofill.accessibility.di
import android.content.Context
import androidx.lifecycle.LifecycleCoroutineScope
import com.x8bit.bitwarden.data.autofill.accessibility.manager.AccessibilityActivityManager
import com.x8bit.bitwarden.data.autofill.accessibility.manager.AccessibilityActivityManagerImpl
import com.x8bit.bitwarden.data.autofill.accessibility.manager.AccessibilityEnabledManager
import com.x8bit.bitwarden.data.platform.manager.AppStateManager
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.components.ActivityComponent
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.android.scopes.ActivityScoped
/**
* Provides dependencies within the accessibility package scoped to the activity.
*/
@Module
@InstallIn(ActivityComponent::class)
object ActivityAccessibilityModule {
@ActivityScoped
@Provides
fun providesAccessibilityActivityManager(
@ApplicationContext context: Context,
accessibilityEnabledManager: AccessibilityEnabledManager,
appStateManager: AppStateManager,
lifecycleScope: LifecycleCoroutineScope,
): AccessibilityActivityManager =
AccessibilityActivityManagerImpl(
context = context,
accessibilityEnabledManager = accessibilityEnabledManager,
appStateManager = appStateManager,
lifecycleScope = lifecycleScope,
)
}

View File

@@ -0,0 +1,10 @@
package com.x8bit.bitwarden.data.autofill.accessibility.manager
import android.app.Activity
/**
* A helper for dealing with accessibility configuration that must be scoped to a specific
* [Activity]. In particular, this should be injected into an [Activity] to ensure that the
* [AccessibilityEnabledManager] reports correct values.
*/
interface AccessibilityActivityManager

View File

@@ -0,0 +1,28 @@
package com.x8bit.bitwarden.data.autofill.accessibility.manager
import android.content.Context
import androidx.lifecycle.LifecycleCoroutineScope
import com.x8bit.bitwarden.data.autofill.accessibility.util.isAccessibilityServiceEnabled
import com.x8bit.bitwarden.data.platform.manager.AppStateManager
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
/**
* The default implementation of the [AccessibilityActivityManager].
*/
class AccessibilityActivityManagerImpl(
private val context: Context,
private val accessibilityEnabledManager: AccessibilityEnabledManager,
appStateManager: AppStateManager,
lifecycleScope: LifecycleCoroutineScope,
) : AccessibilityActivityManager {
init {
appStateManager
.appForegroundStateFlow
.onEach {
accessibilityEnabledManager.isAccessibilityEnabled =
context.isAccessibilityServiceEnabled
}
.launchIn(lifecycleScope)
}
}

View File

@@ -7,7 +7,15 @@ import kotlinx.coroutines.flow.StateFlow
*/
interface AccessibilityEnabledManager {
/**
* Emits updates that track whether the accessibility autofill service is enabled..
* Whether or not the accessibility service should be considered enabled.
*
* Note that changing this does not enable or disable autofill; it is only an indicator that
* this has occurred elsewhere.
*/
var isAccessibilityEnabled: Boolean
/**
* Emits updates that track [isAccessibilityEnabled] values.
*/
val isAccessibilityEnabledStateFlow: StateFlow<Boolean>
}

View File

@@ -1,6 +1,5 @@
package com.x8bit.bitwarden.data.autofill.accessibility.manager
import android.view.accessibility.AccessibilityManager
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
@@ -8,20 +7,14 @@ import kotlinx.coroutines.flow.asStateFlow
/**
* The default implementation of [AccessibilityEnabledManager].
*/
class AccessibilityEnabledManagerImpl(
accessibilityManager: AccessibilityManager,
) : AccessibilityEnabledManager {
private val mutableIsAccessibilityEnabledStateFlow = MutableStateFlow(
value = accessibilityManager.isEnabled,
)
class AccessibilityEnabledManagerImpl : AccessibilityEnabledManager {
private val mutableIsAccessibilityEnabledStateFlow = MutableStateFlow(value = false)
init {
accessibilityManager.addAccessibilityStateChangeListener(
AccessibilityManager.AccessibilityStateChangeListener { isEnabled ->
mutableIsAccessibilityEnabledStateFlow.value = isEnabled
},
)
}
override var isAccessibilityEnabled: Boolean
get() = mutableIsAccessibilityEnabledStateFlow.value
set(value) {
mutableIsAccessibilityEnabledStateFlow.value = value
}
override val isAccessibilityEnabledStateFlow: StateFlow<Boolean>
get() = mutableIsAccessibilityEnabledStateFlow.asStateFlow()

View File

@@ -8,6 +8,4 @@ import android.view.accessibility.AccessibilityNodeInfo
data class FillableFields(
val usernameField: AccessibilityNodeInfo?,
val passwordFields: List<AccessibilityNodeInfo>,
) {
val hasFields: Boolean = usernameField != null || passwordFields.isNotEmpty()
}
)

View File

@@ -35,10 +35,12 @@ class BitwardenAccessibilityProcessorImpl(
if (!powerManager.isInteractive) return
// We skip if the system package
if (eventNode.isSystemPackage) return
// We skip any package that is unsupported
if (eventNode.shouldSkipPackage) return
// We skip any package that is a launcher
if (launcherPackageNameManager.launcherPackages.any { it == eventNode.packageName }) {
// We skip any package that is a launcher or unsupported
if (eventNode.shouldSkipPackage ||
launcherPackageNameManager.launcherPackages.any { it == eventNode.packageName }
) {
// Clear the action since this event needs to be ignored completely
accessibilityAutofillManager.accessibilityAction = null
return
}
@@ -63,11 +65,6 @@ class BitwardenAccessibilityProcessorImpl(
private fun handleAttemptParseUri(rootNode: AccessibilityNodeInfo) {
accessibilityParser
.parseForUriOrPackageName(rootNode = rootNode)
?.takeIf {
accessibilityParser
.parseForFillableFields(rootNode = rootNode, uri = it)
.hasFields
}
?.let { uri ->
context.startActivity(
createAutofillSelectionIntent(

View File

@@ -128,11 +128,6 @@ private val ACCESSIBILITY_SUPPORTED_BROWSERS = listOf(
// 2nd = Anticipation
possibleUrlFieldIds = listOf("url_bar_title", "mozac_browser_toolbar_url_view"),
),
Browser(
packageName = "org.ironfoxoss.ironfox",
// 2nd = Legacy
possibleUrlFieldIds = listOf("mozac_browser_toolbar_url_view", "url_bar_title"),
),
Browser(packageName = "org.mozilla.fenix", urlFieldId = "mozac_browser_toolbar_url_view"),
// [DEPRECATED ENTRY]
Browser(
@@ -196,6 +191,11 @@ private val ACCESSIBILITY_SUPPORTED_BROWSERS = listOf(
),
Browser(packageName = "org.ungoogled.chromium.extensions.stable", urlFieldId = "url_bar"),
Browser(packageName = "org.ungoogled.chromium.stable", urlFieldId = "url_bar"),
Browser(
packageName = "us.spotco.fennec_dos",
// 2nd = Legacy
possibleUrlFieldIds = listOf("mozac_browser_toolbar_url_view", "url_bar_title"),
),
// [Section B] Entries only present here
// TODO: Test the compatibility of these with Autofill Framework

View File

@@ -8,9 +8,6 @@ import androidx.lifecycle.lifecycleScope
import com.x8bit.bitwarden.data.autofill.manager.AutofillActivityManager
import com.x8bit.bitwarden.data.autofill.manager.AutofillActivityManagerImpl
import com.x8bit.bitwarden.data.autofill.manager.AutofillEnabledManager
import com.x8bit.bitwarden.data.autofill.manager.chrome.ChromeThirdPartyAutofillEnabledManager
import com.x8bit.bitwarden.data.autofill.manager.chrome.ChromeThirdPartyAutofillManager
import com.x8bit.bitwarden.data.autofill.manager.chrome.ChromeThirdPartyAutofillManagerImpl
import com.x8bit.bitwarden.data.platform.manager.AppStateManager
import dagger.Module
import dagger.Provides
@@ -26,32 +23,19 @@ import dagger.hilt.android.scopes.ActivityScoped
@InstallIn(ActivityComponent::class)
object ActivityAutofillModule {
@ActivityScoped
@ActivityScopedManager
@Provides
fun provideActivityScopedChromeThirdPartyAutofillManager(
activity: Activity,
): ChromeThirdPartyAutofillManager = ChromeThirdPartyAutofillManagerImpl(
context = activity.baseContext,
)
@ActivityScoped
@Provides
fun provideAutofillActivityManager(
@ActivityScopedManager autofillManager: AutofillManager,
@ActivityScopedManager chromeThirdPartyAutofillManager: ChromeThirdPartyAutofillManager,
appStateManager: AppStateManager,
autofillEnabledManager: AutofillEnabledManager,
lifecycleScope: LifecycleCoroutineScope,
chromeThirdPartyAutofillEnabledManager: ChromeThirdPartyAutofillEnabledManager,
): AutofillActivityManager =
AutofillActivityManagerImpl(
autofillManager = autofillManager,
chromeThirdPartyAutofillManager = chromeThirdPartyAutofillManager,
appStateManager = appStateManager,
autofillEnabledManager = autofillEnabledManager,
lifecycleScope = lifecycleScope,
chromeThirdPartyAutofillEnabledManager = chromeThirdPartyAutofillEnabledManager,
)
/**

View File

@@ -15,15 +15,12 @@ import com.x8bit.bitwarden.data.autofill.manager.AutofillEnabledManager
import com.x8bit.bitwarden.data.autofill.manager.AutofillEnabledManagerImpl
import com.x8bit.bitwarden.data.autofill.manager.AutofillTotpManager
import com.x8bit.bitwarden.data.autofill.manager.AutofillTotpManagerImpl
import com.x8bit.bitwarden.data.autofill.manager.chrome.ChromeThirdPartyAutofillEnabledManager
import com.x8bit.bitwarden.data.autofill.manager.chrome.ChromeThirdPartyAutofillEnabledManagerImpl
import com.x8bit.bitwarden.data.autofill.parser.AutofillParser
import com.x8bit.bitwarden.data.autofill.parser.AutofillParserImpl
import com.x8bit.bitwarden.data.autofill.processor.AutofillProcessor
import com.x8bit.bitwarden.data.autofill.processor.AutofillProcessorImpl
import com.x8bit.bitwarden.data.autofill.provider.AutofillCipherProvider
import com.x8bit.bitwarden.data.autofill.provider.AutofillCipherProviderImpl
import com.x8bit.bitwarden.data.platform.manager.FeatureFlagManager
import com.x8bit.bitwarden.data.platform.manager.PolicyManager
import com.x8bit.bitwarden.data.platform.manager.ciphermatching.CipherMatchingManager
import com.x8bit.bitwarden.data.platform.manager.clipboard.BitwardenClipboardManager
@@ -57,15 +54,6 @@ object AutofillModule {
fun providesAutofillEnabledManager(): AutofillEnabledManager =
AutofillEnabledManagerImpl()
@Singleton
@Provides
fun providesChromeAutofillEnabledManager(
featureFlagManager: FeatureFlagManager,
): ChromeThirdPartyAutofillEnabledManager =
ChromeThirdPartyAutofillEnabledManagerImpl(
featureFlagManager = featureFlagManager,
)
@Singleton
@Provides
fun provideAutofillCompletionManager(

View File

@@ -8,13 +8,9 @@ import com.x8bit.bitwarden.data.auth.repository.AuthRepository
import com.x8bit.bitwarden.data.autofill.fido2.datasource.network.service.DigitalAssetLinkService
import com.x8bit.bitwarden.data.autofill.fido2.manager.Fido2CredentialManager
import com.x8bit.bitwarden.data.autofill.fido2.manager.Fido2CredentialManagerImpl
import com.x8bit.bitwarden.data.autofill.fido2.manager.Fido2OriginManager
import com.x8bit.bitwarden.data.autofill.fido2.manager.Fido2OriginManagerImpl
import com.x8bit.bitwarden.data.autofill.fido2.processor.Fido2ProviderProcessor
import com.x8bit.bitwarden.data.autofill.fido2.processor.Fido2ProviderProcessorImpl
import com.x8bit.bitwarden.data.platform.manager.AssetManager
import com.x8bit.bitwarden.data.platform.manager.BiometricsEncryptionManager
import com.x8bit.bitwarden.data.platform.manager.FeatureFlagManager
import com.x8bit.bitwarden.data.platform.manager.dispatcher.DispatcherManager
import com.x8bit.bitwarden.data.vault.datasource.sdk.VaultSdkSource
import com.x8bit.bitwarden.data.vault.repository.VaultRepository
@@ -46,8 +42,6 @@ object Fido2ProviderModule {
fido2CredentialManager: Fido2CredentialManager,
dispatcherManager: DispatcherManager,
intentManager: IntentManager,
biometricsEncryptionManager: BiometricsEncryptionManager,
featureFlagManager: FeatureFlagManager,
clock: Clock,
): Fido2ProviderProcessor =
Fido2ProviderProcessorImpl(
@@ -58,34 +52,23 @@ object Fido2ProviderModule {
fido2CredentialManager,
intentManager,
clock,
biometricsEncryptionManager,
featureFlagManager,
dispatcherManager,
)
@Provides
@Singleton
fun provideFido2CredentialManager(
assetManager: AssetManager,
digitalAssetLinkService: DigitalAssetLinkService,
vaultSdkSource: VaultSdkSource,
fido2CredentialStore: Fido2CredentialStore,
fido2OriginManager: Fido2OriginManager,
json: Json,
): Fido2CredentialManager =
Fido2CredentialManagerImpl(
vaultSdkSource = vaultSdkSource,
fido2CredentialStore = fido2CredentialStore,
fido2OriginManager = fido2OriginManager,
json = json,
)
@Provides
@Singleton
fun provideFido2OriginManager(
assetManager: AssetManager,
digitalAssetLinkService: DigitalAssetLinkService,
): Fido2OriginManager =
Fido2OriginManagerImpl(
assetManager = assetManager,
digitalAssetLinkService = digitalAssetLinkService,
vaultSdkSource = vaultSdkSource,
fido2CredentialStore = fido2CredentialStore,
json = json,
)
}

View File

@@ -1,10 +1,12 @@
package com.x8bit.bitwarden.data.autofill.fido2.manager
import androidx.credentials.provider.CallingAppInfo
import com.bitwarden.vault.CipherView
import com.x8bit.bitwarden.data.autofill.fido2.model.Fido2CreateCredentialRequest
import com.x8bit.bitwarden.data.autofill.fido2.model.Fido2CredentialAssertionRequest
import com.x8bit.bitwarden.data.autofill.fido2.model.Fido2CredentialAssertionResult
import com.x8bit.bitwarden.data.autofill.fido2.model.Fido2CredentialRequest
import com.x8bit.bitwarden.data.autofill.fido2.model.Fido2RegisterCredentialResult
import com.x8bit.bitwarden.data.autofill.fido2.model.Fido2ValidateOriginResult
import com.x8bit.bitwarden.data.autofill.fido2.model.PasskeyAssertionOptions
import com.x8bit.bitwarden.data.autofill.fido2.model.PasskeyAttestationOptions
@@ -24,6 +26,14 @@ interface Fido2CredentialManager {
*/
var authenticationAttempts: Int
/**
* Attempt to validate the RP and origin of the provided [callingAppInfo] and [relyingPartyId].
*/
suspend fun validateOrigin(
callingAppInfo: CallingAppInfo,
relyingPartyId: String,
): Fido2ValidateOriginResult
/**
* Attempt to extract FIDO 2 passkey attestation options from the system [requestJson], or null.
*/
@@ -43,7 +53,7 @@ interface Fido2CredentialManager {
*/
suspend fun registerFido2Credential(
userId: String,
fido2CreateCredentialRequest: Fido2CreateCredentialRequest,
fido2CredentialRequest: Fido2CredentialRequest,
selectedCipherView: CipherView,
): Fido2RegisterCredentialResult

View File

@@ -6,37 +6,43 @@ import com.bitwarden.fido.Origin
import com.bitwarden.fido.UnverifiedAssetLink
import com.bitwarden.sdk.Fido2CredentialStore
import com.bitwarden.vault.CipherView
import com.x8bit.bitwarden.R
import com.x8bit.bitwarden.data.autofill.fido2.model.Fido2CreateCredentialRequest
import com.x8bit.bitwarden.data.autofill.fido2.datasource.network.model.DigitalAssetLinkResponseJson
import com.x8bit.bitwarden.data.autofill.fido2.datasource.network.service.DigitalAssetLinkService
import com.x8bit.bitwarden.data.autofill.fido2.model.Fido2CredentialAssertionRequest
import com.x8bit.bitwarden.data.autofill.fido2.model.Fido2CredentialAssertionResult
import com.x8bit.bitwarden.data.autofill.fido2.model.Fido2CredentialRequest
import com.x8bit.bitwarden.data.autofill.fido2.model.Fido2RegisterCredentialResult
import com.x8bit.bitwarden.data.autofill.fido2.model.Fido2ValidateOriginResult
import com.x8bit.bitwarden.data.autofill.fido2.model.PasskeyAssertionOptions
import com.x8bit.bitwarden.data.autofill.fido2.model.PasskeyAttestationOptions
import com.x8bit.bitwarden.data.platform.manager.AssetManager
import com.x8bit.bitwarden.data.platform.util.decodeFromStringOrNull
import com.x8bit.bitwarden.data.platform.util.getAppOrigin
import com.x8bit.bitwarden.data.platform.util.getAppSigningSignatureFingerprint
import com.x8bit.bitwarden.data.platform.util.getSignatureFingerprintAsHexString
import com.x8bit.bitwarden.data.platform.util.validatePrivilegedApp
import com.x8bit.bitwarden.data.vault.datasource.sdk.VaultSdkSource
import com.x8bit.bitwarden.data.vault.datasource.sdk.model.AuthenticateFido2CredentialRequest
import com.x8bit.bitwarden.data.vault.datasource.sdk.model.RegisterFido2CredentialRequest
import com.x8bit.bitwarden.data.vault.datasource.sdk.util.toAndroidAttestationResponse
import com.x8bit.bitwarden.data.vault.datasource.sdk.util.toAndroidFido2PublicKeyCredential
import com.x8bit.bitwarden.ui.platform.base.util.asText
import com.x8bit.bitwarden.ui.platform.base.util.prefixHttpsIfNecessaryOrNull
import com.x8bit.bitwarden.ui.platform.base.util.toHostOrPathOrNull
import kotlinx.serialization.SerializationException
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import timber.log.Timber
private const val GOOGLE_ALLOW_LIST_FILE_NAME = "fido2_privileged_google.json"
private const val COMMUNITY_ALLOW_LIST_FILE_NAME = "fido2_privileged_community.json"
/**
* Primary implementation of [Fido2CredentialManager].
*/
@Suppress("TooManyFunctions")
class Fido2CredentialManagerImpl(
private val assetManager: AssetManager,
private val digitalAssetLinkService: DigitalAssetLinkService,
private val vaultSdkSource: VaultSdkSource,
private val fido2CredentialStore: Fido2CredentialStore,
private val fido2OriginManager: Fido2OriginManager,
private val json: Json,
) : Fido2CredentialManager,
Fido2CredentialStore by fido2CredentialStore {
@@ -47,49 +53,45 @@ class Fido2CredentialManagerImpl(
override suspend fun registerFido2Credential(
userId: String,
fido2CreateCredentialRequest: Fido2CreateCredentialRequest,
fido2CredentialRequest: Fido2CredentialRequest,
selectedCipherView: CipherView,
): Fido2RegisterCredentialResult {
val callingAppInfo = fido2CreateCredentialRequest.callingAppInfo
val clientData = if (fido2CreateCredentialRequest.origin.isNullOrEmpty()) {
ClientData.DefaultWithExtraData(androidPackageName = callingAppInfo.packageName)
} else {
callingAppInfo
val clientData = if (fido2CredentialRequest.callingAppInfo.isOriginPopulated()) {
fido2CredentialRequest
.callingAppInfo
.getAppSigningSignatureFingerprint()
?.let { ClientData.DefaultWithCustomHash(hash = it) }
?: return Fido2RegisterCredentialResult.Error(
R.string.passkey_operation_failed_because_app_is_signed_incorrectly.asText(),
)
}
val sdkOrigin = if (fido2CreateCredentialRequest.origin.isNullOrEmpty()) {
val host = getOriginUrlFromAttestationOptionsOrNull(
requestJson = fido2CreateCredentialRequest.requestJson,
)
?: return Fido2RegisterCredentialResult.Error(
R.string.passkey_operation_failed_because_host_url_is_not_present_in_request
.asText(),
)
Origin.Android(
UnverifiedAssetLink(
packageName = callingAppInfo.packageName,
sha256CertFingerprint = callingAppInfo.getSignatureFingerprintAsHexString()
?: return Fido2RegisterCredentialResult.Error(
R.string.passkey_operation_failed_because_app_signature_is_invalid
.asText(),
),
host = host,
assetLinkUrl = host,
),
)
?: return Fido2RegisterCredentialResult.Error
} else {
Origin.Web(fido2CreateCredentialRequest.origin)
ClientData.DefaultWithExtraData(
androidPackageName = fido2CredentialRequest
.callingAppInfo
.packageName,
)
}
val assetLinkUrl = fido2CredentialRequest
.origin
?: getOriginUrlFromAttestationOptionsOrNull(fido2CredentialRequest.requestJson)
?: return Fido2RegisterCredentialResult.Error
val origin = Origin.Android(
UnverifiedAssetLink(
packageName = fido2CredentialRequest.packageName,
sha256CertFingerprint = fido2CredentialRequest
.callingAppInfo
.getSignatureFingerprintAsHexString()
?: return Fido2RegisterCredentialResult.Error,
host = assetLinkUrl.toHostOrPathOrNull()
?: return Fido2RegisterCredentialResult.Error,
assetLinkUrl = assetLinkUrl,
),
)
return vaultSdkSource
.registerFido2Credential(
request = RegisterFido2CredentialRequest(
userId = userId,
origin = sdkOrigin,
requestJson = """{"publicKey": ${fido2CreateCredentialRequest.requestJson}}""",
origin = origin,
requestJson = """{"publicKey": ${fido2CredentialRequest.requestJson}}""",
clientData = clientData,
selectedCipherView = selectedCipherView,
// User verification is handled prior to engaging the SDK. We always respond
@@ -102,22 +104,20 @@ class Fido2CredentialManagerImpl(
.mapCatching { json.encodeToString(it) }
.fold(
onSuccess = { Fido2RegisterCredentialResult.Success(it) },
onFailure = {
Fido2RegisterCredentialResult.Error(
R.string.passkey_registration_failed_due_to_an_internal_error.asText(),
)
},
onFailure = { Fido2RegisterCredentialResult.Error },
)
}
private suspend fun validateOrigin(
override suspend fun validateOrigin(
callingAppInfo: CallingAppInfo,
relyingPartyId: String,
): Fido2ValidateOriginResult = fido2OriginManager
.validateOrigin(
callingAppInfo = callingAppInfo,
relyingPartyId = relyingPartyId,
)
): Fido2ValidateOriginResult {
return if (callingAppInfo.isOriginPopulated()) {
validatePrivilegedAppOrigin(callingAppInfo)
} else {
validateCallingApplicationAssetLinks(callingAppInfo, relyingPartyId)
}
}
override fun getPasskeyAttestationOptionsOrNull(
requestJson: String,
@@ -125,10 +125,8 @@ class Fido2CredentialManagerImpl(
try {
json.decodeFromString<PasskeyAttestationOptions>(requestJson)
} catch (e: SerializationException) {
Timber.e(e, "Failed to decode passkey attestation options.")
null
} catch (e: IllegalArgumentException) {
Timber.e(e, "Failed to decode passkey attestation options.")
null
}
@@ -138,14 +136,11 @@ class Fido2CredentialManagerImpl(
try {
json.decodeFromString<PasskeyAssertionOptions>(requestJson)
} catch (e: SerializationException) {
Timber.e(e, "Failed to decode passkey assertion options: $e")
null
} catch (e: IllegalArgumentException) {
Timber.e(e, "Failed to decode passkey assertion options: $e")
null
}
@Suppress("LongMethod")
override suspend fun authenticateFido2Credential(
userId: String,
request: Fido2CredentialAssertionRequest,
@@ -155,52 +150,39 @@ class Fido2CredentialManagerImpl(
val clientData = request.clientDataHash
?.let { ClientData.DefaultWithCustomHash(hash = it) }
?: ClientData.DefaultWithExtraData(androidPackageName = callingAppInfo.getAppOrigin())
val origin = callingAppInfo.origin
?: getOriginUrlFromAssertionOptionsOrNull(request.requestJson)
?: return Fido2CredentialAssertionResult.Error
val relyingPartyId = json
.decodeFromStringOrNull<PasskeyAssertionOptions>(request.requestJson)
?.relyingPartyId
?: return Fido2CredentialAssertionResult.Error(
R.string.passkey_operation_failed_because_relying_party_cannot_be_identified
.asText(),
)
?: return Fido2CredentialAssertionResult.Error
val validateOriginResult = validateOrigin(
callingAppInfo = callingAppInfo,
relyingPartyId = relyingPartyId,
)
val sdkOrigin = if (!request.origin.isNullOrEmpty()) {
Origin.Web(request.origin)
} else {
val hostUrl = getOriginUrlFromAssertionOptionsOrNull(request.requestJson)
?: return Fido2CredentialAssertionResult.Error(
R.string.passkey_operation_failed_because_host_url_is_not_present_in_request
.asText(),
)
Origin.Android(
UnverifiedAssetLink(
packageName = callingAppInfo.packageName,
sha256CertFingerprint = callingAppInfo.getSignatureFingerprintAsHexString()
?: return Fido2CredentialAssertionResult.Error(
R.string.passkey_operation_failed_because_app_signature_is_invalid
.asText(),
),
host = hostUrl,
assetLinkUrl = hostUrl,
),
)
}
return when (validateOriginResult) {
is Fido2ValidateOriginResult.Error -> {
Fido2CredentialAssertionResult.Error(validateOriginResult.messageResId.asText())
Fido2CredentialAssertionResult.Error
}
is Fido2ValidateOriginResult.Success -> {
Fido2ValidateOriginResult.Success -> {
vaultSdkSource
.authenticateFido2Credential(
request = AuthenticateFido2CredentialRequest(
userId = userId,
origin = sdkOrigin,
origin = Origin.Android(
UnverifiedAssetLink(
callingAppInfo.packageName,
callingAppInfo.getSignatureFingerprintAsHexString()
?: return Fido2CredentialAssertionResult.Error,
origin.toHostOrPathOrNull()
?: return Fido2CredentialAssertionResult.Error,
origin,
),
),
requestJson = """{"publicKey": ${request.requestJson}}""",
clientData = clientData,
selectedCipherView = selectedCipherView,
@@ -212,31 +194,146 @@ class Fido2CredentialManagerImpl(
.mapCatching { json.encodeToString(it) }
.fold(
onSuccess = { Fido2CredentialAssertionResult.Success(it) },
onFailure = {
Timber.e(it, "Failed to authenticate FIDO2 credential.")
Fido2CredentialAssertionResult.Error(
R.string.passkey_authentication_failed_due_to_an_internal_error
.asText(),
)
},
onFailure = { Fido2CredentialAssertionResult.Error },
)
}
}
}
private suspend fun validateCallingApplicationAssetLinks(
callingAppInfo: CallingAppInfo,
relyingPartyId: String,
): Fido2ValidateOriginResult {
return digitalAssetLinkService
.getDigitalAssetLinkForRp(relyingParty = relyingPartyId)
.onFailure {
return Fido2ValidateOriginResult.Error.AssetLinkNotFound
}
.map { statements ->
statements
.filterMatchingAppStatementsOrNull(
rpPackageName = callingAppInfo.packageName,
)
?: return Fido2ValidateOriginResult.Error.ApplicationNotFound
}
.map { matchingStatements ->
callingAppInfo
.getSignatureFingerprintAsHexString()
?.let { certificateFingerprint ->
matchingStatements
.filterMatchingAppSignaturesOrNull(
signature = certificateFingerprint,
)
}
?: return Fido2ValidateOriginResult.Error.ApplicationNotVerified
}
.fold(
onSuccess = {
Fido2ValidateOriginResult.Success
},
onFailure = {
Fido2ValidateOriginResult.Error.Unknown
},
)
}
private suspend fun validatePrivilegedAppOrigin(
callingAppInfo: CallingAppInfo,
): Fido2ValidateOriginResult {
val googleAllowListResult =
validatePrivilegedAppSignatureWithGoogleList(callingAppInfo)
return when (googleAllowListResult) {
is Fido2ValidateOriginResult.Success -> {
// Application was found and successfully validated against the Google allow list so
// we can return the result as the final validation result.
googleAllowListResult
}
is Fido2ValidateOriginResult.Error -> {
// Check the community allow list if the Google allow list failed, and return the
// result as the final validation result.
validatePrivilegedAppSignatureWithCommunityList(callingAppInfo)
}
}
}
private suspend fun validatePrivilegedAppSignatureWithGoogleList(
callingAppInfo: CallingAppInfo,
): Fido2ValidateOriginResult =
validatePrivilegedAppSignatureWithAllowList(
callingAppInfo = callingAppInfo,
fileName = GOOGLE_ALLOW_LIST_FILE_NAME,
)
private suspend fun validatePrivilegedAppSignatureWithCommunityList(
callingAppInfo: CallingAppInfo,
): Fido2ValidateOriginResult =
validatePrivilegedAppSignatureWithAllowList(
callingAppInfo = callingAppInfo,
fileName = COMMUNITY_ALLOW_LIST_FILE_NAME,
)
private suspend fun validatePrivilegedAppSignatureWithAllowList(
callingAppInfo: CallingAppInfo,
fileName: String,
): Fido2ValidateOriginResult =
assetManager
.readAsset(fileName)
.map { allowList ->
callingAppInfo.validatePrivilegedApp(
allowList = allowList,
)
}
.fold(
onSuccess = { it },
onFailure = { Fido2ValidateOriginResult.Error.Unknown },
)
/**
* Returns statements targeting the calling Android application, or null.
*/
private fun List<DigitalAssetLinkResponseJson>.filterMatchingAppStatementsOrNull(
rpPackageName: String,
): List<DigitalAssetLinkResponseJson>? =
filter { statement ->
val target = statement.target
target.namespace == "android_app" &&
target.packageName == rpPackageName &&
statement.relation.containsAll(
listOf(
"delegate_permission/common.get_login_creds",
"delegate_permission/common.handle_all_urls",
),
)
}
.takeUnless { it.isEmpty() }
/**
* Returns statements that match the given [signature], or null.
*/
private fun List<DigitalAssetLinkResponseJson>.filterMatchingAppSignaturesOrNull(
signature: String,
): List<DigitalAssetLinkResponseJson>? =
filter { statement ->
statement.target.sha256CertFingerprints
?.contains(signature)
?: false
}
.takeUnless { it.isEmpty() }
override fun hasAuthenticationAttemptsRemaining(): Boolean =
authenticationAttempts < MAX_AUTHENTICATION_ATTEMPTS
private fun getOriginUrlFromAssertionOptionsOrNull(requestJson: String) =
getPasskeyAssertionOptionsOrNull(requestJson)
?.relyingPartyId
?.prefixHttpsIfNecessaryOrNull()
?.let { "$HTTPS$it" }
private fun getOriginUrlFromAttestationOptionsOrNull(requestJson: String) =
getPasskeyAttestationOptionsOrNull(requestJson)
?.relyingParty
?.id
?.prefixHttpsIfNecessaryOrNull()
?.let { "$HTTPS$it" }
}
private const val MAX_AUTHENTICATION_ATTEMPTS = 5

View File

@@ -1,23 +0,0 @@
package com.x8bit.bitwarden.data.autofill.fido2.manager
import androidx.credentials.provider.CallingAppInfo
import com.x8bit.bitwarden.data.autofill.fido2.model.Fido2ValidateOriginResult
/**
* Responsible for managing FIDO2 origin validation.
*/
interface Fido2OriginManager {
/**
* Validates the origin of a calling app.
*
* @param callingAppInfo The calling app info.
* @param relyingPartyId The relying party ID.
*
* @return The result of the validation.
*/
suspend fun validateOrigin(
callingAppInfo: CallingAppInfo,
relyingPartyId: String,
): Fido2ValidateOriginResult
}

View File

@@ -1,156 +0,0 @@
package com.x8bit.bitwarden.data.autofill.fido2.manager
import androidx.credentials.provider.CallingAppInfo
import com.x8bit.bitwarden.data.autofill.fido2.datasource.network.model.DigitalAssetLinkResponseJson
import com.x8bit.bitwarden.data.autofill.fido2.datasource.network.service.DigitalAssetLinkService
import com.x8bit.bitwarden.data.autofill.fido2.model.Fido2ValidateOriginResult
import com.x8bit.bitwarden.data.platform.manager.AssetManager
import com.x8bit.bitwarden.data.platform.util.getSignatureFingerprintAsHexString
import com.x8bit.bitwarden.data.platform.util.validatePrivilegedApp
import timber.log.Timber
private const val GOOGLE_ALLOW_LIST_FILE_NAME = "fido2_privileged_google.json"
private const val COMMUNITY_ALLOW_LIST_FILE_NAME = "fido2_privileged_community.json"
/**
* Primary implementation of [Fido2OriginManager].
*/
@Suppress("TooManyFunctions")
class Fido2OriginManagerImpl(
private val assetManager: AssetManager,
private val digitalAssetLinkService: DigitalAssetLinkService,
) : Fido2OriginManager {
override suspend fun validateOrigin(
callingAppInfo: CallingAppInfo,
relyingPartyId: String,
): Fido2ValidateOriginResult {
return if (callingAppInfo.isOriginPopulated()) {
validatePrivilegedAppOrigin(callingAppInfo)
} else {
validateCallingApplicationAssetLinks(callingAppInfo, relyingPartyId)
}
}
private suspend fun validateCallingApplicationAssetLinks(
callingAppInfo: CallingAppInfo,
relyingPartyId: String,
): Fido2ValidateOriginResult = digitalAssetLinkService
.getDigitalAssetLinkForRp(relyingParty = relyingPartyId)
.onFailure {
return Fido2ValidateOriginResult.Error.AssetLinkNotFound
}
.mapCatching { statements ->
statements
.filterMatchingAppStatementsOrNull(
rpPackageName = callingAppInfo.packageName,
)
?: return Fido2ValidateOriginResult.Error.ApplicationNotFound
}
.mapCatching { matchingStatements ->
callingAppInfo
.getSignatureFingerprintAsHexString()
?.let { certificateFingerprint ->
matchingStatements
.filterMatchingAppSignaturesOrNull(
signature = certificateFingerprint,
)
}
?: return Fido2ValidateOriginResult.Error.ApplicationFingerprintNotVerified
}
.fold(
onSuccess = {
Fido2ValidateOriginResult.Success(null)
},
onFailure = {
Fido2ValidateOriginResult.Error.Unknown
},
)
private suspend fun validatePrivilegedAppOrigin(
callingAppInfo: CallingAppInfo,
): Fido2ValidateOriginResult {
val googleAllowListResult =
validatePrivilegedAppSignatureWithGoogleList(callingAppInfo)
return when (googleAllowListResult) {
is Fido2ValidateOriginResult.Success -> {
// Application was found and successfully validated against the Google allow list so
// we can return the result as the final validation result.
googleAllowListResult
}
is Fido2ValidateOriginResult.Error -> {
// Check the community allow list if the Google allow list failed, and return the
// result as the final validation result.
validatePrivilegedAppSignatureWithCommunityList(callingAppInfo)
}
}
}
private suspend fun validatePrivilegedAppSignatureWithGoogleList(
callingAppInfo: CallingAppInfo,
): Fido2ValidateOriginResult =
validatePrivilegedAppSignatureWithAllowList(
callingAppInfo = callingAppInfo,
fileName = GOOGLE_ALLOW_LIST_FILE_NAME,
)
private suspend fun validatePrivilegedAppSignatureWithCommunityList(
callingAppInfo: CallingAppInfo,
): Fido2ValidateOriginResult =
validatePrivilegedAppSignatureWithAllowList(
callingAppInfo = callingAppInfo,
fileName = COMMUNITY_ALLOW_LIST_FILE_NAME,
)
private suspend fun validatePrivilegedAppSignatureWithAllowList(
callingAppInfo: CallingAppInfo,
fileName: String,
): Fido2ValidateOriginResult =
assetManager
.readAsset(fileName)
.mapCatching { allowList ->
callingAppInfo.validatePrivilegedApp(
allowList = allowList,
)
}
.fold(
onSuccess = { it },
onFailure = {
Timber.e(it, "Failed to validate privileged app: ${callingAppInfo.packageName}")
Fido2ValidateOriginResult.Error.Unknown
},
)
/**
* Returns statements targeting the calling Android application, or null.
*/
private fun List<DigitalAssetLinkResponseJson>.filterMatchingAppStatementsOrNull(
rpPackageName: String,
): List<DigitalAssetLinkResponseJson>? =
filter { statement ->
val target = statement.target
target.namespace == "android_app" &&
target.packageName == rpPackageName &&
statement.relation.containsAll(
listOf(
"delegate_permission/common.get_login_creds",
"delegate_permission/common.handle_all_urls",
),
)
}
.takeUnless { it.isEmpty() }
/**
* Returns statements that match the given [signature], or null.
*/
private fun List<DigitalAssetLinkResponseJson>.filterMatchingAppSignaturesOrNull(
signature: String,
): List<DigitalAssetLinkResponseJson>? =
filter { statement ->
statement.target.sha256CertFingerprints
?.contains(signature)
?: false
}
.takeUnless { it.isEmpty() }
}

View File

@@ -7,19 +7,6 @@ import kotlinx.parcelize.Parcelize
/**
* Models a FIDO 2 credential authentication request parsed from the launching intent.
*
* @param userId The ID of the Bitwarden user to authenticate.
* @param cipherId The ID of the cipher that contains the passkey to authenticate.
* @param credentialId The ID of the credential to authenticate.
* @param requestJson The JSON representation of the FIDO 2 request.
* @param clientDataHash The hash of the client data.
* @param packageName The package name of the calling app.
* @param signingInfo The signing info of the calling app.
* @param origin The origin of the calling app. Only populated if the calling application is a
* privileged application. I.e., a web browser.
* @param isUserVerified Whether the user has been verified prior to receiving this request. Only
* populated if device biometric verification was performed. If null, the application is responsible
* for prompting user verification when it is deemed necessary.
*/
@Parcelize
data class Fido2CredentialAssertionRequest(
@@ -31,7 +18,6 @@ data class Fido2CredentialAssertionRequest(
val packageName: String,
val signingInfo: SigningInfo,
val origin: String?,
val isUserVerified: Boolean?,
) : Parcelable {
val callingAppInfo: CallingAppInfo
get() = CallingAppInfo(packageName, signingInfo, origin)

View File

@@ -1,7 +1,5 @@
package com.x8bit.bitwarden.data.autofill.fido2.model
import com.x8bit.bitwarden.ui.platform.base.util.Text
/**
* Represents possible outcomes of a FIDO 2 credential assertion request.
*/
@@ -15,5 +13,5 @@ sealed class Fido2CredentialAssertionResult {
/**
* Indicates there was an error and the assertion was not successful.
*/
data class Error(val message: Text) : Fido2CredentialAssertionResult()
data object Error : Fido2CredentialAssertionResult()
}

View File

@@ -14,13 +14,12 @@ import kotlinx.parcelize.Parcelize
* @property callingAppInfo Information about the application that initiated the request.
*/
@Parcelize
data class Fido2CreateCredentialRequest(
data class Fido2CredentialRequest(
val userId: String,
val requestJson: String,
val packageName: String,
val signingInfo: SigningInfo,
val origin: String?,
val isUserVerified: Boolean?,
) : Parcelable {
val callingAppInfo: CallingAppInfo
get() = CallingAppInfo(

View File

@@ -2,7 +2,6 @@ package com.x8bit.bitwarden.data.autofill.fido2.model
import androidx.credentials.provider.BeginGetPublicKeyCredentialOption
import com.bitwarden.fido.Fido2CredentialAutofillView
import com.x8bit.bitwarden.ui.platform.base.util.Text
/**
* Represents the result of a FIDO 2 Get Credentials request.
@@ -25,7 +24,5 @@ sealed class Fido2GetCredentialsResult {
/**
* Indicates an error was encountered when querying for matching credentials.
*/
data class Error(
val message: Text,
) : Fido2GetCredentialsResult()
data object Error : Fido2GetCredentialsResult()
}

View File

@@ -1,7 +1,5 @@
package com.x8bit.bitwarden.data.autofill.fido2.model
import com.x8bit.bitwarden.ui.platform.base.util.Text
/**
* Models the data returned from creating a FIDO 2 credential.
*/
@@ -11,13 +9,13 @@ sealed class Fido2RegisterCredentialResult {
* Indicates the credential has been successfully registered.
*/
data class Success(
val responseJson: String,
val registrationResponse: String,
) : Fido2RegisterCredentialResult()
/**
* Indicates there was an error and the credential was not registered.
*/
data class Error(val message: Text) : Fido2RegisterCredentialResult()
data object Error : Fido2RegisterCredentialResult()
/**
* Indicates the user cancelled the request.

View File

@@ -1,8 +1,5 @@
package com.x8bit.bitwarden.data.autofill.fido2.model
import androidx.annotation.StringRes
import com.x8bit.bitwarden.R
/**
* Models the result of validating the origin of a FIDO2 request.
*/
@@ -10,75 +7,49 @@ sealed class Fido2ValidateOriginResult {
/**
* Represents a successful origin validation.
*
* @param origin The origin of the calling app, or null if the calling app is not privileged.
*/
data class Success(val origin: String?) : Fido2ValidateOriginResult()
data object Success : Fido2ValidateOriginResult()
/**
* Represents a validation error.
*/
sealed class Error : Fido2ValidateOriginResult() {
/**
* The string resource ID of the error message.
*/
@get:StringRes
abstract val messageResId: Int
/**
* Indicates the digital asset links file could not be located.
*/
data object AssetLinkNotFound : Error() {
override val messageResId =
R.string.passkey_operation_failed_because_of_missing_asset_links
}
data object AssetLinkNotFound : Error()
/**
* Indicates the application package name was not found in the digital asset links file.
*/
data object ApplicationNotFound : Error() {
override val messageResId =
R.string.passkey_operation_failed_because_app_not_found_in_asset_links
}
data object ApplicationNotFound : Error()
/**
* Indicates the application fingerprint was not found the digital asset links file.
*/
data object ApplicationFingerprintNotVerified : Error() {
override val messageResId =
R.string.passkey_operation_failed_because_app_could_not_be_verified
}
data object ApplicationNotVerified : Error()
/**
* Indicates the calling application is privileged but its package name is not found within
* the privileged app allow list.
*/
data object PrivilegedAppNotAllowed : Error() {
override val messageResId =
R.string.passkey_operation_failed_because_browser_is_not_privileged
}
data object PrivilegedAppNotAllowed : Error()
/**
* Indicates the calling app is privileged but but no matching signing certificate signature
* is present in the allow list.
*/
data object PrivilegedAppSignatureNotFound : Error() {
override val messageResId =
R.string.passkey_operation_failed_because_browser_signature_does_not_match
}
data object PrivilegedAppSignatureNotFound : Error()
/**
* Indicates passkeys are not supported for the requesting application.
*/
data object PasskeyNotSupportedForApp : Error() {
override val messageResId = R.string.passkeys_not_supported_for_this_app
}
data object PasskeyNotSupportedForApp : Error()
/**
* Indicates an unknown error was encountered while validating the origin.
*/
data object Unknown : Error() {
override val messageResId = R.string.generic_error_message
}
data object Unknown : Error()
}
}

View File

@@ -13,5 +13,5 @@ data class PublicKeyCredentialDescriptor(
@SerialName("id")
val id: String,
@SerialName("transports")
val transports: List<String>?,
val transports: List<String>,
)

View File

@@ -1,13 +1,10 @@
package com.x8bit.bitwarden.data.autofill.fido2.processor
import android.content.Context
import android.graphics.drawable.Icon
import android.os.Build
import android.os.CancellationSignal
import android.os.OutcomeReceiver
import androidx.annotation.RequiresApi
import androidx.biometric.BiometricManager
import androidx.biometric.BiometricPrompt
import androidx.credentials.exceptions.ClearCredentialException
import androidx.credentials.exceptions.ClearCredentialUnsupportedException
import androidx.credentials.exceptions.CreateCredentialCancellationException
@@ -24,35 +21,25 @@ import androidx.credentials.provider.BeginCreatePublicKeyCredentialRequest
import androidx.credentials.provider.BeginGetCredentialRequest
import androidx.credentials.provider.BeginGetCredentialResponse
import androidx.credentials.provider.BeginGetPublicKeyCredentialOption
import androidx.credentials.provider.BiometricPromptData
import androidx.credentials.provider.CreateEntry
import androidx.credentials.provider.CredentialEntry
import androidx.credentials.provider.ProviderClearCredentialStateRequest
import androidx.credentials.provider.PublicKeyCredentialEntry
import com.bitwarden.fido.Fido2CredentialAutofillView
import com.bitwarden.sdk.Fido2CredentialStore
import com.bitwarden.vault.CipherView
import com.x8bit.bitwarden.R
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
import com.x8bit.bitwarden.data.auth.repository.model.UserState
import com.x8bit.bitwarden.data.autofill.fido2.manager.Fido2CredentialManager
import com.x8bit.bitwarden.data.autofill.util.isActiveWithFido2Credentials
import com.x8bit.bitwarden.data.platform.manager.BiometricsEncryptionManager
import com.x8bit.bitwarden.data.platform.manager.FeatureFlagManager
import com.x8bit.bitwarden.data.platform.manager.dispatcher.DispatcherManager
import com.x8bit.bitwarden.data.platform.manager.model.FlagKey
import com.x8bit.bitwarden.data.platform.repository.model.DataState
import com.x8bit.bitwarden.data.platform.repository.util.takeUntilLoaded
import com.x8bit.bitwarden.data.platform.util.isBuildVersionBelow
import com.x8bit.bitwarden.data.vault.repository.VaultRepository
import com.x8bit.bitwarden.data.vault.repository.model.DecryptFido2CredentialAutofillViewResult
import com.x8bit.bitwarden.ui.platform.manager.intent.IntentManager
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.fold
import kotlinx.coroutines.launch
import java.time.Clock
import java.util.concurrent.atomic.AtomicInteger
import javax.crypto.Cipher
private const val CREATE_PASSKEY_INTENT = "com.x8bit.bitwarden.fido2.ACTION_CREATE_PASSKEY"
const val GET_PASSKEY_INTENT = "com.x8bit.bitwarden.fido2.ACTION_GET_PASSKEY"
@@ -62,7 +49,7 @@ const val UNLOCK_ACCOUNT_INTENT = "com.x8bit.bitwarden.fido2.ACTION_UNLOCK_ACCOU
* The default implementation of [Fido2ProviderProcessor]. Its purpose is to handle FIDO2 related
* processing.
*/
@Suppress("LongParameterList", "TooManyFunctions")
@Suppress("LongParameterList")
@RequiresApi(Build.VERSION_CODES.S)
class Fido2ProviderProcessorImpl(
private val context: Context,
@@ -72,8 +59,6 @@ class Fido2ProviderProcessorImpl(
private val fido2CredentialManager: Fido2CredentialManager,
private val intentManager: IntentManager,
private val clock: Clock,
private val biometricsEncryptionManager: BiometricsEncryptionManager,
private val featureFlagManager: FeatureFlagManager,
dispatcherManager: DispatcherManager,
) : Fido2ProviderProcessor {
@@ -104,6 +89,60 @@ class Fido2ProviderProcessorImpl(
}
}
private fun processCreateCredentialRequest(
request: BeginCreateCredentialRequest,
): BeginCreateCredentialResponse? {
return when (request) {
is BeginCreatePublicKeyCredentialRequest -> {
handleCreatePasskeyQuery(request)
}
else -> null
}
}
private fun handleCreatePasskeyQuery(
request: BeginCreatePublicKeyCredentialRequest,
): BeginCreateCredentialResponse? {
val requestJson = request
.candidateQueryData
.getString("androidx.credentials.BUNDLE_KEY_REQUEST_JSON")
if (requestJson.isNullOrEmpty()) return null
val userState = authRepository.userStateFlow.value ?: return null
return BeginCreateCredentialResponse.Builder()
.setCreateEntries(userState.accounts.toCreateEntries(userState.activeUserId))
.build()
}
private fun List<UserState.Account>.toCreateEntries(activeUserId: String) =
map { it.toCreateEntry(isActive = activeUserId == it.userId) }
private fun UserState.Account.toCreateEntry(isActive: Boolean): CreateEntry {
val accountName = name ?: email
return CreateEntry
.Builder(
accountName = accountName,
pendingIntent = intentManager.createFido2CreationPendingIntent(
CREATE_PASSKEY_INTENT,
userId,
requestCode.getAndIncrement(),
),
)
.setDescription(
context.getString(
R.string.your_passkey_will_be_saved_to_your_bitwarden_vault_for_x,
accountName,
),
)
// Set the last used time to "now" so the active account is the default option in the
// system prompt.
.setLastUsedTime(if (isActive) clock.instant() else null)
.build()
}
override fun processGetCredentialRequest(
request: BeginGetCredentialRequest,
cancellationSignal: CancellationSignal,
@@ -158,78 +197,6 @@ class Fido2ProviderProcessorImpl(
}
}
override fun processClearCredentialStateRequest(
request: ProviderClearCredentialStateRequest,
cancellationSignal: CancellationSignal,
callback: OutcomeReceiver<Void?, ClearCredentialException>,
) {
// no-op: RFU
callback.onError(ClearCredentialUnsupportedException())
}
private fun processCreateCredentialRequest(
request: BeginCreateCredentialRequest,
): BeginCreateCredentialResponse? {
return when (request) {
is BeginCreatePublicKeyCredentialRequest -> {
handleCreatePasskeyQuery(request)
}
else -> null
}
}
private fun handleCreatePasskeyQuery(
request: BeginCreatePublicKeyCredentialRequest,
): BeginCreateCredentialResponse? {
val requestJson = request
.candidateQueryData
.getString("androidx.credentials.BUNDLE_KEY_REQUEST_JSON")
if (requestJson.isNullOrEmpty()) return null
val userState = authRepository.userStateFlow.value ?: return null
return BeginCreateCredentialResponse.Builder()
.setCreateEntries(userState.accounts.toCreateEntries(userState.activeUserId))
.build()
}
private fun List<UserState.Account>.toCreateEntries(activeUserId: String) =
map { it.toCreateEntry(isActive = activeUserId == it.userId) }
private fun UserState.Account.toCreateEntry(isActive: Boolean): CreateEntry {
val accountName = name ?: email
val entryBuilder = CreateEntry
.Builder(
accountName = accountName,
pendingIntent = intentManager.createFido2CreationPendingIntent(
CREATE_PASSKEY_INTENT,
userId,
requestCode.getAndIncrement(),
),
)
.setDescription(
context.getString(
R.string.your_passkey_will_be_saved_to_your_bitwarden_vault_for_x,
accountName,
),
)
// Set the last used time to "now" so the active account is the default option in the
// system prompt.
.setLastUsedTime(if (isActive) clock.instant() else null)
.setAutoSelectAllowed(true)
if (isVaultUnlocked &&
featureFlagManager.getFeatureFlag(FlagKey.SingleTapPasskeyCreation)
) {
biometricsEncryptionManager
.getOrCreateCipher(userId)
?.let { entryBuilder.setBiometricPromptDataIfSupported(cipher = it) }
}
return entryBuilder.build()
}
@Throws(GetCredentialUnsupportedException::class)
private suspend fun getMatchingFido2CredentialEntries(
userId: String,
@@ -256,14 +223,10 @@ class Fido2ProviderProcessorImpl(
): List<CredentialEntry> {
val cipherViews = vaultRepository
.ciphersStateFlow
.takeUntilLoaded()
.fold(emptyList<CipherView>()) { _, dataState ->
when (dataState) {
is DataState.Loaded -> dataState.data.filter { it.isActiveWithFido2Credentials }
else -> emptyList()
}
}
.value
.data
?.filter { it.isActiveWithFido2Credentials }
?: emptyList()
val result = vaultRepository
.getDecryptedFido2CredentialAutofillViews(cipherViews)
return when (result) {
@@ -289,70 +252,29 @@ class Fido2ProviderProcessorImpl(
): List<CredentialEntry> =
this
.map {
val publicKeyEntryBuilder = PublicKeyCredentialEntry
PublicKeyCredentialEntry
.Builder(
context = context,
username = it.userNameForUi ?: context.getString(R.string.no_username),
pendingIntent = intentManager.createFido2GetCredentialPendingIntent(
action = GET_PASSKEY_INTENT,
userId = userId,
credentialId = it.credentialId.toString(),
cipherId = it.cipherId,
requestCode = requestCode.getAndIncrement(),
),
pendingIntent = intentManager
.createFido2GetCredentialPendingIntent(
action = GET_PASSKEY_INTENT,
userId = userId,
credentialId = it.credentialId.toString(),
cipherId = it.cipherId,
requestCode = requestCode.getAndIncrement(),
),
beginGetPublicKeyCredentialOption = option,
)
.setIcon(
Icon.createWithResource(
context,
R.drawable.ic_bw_passkey,
),
)
if (featureFlagManager.getFeatureFlag(FlagKey.SingleTapPasskeyAuthentication)) {
biometricsEncryptionManager
.getOrCreateCipher(userId)
?.let {
publicKeyEntryBuilder
.setBiometricPromptDataIfSupported(cipher = it)
}
}
publicKeyEntryBuilder.build()
.build()
}
private fun PublicKeyCredentialEntry.Builder.setBiometricPromptDataIfSupported(
cipher: Cipher,
): PublicKeyCredentialEntry.Builder {
return if (isBuildVersionBelow(Build.VERSION_CODES.VANILLA_ICE_CREAM)) {
this
} else {
setBiometricPromptData(
biometricPromptData = BiometricPromptData
.Builder()
.buildPromptDataWithCipher(cipher),
)
}
override fun processClearCredentialStateRequest(
request: ProviderClearCredentialStateRequest,
cancellationSignal: CancellationSignal,
callback: OutcomeReceiver<Void?, ClearCredentialException>,
) {
// no-op: RFU
callback.onError(ClearCredentialUnsupportedException())
}
private fun CreateEntry.Builder.setBiometricPromptDataIfSupported(
cipher: Cipher,
): CreateEntry.Builder {
return if (isBuildVersionBelow(Build.VERSION_CODES.VANILLA_ICE_CREAM)) {
this
} else {
setBiometricPromptData(
biometricPromptData = BiometricPromptData
.Builder()
.buildPromptDataWithCipher(cipher),
)
}
}
@RequiresApi(Build.VERSION_CODES.VANILLA_ICE_CREAM)
private fun BiometricPromptData.Builder.buildPromptDataWithCipher(
cipher: Cipher,
): BiometricPromptData = BiometricPromptData.Builder()
.setAllowedAuthenticators(BiometricManager.Authenticators.BIOMETRIC_STRONG)
.setCryptoObject(BiometricPrompt.CryptoObject(cipher))
.build()
}

View File

@@ -6,8 +6,8 @@ import androidx.credentials.CreatePublicKeyCredentialRequest
import androidx.credentials.GetPublicKeyCredentialOption
import androidx.credentials.provider.BeginGetPublicKeyCredentialOption
import androidx.credentials.provider.PendingIntentHandler
import com.x8bit.bitwarden.data.autofill.fido2.model.Fido2CreateCredentialRequest
import com.x8bit.bitwarden.data.autofill.fido2.model.Fido2CredentialAssertionRequest
import com.x8bit.bitwarden.data.autofill.fido2.model.Fido2CredentialRequest
import com.x8bit.bitwarden.data.autofill.fido2.model.Fido2GetCredentialsRequest
import com.x8bit.bitwarden.data.platform.util.isBuildVersionBelow
import com.x8bit.bitwarden.ui.platform.manager.intent.EXTRA_KEY_CIPHER_ID
@@ -15,10 +15,10 @@ import com.x8bit.bitwarden.ui.platform.manager.intent.EXTRA_KEY_CREDENTIAL_ID
import com.x8bit.bitwarden.ui.platform.manager.intent.EXTRA_KEY_USER_ID
/**
* Checks if this [Intent] contains a [Fido2CreateCredentialRequest] related to an ongoing FIDO 2
* Checks if this [Intent] contains a [Fido2CredentialRequest] related to an ongoing FIDO 2
* credential creation process.
*/
fun Intent.getFido2CreateCredentialRequestOrNull(): Fido2CreateCredentialRequest? {
fun Intent.getFido2CredentialRequestOrNull(): Fido2CredentialRequest? {
if (isBuildVersionBelow(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)) return null
val systemRequest = PendingIntentHandler
@@ -33,13 +33,12 @@ fun Intent.getFido2CreateCredentialRequestOrNull(): Fido2CreateCredentialRequest
val userId = getStringExtra(EXTRA_KEY_USER_ID)
?: return null
return Fido2CreateCredentialRequest(
return Fido2CredentialRequest(
userId = userId,
requestJson = createPublicKeyRequest.requestJson,
packageName = systemRequest.callingAppInfo.packageName,
signingInfo = systemRequest.callingAppInfo.signingInfo,
origin = systemRequest.callingAppInfo.origin,
isUserVerified = systemRequest.biometricPromptResult?.isSuccessful,
)
}
@@ -68,8 +67,6 @@ fun Intent.getFido2AssertionRequestOrNull(): Fido2CredentialAssertionRequest? {
val userId: String = getStringExtra(EXTRA_KEY_USER_ID)
?: return null
val isUserVerified = systemRequest.biometricPromptResult?.isSuccessful
return Fido2CredentialAssertionRequest(
userId = userId,
cipherId = cipherId,
@@ -79,7 +76,6 @@ fun Intent.getFido2AssertionRequestOrNull(): Fido2CredentialAssertionRequest? {
packageName = systemRequest.callingAppInfo.packageName,
signingInfo = systemRequest.callingAppInfo.signingInfo,
origin = systemRequest.callingAppInfo.origin,
isUserVerified = isUserVerified,
)
}

View File

@@ -2,9 +2,6 @@ package com.x8bit.bitwarden.data.autofill.manager
import android.view.autofill.AutofillManager
import androidx.lifecycle.LifecycleCoroutineScope
import com.x8bit.bitwarden.data.autofill.manager.chrome.ChromeThirdPartyAutofillEnabledManager
import com.x8bit.bitwarden.data.autofill.manager.chrome.ChromeThirdPartyAutofillManager
import com.x8bit.bitwarden.data.autofill.model.chrome.ChromeThirdPartyAutofillStatus
import com.x8bit.bitwarden.data.platform.manager.AppStateManager
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
@@ -14,31 +11,19 @@ import kotlinx.coroutines.flow.onEach
*/
class AutofillActivityManagerImpl(
private val autofillManager: AutofillManager,
private val chromeThirdPartyAutofillManager: ChromeThirdPartyAutofillManager,
autofillEnabledManager: AutofillEnabledManager,
private val autofillEnabledManager: AutofillEnabledManager,
appStateManager: AppStateManager,
lifecycleScope: LifecycleCoroutineScope,
chromeThirdPartyAutofillEnabledManager: ChromeThirdPartyAutofillEnabledManager,
) : AutofillActivityManager {
private val isAutofillEnabledAndSupported: Boolean
get() = autofillManager.isEnabled &&
autofillManager.hasEnabledAutofillServices() &&
autofillManager.isAutofillSupported
private val chromeAutofillStatus: ChromeThirdPartyAutofillStatus
get() = ChromeThirdPartyAutofillStatus(
stableStatusData = chromeThirdPartyAutofillManager.stableChromeAutofillStatus,
betaChannelStatusData = chromeThirdPartyAutofillManager.betaChromeAutofillStatus,
)
init {
appStateManager
.appForegroundStateFlow
.onEach {
autofillEnabledManager.isAutofillEnabled = isAutofillEnabledAndSupported
chromeThirdPartyAutofillEnabledManager.chromeThirdPartyAutofillStatus =
chromeAutofillStatus
}
.onEach { autofillEnabledManager.isAutofillEnabled = isAutofillEnabledAndSupported }
.launchIn(lifecycleScope)
}
}

View File

@@ -9,7 +9,6 @@ import com.x8bit.bitwarden.data.platform.manager.clipboard.BitwardenClipboardMan
import com.x8bit.bitwarden.data.platform.repository.SettingsRepository
import com.x8bit.bitwarden.data.vault.repository.VaultRepository
import com.x8bit.bitwarden.data.vault.repository.model.GenerateTotpResult
import com.x8bit.bitwarden.ui.platform.base.util.asText
import java.time.Clock
/**
@@ -35,10 +34,7 @@ class AutofillTotpManagerImpl(
)
if (totpResult is GenerateTotpResult.Success) {
clipboardManager.setText(
text = totpResult.code,
toastDescriptorOverride = R.string.verification_code_totp.asText(),
)
clipboardManager.setText(text = totpResult.code)
Toast
.makeText(
context.applicationContext,

View File

@@ -1,22 +0,0 @@
package com.x8bit.bitwarden.data.autofill.manager.chrome
import com.x8bit.bitwarden.data.autofill.model.chrome.ChromeThirdPartyAutofillStatus
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.StateFlow
/**
* Manager which provides whether specific Chrome versions have third party autofill available and
* enabled.
*/
interface ChromeThirdPartyAutofillEnabledManager {
/**
* Combined status for all concerned Chrome versions.
*/
var chromeThirdPartyAutofillStatus: ChromeThirdPartyAutofillStatus
/**
* An observable [StateFlow] of the combined third party autofill status of all concerned
* chrome versions.
*/
val chromeThirdPartyAutofillStatusFlow: Flow<ChromeThirdPartyAutofillStatus>
}

View File

@@ -1,52 +0,0 @@
package com.x8bit.bitwarden.data.autofill.manager.chrome
import com.x8bit.bitwarden.data.autofill.model.chrome.ChromeThirdPartyAutoFillData
import com.x8bit.bitwarden.data.autofill.model.chrome.ChromeThirdPartyAutofillStatus
import com.x8bit.bitwarden.data.platform.manager.FeatureFlagManager
import com.x8bit.bitwarden.data.platform.manager.model.FlagKey
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.update
/**
* Default implementation of [ChromeThirdPartyAutofillEnabledManager].
*/
class ChromeThirdPartyAutofillEnabledManagerImpl(
private val featureFlagManager: FeatureFlagManager,
) : ChromeThirdPartyAutofillEnabledManager {
override var chromeThirdPartyAutofillStatus: ChromeThirdPartyAutofillStatus = DEFAULT_STATUS
set(value) {
field = value
mutableChromeThirdPartyAutofillStatusStateFlow.update {
value
}
}
private val mutableChromeThirdPartyAutofillStatusStateFlow = MutableStateFlow(
chromeThirdPartyAutofillStatus,
)
override val chromeThirdPartyAutofillStatusFlow: Flow<ChromeThirdPartyAutofillStatus>
get() = mutableChromeThirdPartyAutofillStatusStateFlow
.combine(
featureFlagManager.getFeatureFlagFlow(FlagKey.ChromeAutofill),
) { data, enabled ->
if (enabled) {
data
} else {
DEFAULT_STATUS
}
}
}
private val DEFAULT_STATUS = ChromeThirdPartyAutofillStatus(
ChromeThirdPartyAutoFillData(
isAvailable = false,
isThirdPartyEnabled = false,
),
ChromeThirdPartyAutoFillData(
isAvailable = false,
isThirdPartyEnabled = false,
),
)

View File

@@ -1,20 +0,0 @@
package com.x8bit.bitwarden.data.autofill.manager.chrome
import com.x8bit.bitwarden.data.autofill.model.chrome.ChromeThirdPartyAutoFillData
/**
* Manager class used to determine if a device has installed versions of Chrome (either the
* stable release or beta channel) which support and require opt in to third party autofill.
*/
interface ChromeThirdPartyAutofillManager {
/**
* The data representing the status of the stable chrome version
*/
val stableChromeAutofillStatus: ChromeThirdPartyAutoFillData
/**
* The data representing the status of the beta chrome version
*/
val betaChromeAutofillStatus: ChromeThirdPartyAutoFillData
}

View File

@@ -1,62 +0,0 @@
package com.x8bit.bitwarden.data.autofill.manager.chrome
import android.content.ContentResolver
import android.content.Context
import android.net.Uri
import com.x8bit.bitwarden.data.autofill.model.chrome.ChromeReleaseChannel
import com.x8bit.bitwarden.data.autofill.model.chrome.ChromeThirdPartyAutoFillData
import com.x8bit.bitwarden.data.platform.annotation.OmitFromCoverage
private const val CONTENT_PROVIDER_NAME = ".AutofillThirdPartyModeContentProvider"
private const val THIRD_PARTY_MODE_COLUMN = "autofill_third_party_state"
private const val THIRD_PARTY_MODE_ACTIONS_URI_PATH = "autofill_third_party_mode"
/**
* Default implementation of the [ChromeThirdPartyAutofillManager] which uses a
* [ContentResolver] to determine if the installed Chrome packages support and enable
* third party autofill services.
*
* Based off of [this blog post](https://android-developers.googleblog.com/2025/02/chrome-3p-autofill-services-update.html)
*/
@OmitFromCoverage
class ChromeThirdPartyAutofillManagerImpl(
private val context: Context,
) : ChromeThirdPartyAutofillManager {
override val stableChromeAutofillStatus: ChromeThirdPartyAutoFillData
get() = getThirdPartyAutoFillStatusForChannel(ChromeReleaseChannel.STABLE)
override val betaChromeAutofillStatus: ChromeThirdPartyAutoFillData
get() = getThirdPartyAutoFillStatusForChannel(ChromeReleaseChannel.BETA)
private fun getThirdPartyAutoFillStatusForChannel(
releaseChannel: ChromeReleaseChannel,
): ChromeThirdPartyAutoFillData {
val uri = Uri.Builder()
.scheme(ContentResolver.SCHEME_CONTENT)
.authority(releaseChannel.packageName + CONTENT_PROVIDER_NAME)
.path(THIRD_PARTY_MODE_ACTIONS_URI_PATH)
.build()
val cursor = context
.contentResolver
.query(
/* uri = */ uri,
/* projection = */ arrayOf(THIRD_PARTY_MODE_COLUMN),
/* selection = */ null,
/* selectionArgs = */ null,
/* sortOrder = */ null,
)
var thirdPartyEnabled = false
val isThirdPartyAvailable = cursor
?.let {
it.moveToFirst()
val columnIndex = it.getColumnIndex(THIRD_PARTY_MODE_COLUMN)
thirdPartyEnabled = it.getInt(columnIndex) != 0
it.close()
true
}
?: false
return ChromeThirdPartyAutoFillData(
isAvailable = isThirdPartyAvailable,
isThirdPartyEnabled = thirdPartyEnabled,
)
}
}

View File

@@ -1,14 +0,0 @@
package com.x8bit.bitwarden.data.autofill.model.chrome
private const val BETA_CHANNEL_PACKAGE = "com.chrome.beta"
private const val CHROME_CHANNEL_PACKAGE = "com.android.chrome"
/**
* Enumerated values of each version of Chrome supported for third party autofill checks.
*
* @property packageName the package name of the release channel for the Chrome version.
*/
enum class ChromeReleaseChannel(val packageName: String) {
STABLE(CHROME_CHANNEL_PACKAGE),
BETA(BETA_CHANNEL_PACKAGE),
}

View File

@@ -1,17 +0,0 @@
package com.x8bit.bitwarden.data.autofill.model.chrome
/**
* Relevant data relating to the third party autofill status of a version of the Chrome browser app.
*/
data class ChromeThirdPartyAutoFillData(
val isAvailable: Boolean,
val isThirdPartyEnabled: Boolean,
)
/**
* The overall status for all relevant release channels of Chrome.
*/
data class ChromeThirdPartyAutofillStatus(
val stableStatusData: ChromeThirdPartyAutoFillData,
val betaChannelStatusData: ChromeThirdPartyAutoFillData,
)

View File

@@ -2,7 +2,6 @@
package com.x8bit.bitwarden.data.autofill.util
import android.app.Activity
import android.app.PendingIntent
import android.app.assist.AssistStructure
import android.content.Context
@@ -148,12 +147,3 @@ fun Intent.getAutofillSelectionDataOrNull(): AutofillSelectionData? =
fun Intent.getTotpCopyIntentOrNull(): AutofillTotpCopyData? =
getBundleExtra(AUTOFILL_BUNDLE_KEY)
?.getSafeParcelableExtra(AUTOFILL_TOTP_COPY_DATA_KEY)
/**
* Checks if the given [Activity] was created for Autofill. This is useful to avoid locking the
* vault if one of the Autofill services starts the only instance of the [MainActivity].
*/
val Activity.createdForAutofill: Boolean
get() = intent.getAutofillSelectionDataOrNull() != null ||
intent.getAutofillSaveItemOrNull() != null ||
intent.getAutofillAssistStructureOrNull() != null

View File

@@ -24,6 +24,7 @@ fun AutofillRequest.Fillable.toAutofillSaveItem(): AutofillSaveItem =
.uri
?.replace("https://", "")
?.replace("http://", "")
?.replace("androidapp://", "")
AutofillSaveItem.Login(
username = partition.usernameSaveValue,

View File

@@ -6,6 +6,7 @@ import com.x8bit.bitwarden.data.platform.repository.util.bufferedMutableSharedFl
import com.x8bit.bitwarden.data.platform.util.decodeFromStringOrNull
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.onSubscription
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
private const val SERVER_CONFIGURATIONS = "serverConfigurations"

View File

@@ -6,6 +6,7 @@ import com.x8bit.bitwarden.data.platform.repository.util.bufferedMutableSharedFl
import com.x8bit.bitwarden.data.platform.util.decodeFromStringOrNull
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.onSubscription
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
private const val PRE_AUTH_URLS_KEY = "preAuthEnvironmentUrls"

View File

@@ -6,6 +6,7 @@ import com.x8bit.bitwarden.data.platform.datasource.network.model.OrganizationEv
import com.x8bit.bitwarden.data.platform.manager.dispatcher.DispatcherManager
import com.x8bit.bitwarden.data.platform.manager.model.OrganizationEventType
import kotlinx.coroutines.withContext
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
/**

View File

@@ -1,6 +1,5 @@
package com.x8bit.bitwarden.data.platform.datasource.disk
import com.x8bit.bitwarden.data.platform.manager.model.AppResumeScreenData
import com.x8bit.bitwarden.data.platform.repository.model.UriMatchType
import com.x8bit.bitwarden.data.platform.repository.model.VaultTimeoutAction
import com.x8bit.bitwarden.ui.platform.feature.settings.appearance.model.AppLanguage
@@ -19,11 +18,6 @@ interface SettingsDiskSource {
*/
var appLanguage: AppLanguage?
/**
* Emits updates that track [AppLanguage].
*/
val appLanguageFlow: Flow<AppLanguage?>
/**
* Has the initial autofill dialog been shown to the user.
*/
@@ -74,6 +68,12 @@ interface SettingsDiskSource {
*/
val hasUserLoggedInOrCreatedAccountFlow: Flow<Boolean?>
/**
* The instant when the last database scheme change was applied. `null` if no scheme changes
* have been applied yet.
*/
var lastDatabaseSchemeChangeInstant: Instant?
/**
* Clears all the settings data for the given user.
*/
@@ -314,92 +314,4 @@ interface SettingsDiskSource {
* Emits updates that track [getShowImportLoginsSettingBadge] for the given [userId].
*/
fun getShowImportLoginsSettingBadgeFlow(userId: String): Flow<Boolean?>
/**
* Gets whether or not the given [userId] has registered for export via the credential exchange
* protocol.
*/
fun getVaultRegisteredForExport(userId: String): Boolean?
/**
* Stores the given value for whether or not the given [userId] has registered for export via
* the credential exchange protocol.
*/
fun storeVaultRegisteredForExport(userId: String, isRegistered: Boolean?)
/**
* Emits updates that track [getVaultRegisteredForExport] for the given [userId].
*/
fun getVaultRegisteredForExportFlow(userId: String): Flow<Boolean?>
/**
* Gets the number of qualifying add cipher actions for the device.
*/
fun getAddCipherActionCount(): Int?
/**
* Stores the given [count] completed "add" cipher actions taken place on the device.
*/
fun storeAddCipherActionCount(count: Int?)
/**
* Gets the number of qualifying generated result actions for the device.
*/
fun getGeneratedResultActionCount(): Int?
/**
* Stores the given [count] completed generated password or username result actions taken
* for the device.
*/
fun storeGeneratedResultActionCount(count: Int?)
/**
* Gets the number of qualifying create send actions for the device.
*/
fun getCreateSendActionCount(): Int?
/**
* Stores the given [count] completed create send actions for the device.
*/
fun storeCreateSendActionCount(count: Int?)
/**
* Gets the Boolean value of if the Add Login CoachMark tour has been interacted with.
*/
fun getShouldShowAddLoginCoachMark(): Boolean?
/**
* Stores a value for if the Add Login CoachMark tour has been interacted with
*/
fun storeShouldShowAddLoginCoachMark(shouldShow: Boolean?)
/**
* Returns an [Flow] to observe updates to the "ShouldShowAddLoginCoachMark" value.
*/
fun getShouldShowAddLoginCoachMarkFlow(): Flow<Boolean?>
/**
* Gets the Boolean value of if the Generator CoachMark tour has been interacted with.
*/
fun getShouldShowGeneratorCoachMark(): Boolean?
/**
* Stores a value for if the Generator CoachMark tour has been interacted with
*/
fun storeShouldShowGeneratorCoachMark(shouldShow: Boolean?)
/**
* Returns an [Flow] to observe updates to the "ShouldShowGeneratorCoachMark" value.
*/
fun getShouldShowGeneratorCoachMarkFlow(): Flow<Boolean?>
/**
* Stores the given [screenData] as the screen to resume to identified by [userId].
*/
fun storeAppResumeScreen(userId: String, screenData: AppResumeScreenData?)
/**
* Gets the screen data to resume to for the device identified by [userId] or null if no screen
*/
fun getAppResumeScreen(userId: String): AppResumeScreenData?
}

View File

@@ -1,7 +1,6 @@
package com.x8bit.bitwarden.data.platform.datasource.disk
import android.content.SharedPreferences
import com.x8bit.bitwarden.data.platform.manager.model.AppResumeScreenData
import com.x8bit.bitwarden.data.platform.repository.model.UriMatchType
import com.x8bit.bitwarden.data.platform.repository.model.VaultTimeoutAction
import com.x8bit.bitwarden.data.platform.repository.util.bufferedMutableSharedFlow
@@ -11,6 +10,7 @@ import com.x8bit.bitwarden.ui.platform.feature.settings.appearance.model.AppThem
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.onSubscription
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import java.time.Instant
@@ -36,13 +36,7 @@ private const val HAS_USER_LOGGED_IN_OR_CREATED_AN_ACCOUNT_KEY = "hasUserLoggedI
private const val SHOW_AUTOFILL_SETTING_BADGE = "showAutofillSettingBadge"
private const val SHOW_UNLOCK_SETTING_BADGE = "showUnlockSettingBadge"
private const val SHOW_IMPORT_LOGINS_SETTING_BADGE = "showImportLoginsSettingBadge"
private const val IS_VAULT_REGISTERED_FOR_EXPORT = "isVaultRegisteredForExport"
private const val ADD_ACTION_COUNT = "addActionCount"
private const val COPY_ACTION_COUNT = "copyActionCount"
private const val CREATE_ACTION_COUNT = "createActionCount"
private const val SHOULD_SHOW_ADD_LOGIN_COACH_MARK = "shouldShowAddLoginCoachMark"
private const val SHOULD_SHOW_GENERATOR_COACH_MARK = "shouldShowGeneratorCoachMark"
private const val RESUME_SCREEN = "resumeScreen"
private const val LAST_SCHEME_CHANGE_INSTANT = "lastDatabaseSchemeChangeInstant"
/**
* Primary implementation of [SettingsDiskSource].
@@ -53,7 +47,6 @@ class SettingsDiskSourceImpl(
private val json: Json,
) : BaseDiskSource(sharedPreferences = sharedPreferences),
SettingsDiskSource {
private val mutableAppLanguageFlow = bufferedMutableSharedFlow<AppLanguage?>(replay = 1)
private val mutableAppThemeFlow = bufferedMutableSharedFlow<AppTheme>(replay = 1)
private val mutableLastSyncFlowMap = mutableMapOf<String, MutableSharedFlow<Instant?>>()
@@ -82,16 +75,9 @@ class SettingsDiskSourceImpl(
private val mutableHasUserLoggedInOrCreatedAccountFlow = bufferedMutableSharedFlow<Boolean?>()
private val mutableHasSeenAddLoginCoachMarkFlow = bufferedMutableSharedFlow<Boolean?>()
private val mutableHasSeenGeneratorCoachMarkFlow = bufferedMutableSharedFlow<Boolean?>()
private val mutableScreenCaptureAllowedFlowMap =
mutableMapOf<String, MutableSharedFlow<Boolean?>>()
private val mutableVaultRegisteredForExportFlow =
mutableMapOf<String, MutableSharedFlow<Boolean?>>()
override var appLanguage: AppLanguage?
get() = getString(key = APP_LANGUAGE_KEY)
?.let { storedValue ->
@@ -102,12 +88,8 @@ class SettingsDiskSourceImpl(
key = APP_LANGUAGE_KEY,
value = value?.localeName,
)
mutableAppLanguageFlow.tryEmit(value)
}
override val appLanguageFlow: Flow<AppLanguage?>
get() = mutableAppLanguageFlow.onSubscription { emit(appLanguage) }
override var initialAutofillDialogShown: Boolean?
get() = getBoolean(key = INITIAL_AUTOFILL_DIALOG_SHOWN)
set(value) {
@@ -174,6 +156,10 @@ class SettingsDiskSourceImpl(
get() = mutableHasUserLoggedInOrCreatedAccountFlow
.onSubscription { emit(getBoolean(HAS_USER_LOGGED_IN_OR_CREATED_AN_ACCOUNT_KEY)) }
override var lastDatabaseSchemeChangeInstant: Instant?
get() = getLong(LAST_SCHEME_CHANGE_INSTANT)?.let { Instant.ofEpochMilli(it) }
set(value) = putLong(LAST_SCHEME_CHANGE_INSTANT, value?.toEpochMilli())
override fun clearData(userId: String) {
storeVaultTimeoutInMinutes(userId = userId, vaultTimeoutInMinutes = null)
storeVaultTimeoutAction(userId = userId, vaultTimeoutAction = null)
@@ -186,16 +172,12 @@ class SettingsDiskSourceImpl(
storeLastSyncTime(userId = userId, lastSyncTime = null)
storeClearClipboardFrequencySeconds(userId = userId, frequency = null)
removeWithPrefix(prefix = ACCOUNT_BIOMETRIC_INTEGRITY_VALID_KEY.appendIdentifier(userId))
storeVaultRegisteredForExport(userId = userId, isRegistered = null)
storeAppResumeScreen(userId = userId, screenData = null)
// The following are intentionally not cleared so they can be
// restored after logging out and back in:
// - screen capture allowed
// - show autofill setting badge
// - show unlock setting badge
// - should show add login coach mark
// - should show generator coach mark
}
override fun getAccountBiometricIntegrityValidity(
@@ -452,93 +434,6 @@ class SettingsDiskSourceImpl(
getMutableShowImportLoginsSettingBadgeFlow(userId)
.onSubscription { emit(getShowImportLoginsSettingBadge(userId)) }
override fun getVaultRegisteredForExport(userId: String): Boolean? =
getBoolean(IS_VAULT_REGISTERED_FOR_EXPORT.appendIdentifier(userId))
override fun storeVaultRegisteredForExport(userId: String, isRegistered: Boolean?) {
putBoolean(IS_VAULT_REGISTERED_FOR_EXPORT.appendIdentifier(userId), isRegistered)
getMutableVaultRegisteredForExportFlow(userId).tryEmit(isRegistered)
}
override fun getVaultRegisteredForExportFlow(userId: String): Flow<Boolean?> =
getMutableVaultRegisteredForExportFlow(userId)
.onSubscription { emit(getVaultRegisteredForExport(userId)) }
override fun getAddCipherActionCount(): Int? = getInt(
key = ADD_ACTION_COUNT,
)
override fun storeAddCipherActionCount(count: Int?) {
putInt(
key = ADD_ACTION_COUNT,
value = count,
)
}
override fun getGeneratedResultActionCount(): Int? = getInt(
key = COPY_ACTION_COUNT,
)
override fun storeGeneratedResultActionCount(count: Int?) {
putInt(
key = COPY_ACTION_COUNT,
value = count,
)
}
override fun getCreateSendActionCount(): Int? = getInt(
key = CREATE_ACTION_COUNT,
)
override fun storeCreateSendActionCount(count: Int?) {
putInt(
key = CREATE_ACTION_COUNT,
value = count,
)
}
override fun getShouldShowAddLoginCoachMark(): Boolean? =
getBoolean(key = SHOULD_SHOW_ADD_LOGIN_COACH_MARK)
override fun storeShouldShowAddLoginCoachMark(shouldShow: Boolean?) {
putBoolean(
key = SHOULD_SHOW_ADD_LOGIN_COACH_MARK,
value = shouldShow,
)
mutableHasSeenAddLoginCoachMarkFlow.tryEmit(shouldShow)
}
override fun getShouldShowAddLoginCoachMarkFlow(): Flow<Boolean?> =
mutableHasSeenAddLoginCoachMarkFlow.onSubscription {
emit(getBoolean(key = SHOULD_SHOW_ADD_LOGIN_COACH_MARK))
}
override fun getShouldShowGeneratorCoachMark(): Boolean? =
getBoolean(key = SHOULD_SHOW_GENERATOR_COACH_MARK)
override fun storeShouldShowGeneratorCoachMark(shouldShow: Boolean?) {
putBoolean(
key = SHOULD_SHOW_GENERATOR_COACH_MARK,
value = shouldShow,
)
mutableHasSeenGeneratorCoachMarkFlow.tryEmit(shouldShow)
}
override fun getShouldShowGeneratorCoachMarkFlow(): Flow<Boolean?> =
mutableHasSeenGeneratorCoachMarkFlow.onSubscription {
emit(getShouldShowGeneratorCoachMark())
}
override fun storeAppResumeScreen(userId: String, screenData: AppResumeScreenData?) {
putString(
key = RESUME_SCREEN.appendIdentifier(userId),
value = screenData?.let { json.encodeToString(it) },
)
}
override fun getAppResumeScreen(userId: String): AppResumeScreenData? =
getString(RESUME_SCREEN.appendIdentifier(userId))?.let { json.decodeFromStringOrNull(it) }
private fun getMutableLastSyncFlow(
userId: String,
): MutableSharedFlow<Instant?> =
@@ -589,10 +484,4 @@ class SettingsDiskSourceImpl(
mutableShowImportLoginsSettingBadgeFlowMap.getOrPut(userId) {
bufferedMutableSharedFlow(replay = 1)
}
private fun getMutableVaultRegisteredForExportFlow(
userId: String,
): MutableSharedFlow<Boolean?> = mutableVaultRegisteredForExportFlow.getOrPut(userId) {
bufferedMutableSharedFlow(replay = 1)
}
}

View File

@@ -37,6 +37,7 @@ import dagger.hilt.InstallIn
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent
import kotlinx.serialization.json.Json
import java.time.Clock
import javax.inject.Singleton
/**
@@ -73,6 +74,7 @@ object PlatformDiskModule {
fun provideEventDatabase(
app: Application,
databaseSchemeManager: DatabaseSchemeManager,
clock: Clock,
): PlatformDatabase =
Room
.databaseBuilder(
@@ -82,7 +84,12 @@ object PlatformDiskModule {
)
.fallbackToDestructiveMigration()
.addTypeConverter(ZonedDateTimeTypeConverter())
.addCallback(DatabaseSchemeCallback(databaseSchemeManager = databaseSchemeManager))
.addCallback(
DatabaseSchemeCallback(
databaseSchemeManager = databaseSchemeManager,
clock = clock,
),
)
.build()
@Provides

View File

@@ -172,7 +172,7 @@ private class AndroidKeyStore(
private val useSymmetricPreferenceKey: String = "essentials_use_symmetric"
private val prefsMasterKey = "SecureStorageKey"
private val initializationVectorLen = 12 // Android supports an IV of 12 for AES/GCM
private val initializationVectorLen = 12; // Android supports an IV of 12 for AES/GCM
init {
keyStore.load(null)

View File

@@ -1,36 +0,0 @@
package com.x8bit.bitwarden.data.platform.datasource.disk.model
import java.security.PrivateKey
import java.security.cert.X509Certificate
/**
* Represents a mutual TLS certificate.
*/
data class MutualTlsCertificate(
val alias: String,
val privateKey: PrivateKey,
val certificateChain: List<X509Certificate>,
) {
/**
* Leaf certificate of the chain.
*/
val leafCertificate: X509Certificate?
get() = certificateChain.lastOrNull()
/**
* Root certificate of the chain.
*/
val rootCertificate: X509Certificate?
get() = certificateChain.firstOrNull()
override fun toString(): String = leafCertificate
?.let {
buildString {
appendLine("Subject: ${it.subjectDN}")
appendLine("Issuer: ${it.issuerDN}")
appendLine("Valid From: ${it.notBefore}")
appendLine("Valid Until: ${it.notAfter}")
}
}
?: ""
}

View File

@@ -1,16 +0,0 @@
package com.x8bit.bitwarden.data.platform.datasource.disk.model
/**
* Location of the key data.
*/
enum class MutualTlsKeyHost {
/**
* Key is stored in the system key chain.
*/
KEY_CHAIN,
/**
* Key is stored in a private instance of the Android Key Store.
*/
ANDROID_KEY_STORE,
}

View File

@@ -14,10 +14,6 @@ import com.x8bit.bitwarden.data.platform.datasource.network.service.EventService
import com.x8bit.bitwarden.data.platform.datasource.network.service.EventServiceImpl
import com.x8bit.bitwarden.data.platform.datasource.network.service.PushService
import com.x8bit.bitwarden.data.platform.datasource.network.service.PushServiceImpl
import com.x8bit.bitwarden.data.platform.datasource.network.ssl.SslManager
import com.x8bit.bitwarden.data.platform.datasource.network.ssl.SslManagerImpl
import com.x8bit.bitwarden.data.platform.manager.KeyManager
import com.x8bit.bitwarden.data.platform.repository.EnvironmentRepository
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
@@ -74,17 +70,6 @@ object PlatformNetworkModule {
@Singleton
fun providesRefreshAuthenticator(): RefreshAuthenticator = RefreshAuthenticator()
@Provides
@Singleton
fun provideSslManager(
keyManager: KeyManager,
environmentRepository: EnvironmentRepository,
): SslManager =
SslManagerImpl(
keyManager = keyManager,
environmentRepository = environmentRepository,
)
@Provides
@Singleton
fun provideRetrofits(
@@ -92,7 +77,6 @@ object PlatformNetworkModule {
baseUrlInterceptors: BaseUrlInterceptors,
headersInterceptor: HeadersInterceptor,
refreshAuthenticator: RefreshAuthenticator,
sslManager: SslManager,
json: Json,
): Retrofits =
RetrofitsImpl(
@@ -100,7 +84,6 @@ object PlatformNetworkModule {
baseUrlInterceptors = baseUrlInterceptors,
headersInterceptor = headersInterceptor,
refreshAuthenticator = refreshAuthenticator,
sslManager = sslManager,
json = json,
)

Some files were not shown because too many files have changed in this diff Show More