mirror of
https://github.com/bitwarden/android.git
synced 2026-05-11 10:54:26 -05:00
Compare commits
4 Commits
v2025.1.1
...
v2024.11.7
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0413cdc70d | ||
|
|
019bf8d0fa | ||
|
|
366c86da41 | ||
|
|
a1881ce4d9 |
4
.github/CODEOWNERS
vendored
4
.github/CODEOWNERS
vendored
@@ -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
|
||||
|
||||
2
.github/ISSUE_TEMPLATE/config.yml
vendored
2
.github/ISSUE_TEMPLATE/config.yml
vendored
@@ -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
|
||||
|
||||
106
.github/workflows/build.yml
vendored
106
.github/workflows/build.yml
vendored
@@ -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@4a9ddd6f338a97768b8006bf671dfbad383215f4 # v1.207.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@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.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@4a9ddd6f338a97768b8006bf671dfbad383215f4 # v1.207.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@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.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@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.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@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.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@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.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@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.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@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.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@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.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@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.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@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.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@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.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@4a9ddd6f338a97768b8006bf671dfbad383215f4 # v1.207.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,20 +455,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
|
||||
|
||||
# Start from 11000 to prevent collisions with mobile build version codes
|
||||
- name: Increment version
|
||||
run: |
|
||||
@@ -515,38 +497,38 @@ jobs:
|
||||
keyPassword:"${{ env.FDROID_BETA_KEY_PASSWORD }}"
|
||||
|
||||
- name: Upload F-Droid .apk artifact
|
||||
uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.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@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.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@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.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@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.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
|
||||
|
||||
13
.github/workflows/crowdin-pull.yml
vendored
13
.github/workflows/crowdin-pull.yml
vendored
@@ -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@8dfaf9c206381653e3767e3cb5ea5f08b45f02bf # v2.5.0
|
||||
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
|
||||
|
||||
4
.github/workflows/crowdin-push.yml
vendored
4
.github/workflows/crowdin-push.yml
vendored
@@ -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@8dfaf9c206381653e3767e3cb5ea5f08b45f02bf # v2.5.0
|
||||
uses: crowdin/github-action@2d540f18b0a416b1fbf2ee5be35841bd380fc1da # v2.3.0
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
CROWDIN_API_TOKEN: ${{ steps.retrieve-secrets.outputs.crowdin-api-token }}
|
||||
|
||||
22
.github/workflows/github-release.yml
vendored
22
.github/workflows/github-release.yml
vendored
@@ -11,7 +11,7 @@ on:
|
||||
description: 'Version Number - E.g. "123456"'
|
||||
required: true
|
||||
type: string
|
||||
artifact-run-id:
|
||||
artifact_run_id:
|
||||
description: 'GitHub Action Run ID containing artifacts'
|
||||
required: true
|
||||
type: string
|
||||
@@ -22,8 +22,7 @@ on:
|
||||
prerelease:
|
||||
description: 'Mark as pre-release'
|
||||
type: boolean
|
||||
default: true
|
||||
make-latest:
|
||||
make_latest:
|
||||
description: 'Set as the latest release'
|
||||
type: boolean
|
||||
branch-protection-type:
|
||||
@@ -37,7 +36,6 @@ env:
|
||||
ARTIFACTS_PATH: artifacts
|
||||
jobs:
|
||||
create-release:
|
||||
name: Create GitHub Release
|
||||
runs-on: ubuntu-24.04
|
||||
permissions:
|
||||
contents: write
|
||||
@@ -45,7 +43,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Check out repository
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
@@ -53,7 +51,7 @@ jobs:
|
||||
id: get_release_branch
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
ARTIFACT_RUN_ID: ${{ inputs.artifact-run-id }}
|
||||
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)
|
||||
@@ -83,7 +81,7 @@ jobs:
|
||||
- name: Download artifacts
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
ARTIFACT_RUN_ID: ${{ inputs.artifact-run-id }}
|
||||
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)
|
||||
@@ -95,13 +93,13 @@ jobs:
|
||||
|
||||
- name: Create Release
|
||||
id: create_release
|
||||
uses: softprops/action-gh-release@7b4da11513bf3f43f9999e90eabced41ab8bb048 # v2.2.0
|
||||
uses: softprops/action-gh-release@e7a8f85e1c67a31e6ed99a94b41bd0b71bbee6b8 # v2.0.9
|
||||
with:
|
||||
tag_name: "v${{ inputs.version-name }}"
|
||||
name: "${{ inputs.version-name }} (${{ inputs.version-number }})"
|
||||
tag_name: ${{ inputs.version-name }}
|
||||
name: "v${{ inputs.version-name }} (${{ inputs.version-number }})"
|
||||
prerelease: ${{ inputs.prerelease }}
|
||||
draft: ${{ inputs.draft }}
|
||||
make_latest: ${{ inputs.make-latest }}
|
||||
make_latest: ${{ inputs.make_latest }}
|
||||
target_commitish: ${{ steps.get_release_branch.outputs.release_branch }}
|
||||
generate_release_notes: true
|
||||
files: |
|
||||
@@ -112,7 +110,7 @@ jobs:
|
||||
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 }}
|
||||
ARTIFACT_RUN_ID: ${{ inputs.artifact_run_id }}
|
||||
run: |
|
||||
# Get current release body
|
||||
current_body=$(gh api /repos/${{ github.repository }}/releases/$RELEASE_ID --jq .body)
|
||||
|
||||
16
.github/workflows/release-branch.yml
vendored
16
.github/workflows/release-branch.yml
vendored
@@ -10,23 +10,26 @@ on:
|
||||
options:
|
||||
- RC
|
||||
- Hotfix
|
||||
rc_prefix_date:
|
||||
description: 'RC - Prefix with date. E.g. 2024.11-rc1'
|
||||
type: boolean
|
||||
default: true
|
||||
|
||||
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
|
||||
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Create RC Branch
|
||||
if: inputs.release_type == 'RC'
|
||||
env:
|
||||
RC_PREFIX_DATE: "true" # replace with input if needed
|
||||
RC_PREFIX_DATE: ${{ inputs.rc_prefix_date }}
|
||||
run: |
|
||||
if [ "$RC_PREFIX_DATE" = "true" ]; then
|
||||
current_date=$(date +'%Y.%m')
|
||||
@@ -42,17 +45,12 @@ jobs:
|
||||
- name: Create Hotfix Branch
|
||||
if: inputs.release_type == 'Hotfix'
|
||||
run: |
|
||||
latest_tag=$(git tag -l --sort=-creatordate | head -n 1)
|
||||
latest_tag=$(git describe --tags --abbrev=0)
|
||||
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
|
||||
|
||||
60
.github/workflows/scan-ci.yml
vendored
60
.github/workflows/scan-ci.yml
vendored
@@ -1,60 +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@48ab28a6f5dbc2a99bf1e0131198dd8f1df78169 # v3.28.0
|
||||
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 }}
|
||||
14
.github/workflows/scan.yml
vendored
14
.github/workflows/scan.yml
vendored
@@ -1,7 +1,12 @@
|
||||
name: Scan Pull Requests
|
||||
name: Scan
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches:
|
||||
- "main"
|
||||
- "rc"
|
||||
- "hotfix-rc"
|
||||
pull_request_target:
|
||||
types: [opened, synchronize]
|
||||
merge_group:
|
||||
@@ -28,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:
|
||||
@@ -43,7 +48,7 @@ jobs:
|
||||
--output-path . ${{ env.INCREMENTAL }}
|
||||
|
||||
- name: Upload Checkmarx results to GitHub
|
||||
uses: github/codeql-action/upload-sarif@48ab28a6f5dbc2a99bf1e0131198dd8f1df78169 # v3.28.0
|
||||
uses: github/codeql-action/upload-sarif@9278e421667d5d90a2839487a482448c4ec7df4d # v3.27.2
|
||||
with:
|
||||
sarif_file: cx_result.sarif
|
||||
|
||||
@@ -63,9 +68,10 @@ 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 }}
|
||||
|
||||
80
.github/workflows/test.yml
vendored
80
.github/workflows/test.yml
vendored
@@ -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@4a9ddd6f338a97768b8006bf671dfbad383215f4 # v1.207.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,56 +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@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.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: always()
|
||||
|
||||
steps:
|
||||
- name: Download test artifacts
|
||||
uses: actions/download-artifact@c850b930e6ba138125429b7e5c93fc707a7f8427 # v4.1.4
|
||||
- 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
|
||||
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'
|
||||
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 }}
|
||||
|
||||
30
Gemfile.lock
30
Gemfile.lock
@@ -10,16 +10,16 @@ GEM
|
||||
artifactory (3.0.17)
|
||||
atomos (0.1.3)
|
||||
aws-eventstream (1.3.0)
|
||||
aws-partitions (1.1027.0)
|
||||
aws-sdk-core (3.214.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.96.0)
|
||||
aws-sdk-kms (1.95.0)
|
||||
aws-sdk-core (~> 3, >= 3.210.0)
|
||||
aws-sigv4 (~> 1.5)
|
||||
aws-sdk-s3 (1.176.1)
|
||||
aws-sdk-s3 (1.170.0)
|
||||
aws-sdk-core (~> 3, >= 3.210.0)
|
||||
aws-sdk-kms (~> 1)
|
||||
aws-sigv4 (~> 1.5)
|
||||
@@ -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)
|
||||
@@ -69,7 +69,7 @@ GEM
|
||||
faraday_middleware (1.2.1)
|
||||
faraday (~> 1.0)
|
||||
fastimage (2.3.1)
|
||||
fastlane (2.226.0)
|
||||
fastlane (2.225.0)
|
||||
CFPropertyList (>= 2.3, < 4.0.0)
|
||||
addressable (>= 2.8, < 3.0.0)
|
||||
artifactory (~> 3.0)
|
||||
@@ -109,7 +109,7 @@ 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.9.1)
|
||||
google-apis-firebaseappdistribution_v1 (~> 0.3.0)
|
||||
@@ -158,11 +158,11 @@ 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)
|
||||
json (2.8.1)
|
||||
jwt (2.9.3)
|
||||
base64
|
||||
mini_magick (4.13.2)
|
||||
@@ -182,8 +182,8 @@ 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.3.2)
|
||||
security (0.1.5)
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
@@ -351,10 +298,6 @@ tasks {
|
||||
dependsOn("detekt")
|
||||
}
|
||||
|
||||
getByName("sonar") {
|
||||
dependsOn("check")
|
||||
}
|
||||
|
||||
withType<io.gitlab.arturbosch.detekt.Detekt>().configureEach {
|
||||
jvmTarget = libs.versions.jvmTarget.get()
|
||||
}
|
||||
@@ -367,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 }
|
||||
}
|
||||
@@ -393,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")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -1,17 +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": {
|
||||
@@ -36,18 +24,6 @@
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"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": {
|
||||
@@ -87,6 +63,18 @@
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"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"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ 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
|
||||
@@ -38,6 +39,9 @@ class MainActivity : AppCompatActivity() {
|
||||
|
||||
private val mainViewModel: MainViewModel by viewModels()
|
||||
|
||||
@Inject
|
||||
lateinit var accessibilityActivityManager: AccessibilityActivityManager
|
||||
|
||||
@Inject
|
||||
lateinit var autofillActivityManager: AutofillActivityManager
|
||||
|
||||
|
||||
@@ -325,7 +325,7 @@ class MainViewModel @Inject constructor(
|
||||
fido2CredentialManager.isUserVerified = false
|
||||
specialCircumstanceManager.specialCircumstance =
|
||||
SpecialCircumstance.Fido2Save(
|
||||
fido2CreateCredentialRequest = fido2CredentialRequestData,
|
||||
fido2CredentialRequest = fido2CredentialRequestData,
|
||||
)
|
||||
|
||||
// Switch accounts if the selected user is not the active user.
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
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
|
||||
@@ -172,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].
|
||||
*/
|
||||
@@ -339,17 +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?)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
@@ -23,7 +21,6 @@ import java.util.UUID
|
||||
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,7 +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"
|
||||
|
||||
/**
|
||||
* Primary implementation of [AuthDiskSource].
|
||||
@@ -146,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)
|
||||
@@ -282,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))
|
||||
|
||||
@@ -487,22 +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) },
|
||||
)
|
||||
}
|
||||
|
||||
private fun generateAndStoreUniqueAppId(): String =
|
||||
UUID
|
||||
.randomUUID()
|
||||
|
||||
@@ -2,12 +2,10 @@ 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 +35,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,7 +46,6 @@ 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
|
||||
@@ -63,9 +59,6 @@ data class AccountJson(
|
||||
@SerialName("emailVerified")
|
||||
val isEmailVerified: Boolean?,
|
||||
|
||||
@SerialName("isTwoFactorEnabled")
|
||||
val isTwoFactorEnabled: Boolean?,
|
||||
|
||||
@SerialName("name")
|
||||
val name: String?,
|
||||
|
||||
@@ -99,10 +92,6 @@ data class AccountJson(
|
||||
@SerialName("userDecryptionOptions")
|
||||
@JsonNames("accountDecryptionOptions")
|
||||
val userDecryptionOptions: UserDecryptionOptionsJson?,
|
||||
|
||||
@SerialName("creationDate")
|
||||
@Contextual
|
||||
val creationDate: ZonedDateTime?,
|
||||
)
|
||||
|
||||
/**
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
@@ -69,7 +68,7 @@ interface IdentityService {
|
||||
*/
|
||||
suspend fun sendVerificationEmail(
|
||||
body: SendVerificationEmailRequestJson,
|
||||
): Result<SendVerificationEmailResponseJson>
|
||||
): Result<String?>
|
||||
|
||||
/**
|
||||
* Register a new account to Bitwarden using email verification flow.
|
||||
|
||||
@@ -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
|
||||
@@ -133,20 +132,11 @@ class IdentityServiceImpl(
|
||||
|
||||
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 }
|
||||
}
|
||||
|
||||
override suspend fun verifyEmailRegistrationToken(
|
||||
|
||||
@@ -55,5 +55,5 @@ class TrustedDeviceManagerImpl(
|
||||
authDiskSource.storeIsTdeLoginComplete(userId = userId, isTdeLoginComplete = true)
|
||||
}
|
||||
.also { authDiskSource.storeShouldTrustDevice(userId = userId, shouldTrustDevice = null) }
|
||||
.map { }
|
||||
.map { Unit }
|
||||
}
|
||||
|
||||
@@ -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
|
||||
@@ -402,19 +401,4 @@ interface AuthRepository : AuthenticatorProvider, AuthRequestManager {
|
||||
* Update the value of the onboarding status for the user.
|
||||
*/
|
||||
fun setOnboardingStatus(userId: String, 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?)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
@@ -24,7 +23,6 @@ import com.x8bit.bitwarden.data.auth.datasource.network.model.RegisterResponseJs
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.ResendEmailRequestJson
|
||||
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
|
||||
@@ -96,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
|
||||
@@ -108,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
|
||||
@@ -118,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
|
||||
@@ -144,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
|
||||
|
||||
/**
|
||||
@@ -629,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 },
|
||||
)
|
||||
|
||||
@@ -1258,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 },
|
||||
)
|
||||
}
|
||||
@@ -1293,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)
|
||||
@@ -1337,91 +1344,6 @@ class AuthRepositoryImpl(
|
||||
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
|
||||
}
|
||||
|
||||
@Suppress("CyclomaticComplexMethod")
|
||||
private suspend fun validatePasswordAgainstPolicy(
|
||||
password: String,
|
||||
@@ -1586,12 +1508,9 @@ class AuthRepositoryImpl(
|
||||
captchaToken = captchaToken,
|
||||
)
|
||||
.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)
|
||||
}
|
||||
},
|
||||
|
||||
@@ -28,9 +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()
|
||||
}
|
||||
|
||||
@@ -18,5 +18,4 @@ data class Organization(
|
||||
val shouldManageResetPassword: Boolean,
|
||||
val shouldUseKeyConnector: Boolean,
|
||||
val role: OrganizationType,
|
||||
val shouldUsersGetPremium: Boolean,
|
||||
)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,7 +22,6 @@ fun SyncResponseJson.Profile.Organization.toOrganization(): Organization =
|
||||
shouldUseKeyConnector = this.shouldUseKeyConnector,
|
||||
role = this.type,
|
||||
shouldManageResetPassword = this.permissions.shouldManageResetPassword,
|
||||
shouldUsersGetPremium = this.shouldUsersGetPremium,
|
||||
)
|
||||
|
||||
/**
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
}
|
||||
@@ -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
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
}
|
||||
|
||||
@@ -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,18 +7,14 @@ import kotlinx.coroutines.flow.asStateFlow
|
||||
/**
|
||||
* The default implementation of [AccessibilityEnabledManager].
|
||||
*/
|
||||
class AccessibilityEnabledManagerImpl(
|
||||
accessibilityManager: AccessibilityManager,
|
||||
) : AccessibilityEnabledManager {
|
||||
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()
|
||||
|
||||
@@ -9,5 +9,5 @@ data class FillableFields(
|
||||
val usernameField: AccessibilityNodeInfo?,
|
||||
val passwordFields: List<AccessibilityNodeInfo>,
|
||||
) {
|
||||
val hasFields: Boolean = usernameField != null || passwordFields.isNotEmpty()
|
||||
val hasFields: Boolean = usernameField != null && passwordFields.isNotEmpty()
|
||||
}
|
||||
|
||||
@@ -8,8 +8,6 @@ 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
|
||||
@@ -60,26 +58,17 @@ object Fido2ProviderModule {
|
||||
@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,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -6,17 +6,21 @@ import com.bitwarden.fido.Origin
|
||||
import com.bitwarden.fido.UnverifiedAssetLink
|
||||
import com.bitwarden.sdk.Fido2CredentialStore
|
||||
import com.bitwarden.vault.CipherView
|
||||
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
|
||||
@@ -27,14 +31,18 @@ import kotlinx.serialization.SerializationException
|
||||
import kotlinx.serialization.encodeToString
|
||||
import kotlinx.serialization.json.Json
|
||||
|
||||
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 {
|
||||
@@ -45,31 +53,31 @@ class Fido2CredentialManagerImpl(
|
||||
|
||||
override suspend fun registerFido2Credential(
|
||||
userId: String,
|
||||
fido2CreateCredentialRequest: Fido2CreateCredentialRequest,
|
||||
fido2CredentialRequest: Fido2CredentialRequest,
|
||||
selectedCipherView: CipherView,
|
||||
): Fido2RegisterCredentialResult {
|
||||
val clientData = if (fido2CreateCredentialRequest.callingAppInfo.isOriginPopulated()) {
|
||||
fido2CreateCredentialRequest
|
||||
val clientData = if (fido2CredentialRequest.callingAppInfo.isOriginPopulated()) {
|
||||
fido2CredentialRequest
|
||||
.callingAppInfo
|
||||
.getAppSigningSignatureFingerprint()
|
||||
?.let { ClientData.DefaultWithCustomHash(hash = it) }
|
||||
?: return Fido2RegisterCredentialResult.Error
|
||||
} else {
|
||||
ClientData.DefaultWithExtraData(
|
||||
androidPackageName = fido2CreateCredentialRequest
|
||||
androidPackageName = fido2CredentialRequest
|
||||
.callingAppInfo
|
||||
.packageName,
|
||||
)
|
||||
}
|
||||
val assetLinkUrl = fido2CreateCredentialRequest
|
||||
val assetLinkUrl = fido2CredentialRequest
|
||||
.origin
|
||||
?: getOriginUrlFromAttestationOptionsOrNull(fido2CreateCredentialRequest.requestJson)
|
||||
?: getOriginUrlFromAttestationOptionsOrNull(fido2CredentialRequest.requestJson)
|
||||
?: return Fido2RegisterCredentialResult.Error
|
||||
|
||||
val origin = Origin.Android(
|
||||
UnverifiedAssetLink(
|
||||
packageName = fido2CreateCredentialRequest.packageName,
|
||||
sha256CertFingerprint = fido2CreateCredentialRequest
|
||||
packageName = fido2CredentialRequest.packageName,
|
||||
sha256CertFingerprint = fido2CredentialRequest
|
||||
.callingAppInfo
|
||||
.getSignatureFingerprintAsHexString()
|
||||
?: return Fido2RegisterCredentialResult.Error,
|
||||
@@ -83,7 +91,7 @@ class Fido2CredentialManagerImpl(
|
||||
request = RegisterFido2CredentialRequest(
|
||||
userId = userId,
|
||||
origin = origin,
|
||||
requestJson = """{"publicKey": ${fido2CreateCredentialRequest.requestJson}}""",
|
||||
requestJson = """{"publicKey": ${fido2CredentialRequest.requestJson}}""",
|
||||
clientData = clientData,
|
||||
selectedCipherView = selectedCipherView,
|
||||
// User verification is handled prior to engaging the SDK. We always respond
|
||||
@@ -100,14 +108,16 @@ class Fido2CredentialManagerImpl(
|
||||
)
|
||||
}
|
||||
|
||||
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,
|
||||
@@ -158,7 +168,7 @@ class Fido2CredentialManagerImpl(
|
||||
Fido2CredentialAssertionResult.Error
|
||||
}
|
||||
|
||||
is Fido2ValidateOriginResult.Success -> {
|
||||
Fido2ValidateOriginResult.Success -> {
|
||||
vaultSdkSource
|
||||
.authenticateFido2Credential(
|
||||
request = AuthenticateFido2CredentialRequest(
|
||||
@@ -190,6 +200,127 @@ class Fido2CredentialManagerImpl(
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
|
||||
@@ -1,32 +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
|
||||
|
||||
/**
|
||||
* Returns the privileged app origin, or null if the calling app is not allowed.
|
||||
*
|
||||
* @param callingAppInfo The calling app info.
|
||||
*
|
||||
* @return The privileged app origin, or null.
|
||||
*/
|
||||
suspend fun getPrivilegedAppOriginOrNull(callingAppInfo: CallingAppInfo): String?
|
||||
}
|
||||
@@ -1,172 +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)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun getPrivilegedAppOriginOrNull(callingAppInfo: CallingAppInfo): String? {
|
||||
if (!callingAppInfo.isOriginPopulated()) return null
|
||||
return callingAppInfo.getOrigin(getGoogleAllowListOrNull().orEmpty())
|
||||
?: callingAppInfo.getOrigin(getCommunityAllowListOrNull().orEmpty())
|
||||
?.takeUnless { !callingAppInfo.isOriginPopulated() }
|
||||
}
|
||||
|
||||
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 = { 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() }
|
||||
|
||||
private suspend fun getGoogleAllowListOrNull(): String? =
|
||||
assetManager
|
||||
.readAsset(GOOGLE_ALLOW_LIST_FILE_NAME)
|
||||
.onFailure { Timber.e(it, "Failed to read Google allow list.") }
|
||||
.getOrNull()
|
||||
|
||||
private suspend fun getCommunityAllowListOrNull(): String? =
|
||||
assetManager
|
||||
.readAsset(COMMUNITY_ALLOW_LIST_FILE_NAME)
|
||||
.onFailure { Timber.e(it, "Failed to read Community allow list.") }
|
||||
.getOrNull()
|
||||
}
|
||||
@@ -14,7 +14,7 @@ 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,
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
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
|
||||
@@ -28,20 +27,16 @@ 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.dispatcher.DispatcherManager
|
||||
import com.x8bit.bitwarden.data.platform.repository.model.DataState
|
||||
import com.x8bit.bitwarden.data.platform.repository.util.takeUntilLoaded
|
||||
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
|
||||
@@ -228,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) {
|
||||
@@ -275,13 +266,6 @@ class Fido2ProviderProcessorImpl(
|
||||
),
|
||||
beginGetPublicKeyCredentialOption = option,
|
||||
)
|
||||
.setIcon(
|
||||
Icon
|
||||
.createWithResource(
|
||||
context,
|
||||
R.drawable.ic_bw_passkey,
|
||||
),
|
||||
)
|
||||
.build()
|
||||
}
|
||||
|
||||
|
||||
@@ -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.getFido2CredentialRequestOrNull(): Fido2CreateCredentialRequest? {
|
||||
fun Intent.getFido2CredentialRequestOrNull(): Fido2CredentialRequest? {
|
||||
if (isBuildVersionBelow(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)) return null
|
||||
|
||||
val systemRequest = PendingIntentHandler
|
||||
@@ -33,7 +33,7 @@ fun Intent.getFido2CredentialRequestOrNull(): Fido2CreateCredentialRequest? {
|
||||
val userId = getStringExtra(EXTRA_KEY_USER_ID)
|
||||
?: return null
|
||||
|
||||
return Fido2CreateCredentialRequest(
|
||||
return Fido2CredentialRequest(
|
||||
userId = userId,
|
||||
requestJson = createPublicKeyRequest.requestJson,
|
||||
packageName = systemRequest.callingAppInfo.packageName,
|
||||
|
||||
@@ -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.vault.feature.vault.util.getOrganizationPremiumStatusMap
|
||||
import java.time.Clock
|
||||
|
||||
/**
|
||||
@@ -25,15 +24,8 @@ class AutofillTotpManagerImpl(
|
||||
) : AutofillTotpManager {
|
||||
override suspend fun tryCopyTotpToClipboard(cipherView: CipherView) {
|
||||
if (settingsRepository.isAutoCopyTotpDisabled) return
|
||||
val organizationPremiumStatusMap = authRepository
|
||||
.userStateFlow
|
||||
.value
|
||||
?.activeAccount
|
||||
?.getOrganizationPremiumStatusMap()
|
||||
.orEmpty()
|
||||
val isPremium = authRepository.userStateFlow.value?.activeAccount?.isPremium == true
|
||||
val premiumStatus = organizationPremiumStatusMap[cipherView.organizationId] ?: isPremium
|
||||
if (!premiumStatus && !cipherView.organizationUseTotp) return
|
||||
if (!isPremium && !cipherView.organizationUseTotp) return
|
||||
val totpCode = cipherView.login?.totp ?: return
|
||||
|
||||
val totpResult = vaultRepository.generateTotp(
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -24,6 +24,7 @@ fun AutofillRequest.Fillable.toAutofillSaveItem(): AutofillSaveItem =
|
||||
.uri
|
||||
?.replace("https://", "")
|
||||
?.replace("http://", "")
|
||||
?.replace("androidapp://", "")
|
||||
|
||||
AutofillSaveItem.Login(
|
||||
username = partition.usernameSaveValue,
|
||||
|
||||
@@ -68,6 +68,17 @@ 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?
|
||||
|
||||
/**
|
||||
* Emits updates that track [lastDatabaseSchemeChangeInstant].
|
||||
*/
|
||||
val lastDatabaseSchemeChangeInstantFlow: Flow<Instant?>
|
||||
|
||||
/**
|
||||
* Clears all the settings data for the given user.
|
||||
*/
|
||||
@@ -308,52 +319,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?)
|
||||
}
|
||||
|
||||
@@ -36,10 +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 LAST_SCHEME_CHANGE_INSTANT = "lastDatabaseSchemeChangeInstant"
|
||||
|
||||
/**
|
||||
* Primary implementation of [SettingsDiskSource].
|
||||
@@ -78,10 +75,9 @@ class SettingsDiskSourceImpl(
|
||||
|
||||
private val mutableHasUserLoggedInOrCreatedAccountFlow = bufferedMutableSharedFlow<Boolean?>()
|
||||
|
||||
private val mutableScreenCaptureAllowedFlowMap =
|
||||
mutableMapOf<String, MutableSharedFlow<Boolean?>>()
|
||||
private val mutableLastDatabaseSchemeChangeInstantFlow = bufferedMutableSharedFlow<Instant?>()
|
||||
|
||||
private val mutableVaultRegisteredForExportFlow =
|
||||
private val mutableScreenCaptureAllowedFlowMap =
|
||||
mutableMapOf<String, MutableSharedFlow<Boolean?>>()
|
||||
|
||||
override var appLanguage: AppLanguage?
|
||||
@@ -162,6 +158,17 @@ 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())
|
||||
mutableLastDatabaseSchemeChangeInstantFlow.tryEmit(value)
|
||||
}
|
||||
|
||||
override val lastDatabaseSchemeChangeInstantFlow: Flow<Instant?>
|
||||
get() = mutableLastDatabaseSchemeChangeInstantFlow
|
||||
.onSubscription { emit(lastDatabaseSchemeChangeInstant) }
|
||||
|
||||
override fun clearData(userId: String) {
|
||||
storeVaultTimeoutInMinutes(userId = userId, vaultTimeoutInMinutes = null)
|
||||
storeVaultTimeoutAction(userId = userId, vaultTimeoutAction = null)
|
||||
@@ -174,7 +181,6 @@ 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)
|
||||
|
||||
// The following are intentionally not cleared so they can be
|
||||
// restored after logging out and back in:
|
||||
@@ -437,51 +443,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,
|
||||
)
|
||||
}
|
||||
|
||||
private fun getMutableLastSyncFlow(
|
||||
userId: String,
|
||||
): MutableSharedFlow<Instant?> =
|
||||
@@ -532,10 +493,4 @@ class SettingsDiskSourceImpl(
|
||||
mutableShowImportLoginsSettingBadgeFlowMap.getOrPut(userId) {
|
||||
bufferedMutableSharedFlow(replay = 1)
|
||||
}
|
||||
|
||||
private fun getMutableVaultRegisteredForExportFlow(
|
||||
userId: String,
|
||||
): MutableSharedFlow<Boolean?> = mutableVaultRegisteredForExportFlow.getOrPut(userId) {
|
||||
bufferedMutableSharedFlow(replay = 1)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -6,26 +6,37 @@ import okhttp3.Interceptor
|
||||
import okhttp3.Response
|
||||
|
||||
/**
|
||||
* An [Interceptor] that optionally takes the current base URL of a request and replaces it with
|
||||
* the currently set base URL from the [baseUrlProvider].
|
||||
* A [Interceptor] that optionally takes the current base URL of a request and replaces it with
|
||||
* the currently set [baseUrl]
|
||||
*/
|
||||
class BaseUrlInterceptor(
|
||||
private val baseUrlProvider: () -> String?,
|
||||
) : Interceptor {
|
||||
class BaseUrlInterceptor : Interceptor {
|
||||
|
||||
private val baseHttpUrl: HttpUrl? get() = baseUrlProvider()?.toHttpUrlOrNull()
|
||||
/**
|
||||
* The base URL to use as an override, or `null` if no override should be performed.
|
||||
*/
|
||||
var baseUrl: String? = null
|
||||
set(value) {
|
||||
field = value
|
||||
baseHttpUrl = baseUrl?.let { requireNotNull(it.toHttpUrlOrNull()) }
|
||||
}
|
||||
|
||||
private var baseHttpUrl: HttpUrl? = null
|
||||
|
||||
override fun intercept(chain: Interceptor.Chain): Response {
|
||||
val request = chain.request()
|
||||
|
||||
// If no base URL is set, we can simply skip
|
||||
val base = baseHttpUrl ?: return chain.proceed(request = request)
|
||||
val base = baseHttpUrl ?: return chain.proceed(request)
|
||||
|
||||
// Update the base URL used.
|
||||
return chain.proceed(
|
||||
request = request
|
||||
request
|
||||
.newBuilder()
|
||||
.url(url = request.url.replaceBaseUrlWith(baseUrl = base))
|
||||
.url(
|
||||
request
|
||||
.url
|
||||
.replaceBaseUrlWith(base),
|
||||
)
|
||||
.build(),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,44 +1,47 @@
|
||||
package com.x8bit.bitwarden.data.platform.datasource.network.interceptor
|
||||
|
||||
import com.x8bit.bitwarden.data.platform.annotation.OmitFromCoverage
|
||||
import com.x8bit.bitwarden.data.platform.datasource.disk.EnvironmentDiskSource
|
||||
import com.x8bit.bitwarden.data.platform.repository.model.Environment
|
||||
import com.x8bit.bitwarden.data.platform.repository.util.baseApiUrl
|
||||
import com.x8bit.bitwarden.data.platform.repository.util.baseEventsUrl
|
||||
import com.x8bit.bitwarden.data.platform.repository.util.baseIdentityUrl
|
||||
import com.x8bit.bitwarden.data.platform.repository.util.toEnvironmentUrlsOrDefault
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
/**
|
||||
* An overall container for various [BaseUrlInterceptor] implementations for different API groups.
|
||||
*/
|
||||
@OmitFromCoverage
|
||||
@Singleton
|
||||
class BaseUrlInterceptors @Inject constructor(
|
||||
private val environmentDiskSource: EnvironmentDiskSource,
|
||||
) {
|
||||
private val environment: Environment
|
||||
get() = environmentDiskSource.preAuthEnvironmentUrlData.toEnvironmentUrlsOrDefault()
|
||||
class BaseUrlInterceptors @Inject constructor() {
|
||||
var environment: Environment = Environment.Us
|
||||
set(value) {
|
||||
field = value
|
||||
updateBaseUrls(environment = value)
|
||||
}
|
||||
|
||||
/**
|
||||
* An interceptor for "/api" calls.
|
||||
*/
|
||||
val apiInterceptor: BaseUrlInterceptor = BaseUrlInterceptor {
|
||||
environment.environmentUrlData.baseApiUrl
|
||||
}
|
||||
val apiInterceptor: BaseUrlInterceptor = BaseUrlInterceptor()
|
||||
|
||||
/**
|
||||
* An interceptor for "/identity" calls.
|
||||
*/
|
||||
val identityInterceptor: BaseUrlInterceptor = BaseUrlInterceptor {
|
||||
environment.environmentUrlData.baseIdentityUrl
|
||||
}
|
||||
val identityInterceptor: BaseUrlInterceptor = BaseUrlInterceptor()
|
||||
|
||||
/**
|
||||
* An interceptor for "/events" calls.
|
||||
*/
|
||||
val eventsInterceptor: BaseUrlInterceptor = BaseUrlInterceptor {
|
||||
environment.environmentUrlData.baseEventsUrl
|
||||
val eventsInterceptor: BaseUrlInterceptor = BaseUrlInterceptor()
|
||||
|
||||
init {
|
||||
// Ensure all interceptors begin with a default value
|
||||
environment = Environment.Us
|
||||
}
|
||||
|
||||
private fun updateBaseUrls(environment: Environment) {
|
||||
val environmentUrlData = environment.environmentUrlData
|
||||
apiInterceptor.baseUrl = environmentUrlData.baseApiUrl
|
||||
identityInterceptor.baseUrl = environmentUrlData.baseIdentityUrl
|
||||
eventsInterceptor.baseUrl = environmentUrlData.baseEventsUrl
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,9 +3,7 @@ package com.x8bit.bitwarden.data.platform.datasource.network.util
|
||||
import okio.ByteString.Companion.decodeBase64
|
||||
import java.net.UnknownHostException
|
||||
import java.nio.charset.Charset
|
||||
import java.security.cert.CertPathValidatorException
|
||||
import java.util.Base64
|
||||
import javax.net.ssl.SSLHandshakeException
|
||||
|
||||
/**
|
||||
* Base 64 encode the string as well as make special modifications required by the backend:
|
||||
@@ -43,12 +41,3 @@ fun Throwable?.isNoConnectionError(): Boolean {
|
||||
return this is UnknownHostException ||
|
||||
this?.cause?.isNoConnectionError() ?: false
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the throwable represents a SSL handshake error.
|
||||
*/
|
||||
fun Throwable?.isSslHandShakeError(): Boolean {
|
||||
return this is SSLHandshakeException ||
|
||||
this is CertPathValidatorException ||
|
||||
this?.cause?.isSslHandShakeError() ?: false
|
||||
}
|
||||
|
||||
@@ -6,7 +6,6 @@ import android.os.Bundle
|
||||
import androidx.lifecycle.DefaultLifecycleObserver
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import androidx.lifecycle.ProcessLifecycleOwner
|
||||
import com.x8bit.bitwarden.data.autofill.util.createdForAutofill
|
||||
import com.x8bit.bitwarden.data.platform.manager.model.AppCreationState
|
||||
import com.x8bit.bitwarden.data.platform.manager.model.AppForegroundState
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
@@ -20,8 +19,7 @@ class AppStateManagerImpl(
|
||||
application: Application,
|
||||
processLifecycleOwner: LifecycleOwner = ProcessLifecycleOwner.get(),
|
||||
) : AppStateManager {
|
||||
private val mutableAppCreationStateFlow =
|
||||
MutableStateFlow<AppCreationState>(AppCreationState.Destroyed)
|
||||
private val mutableAppCreationStateFlow = MutableStateFlow(AppCreationState.DESTROYED)
|
||||
private val mutableAppForegroundStateFlow = MutableStateFlow(AppForegroundState.BACKGROUNDED)
|
||||
|
||||
override val appCreatedStateFlow: StateFlow<AppCreationState>
|
||||
@@ -51,15 +49,13 @@ class AppStateManagerImpl(
|
||||
override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) {
|
||||
activityCount++
|
||||
// Always be in a created state if we have an activity
|
||||
mutableAppCreationStateFlow.value = AppCreationState.Created(
|
||||
isAutoFill = activity.createdForAutofill,
|
||||
)
|
||||
mutableAppCreationStateFlow.value = AppCreationState.CREATED
|
||||
}
|
||||
|
||||
override fun onActivityDestroyed(activity: Activity) {
|
||||
activityCount--
|
||||
if (activityCount == 0 && !activity.isChangingConfigurations) {
|
||||
mutableAppCreationStateFlow.value = AppCreationState.Destroyed
|
||||
mutableAppCreationStateFlow.value = AppCreationState.DESTROYED
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -20,6 +20,12 @@ interface BiometricsEncryptionManager {
|
||||
userId: String,
|
||||
): Cipher?
|
||||
|
||||
/**
|
||||
* Sets up biometrics to ensure future integrity checks work properly. If this method has never
|
||||
* been called [isBiometricIntegrityValid] will return false.
|
||||
*/
|
||||
fun setupBiometrics(userId: String)
|
||||
|
||||
/**
|
||||
* Checks to verify that the biometrics integrity is still valid. This returns `true` if the
|
||||
* biometrics data has not changed since the app setup biometrics; `false` will be returned if
|
||||
|
||||
@@ -4,9 +4,9 @@ import android.security.keystore.KeyGenParameterSpec
|
||||
import android.security.keystore.KeyPermanentlyInvalidatedException
|
||||
import android.security.keystore.KeyProperties
|
||||
import com.x8bit.bitwarden.BuildConfig
|
||||
import com.x8bit.bitwarden.data.auth.datasource.disk.AuthDiskSource
|
||||
import com.x8bit.bitwarden.data.platform.annotation.OmitFromCoverage
|
||||
import com.x8bit.bitwarden.data.platform.datasource.disk.SettingsDiskSource
|
||||
import java.io.IOException
|
||||
import java.security.InvalidAlgorithmParameterException
|
||||
import java.security.InvalidKeyException
|
||||
import java.security.KeyStore
|
||||
@@ -15,12 +15,12 @@ import java.security.NoSuchAlgorithmException
|
||||
import java.security.NoSuchProviderException
|
||||
import java.security.ProviderException
|
||||
import java.security.UnrecoverableKeyException
|
||||
import java.security.cert.CertificateException
|
||||
import java.util.UUID
|
||||
import javax.crypto.Cipher
|
||||
import javax.crypto.KeyGenerator
|
||||
import javax.crypto.NoSuchPaddingException
|
||||
import javax.crypto.SecretKey
|
||||
import javax.crypto.spec.IvParameterSpec
|
||||
|
||||
/**
|
||||
* Default implementation of [BiometricsEncryptionManager] for managing Android keystore encryption
|
||||
@@ -28,7 +28,6 @@ import javax.crypto.spec.IvParameterSpec
|
||||
*/
|
||||
@OmitFromCoverage
|
||||
class BiometricsEncryptionManagerImpl(
|
||||
private val authDiskSource: AuthDiskSource,
|
||||
private val settingsDiskSource: SettingsDiskSource,
|
||||
) : BiometricsEncryptionManager {
|
||||
private val keystore = KeyStore
|
||||
@@ -51,7 +50,7 @@ class BiometricsEncryptionManagerImpl(
|
||||
val secretKey: SecretKey = generateKeyOrNull()
|
||||
?: run {
|
||||
// user removed all biometrics from the device
|
||||
destroyBiometrics(userId = userId)
|
||||
settingsDiskSource.systemBiometricIntegritySource = null
|
||||
return null
|
||||
}
|
||||
val cipher = try {
|
||||
@@ -61,27 +60,37 @@ class BiometricsEncryptionManagerImpl(
|
||||
} catch (_: NoSuchPaddingException) {
|
||||
return null
|
||||
}
|
||||
// Instantiate integrity values.
|
||||
createIntegrityValues(userId = userId)
|
||||
// This should never fail to initialize / return false because the cipher is newly generated
|
||||
cipher.initializeCipher(userId = userId, secretKey = secretKey)
|
||||
initializeCipher(
|
||||
userId = userId,
|
||||
cipher = cipher,
|
||||
secretKey = secretKey,
|
||||
)
|
||||
return cipher
|
||||
}
|
||||
|
||||
override fun getOrCreateCipher(userId: String): Cipher? {
|
||||
val secretKey: SecretKey = getSecretKeyOrNull()
|
||||
val secretKey = getSecretKeyOrNull()
|
||||
?: generateKeyOrNull()
|
||||
?: run {
|
||||
// user removed all biometrics from the device
|
||||
destroyBiometrics(userId = userId)
|
||||
settingsDiskSource.systemBiometricIntegritySource = null
|
||||
return null
|
||||
}
|
||||
|
||||
val cipher = Cipher.getInstance(CIPHER_TRANSFORMATION)
|
||||
val isCipherInitialized = cipher.initializeCipher(userId = userId, secretKey = secretKey)
|
||||
val isCipherInitialized = initializeCipher(
|
||||
userId = userId,
|
||||
cipher = cipher,
|
||||
secretKey = secretKey,
|
||||
)
|
||||
return cipher?.takeIf { isCipherInitialized }
|
||||
}
|
||||
|
||||
override fun setupBiometrics(userId: String) {
|
||||
createIntegrityValues(userId)
|
||||
}
|
||||
|
||||
override fun isBiometricIntegrityValid(userId: String, cipher: Cipher?): Boolean =
|
||||
isSystemBiometricIntegrityValid(userId, cipher) && isAccountBiometricIntegrityValid(userId)
|
||||
|
||||
@@ -103,7 +112,10 @@ class BiometricsEncryptionManagerImpl(
|
||||
*/
|
||||
private fun generateKeyOrNull(): SecretKey? {
|
||||
val keyGen = try {
|
||||
KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES, ENCRYPTION_KEYSTORE_NAME)
|
||||
KeyGenerator.getInstance(
|
||||
KeyProperties.KEY_ALGORITHM_AES,
|
||||
ENCRYPTION_KEYSTORE_NAME,
|
||||
)
|
||||
} catch (_: NoSuchAlgorithmException) {
|
||||
return null
|
||||
} catch (_: NoSuchProviderException) {
|
||||
@@ -112,24 +124,40 @@ class BiometricsEncryptionManagerImpl(
|
||||
return null
|
||||
}
|
||||
|
||||
return try {
|
||||
try {
|
||||
keyGen.init(keyGenParameterSpec)
|
||||
keyGen.generateKey()
|
||||
} catch (_: InvalidAlgorithmParameterException) {
|
||||
null
|
||||
return null
|
||||
} catch (_: ProviderException) {
|
||||
null
|
||||
return null
|
||||
}
|
||||
|
||||
return getSecretKeyOrNull()
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the [SecretKey] stored in the keystore, or null if there isn't one.
|
||||
*/
|
||||
private fun getSecretKeyOrNull(): SecretKey? =
|
||||
private fun getSecretKeyOrNull(): SecretKey? {
|
||||
try {
|
||||
keystore
|
||||
.getKey(ENCRYPTION_KEY_NAME, null)
|
||||
?.let { it as SecretKey }
|
||||
keystore.load(null)
|
||||
} catch (_: IllegalArgumentException) {
|
||||
// keystore could not be loaded because [param] is unrecognized.
|
||||
return null
|
||||
} catch (_: IOException) {
|
||||
// keystore data format is invalid or the password is incorrect.
|
||||
return null
|
||||
} catch (_: NoSuchAlgorithmException) {
|
||||
// keystore integrity could not be checked due to missing algorithm.
|
||||
return null
|
||||
} catch (_: CertificateException) {
|
||||
// keystore certificates could not be loaded
|
||||
return null
|
||||
}
|
||||
|
||||
return try {
|
||||
keystore.getKey(ENCRYPTION_KEY_NAME, null) as? SecretKey
|
||||
} catch (_: KeyStoreException) {
|
||||
// keystore was not loaded
|
||||
null
|
||||
@@ -140,31 +168,30 @@ class BiometricsEncryptionManagerImpl(
|
||||
// key could not be recovered
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize a [Cipher] and return a boolean indicating whether it is valid.
|
||||
*/
|
||||
private fun Cipher.initializeCipher(
|
||||
private fun initializeCipher(
|
||||
userId: String,
|
||||
cipher: Cipher,
|
||||
secretKey: SecretKey,
|
||||
): Boolean =
|
||||
try {
|
||||
authDiskSource
|
||||
.getUserBiometricInitVector(userId = userId)
|
||||
?.let { init(Cipher.DECRYPT_MODE, secretKey, IvParameterSpec(it)) }
|
||||
?: init(Cipher.ENCRYPT_MODE, secretKey)
|
||||
cipher.init(Cipher.ENCRYPT_MODE, secretKey)
|
||||
true
|
||||
} catch (_: KeyPermanentlyInvalidatedException) {
|
||||
// Biometric has changed
|
||||
destroyBiometrics(userId = userId)
|
||||
settingsDiskSource.systemBiometricIntegritySource = null
|
||||
false
|
||||
} catch (_: UnrecoverableKeyException) {
|
||||
// Biometric was disabled and re-enabled
|
||||
destroyBiometrics(userId = userId)
|
||||
settingsDiskSource.systemBiometricIntegritySource = null
|
||||
false
|
||||
} catch (_: InvalidKeyException) {
|
||||
// User has no key
|
||||
destroyBiometrics(userId = userId)
|
||||
// Fallback for old Bitwarden users without a key
|
||||
createIntegrityValues(userId)
|
||||
true
|
||||
}
|
||||
|
||||
@@ -174,7 +201,11 @@ class BiometricsEncryptionManagerImpl(
|
||||
private fun isSystemBiometricIntegrityValid(userId: String, cipher: Cipher?): Boolean {
|
||||
val secretKey = getSecretKeyOrNull()
|
||||
return if (cipher != null && secretKey != null) {
|
||||
cipher.initializeCipher(userId = userId, secretKey = secretKey)
|
||||
initializeCipher(
|
||||
userId = userId,
|
||||
cipher = cipher,
|
||||
secretKey = secretKey,
|
||||
)
|
||||
} else {
|
||||
false
|
||||
}
|
||||
@@ -184,6 +215,7 @@ class BiometricsEncryptionManagerImpl(
|
||||
* Creates the initial values to be used for biometrics, including the key from which the
|
||||
* master [Cipher] will be generated.
|
||||
*/
|
||||
@Suppress("TooGenericExceptionCaught")
|
||||
private fun createIntegrityValues(userId: String) {
|
||||
val systemBiometricIntegritySource = settingsDiskSource
|
||||
.systemBiometricIntegritySource
|
||||
@@ -194,20 +226,10 @@ class BiometricsEncryptionManagerImpl(
|
||||
systemBioIntegrityState = systemBiometricIntegritySource,
|
||||
value = true,
|
||||
)
|
||||
}
|
||||
|
||||
private fun destroyBiometrics(userId: String) {
|
||||
settingsDiskSource.systemBiometricIntegritySource?.let { systemBioIntegrityState ->
|
||||
settingsDiskSource.storeAccountBiometricIntegrityValidity(
|
||||
userId = userId,
|
||||
systemBioIntegrityState = systemBioIntegrityState,
|
||||
value = null,
|
||||
)
|
||||
}
|
||||
settingsDiskSource.systemBiometricIntegritySource = null
|
||||
authDiskSource.storeUserBiometricUnlockKey(userId = userId, biometricsKey = null)
|
||||
authDiskSource.storeUserBiometricInitVector(userId = userId, iv = null)
|
||||
keystore.deleteEntry(ENCRYPTION_KEY_NAME)
|
||||
// Ignore result so biometrics function on devices that are in a state where key generation
|
||||
// is not functioning
|
||||
createCipherOrNull(userId)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -20,7 +20,10 @@ class FeatureFlagManagerImpl(
|
||||
override val sdkFeatureFlags: Map<String, Boolean>
|
||||
get() = mapOf(
|
||||
CIPHER_KEY_ENCRYPTION_KEY to
|
||||
getCipherKeyEncryptionFlagState(),
|
||||
isServerVersionAtLeast(
|
||||
serverConfigRepository.serverConfigStateFlow.value,
|
||||
CIPHER_KEY_ENC_MIN_SERVER_VERSION,
|
||||
),
|
||||
)
|
||||
|
||||
override fun <T : Any> getFeatureFlagFlow(key: FlagKey<T>): Flow<T> =
|
||||
@@ -43,16 +46,6 @@ class FeatureFlagManagerImpl(
|
||||
.serverConfigStateFlow
|
||||
.value
|
||||
.getFlagValueOrDefault(key = key)
|
||||
|
||||
/**
|
||||
* Get the computed value of the cipher key encryption flag based on server version and
|
||||
* remote flag.
|
||||
*/
|
||||
private fun getCipherKeyEncryptionFlagState() =
|
||||
isServerVersionAtLeast(
|
||||
serverConfigRepository.serverConfigStateFlow.value,
|
||||
CIPHER_KEY_ENC_MIN_SERVER_VERSION,
|
||||
) && getFeatureFlag(FlagKey.CipherKeyEncryption)
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -2,6 +2,7 @@ package com.x8bit.bitwarden.data.platform.manager
|
||||
|
||||
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
|
||||
import com.x8bit.bitwarden.data.platform.datasource.network.authenticator.RefreshAuthenticator
|
||||
import com.x8bit.bitwarden.data.platform.datasource.network.interceptor.BaseUrlInterceptors
|
||||
import com.x8bit.bitwarden.data.platform.manager.dispatcher.DispatcherManager
|
||||
import com.x8bit.bitwarden.data.platform.repository.EnvironmentRepository
|
||||
import com.x8bit.bitwarden.data.platform.repository.ServerConfigRepository
|
||||
@@ -19,6 +20,7 @@ class NetworkConfigManagerImpl(
|
||||
authRepository: AuthRepository,
|
||||
environmentRepository: EnvironmentRepository,
|
||||
serverConfigRepository: ServerConfigRepository,
|
||||
private val baseUrlInterceptors: BaseUrlInterceptors,
|
||||
refreshAuthenticator: RefreshAuthenticator,
|
||||
dispatcherManager: DispatcherManager,
|
||||
) : NetworkConfigManager {
|
||||
@@ -29,6 +31,9 @@ class NetworkConfigManagerImpl(
|
||||
@Suppress("OPT_IN_USAGE")
|
||||
environmentRepository
|
||||
.environmentStateFlow
|
||||
.onEach { environment ->
|
||||
baseUrlInterceptors.environment = environment
|
||||
}
|
||||
.debounce(timeoutMillis = ENVIRONMENT_DEBOUNCE_TIMEOUT_MS)
|
||||
.onEach { _ ->
|
||||
// This updates the stored service configuration by performing a network request.
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
package com.x8bit.bitwarden.data.platform.manager
|
||||
|
||||
/**
|
||||
* Responsible for managing whether or not the app review prompt should be shown.
|
||||
*/
|
||||
interface ReviewPromptManager {
|
||||
/**
|
||||
* Register an add cipher item action.
|
||||
*/
|
||||
fun registerAddCipherAction()
|
||||
|
||||
/**
|
||||
* Register a generated result action.
|
||||
*/
|
||||
fun registerGeneratedResultAction()
|
||||
|
||||
/**
|
||||
* Register a create send action.
|
||||
*/
|
||||
fun registerCreateSendAction()
|
||||
|
||||
/**
|
||||
* Returns a boolean value indicating whether or not the user should be prompted to
|
||||
* review the app.
|
||||
*/
|
||||
fun shouldPromptForAppReview(): Boolean
|
||||
}
|
||||
@@ -1,73 +0,0 @@
|
||||
package com.x8bit.bitwarden.data.platform.manager
|
||||
|
||||
import com.x8bit.bitwarden.data.auth.datasource.disk.AuthDiskSource
|
||||
import com.x8bit.bitwarden.data.autofill.accessibility.manager.AccessibilityEnabledManager
|
||||
import com.x8bit.bitwarden.data.autofill.manager.AutofillEnabledManager
|
||||
import com.x8bit.bitwarden.data.platform.datasource.disk.SettingsDiskSource
|
||||
import com.x8bit.bitwarden.ui.platform.util.orZero
|
||||
|
||||
private const val ADD_ACTION_REQUIREMENT = 3
|
||||
private const val COPY_ACTION_REQUIREMENT = 3
|
||||
private const val CREATE_ACTION_REQUIREMENT = 3
|
||||
|
||||
/**
|
||||
* Default implementation of [ReviewPromptManager].
|
||||
*/
|
||||
class ReviewPromptManagerImpl(
|
||||
private val authDiskSource: AuthDiskSource,
|
||||
private val settingsDiskSource: SettingsDiskSource,
|
||||
private val autofillEnabledManager: AutofillEnabledManager,
|
||||
private val accessibilityEnabledManager: AccessibilityEnabledManager,
|
||||
) : ReviewPromptManager {
|
||||
|
||||
override fun registerAddCipherAction() {
|
||||
authDiskSource.userState?.activeUserId ?: return
|
||||
if (isMinimumAddActionsMet()) return
|
||||
val currentValue = settingsDiskSource.getAddCipherActionCount().orZero()
|
||||
settingsDiskSource.storeAddCipherActionCount(
|
||||
count = currentValue + 1,
|
||||
)
|
||||
}
|
||||
|
||||
override fun registerGeneratedResultAction() {
|
||||
authDiskSource.userState?.activeUserId ?: return
|
||||
if (isMinimumCopyActionsMet()) return
|
||||
val currentValue = settingsDiskSource
|
||||
.getGeneratedResultActionCount()
|
||||
.orZero()
|
||||
settingsDiskSource.storeGeneratedResultActionCount(
|
||||
count = currentValue + 1,
|
||||
)
|
||||
}
|
||||
|
||||
override fun registerCreateSendAction() {
|
||||
authDiskSource.userState?.activeUserId ?: return
|
||||
if (isMinimumCreateActionsMet()) return
|
||||
val currentValue = settingsDiskSource.getCreateSendActionCount().orZero()
|
||||
settingsDiskSource.storeCreateSendActionCount(
|
||||
count = currentValue + 1,
|
||||
)
|
||||
}
|
||||
|
||||
override fun shouldPromptForAppReview(): Boolean {
|
||||
authDiskSource.userState?.activeUserId ?: return false
|
||||
val autofillEnabled = autofillEnabledManager.isAutofillEnabledStateFlow.value
|
||||
val accessibilityEnabled = accessibilityEnabledManager.isAccessibilityEnabledStateFlow.value
|
||||
val minAddActionsMet = isMinimumAddActionsMet()
|
||||
val minCopyActionsMet = isMinimumCopyActionsMet()
|
||||
val minCreateActionsMet = isMinimumCreateActionsMet()
|
||||
return (autofillEnabled || accessibilityEnabled) &&
|
||||
(minAddActionsMet || minCopyActionsMet || minCreateActionsMet)
|
||||
}
|
||||
|
||||
private fun isMinimumAddActionsMet(): Boolean =
|
||||
settingsDiskSource.getAddCipherActionCount().orZero() >= ADD_ACTION_REQUIREMENT
|
||||
|
||||
private fun isMinimumCopyActionsMet(): Boolean =
|
||||
settingsDiskSource
|
||||
.getGeneratedResultActionCount()
|
||||
.orZero() >= COPY_ACTION_REQUIREMENT
|
||||
|
||||
private fun isMinimumCreateActionsMet(): Boolean =
|
||||
settingsDiskSource.getCreateSendActionCount().orZero() >= CREATE_ACTION_REQUIREMENT
|
||||
}
|
||||
@@ -46,10 +46,14 @@ class BitwardenClipboardManagerImpl(
|
||||
},
|
||||
)
|
||||
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.S_V2) {
|
||||
val descriptor = toastDescriptorOverride
|
||||
?.let { context.resources.getString(R.string.value_has_been_copied, it) }
|
||||
?: context.resources.getString(R.string.copied_to_clipboard)
|
||||
Toast.makeText(context, descriptor, Toast.LENGTH_SHORT).show()
|
||||
val descriptor = toastDescriptorOverride ?: text
|
||||
Toast
|
||||
.makeText(
|
||||
context,
|
||||
context.resources.getString(R.string.value_has_been_copied, descriptor),
|
||||
Toast.LENGTH_SHORT,
|
||||
)
|
||||
.show()
|
||||
}
|
||||
|
||||
val frequency = clearClipboardFrequencySeconds ?: return
|
||||
|
||||
@@ -6,13 +6,13 @@ import androidx.core.content.getSystemService
|
||||
import com.x8bit.bitwarden.data.auth.datasource.disk.AuthDiskSource
|
||||
import com.x8bit.bitwarden.data.auth.manager.AddTotpItemFromAuthenticatorManager
|
||||
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
|
||||
import com.x8bit.bitwarden.data.autofill.accessibility.manager.AccessibilityEnabledManager
|
||||
import com.x8bit.bitwarden.data.autofill.manager.AutofillEnabledManager
|
||||
import com.x8bit.bitwarden.data.platform.datasource.disk.EventDiskSource
|
||||
import com.x8bit.bitwarden.data.platform.datasource.disk.PushDiskSource
|
||||
import com.x8bit.bitwarden.data.platform.datasource.disk.SettingsDiskSource
|
||||
import com.x8bit.bitwarden.data.platform.datasource.disk.legacy.LegacyAppCenterMigrator
|
||||
import com.x8bit.bitwarden.data.platform.datasource.network.authenticator.RefreshAuthenticator
|
||||
import com.x8bit.bitwarden.data.platform.datasource.network.interceptor.BaseUrlInterceptors
|
||||
import com.x8bit.bitwarden.data.platform.datasource.network.service.EventService
|
||||
import com.x8bit.bitwarden.data.platform.datasource.network.service.PushService
|
||||
import com.x8bit.bitwarden.data.platform.manager.AppStateManager
|
||||
@@ -40,8 +40,6 @@ import com.x8bit.bitwarden.data.platform.manager.PushManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.PushManagerImpl
|
||||
import com.x8bit.bitwarden.data.platform.manager.ResourceCacheManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.ResourceCacheManagerImpl
|
||||
import com.x8bit.bitwarden.data.platform.manager.ReviewPromptManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.ReviewPromptManagerImpl
|
||||
import com.x8bit.bitwarden.data.platform.manager.SdkClientManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.SdkClientManagerImpl
|
||||
import com.x8bit.bitwarden.data.platform.manager.ciphermatching.CipherMatchingManager
|
||||
@@ -141,10 +139,8 @@ object PlatformManagerModule {
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideBiometricsEncryptionManager(
|
||||
authDiskSource: AuthDiskSource,
|
||||
settingsDiskSource: SettingsDiskSource,
|
||||
): BiometricsEncryptionManager = BiometricsEncryptionManagerImpl(
|
||||
authDiskSource = authDiskSource,
|
||||
settingsDiskSource = settingsDiskSource,
|
||||
)
|
||||
|
||||
@@ -202,6 +198,7 @@ object PlatformManagerModule {
|
||||
authRepository: AuthRepository,
|
||||
environmentRepository: EnvironmentRepository,
|
||||
serverConfigRepository: ServerConfigRepository,
|
||||
baseUrlInterceptors: BaseUrlInterceptors,
|
||||
refreshAuthenticator: RefreshAuthenticator,
|
||||
dispatcherManager: DispatcherManager,
|
||||
): NetworkConfigManager =
|
||||
@@ -209,6 +206,7 @@ object PlatformManagerModule {
|
||||
authRepository = authRepository,
|
||||
environmentRepository = environmentRepository,
|
||||
serverConfigRepository = serverConfigRepository,
|
||||
baseUrlInterceptors = baseUrlInterceptors,
|
||||
refreshAuthenticator = refreshAuthenticator,
|
||||
dispatcherManager = dispatcherManager,
|
||||
)
|
||||
@@ -315,18 +313,4 @@ object PlatformManagerModule {
|
||||
authDiskSource = authDiskSource,
|
||||
settingsDiskSource = settingsDiskSource,
|
||||
)
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideReviewPromptManager(
|
||||
authDiskSource: AuthDiskSource,
|
||||
settingsDiskSource: SettingsDiskSource,
|
||||
autofillEnabledManager: AutofillEnabledManager,
|
||||
accessibilityEnabledManager: AccessibilityEnabledManager,
|
||||
): ReviewPromptManager = ReviewPromptManagerImpl(
|
||||
authDiskSource = authDiskSource,
|
||||
settingsDiskSource = settingsDiskSource,
|
||||
autofillEnabledManager = autofillEnabledManager,
|
||||
accessibilityEnabledManager = accessibilityEnabledManager,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -3,16 +3,14 @@ package com.x8bit.bitwarden.data.platform.manager.model
|
||||
/**
|
||||
* Represents the creation state of the app.
|
||||
*/
|
||||
sealed class AppCreationState {
|
||||
enum class AppCreationState {
|
||||
/**
|
||||
* Denotes that the app is currently created.
|
||||
*
|
||||
* @param isAutoFill Whether the app was created for autofill.
|
||||
*/
|
||||
data class Created(val isAutoFill: Boolean) : AppCreationState()
|
||||
CREATED,
|
||||
|
||||
/**
|
||||
* Denotes that the app is currently destroyed.
|
||||
*/
|
||||
data object Destroyed : AppCreationState()
|
||||
DESTROYED,
|
||||
}
|
||||
|
||||
@@ -33,12 +33,6 @@ sealed class FlagKey<out T : Any> {
|
||||
ImportLoginsFlow,
|
||||
SshKeyCipherItems,
|
||||
VerifiedSsoDomainEndpoint,
|
||||
CredentialExchangeProtocolImport,
|
||||
CredentialExchangeProtocolExport,
|
||||
AppReviewPrompt,
|
||||
NewDevicePermanentDismiss,
|
||||
NewDeviceTemporaryDismiss,
|
||||
IgnoreEnvironmentCheck,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -49,7 +43,7 @@ sealed class FlagKey<out T : Any> {
|
||||
data object AuthenticatorSync : FlagKey<Boolean>() {
|
||||
override val keyName: String = "enable-authenticator-sync-android"
|
||||
override val defaultValue: Boolean = false
|
||||
override val isRemotelyConfigured: Boolean = true
|
||||
override val isRemotelyConfigured: Boolean = false
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -96,7 +90,6 @@ sealed class FlagKey<out T : Any> {
|
||||
override val defaultValue: Boolean = false
|
||||
override val isRemotelyConfigured: Boolean = true
|
||||
}
|
||||
|
||||
/**
|
||||
* Data object holding the feature flag key for the new verified SSO domain endpoint feature.
|
||||
*/
|
||||
@@ -106,72 +99,6 @@ sealed class FlagKey<out T : Any> {
|
||||
override val isRemotelyConfigured: Boolean = true
|
||||
}
|
||||
|
||||
/**
|
||||
* Data object holding hte feature flag key for the Credential Exchange Protocol (CXP) import
|
||||
* feature.
|
||||
*/
|
||||
data object CredentialExchangeProtocolImport : FlagKey<Boolean>() {
|
||||
override val keyName: String = "cxp-import-mobile"
|
||||
override val defaultValue: Boolean = false
|
||||
override val isRemotelyConfigured: Boolean = true
|
||||
}
|
||||
|
||||
/**
|
||||
* Data object holding the feature flag key for the Credential Exchange Protocol (CXP) export
|
||||
* feature.
|
||||
*/
|
||||
data object CredentialExchangeProtocolExport : FlagKey<Boolean>() {
|
||||
override val keyName: String = "cxp-export-mobile"
|
||||
override val defaultValue: Boolean = false
|
||||
override val isRemotelyConfigured: Boolean = true
|
||||
}
|
||||
|
||||
/**
|
||||
* Data object holding the feature flag key for the App Review Prompt feature.
|
||||
*/
|
||||
data object AppReviewPrompt : FlagKey<Boolean>() {
|
||||
override val keyName: String = "app-review-prompt"
|
||||
override val defaultValue: Boolean = false
|
||||
override val isRemotelyConfigured: Boolean = true
|
||||
}
|
||||
|
||||
/**
|
||||
* Data object holding the feature flag key for the Cipher Key Encryption feature.
|
||||
*/
|
||||
data object CipherKeyEncryption : FlagKey<Boolean>() {
|
||||
override val keyName: String = "cipher-key-encryption"
|
||||
override val defaultValue: Boolean = true
|
||||
override val isRemotelyConfigured: Boolean = true
|
||||
}
|
||||
|
||||
/**
|
||||
* Data object holding the feature flag key for the New Device Temporary Dismiss feature.
|
||||
*/
|
||||
data object NewDeviceTemporaryDismiss : FlagKey<Boolean>() {
|
||||
override val keyName: String = "new-device-temporary-dismiss"
|
||||
override val defaultValue: Boolean = false
|
||||
override val isRemotelyConfigured: Boolean = true
|
||||
}
|
||||
|
||||
/**
|
||||
* Data object holding the feature flag key for the New Device Permanent Dismiss feature.
|
||||
*/
|
||||
data object NewDevicePermanentDismiss : FlagKey<Boolean>() {
|
||||
override val keyName: String = "new-device-permanent-dismiss"
|
||||
override val defaultValue: Boolean = false
|
||||
override val isRemotelyConfigured: Boolean = true
|
||||
}
|
||||
|
||||
/**
|
||||
* Data object holding the feature flag key to ignore an environment check.
|
||||
*/
|
||||
data object IgnoreEnvironmentCheck : FlagKey<Boolean>() {
|
||||
override val keyName: String = "ignore-environment-check"
|
||||
override val defaultValue: Boolean = false
|
||||
override val isRemotelyConfigured: Boolean = false
|
||||
}
|
||||
|
||||
//region Dummy keys for testing
|
||||
/**
|
||||
* Data object holding the key for a [Boolean] flag to be used in tests.
|
||||
*/
|
||||
@@ -199,5 +126,4 @@ sealed class FlagKey<out T : Any> {
|
||||
override val defaultValue: String = "defaultValue"
|
||||
override val isRemotelyConfigured: Boolean = true
|
||||
}
|
||||
//endregion Dummy keys for testing
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
package com.x8bit.bitwarden.data.platform.manager.model
|
||||
|
||||
import android.os.Parcelable
|
||||
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.autofill.model.AutofillSaveItem
|
||||
import com.x8bit.bitwarden.data.autofill.model.AutofillSelectionData
|
||||
@@ -65,7 +65,7 @@ sealed class SpecialCircumstance : Parcelable {
|
||||
*/
|
||||
@Parcelize
|
||||
data class Fido2Save(
|
||||
val fido2CreateCredentialRequest: Fido2CreateCredentialRequest,
|
||||
val fido2CredentialRequest: Fido2CredentialRequest,
|
||||
) : SpecialCircumstance()
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package com.x8bit.bitwarden.data.platform.manager.util
|
||||
|
||||
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.autofill.model.AutofillSaveItem
|
||||
import com.x8bit.bitwarden.data.autofill.model.AutofillSelectionData
|
||||
@@ -27,11 +27,11 @@ fun SpecialCircumstance.toAutofillSelectionDataOrNull(): AutofillSelectionData?
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns [Fido2CreateCredentialRequest] when contained in the given [SpecialCircumstance].
|
||||
* Returns [Fido2CredentialRequest] when contained in the given [SpecialCircumstance].
|
||||
*/
|
||||
fun SpecialCircumstance.toFido2CreateRequestOrNull(): Fido2CreateCredentialRequest? =
|
||||
fun SpecialCircumstance.toFido2RequestOrNull(): Fido2CredentialRequest? =
|
||||
when (this) {
|
||||
is SpecialCircumstance.Fido2Save -> this.fido2CreateCredentialRequest
|
||||
is SpecialCircumstance.Fido2Save -> this.fido2CredentialRequest
|
||||
else -> null
|
||||
}
|
||||
|
||||
|
||||
@@ -33,14 +33,15 @@ class EnvironmentRepositoryImpl(
|
||||
environmentDiskSource.preAuthEnvironmentUrlData = value.environmentUrlData
|
||||
}
|
||||
|
||||
override val environmentStateFlow: StateFlow<Environment> = environmentDiskSource
|
||||
.preAuthEnvironmentUrlDataFlow
|
||||
.map { it.toEnvironmentUrlsOrDefault() }
|
||||
.stateIn(
|
||||
scope = scope,
|
||||
started = SharingStarted.Lazily,
|
||||
initialValue = environment,
|
||||
)
|
||||
override val environmentStateFlow: StateFlow<Environment>
|
||||
get() = environmentDiskSource
|
||||
.preAuthEnvironmentUrlDataFlow
|
||||
.map { it.toEnvironmentUrlsOrDefault() }
|
||||
.stateIn(
|
||||
scope = scope,
|
||||
started = SharingStarted.Lazily,
|
||||
initialValue = Environment.Us,
|
||||
)
|
||||
|
||||
init {
|
||||
authDiskSource
|
||||
|
||||
@@ -11,7 +11,6 @@ import com.x8bit.bitwarden.ui.platform.feature.settings.appearance.model.AppThem
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import java.time.Instant
|
||||
import javax.crypto.Cipher
|
||||
|
||||
/**
|
||||
* Provides an API for observing and modifying settings state.
|
||||
@@ -235,7 +234,7 @@ interface SettingsRepository {
|
||||
* Stores the encrypted user key for biometrics, allowing it to be used to unlock the current
|
||||
* user's vault.
|
||||
*/
|
||||
suspend fun setupBiometricsKey(cipher: Cipher): BiometricsKeyResult
|
||||
suspend fun setupBiometricsKey(): BiometricsKeyResult
|
||||
|
||||
/**
|
||||
* Stores the given PIN, allowing it to be used to unlock the current user's vault.
|
||||
@@ -265,21 +264,4 @@ interface SettingsRepository {
|
||||
* Record that a user has logged in on this device.
|
||||
*/
|
||||
fun storeUserHasLoggedInValue(userId: String)
|
||||
|
||||
/**
|
||||
* Returns true if the given [userId] has previously registered for export via the credential
|
||||
* exchange protocol.
|
||||
*/
|
||||
fun isVaultRegisteredForExport(userId: String): Boolean
|
||||
|
||||
/**
|
||||
* Stores that the given [userId] has previously registered for export via the credential
|
||||
* exchange protocol.
|
||||
*/
|
||||
fun storeVaultRegisteredForExport(userId: String, isRegistered: Boolean)
|
||||
|
||||
/**
|
||||
* Gets updates for the [isVaultRegisteredForExport] value for the given [userId].
|
||||
*/
|
||||
fun getVaultRegisteredForExportFlow(userId: String): StateFlow<Boolean>
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import com.x8bit.bitwarden.data.auth.repository.util.policyInformation
|
||||
import com.x8bit.bitwarden.data.autofill.accessibility.manager.AccessibilityEnabledManager
|
||||
import com.x8bit.bitwarden.data.autofill.manager.AutofillEnabledManager
|
||||
import com.x8bit.bitwarden.data.platform.datasource.disk.SettingsDiskSource
|
||||
import com.x8bit.bitwarden.data.platform.manager.BiometricsEncryptionManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.PolicyManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.dispatcher.DispatcherManager
|
||||
import com.x8bit.bitwarden.data.platform.repository.model.BiometricsKeyResult
|
||||
@@ -36,7 +37,6 @@ import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.launch
|
||||
import java.time.Instant
|
||||
import javax.crypto.Cipher
|
||||
|
||||
private val DEFAULT_IS_SCREEN_CAPTURE_ALLOWED = BuildConfig.DEBUG
|
||||
|
||||
@@ -50,6 +50,7 @@ class SettingsRepositoryImpl(
|
||||
private val authDiskSource: AuthDiskSource,
|
||||
private val settingsDiskSource: SettingsDiskSource,
|
||||
private val vaultSdkSource: VaultSdkSource,
|
||||
private val biometricsEncryptionManager: BiometricsEncryptionManager,
|
||||
accessibilityEnabledManager: AccessibilityEnabledManager,
|
||||
policyManager: PolicyManager,
|
||||
dispatcherManager: DispatcherManager,
|
||||
@@ -481,18 +482,13 @@ class SettingsRepositoryImpl(
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun setupBiometricsKey(cipher: Cipher): BiometricsKeyResult {
|
||||
override suspend fun setupBiometricsKey(): BiometricsKeyResult {
|
||||
val userId = activeUserId ?: return BiometricsKeyResult.Error
|
||||
biometricsEncryptionManager.setupBiometrics(userId)
|
||||
return vaultSdkSource
|
||||
.getUserEncryptionKey(userId = userId)
|
||||
.onSuccess { biometricsKey ->
|
||||
authDiskSource.storeUserBiometricUnlockKey(
|
||||
userId = userId,
|
||||
biometricsKey = cipher
|
||||
.doFinal(biometricsKey.encodeToByteArray())
|
||||
.toString(Charsets.ISO_8859_1),
|
||||
)
|
||||
authDiskSource.storeUserBiometricInitVector(userId = userId, iv = cipher.iv)
|
||||
.onSuccess {
|
||||
authDiskSource.storeUserBiometricUnlockKey(userId = userId, biometricsKey = it)
|
||||
}
|
||||
.fold(
|
||||
onSuccess = { BiometricsKeyResult.Success },
|
||||
@@ -502,7 +498,6 @@ class SettingsRepositoryImpl(
|
||||
|
||||
override fun clearBiometricsKey() {
|
||||
val userId = activeUserId ?: return
|
||||
authDiskSource.storeUserBiometricInitVector(userId = userId, iv = null)
|
||||
authDiskSource.storeUserBiometricUnlockKey(userId = userId, biometricsKey = null)
|
||||
}
|
||||
|
||||
@@ -561,27 +556,6 @@ class SettingsRepositoryImpl(
|
||||
settingsDiskSource.storeUseHasLoggedInPreviously(userId)
|
||||
}
|
||||
|
||||
override fun isVaultRegisteredForExport(userId: String): Boolean {
|
||||
return settingsDiskSource.getVaultRegisteredForExport(userId) == true
|
||||
}
|
||||
|
||||
override fun storeVaultRegisteredForExport(userId: String, isRegistered: Boolean) {
|
||||
settingsDiskSource.storeVaultRegisteredForExport(userId, isRegistered)
|
||||
}
|
||||
|
||||
override fun getVaultRegisteredForExportFlow(userId: String): StateFlow<Boolean> {
|
||||
return settingsDiskSource
|
||||
.getVaultRegisteredForExportFlow(userId)
|
||||
.map { it ?: false }
|
||||
.stateIn(
|
||||
scope = unconfinedScope,
|
||||
started = SharingStarted.Eagerly,
|
||||
initialValue = settingsDiskSource
|
||||
.getVaultRegisteredForExport(userId)
|
||||
?: false,
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* If there isn't already one generated, generate a symmetric sync key that would be used
|
||||
* for communicating via IPC.
|
||||
|
||||
@@ -10,6 +10,7 @@ import com.x8bit.bitwarden.data.platform.datasource.disk.EnvironmentDiskSource
|
||||
import com.x8bit.bitwarden.data.platform.datasource.disk.FeatureFlagOverrideDiskSource
|
||||
import com.x8bit.bitwarden.data.platform.datasource.disk.SettingsDiskSource
|
||||
import com.x8bit.bitwarden.data.platform.datasource.network.service.ConfigService
|
||||
import com.x8bit.bitwarden.data.platform.manager.BiometricsEncryptionManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.PolicyManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.dispatcher.DispatcherManager
|
||||
import com.x8bit.bitwarden.data.platform.repository.AuthenticatorBridgeRepository
|
||||
@@ -91,6 +92,7 @@ object PlatformRepositoryModule {
|
||||
authDiskSource: AuthDiskSource,
|
||||
settingsDiskSource: SettingsDiskSource,
|
||||
vaultSdkSource: VaultSdkSource,
|
||||
encryptionManager: BiometricsEncryptionManager,
|
||||
accessibilityEnabledManager: AccessibilityEnabledManager,
|
||||
dispatcherManager: DispatcherManager,
|
||||
policyManager: PolicyManager,
|
||||
@@ -101,6 +103,7 @@ object PlatformRepositoryModule {
|
||||
authDiskSource = authDiskSource,
|
||||
settingsDiskSource = settingsDiskSource,
|
||||
vaultSdkSource = vaultSdkSource,
|
||||
biometricsEncryptionManager = encryptionManager,
|
||||
accessibilityEnabledManager = accessibilityEnabledManager,
|
||||
dispatcherManager = dispatcherManager,
|
||||
policyManager = policyManager,
|
||||
|
||||
@@ -41,17 +41,16 @@ fun CallingAppInfo.validatePrivilegedApp(allowList: String): Fido2ValidateOrigin
|
||||
}
|
||||
|
||||
return try {
|
||||
val origin = getOrigin(allowList)
|
||||
if (origin.isNullOrEmpty()) {
|
||||
Fido2ValidateOriginResult.Error.PasskeyNotSupportedForApp
|
||||
if (getOrigin(allowList) != null) {
|
||||
Fido2ValidateOriginResult.Success
|
||||
} else {
|
||||
Fido2ValidateOriginResult.Success(origin)
|
||||
Fido2ValidateOriginResult.Error.PasskeyNotSupportedForApp
|
||||
}
|
||||
} catch (_: IllegalStateException) {
|
||||
} catch (e: IllegalStateException) {
|
||||
// We know the package name is in the allow list so we can infer that this exception is
|
||||
// thrown because no matching signature is found.
|
||||
Fido2ValidateOriginResult.Error.PrivilegedAppSignatureNotFound
|
||||
} catch (_: IllegalArgumentException) {
|
||||
} catch (e: IllegalArgumentException) {
|
||||
// The allow list is not formatted correctly so we notify the user passkeys are not
|
||||
// supported for this application
|
||||
Fido2ValidateOriginResult.Error.PasskeyNotSupportedForApp
|
||||
|
||||
@@ -12,27 +12,8 @@ inline fun <reified T> Json.decodeFromStringOrNull(
|
||||
): T? =
|
||||
try {
|
||||
decodeFromString(string = string)
|
||||
} catch (_: SerializationException) {
|
||||
} catch (e: SerializationException) {
|
||||
null
|
||||
} catch (_: IllegalArgumentException) {
|
||||
} catch (e: IllegalArgumentException) {
|
||||
null
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempts to decode the given JSON [string] into the given type [T]. If there is an error in
|
||||
* processing the JSON or deserializing, the exception is still throw after [onFailure] lambda is
|
||||
* invoked.
|
||||
*/
|
||||
inline fun <reified T> Json.decodeFromStringWithErrorCallback(
|
||||
string: String,
|
||||
onFailure: (throwable: Throwable) -> Unit,
|
||||
): T =
|
||||
try {
|
||||
decodeFromString(string = string)
|
||||
} catch (se: SerializationException) {
|
||||
onFailure(se)
|
||||
throw se
|
||||
} catch (iae: IllegalArgumentException) {
|
||||
onFailure(iae)
|
||||
throw iae
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ import com.bitwarden.generators.PasswordGeneratorRequest
|
||||
import com.bitwarden.generators.UsernameGeneratorRequest
|
||||
import com.bitwarden.vault.PasswordHistoryView
|
||||
import com.x8bit.bitwarden.data.auth.datasource.disk.AuthDiskSource
|
||||
import com.x8bit.bitwarden.data.platform.manager.ReviewPromptManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.PolicyManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.dispatcher.DispatcherManager
|
||||
import com.x8bit.bitwarden.data.platform.repository.model.LocalDataState
|
||||
import com.x8bit.bitwarden.data.platform.repository.util.observeWhenSubscribedAndLoggedIn
|
||||
@@ -54,7 +54,7 @@ class GeneratorRepositoryImpl(
|
||||
private val authDiskSource: AuthDiskSource,
|
||||
private val vaultSdkSource: VaultSdkSource,
|
||||
private val passwordHistoryDiskSource: PasswordHistoryDiskSource,
|
||||
private val reviewPromptManager: ReviewPromptManager,
|
||||
private val policyManager: PolicyManager,
|
||||
dispatcherManager: DispatcherManager,
|
||||
) : GeneratorRepository {
|
||||
|
||||
@@ -69,9 +69,7 @@ class GeneratorRepositoryImpl(
|
||||
get() = mutablePasswordHistoryStateFlow.asStateFlow()
|
||||
|
||||
override val generatorResultFlow: Flow<GeneratorResult>
|
||||
get() = generatorResultChannel
|
||||
.receiveAsFlow()
|
||||
.onEach { reviewPromptManager.registerGeneratedResultAction() }
|
||||
get() = generatorResultChannel.receiveAsFlow()
|
||||
|
||||
init {
|
||||
mutablePasswordHistoryStateFlow
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package com.x8bit.bitwarden.data.tools.generator.repository.di
|
||||
|
||||
import com.x8bit.bitwarden.data.auth.datasource.disk.AuthDiskSource
|
||||
import com.x8bit.bitwarden.data.platform.manager.ReviewPromptManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.PolicyManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.dispatcher.DispatcherManager
|
||||
import com.x8bit.bitwarden.data.tools.generator.datasource.disk.GeneratorDiskSource
|
||||
import com.x8bit.bitwarden.data.tools.generator.datasource.disk.PasswordHistoryDiskSource
|
||||
@@ -33,7 +33,7 @@ object GeneratorRepositoryModule {
|
||||
vaultSdkSource: VaultSdkSource,
|
||||
passwordHistoryDiskSource: PasswordHistoryDiskSource,
|
||||
dispatcherManager: DispatcherManager,
|
||||
reviewPromptManager: ReviewPromptManager,
|
||||
policyManager: PolicyManager,
|
||||
): GeneratorRepository = GeneratorRepositoryImpl(
|
||||
clock = clock,
|
||||
generatorSdkSource = generatorSdkSource,
|
||||
@@ -42,6 +42,6 @@ object GeneratorRepositoryModule {
|
||||
vaultSdkSource = vaultSdkSource,
|
||||
passwordHistoryDiskSource = passwordHistoryDiskSource,
|
||||
dispatcherManager = dispatcherManager,
|
||||
reviewPromptManager = reviewPromptManager,
|
||||
policyManager = policyManager,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@ package com.x8bit.bitwarden.data.vault.datasource.disk
|
||||
|
||||
import com.x8bit.bitwarden.data.platform.manager.dispatcher.DispatcherManager
|
||||
import com.x8bit.bitwarden.data.platform.repository.util.bufferedMutableSharedFlow
|
||||
import com.x8bit.bitwarden.data.platform.util.decodeFromStringWithErrorCallback
|
||||
import com.x8bit.bitwarden.data.vault.datasource.disk.dao.CiphersDao
|
||||
import com.x8bit.bitwarden.data.vault.datasource.disk.dao.CollectionsDao
|
||||
import com.x8bit.bitwarden.data.vault.datasource.disk.dao.DomainsDao
|
||||
@@ -25,7 +24,6 @@ import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlinx.serialization.encodeToString
|
||||
import kotlinx.serialization.json.Json
|
||||
import timber.log.Timber
|
||||
|
||||
/**
|
||||
* Default implementation of [VaultDiskSource].
|
||||
@@ -72,9 +70,9 @@ class VaultDiskSourceImpl(
|
||||
entities
|
||||
.map { entity ->
|
||||
async {
|
||||
json.decodeFromStringWithErrorCallback<SyncResponseJson.Cipher>(
|
||||
json.decodeFromString<SyncResponseJson.Cipher>(
|
||||
string = entity.cipherJson,
|
||||
) { Timber.e(it, "Failed to deserialize Cipher in Vault") }
|
||||
)
|
||||
}
|
||||
}
|
||||
.awaitAll()
|
||||
@@ -128,11 +126,7 @@ class VaultDiskSourceImpl(
|
||||
.getDomains(userId)
|
||||
.map { entity ->
|
||||
withContext(dispatcherManager.default) {
|
||||
entity?.domainsJson?.let { domains ->
|
||||
json.decodeFromStringWithErrorCallback<SyncResponseJson.Domains>(
|
||||
string = domains,
|
||||
) { Timber.e(it, "Failed to deserialize Domains in Vault") }
|
||||
}
|
||||
entity?.domainsJson?.let { json.decodeFromString<SyncResponseJson.Domains>(it) }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -198,9 +192,7 @@ class VaultDiskSourceImpl(
|
||||
entities
|
||||
.map { entity ->
|
||||
async {
|
||||
json.decodeFromStringWithErrorCallback<SyncResponseJson.Send>(
|
||||
string = entity.sendJson,
|
||||
) { Timber.e(it, "Failed to deserialize Send in Vault") }
|
||||
json.decodeFromString<SyncResponseJson.Send>(entity.sendJson)
|
||||
}
|
||||
}
|
||||
.awaitAll()
|
||||
|
||||
@@ -5,7 +5,6 @@ import com.x8bit.bitwarden.data.vault.datasource.network.model.AttachmentJsonReq
|
||||
import com.x8bit.bitwarden.data.vault.datasource.network.model.AttachmentJsonResponse
|
||||
import com.x8bit.bitwarden.data.vault.datasource.network.model.CipherJsonRequest
|
||||
import com.x8bit.bitwarden.data.vault.datasource.network.model.CreateCipherInOrganizationJsonRequest
|
||||
import com.x8bit.bitwarden.data.vault.datasource.network.model.ImportCiphersJsonRequest
|
||||
import com.x8bit.bitwarden.data.vault.datasource.network.model.ShareCipherJsonRequest
|
||||
import com.x8bit.bitwarden.data.vault.datasource.network.model.SyncResponseJson
|
||||
import com.x8bit.bitwarden.data.vault.datasource.network.model.UpdateCipherCollectionsJsonRequest
|
||||
@@ -150,9 +149,4 @@ interface CiphersApi {
|
||||
*/
|
||||
@GET("ciphers/has-unassigned-ciphers")
|
||||
suspend fun hasUnassignedCiphers(): NetworkResult<Boolean>
|
||||
|
||||
@POST("ciphers/import")
|
||||
suspend fun importCiphers(
|
||||
@Body body: ImportCiphersJsonRequest,
|
||||
): NetworkResult<Unit>
|
||||
}
|
||||
|
||||
@@ -1,37 +0,0 @@
|
||||
package com.x8bit.bitwarden.data.vault.datasource.network.model
|
||||
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
/**
|
||||
* Represents an import ciphers request.
|
||||
*
|
||||
* @property folders A list of folders to import.
|
||||
* @property ciphers A list of ciphers to import.
|
||||
* @property folderRelationships A map of cipher folder relationships to import. Key correlates to
|
||||
* the index of the cipher in the ciphers list. Value correlates to the index of the folder in the
|
||||
* folders list.
|
||||
*/
|
||||
@Serializable
|
||||
data class ImportCiphersJsonRequest(
|
||||
@SerialName("folders")
|
||||
val folders: List<FolderWithIdJsonRequest>,
|
||||
@SerialName("ciphers")
|
||||
val ciphers: List<CipherJsonRequest>,
|
||||
@SerialName("folderRelationships")
|
||||
val folderRelationships: Map<Int, Int>,
|
||||
) {
|
||||
/**
|
||||
* Represents a folder request with an optional [id] if the folder already exists.
|
||||
*
|
||||
* @property name The name of the folder.
|
||||
* @property id The ID of the folder, if it already exists. Null otherwise.
|
||||
**/
|
||||
@Serializable
|
||||
data class FolderWithIdJsonRequest(
|
||||
@SerialName("name")
|
||||
val name: String?,
|
||||
@SerialName("id")
|
||||
val id: String?,
|
||||
)
|
||||
}
|
||||
@@ -1,41 +0,0 @@
|
||||
package com.x8bit.bitwarden.data.vault.datasource.network.model
|
||||
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
/**
|
||||
* The response body for importing ciphers.
|
||||
*/
|
||||
@Serializable
|
||||
sealed class ImportCiphersResponseJson {
|
||||
|
||||
/**
|
||||
* Models a successful json response.
|
||||
*/
|
||||
@Serializable
|
||||
object Success : ImportCiphersResponseJson()
|
||||
|
||||
/**
|
||||
* Represents the json body of an invalid request.
|
||||
*
|
||||
* @param validationErrors a map where each value is a list of error messages for each key.
|
||||
* The values in the array should be used for display to the user, since the keys tend to come
|
||||
* back as nonsense. (eg: empty string key)
|
||||
*/
|
||||
@Serializable
|
||||
data class Invalid(
|
||||
@SerialName("message")
|
||||
private val invalidMessage: String? = null,
|
||||
|
||||
@SerialName("Message")
|
||||
private val errorMessage: String? = null,
|
||||
|
||||
@SerialName("validationErrors")
|
||||
val validationErrors: Map<String, List<String>>?,
|
||||
) : ImportCiphersResponseJson() {
|
||||
/**
|
||||
* A generic error message.
|
||||
*/
|
||||
val message: String? get() = invalidMessage ?: errorMessage
|
||||
}
|
||||
}
|
||||
@@ -153,7 +153,6 @@ data class SyncResponseJson(
|
||||
* @property key The key of the profile (nullable).
|
||||
* @property securityStamp The secure stamp of the profile (nullable).
|
||||
* @property providers A list of providers associated with the profile (nullable).
|
||||
* @property creationDate The creation date of the account.
|
||||
*/
|
||||
@Serializable
|
||||
data class Profile(
|
||||
@@ -210,10 +209,6 @@ data class SyncResponseJson(
|
||||
|
||||
@SerialName("providers")
|
||||
val providers: List<Provider>?,
|
||||
|
||||
@SerialName("creationDate")
|
||||
@Contextual
|
||||
val creationDate: ZonedDateTime,
|
||||
) {
|
||||
/**
|
||||
* Represents an organization in the vault response.
|
||||
|
||||
@@ -5,8 +5,6 @@ import com.x8bit.bitwarden.data.vault.datasource.network.model.AttachmentJsonReq
|
||||
import com.x8bit.bitwarden.data.vault.datasource.network.model.AttachmentJsonResponse
|
||||
import com.x8bit.bitwarden.data.vault.datasource.network.model.CipherJsonRequest
|
||||
import com.x8bit.bitwarden.data.vault.datasource.network.model.CreateCipherInOrganizationJsonRequest
|
||||
import com.x8bit.bitwarden.data.vault.datasource.network.model.ImportCiphersJsonRequest
|
||||
import com.x8bit.bitwarden.data.vault.datasource.network.model.ImportCiphersResponseJson
|
||||
import com.x8bit.bitwarden.data.vault.datasource.network.model.ShareCipherJsonRequest
|
||||
import com.x8bit.bitwarden.data.vault.datasource.network.model.SyncResponseJson
|
||||
import com.x8bit.bitwarden.data.vault.datasource.network.model.UpdateCipherCollectionsJsonRequest
|
||||
@@ -120,9 +118,4 @@ interface CiphersService {
|
||||
* Returns a boolean indicating if the active user has unassigned ciphers.
|
||||
*/
|
||||
suspend fun hasUnassignedCiphers(): Result<Boolean>
|
||||
|
||||
/**
|
||||
* Attempt to import ciphers.
|
||||
*/
|
||||
suspend fun importCiphers(request: ImportCiphersJsonRequest): Result<ImportCiphersResponseJson>
|
||||
}
|
||||
|
||||
@@ -13,8 +13,6 @@ import com.x8bit.bitwarden.data.vault.datasource.network.model.AttachmentJsonRes
|
||||
import com.x8bit.bitwarden.data.vault.datasource.network.model.CipherJsonRequest
|
||||
import com.x8bit.bitwarden.data.vault.datasource.network.model.CreateCipherInOrganizationJsonRequest
|
||||
import com.x8bit.bitwarden.data.vault.datasource.network.model.FileUploadType
|
||||
import com.x8bit.bitwarden.data.vault.datasource.network.model.ImportCiphersJsonRequest
|
||||
import com.x8bit.bitwarden.data.vault.datasource.network.model.ImportCiphersResponseJson
|
||||
import com.x8bit.bitwarden.data.vault.datasource.network.model.ShareCipherJsonRequest
|
||||
import com.x8bit.bitwarden.data.vault.datasource.network.model.SyncResponseJson
|
||||
import com.x8bit.bitwarden.data.vault.datasource.network.model.UpdateCipherCollectionsJsonRequest
|
||||
@@ -218,23 +216,6 @@ class CiphersServiceImpl(
|
||||
.hasUnassignedCiphers()
|
||||
.toResult()
|
||||
|
||||
override suspend fun importCiphers(
|
||||
request: ImportCiphersJsonRequest,
|
||||
): Result<ImportCiphersResponseJson> =
|
||||
ciphersApi
|
||||
.importCiphers(body = request)
|
||||
.toResult()
|
||||
.map { ImportCiphersResponseJson.Success }
|
||||
.recoverCatching { throwable ->
|
||||
throwable
|
||||
.toBitwardenError()
|
||||
.parseErrorBodyOrNull<ImportCiphersResponseJson.Invalid>(
|
||||
code = 400,
|
||||
json = json,
|
||||
)
|
||||
?: throw throwable
|
||||
}
|
||||
|
||||
private fun createMultipartBodyBuilder(
|
||||
encryptedFile: File,
|
||||
filename: String?,
|
||||
|
||||
@@ -94,15 +94,6 @@ interface VaultSdkSource {
|
||||
encryptedPin: String,
|
||||
): Result<String>
|
||||
|
||||
/**
|
||||
* Validate the user pin using the [pinProtectedUserKey].
|
||||
*/
|
||||
suspend fun validatePin(
|
||||
userId: String,
|
||||
pin: String,
|
||||
pinProtectedUserKey: String,
|
||||
): Result<Boolean>
|
||||
|
||||
/**
|
||||
* Gets the key for an auth request that is required to approve or decline it.
|
||||
*/
|
||||
|
||||
@@ -109,17 +109,6 @@ class VaultSdkSourceImpl(
|
||||
.derivePinUserKey(encryptedPin = encryptedPin)
|
||||
}
|
||||
|
||||
override suspend fun validatePin(
|
||||
userId: String,
|
||||
pin: String,
|
||||
pinProtectedUserKey: String,
|
||||
): Result<Boolean> =
|
||||
runCatchingWithLogs {
|
||||
getClient(userId = userId)
|
||||
.auth()
|
||||
.validatePin(pin = pin, pinProtectedUserKey = pinProtectedUserKey)
|
||||
}
|
||||
|
||||
override suspend fun getAuthRequestKey(
|
||||
publicKey: String,
|
||||
userId: String,
|
||||
|
||||
@@ -6,7 +6,6 @@ import com.bitwarden.vault.AttachmentView
|
||||
import com.bitwarden.vault.Cipher
|
||||
import com.bitwarden.vault.CipherView
|
||||
import com.x8bit.bitwarden.data.auth.datasource.disk.AuthDiskSource
|
||||
import com.x8bit.bitwarden.data.platform.manager.ReviewPromptManager
|
||||
import com.x8bit.bitwarden.data.platform.util.asFailure
|
||||
import com.x8bit.bitwarden.data.platform.util.asSuccess
|
||||
import com.x8bit.bitwarden.data.platform.util.flatMap
|
||||
@@ -36,7 +35,7 @@ import java.time.Clock
|
||||
/**
|
||||
* The default implementation of the [CipherManager].
|
||||
*/
|
||||
@Suppress("TooManyFunctions", "LongParameterList")
|
||||
@Suppress("TooManyFunctions")
|
||||
class CipherManagerImpl(
|
||||
private val fileManager: FileManager,
|
||||
private val authDiskSource: AuthDiskSource,
|
||||
@@ -44,7 +43,6 @@ class CipherManagerImpl(
|
||||
private val vaultDiskSource: VaultDiskSource,
|
||||
private val vaultSdkSource: VaultSdkSource,
|
||||
private val clock: Clock,
|
||||
private val reviewPromptManager: ReviewPromptManager,
|
||||
) : CipherManager {
|
||||
private val activeUserId: String? get() = authDiskSource.userState?.activeUserId
|
||||
|
||||
@@ -59,10 +57,7 @@ class CipherManagerImpl(
|
||||
.onSuccess { vaultDiskSource.saveCipher(userId = userId, cipher = it) }
|
||||
.fold(
|
||||
onFailure = { CreateCipherResult.Error },
|
||||
onSuccess = {
|
||||
reviewPromptManager.registerAddCipherAction()
|
||||
CreateCipherResult.Success
|
||||
},
|
||||
onSuccess = { CreateCipherResult.Success },
|
||||
)
|
||||
}
|
||||
|
||||
@@ -92,10 +87,7 @@ class CipherManagerImpl(
|
||||
}
|
||||
.fold(
|
||||
onFailure = { CreateCipherResult.Error },
|
||||
onSuccess = {
|
||||
reviewPromptManager.registerAddCipherAction()
|
||||
CreateCipherResult.Success
|
||||
},
|
||||
onSuccess = { CreateCipherResult.Success },
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -122,7 +122,6 @@ class TotpCodeManagerImpl(
|
||||
CipherRepromptType.NONE -> false
|
||||
},
|
||||
orgUsesTotp = cipher.organizationUseTotp,
|
||||
orgId = cipher.organizationId,
|
||||
)
|
||||
}
|
||||
.onFailure {
|
||||
|
||||
@@ -1,9 +1,5 @@
|
||||
package com.x8bit.bitwarden.data.vault.manager
|
||||
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
import com.bitwarden.core.InitOrgCryptoRequest
|
||||
import com.bitwarden.core.InitUserCryptoMethod
|
||||
import com.bitwarden.core.InitUserCryptoRequest
|
||||
@@ -54,8 +50,6 @@ import kotlinx.coroutines.flow.onCompletion
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
import java.time.Clock
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
import kotlin.time.Duration.Companion.minutes
|
||||
|
||||
/**
|
||||
@@ -68,7 +62,6 @@ private const val MAXIMUM_INVALID_UNLOCK_ATTEMPTS = 5
|
||||
*/
|
||||
@Suppress("TooManyFunctions", "LongParameterList")
|
||||
class VaultLockManagerImpl(
|
||||
private val clock: Clock,
|
||||
private val authDiskSource: AuthDiskSource,
|
||||
private val authSdkSource: AuthSdkSource,
|
||||
private val vaultSdkSource: VaultSdkSource,
|
||||
@@ -77,15 +70,13 @@ class VaultLockManagerImpl(
|
||||
private val userLogoutManager: UserLogoutManager,
|
||||
private val trustedDeviceManager: TrustedDeviceManager,
|
||||
dispatcherManager: DispatcherManager,
|
||||
context: Context,
|
||||
) : VaultLockManager {
|
||||
private val unconfinedScope = CoroutineScope(dispatcherManager.unconfined)
|
||||
|
||||
/**
|
||||
* This [Map] tracks all active timeout [Job]s that are running and their associated data using
|
||||
* the user ID as the key.
|
||||
* This [Map] tracks all active timeout [Job]s that are running using the user ID as the key.
|
||||
*/
|
||||
private val userIdTimerJobMap: MutableMap<String, TimeoutJobData> = ConcurrentHashMap()
|
||||
private val userIdTimerJobMap = mutableMapOf<String, Job>()
|
||||
|
||||
private val activeUserId: String? get() = authDiskSource.userState?.activeUserId
|
||||
|
||||
@@ -105,10 +96,6 @@ class VaultLockManagerImpl(
|
||||
observeUserSwitchingChanges()
|
||||
observeVaultTimeoutChanges()
|
||||
observeUserLogoutResults()
|
||||
context.registerReceiver(
|
||||
ScreenStateBroadcastReceiver(),
|
||||
IntentFilter(Intent.ACTION_SCREEN_ON),
|
||||
)
|
||||
}
|
||||
|
||||
override fun isVaultUnlocked(userId: String): Boolean =
|
||||
@@ -318,40 +305,29 @@ class VaultLockManagerImpl(
|
||||
}
|
||||
|
||||
private fun observeAppCreationChanges() {
|
||||
var isFirstCreated = true
|
||||
appStateManager
|
||||
.appCreatedStateFlow
|
||||
.onEach { appCreationState ->
|
||||
when (appCreationState) {
|
||||
is AppCreationState.Created -> {
|
||||
handleOnCreated(
|
||||
createdForAutofill = appCreationState.isAutoFill,
|
||||
isFirstCreated = isFirstCreated,
|
||||
)
|
||||
isFirstCreated = false
|
||||
}
|
||||
|
||||
AppCreationState.Destroyed -> Unit
|
||||
AppCreationState.CREATED -> Unit
|
||||
AppCreationState.DESTROYED -> handleOnDestroyed()
|
||||
}
|
||||
}
|
||||
.launchIn(unconfinedScope)
|
||||
}
|
||||
|
||||
private fun handleOnCreated(
|
||||
createdForAutofill: Boolean,
|
||||
isFirstCreated: Boolean,
|
||||
) {
|
||||
val userId = activeUserId ?: return
|
||||
checkForVaultTimeout(
|
||||
userId = userId,
|
||||
checkTimeoutReason = CheckTimeoutReason.AppCreated(
|
||||
firstTimeCreation = isFirstCreated,
|
||||
createdForAutofill = createdForAutofill,
|
||||
),
|
||||
)
|
||||
private fun handleOnDestroyed() {
|
||||
activeUserId?.let { userId ->
|
||||
checkForVaultTimeout(
|
||||
userId = userId,
|
||||
checkTimeoutReason = CheckTimeoutReason.APP_RESTARTED,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun observeAppForegroundChanges() {
|
||||
var isFirstForeground = true
|
||||
|
||||
appStateManager
|
||||
.appForegroundStateFlow
|
||||
.onEach { appForegroundState ->
|
||||
@@ -360,7 +336,10 @@ class VaultLockManagerImpl(
|
||||
handleOnBackground()
|
||||
}
|
||||
|
||||
AppForegroundState.FOREGROUNDED -> handleOnForeground()
|
||||
AppForegroundState.FOREGROUNDED -> {
|
||||
handleOnForeground(isFirstForeground = isFirstForeground)
|
||||
isFirstForeground = false
|
||||
}
|
||||
}
|
||||
}
|
||||
.launchIn(unconfinedScope)
|
||||
@@ -370,13 +349,19 @@ class VaultLockManagerImpl(
|
||||
val userId = activeUserId ?: return
|
||||
checkForVaultTimeout(
|
||||
userId = userId,
|
||||
checkTimeoutReason = CheckTimeoutReason.AppBackgrounded,
|
||||
checkTimeoutReason = CheckTimeoutReason.APP_BACKGROUNDED,
|
||||
)
|
||||
}
|
||||
|
||||
private fun handleOnForeground() {
|
||||
private fun handleOnForeground(isFirstForeground: Boolean) {
|
||||
val userId = activeUserId ?: return
|
||||
userIdTimerJobMap.remove(key = userId)?.job?.cancel()
|
||||
userIdTimerJobMap[userId]?.cancel()
|
||||
if (isFirstForeground) {
|
||||
checkForVaultTimeout(
|
||||
userId = userId,
|
||||
checkTimeoutReason = CheckTimeoutReason.APP_RESTARTED,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun observeUserSwitchingChanges() {
|
||||
@@ -472,11 +457,11 @@ class VaultLockManagerImpl(
|
||||
currentActiveUserId: String,
|
||||
) {
|
||||
// Make sure to clear the now-active user's timeout job.
|
||||
userIdTimerJobMap.remove(key = currentActiveUserId)?.job?.cancel()
|
||||
userIdTimerJobMap[currentActiveUserId]?.cancel()
|
||||
// Check if the user's timeout action should be performed as we switch away.
|
||||
checkForVaultTimeout(
|
||||
userId = previousActiveUserId,
|
||||
checkTimeoutReason = CheckTimeoutReason.UserChanged,
|
||||
checkTimeoutReason = CheckTimeoutReason.USER_CHANGED,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -506,19 +491,10 @@ class VaultLockManagerImpl(
|
||||
|
||||
VaultTimeout.OnAppRestart -> {
|
||||
// If this is an app restart, trigger the timeout action; otherwise ignore.
|
||||
if (checkTimeoutReason is CheckTimeoutReason.AppCreated) {
|
||||
// We need to check the timeout action on the first time creation no matter what
|
||||
// for all subsequent creations we should check if this is for autofill and
|
||||
// and if it is we skip checking the timeout action.
|
||||
if (
|
||||
checkTimeoutReason.firstTimeCreation ||
|
||||
!checkTimeoutReason.createdForAutofill
|
||||
) {
|
||||
handleTimeoutAction(
|
||||
userId = userId,
|
||||
vaultTimeoutAction = vaultTimeoutAction,
|
||||
)
|
||||
}
|
||||
if (checkTimeoutReason == CheckTimeoutReason.APP_RESTARTED) {
|
||||
// On restart the vault should be locked already but we may need to soft-logout
|
||||
// the user.
|
||||
handleTimeoutAction(userId = userId, vaultTimeoutAction = vaultTimeoutAction)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -526,23 +502,21 @@ class VaultLockManagerImpl(
|
||||
when (checkTimeoutReason) {
|
||||
// Always preform the timeout action on app restart to ensure the user is
|
||||
// in the correct state.
|
||||
is CheckTimeoutReason.AppCreated -> {
|
||||
if (checkTimeoutReason.firstTimeCreation) {
|
||||
handleTimeoutAction(
|
||||
userId = userId,
|
||||
vaultTimeoutAction = vaultTimeoutAction,
|
||||
)
|
||||
}
|
||||
CheckTimeoutReason.APP_RESTARTED -> {
|
||||
handleTimeoutAction(
|
||||
userId = userId,
|
||||
vaultTimeoutAction = vaultTimeoutAction,
|
||||
)
|
||||
}
|
||||
|
||||
// User no longer active or engaging with the app.
|
||||
CheckTimeoutReason.AppBackgrounded,
|
||||
CheckTimeoutReason.UserChanged,
|
||||
CheckTimeoutReason.APP_BACKGROUNDED,
|
||||
CheckTimeoutReason.USER_CHANGED,
|
||||
-> {
|
||||
handleTimeoutActionWithDelay(
|
||||
userId = userId,
|
||||
vaultTimeoutAction = vaultTimeoutAction,
|
||||
delayMs = vaultTimeout
|
||||
delayInMs = vaultTimeout
|
||||
.vaultTimeoutInMinutes
|
||||
?.minutes
|
||||
?.inWholeMilliseconds
|
||||
@@ -555,26 +529,20 @@ class VaultLockManagerImpl(
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs the [VaultTimeoutAction] for the given [userId] after the [delayMs] has passed.
|
||||
* Performs the [VaultTimeoutAction] for the given [userId] after the [delayInMs] has passed.
|
||||
*
|
||||
* @see handleTimeoutAction
|
||||
*/
|
||||
private fun handleTimeoutActionWithDelay(
|
||||
userId: String,
|
||||
vaultTimeoutAction: VaultTimeoutAction,
|
||||
delayMs: Long,
|
||||
delayInMs: Long,
|
||||
) {
|
||||
userIdTimerJobMap.remove(key = userId)?.job?.cancel()
|
||||
userIdTimerJobMap[userId] = TimeoutJobData(
|
||||
job = unconfinedScope.launch {
|
||||
delay(timeMillis = delayMs)
|
||||
userIdTimerJobMap.remove(key = userId)
|
||||
handleTimeoutAction(userId = userId, vaultTimeoutAction = vaultTimeoutAction)
|
||||
},
|
||||
vaultTimeoutAction = vaultTimeoutAction,
|
||||
startTimeMs = clock.millis(),
|
||||
durationMs = delayMs,
|
||||
)
|
||||
userIdTimerJobMap[userId]?.cancel()
|
||||
userIdTimerJobMap[userId] = unconfinedScope.launch {
|
||||
delay(timeMillis = delayInMs)
|
||||
handleTimeoutAction(userId = userId, vaultTimeoutAction = vaultTimeoutAction)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -621,60 +589,11 @@ class VaultLockManagerImpl(
|
||||
}
|
||||
|
||||
/**
|
||||
* A custom [BroadcastReceiver] that listens for when the screen is powered on and restarts the
|
||||
* vault timeout jobs to ensure they complete at the correct time.
|
||||
*
|
||||
* This is necessary because the [delay] function in a coroutine will not keep accurate time
|
||||
* when the screen is off. We do not cancel the job when the screen is off, this allows the
|
||||
* job to complete as-soon-as possible if the screen is powered off for an extended period.
|
||||
* Helper enum that indicates the reason we are checking for timeout.
|
||||
*/
|
||||
private inner class ScreenStateBroadcastReceiver : BroadcastReceiver() {
|
||||
override fun onReceive(context: Context, intent: Intent) {
|
||||
userIdTimerJobMap.map { (userId, data) ->
|
||||
handleTimeoutActionWithDelay(
|
||||
userId = userId,
|
||||
vaultTimeoutAction = data.vaultTimeoutAction,
|
||||
delayMs = data.durationMs - (clock.millis() - data.startTimeMs)
|
||||
.coerceAtLeast(minimumValue = 0L),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A wrapper class containing all relevant data concerning a timeout action [Job].
|
||||
*/
|
||||
private data class TimeoutJobData(
|
||||
val job: Job,
|
||||
val vaultTimeoutAction: VaultTimeoutAction,
|
||||
val startTimeMs: Long,
|
||||
val durationMs: Long,
|
||||
)
|
||||
|
||||
/**
|
||||
* Helper sealed class which denotes the reason to check the vault timeout.
|
||||
*/
|
||||
private sealed class CheckTimeoutReason {
|
||||
/**
|
||||
* Indicates the app has been backgrounded but is still running.
|
||||
*/
|
||||
data object AppBackgrounded : CheckTimeoutReason()
|
||||
|
||||
/**
|
||||
* Indicates the app has entered a Created state.
|
||||
*
|
||||
* @param firstTimeCreation if this is the first time the process is being created.
|
||||
* @param createdForAutofill if the the creation event is due to an activity being launched
|
||||
* for autofill.
|
||||
*/
|
||||
data class AppCreated(
|
||||
val firstTimeCreation: Boolean,
|
||||
val createdForAutofill: Boolean,
|
||||
) : CheckTimeoutReason()
|
||||
|
||||
/**
|
||||
* Indicates that the current user has changed.
|
||||
*/
|
||||
data object UserChanged : CheckTimeoutReason()
|
||||
private enum class CheckTimeoutReason {
|
||||
APP_BACKGROUNDED,
|
||||
APP_RESTARTED,
|
||||
USER_CHANGED,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,6 @@ import com.x8bit.bitwarden.data.auth.datasource.sdk.AuthSdkSource
|
||||
import com.x8bit.bitwarden.data.auth.manager.TrustedDeviceManager
|
||||
import com.x8bit.bitwarden.data.auth.manager.UserLogoutManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.AppStateManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.ReviewPromptManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.dispatcher.DispatcherManager
|
||||
import com.x8bit.bitwarden.data.platform.repository.SettingsRepository
|
||||
import com.x8bit.bitwarden.data.vault.datasource.disk.VaultDiskSource
|
||||
@@ -45,7 +44,6 @@ object VaultManagerModule {
|
||||
authDiskSource: AuthDiskSource,
|
||||
fileManager: FileManager,
|
||||
clock: Clock,
|
||||
reviewPromptManager: ReviewPromptManager,
|
||||
): CipherManager = CipherManagerImpl(
|
||||
fileManager = fileManager,
|
||||
authDiskSource = authDiskSource,
|
||||
@@ -53,7 +51,6 @@ object VaultManagerModule {
|
||||
vaultDiskSource = vaultDiskSource,
|
||||
vaultSdkSource = vaultSdkSource,
|
||||
clock = clock,
|
||||
reviewPromptManager = reviewPromptManager,
|
||||
)
|
||||
|
||||
@Provides
|
||||
@@ -71,8 +68,6 @@ object VaultManagerModule {
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideVaultLockManager(
|
||||
@ApplicationContext context: Context,
|
||||
clock: Clock,
|
||||
authDiskSource: AuthDiskSource,
|
||||
authSdkSource: AuthSdkSource,
|
||||
vaultSdkSource: VaultSdkSource,
|
||||
@@ -83,8 +78,6 @@ object VaultManagerModule {
|
||||
trustedDeviceManager: TrustedDeviceManager,
|
||||
): VaultLockManager =
|
||||
VaultLockManagerImpl(
|
||||
context = context,
|
||||
clock = clock,
|
||||
authDiskSource = authDiskSource,
|
||||
authSdkSource = authSdkSource,
|
||||
vaultSdkSource = vaultSdkSource,
|
||||
|
||||
@@ -29,5 +29,4 @@ data class VerificationCodeItem(
|
||||
val username: String?,
|
||||
val hasPasswordReprompt: Boolean,
|
||||
val orgUsesTotp: Boolean,
|
||||
val orgId: String?,
|
||||
)
|
||||
|
||||
@@ -33,7 +33,6 @@ import com.x8bit.bitwarden.data.vault.repository.model.VaultUnlockResult
|
||||
import com.x8bit.bitwarden.ui.vault.feature.vault.model.VaultFilterType
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import javax.crypto.Cipher
|
||||
|
||||
/**
|
||||
* Responsible for managing vault data inside the network layer.
|
||||
@@ -190,7 +189,7 @@ interface VaultRepository : CipherManager, VaultLockManager {
|
||||
/**
|
||||
* Attempt to unlock the vault using the stored biometric key for the currently active user.
|
||||
*/
|
||||
suspend fun unlockVaultWithBiometrics(cipher: Cipher): VaultUnlockResult
|
||||
suspend fun unlockVaultWithBiometrics(): VaultUnlockResult
|
||||
|
||||
/**
|
||||
* Attempt to unlock the vault with the given [masterPassword] and for the currently active
|
||||
|
||||
@@ -23,7 +23,6 @@ import com.x8bit.bitwarden.data.platform.datasource.disk.SettingsDiskSource
|
||||
import com.x8bit.bitwarden.data.platform.datasource.network.util.isNoConnectionError
|
||||
import com.x8bit.bitwarden.data.platform.manager.DatabaseSchemeManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.PushManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.ReviewPromptManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.dispatcher.DispatcherManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.model.SyncCipherDeleteData
|
||||
import com.x8bit.bitwarden.data.platform.manager.model.SyncCipherUpsertData
|
||||
@@ -116,7 +115,6 @@ import kotlinx.coroutines.launch
|
||||
import retrofit2.HttpException
|
||||
import java.time.Clock
|
||||
import java.time.temporal.ChronoUnit
|
||||
import javax.crypto.Cipher
|
||||
|
||||
/**
|
||||
* A "stop timeout delay" in milliseconds used to let a shared coroutine continue to run for the
|
||||
@@ -146,7 +144,6 @@ class VaultRepositoryImpl(
|
||||
pushManager: PushManager,
|
||||
private val clock: Clock,
|
||||
dispatcherManager: DispatcherManager,
|
||||
private val reviewPromptManager: ReviewPromptManager,
|
||||
) : VaultRepository,
|
||||
CipherManager by cipherManager,
|
||||
VaultLockManager by vaultLockManager {
|
||||
@@ -543,36 +540,19 @@ class VaultRepositoryImpl(
|
||||
),
|
||||
)
|
||||
|
||||
override suspend fun unlockVaultWithBiometrics(cipher: Cipher): VaultUnlockResult {
|
||||
override suspend fun unlockVaultWithBiometrics(): VaultUnlockResult {
|
||||
val userId = activeUserId ?: return VaultUnlockResult.InvalidStateError
|
||||
val biometricsKey = authDiskSource
|
||||
.getUserBiometricUnlockKey(userId = userId)
|
||||
?: return VaultUnlockResult.InvalidStateError
|
||||
val iv = authDiskSource.getUserBiometricInitVector(userId = userId)
|
||||
return this
|
||||
.unlockVaultForUser(
|
||||
userId = userId,
|
||||
initUserCryptoMethod = InitUserCryptoMethod.DecryptedKey(
|
||||
decryptedUserKey = iv
|
||||
?.let {
|
||||
cipher
|
||||
.doFinal(biometricsKey.toByteArray(Charsets.ISO_8859_1))
|
||||
.decodeToString()
|
||||
}
|
||||
?: biometricsKey,
|
||||
),
|
||||
)
|
||||
return unlockVaultForUser(
|
||||
userId = userId,
|
||||
initUserCryptoMethod = InitUserCryptoMethod.DecryptedKey(
|
||||
decryptedUserKey = biometricsKey,
|
||||
),
|
||||
)
|
||||
.also {
|
||||
if (it is VaultUnlockResult.Success) {
|
||||
if (iv == null) {
|
||||
authDiskSource.storeUserBiometricUnlockKey(
|
||||
userId = userId,
|
||||
biometricsKey = cipher
|
||||
.doFinal(biometricsKey.encodeToByteArray())
|
||||
.toString(Charsets.ISO_8859_1),
|
||||
)
|
||||
authDiskSource.storeUserBiometricInitVector(userId = userId, iv = cipher.iv)
|
||||
}
|
||||
deriveTemporaryPinProtectedUserKeyIfNecessary(userId = userId)
|
||||
}
|
||||
}
|
||||
@@ -652,10 +632,7 @@ class VaultRepositoryImpl(
|
||||
}
|
||||
.fold(
|
||||
onFailure = { CreateSendResult.Error(message = null) },
|
||||
onSuccess = {
|
||||
reviewPromptManager.registerCreateSendAction()
|
||||
CreateSendResult.Success(it)
|
||||
},
|
||||
onSuccess = { CreateSendResult.Success(it) },
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1375,11 +1352,11 @@ class VaultRepositoryImpl(
|
||||
if (serverRevisionDate < lastSyncTimeMs) {
|
||||
// We can skip the actual sync call if there is no new data or
|
||||
// database scheme changes since the last sync.
|
||||
vaultDiskSource.resyncVaultData(userId = userId)
|
||||
settingsDiskSource.storeLastSyncTime(
|
||||
userId = userId,
|
||||
lastSyncTime = clock.instant(),
|
||||
)
|
||||
vaultDiskSource.resyncVaultData(userId = userId)
|
||||
val itemsAvailable = vaultDiskSource
|
||||
.getCiphers(userId)
|
||||
.firstOrNull()
|
||||
|
||||
@@ -5,7 +5,6 @@ import com.x8bit.bitwarden.data.auth.manager.UserLogoutManager
|
||||
import com.x8bit.bitwarden.data.platform.datasource.disk.SettingsDiskSource
|
||||
import com.x8bit.bitwarden.data.platform.manager.DatabaseSchemeManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.PushManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.ReviewPromptManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.dispatcher.DispatcherManager
|
||||
import com.x8bit.bitwarden.data.vault.datasource.disk.VaultDiskSource
|
||||
import com.x8bit.bitwarden.data.vault.datasource.network.service.CiphersService
|
||||
@@ -53,7 +52,6 @@ object VaultRepositoryModule {
|
||||
userLogoutManager: UserLogoutManager,
|
||||
databaseSchemeManager: DatabaseSchemeManager,
|
||||
clock: Clock,
|
||||
reviewPromptManager: ReviewPromptManager,
|
||||
): VaultRepository = VaultRepositoryImpl(
|
||||
syncService = syncService,
|
||||
sendsService = sendsService,
|
||||
@@ -72,6 +70,5 @@ object VaultRepositoryModule {
|
||||
userLogoutManager = userLogoutManager,
|
||||
databaseSchemeManager = databaseSchemeManager,
|
||||
clock = clock,
|
||||
reviewPromptManager = reviewPromptManager,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -34,11 +34,13 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.x8bit.bitwarden.R
|
||||
import com.x8bit.bitwarden.ui.auth.feature.accountsetup.handlers.rememberSetupAutoFillHandler
|
||||
import com.x8bit.bitwarden.ui.platform.base.util.EventsEffect
|
||||
import com.x8bit.bitwarden.ui.platform.base.util.asText
|
||||
import com.x8bit.bitwarden.ui.platform.base.util.standardHorizontalMargin
|
||||
import com.x8bit.bitwarden.ui.platform.components.appbar.BitwardenTopAppBar
|
||||
import com.x8bit.bitwarden.ui.platform.components.appbar.NavigationIcon
|
||||
import com.x8bit.bitwarden.ui.platform.components.button.BitwardenFilledButton
|
||||
import com.x8bit.bitwarden.ui.platform.components.button.BitwardenTextButton
|
||||
import com.x8bit.bitwarden.ui.platform.components.dialog.BasicDialogState
|
||||
import com.x8bit.bitwarden.ui.platform.components.dialog.BitwardenBasicDialog
|
||||
import com.x8bit.bitwarden.ui.platform.components.dialog.BitwardenTwoButtonDialog
|
||||
import com.x8bit.bitwarden.ui.platform.components.image.BitwardenGifImage
|
||||
@@ -78,8 +80,10 @@ fun SetupAutoFillScreen(
|
||||
when (state.dialogState) {
|
||||
is SetupAutoFillDialogState.AutoFillFallbackDialog -> {
|
||||
BitwardenBasicDialog(
|
||||
title = null,
|
||||
message = stringResource(id = R.string.bitwarden_autofill_go_to_settings),
|
||||
visibilityState = BasicDialogState.Shown(
|
||||
title = null,
|
||||
message = R.string.bitwarden_autofill_go_to_settings.asText(),
|
||||
),
|
||||
onDismissRequest = handler.onDismissDialog,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -43,9 +43,11 @@ import com.x8bit.bitwarden.ui.platform.components.appbar.BitwardenTopAppBar
|
||||
import com.x8bit.bitwarden.ui.platform.components.appbar.NavigationIcon
|
||||
import com.x8bit.bitwarden.ui.platform.components.button.BitwardenFilledButton
|
||||
import com.x8bit.bitwarden.ui.platform.components.button.BitwardenTextButton
|
||||
import com.x8bit.bitwarden.ui.platform.components.dialog.BasicDialogState
|
||||
import com.x8bit.bitwarden.ui.platform.components.dialog.BitwardenBasicDialog
|
||||
import com.x8bit.bitwarden.ui.platform.components.dialog.BitwardenLoadingDialog
|
||||
import com.x8bit.bitwarden.ui.platform.components.dialog.BitwardenTwoButtonDialog
|
||||
import com.x8bit.bitwarden.ui.platform.components.dialog.LoadingDialogState
|
||||
import com.x8bit.bitwarden.ui.platform.components.scaffold.BitwardenScaffold
|
||||
import com.x8bit.bitwarden.ui.platform.components.toggle.BitwardenUnlockWithBiometricsSwitch
|
||||
import com.x8bit.bitwarden.ui.platform.components.toggle.BitwardenUnlockWithPinSwitch
|
||||
@@ -75,7 +77,7 @@ fun SetupUnlockScreen(
|
||||
showBiometricsPrompt = true
|
||||
biometricsManager.promptBiometrics(
|
||||
onSuccess = {
|
||||
handler.unlockWithBiometricToggle(it)
|
||||
handler.unlockWithBiometricToggle()
|
||||
showBiometricsPrompt = false
|
||||
},
|
||||
onCancel = { showBiometricsPrompt = false },
|
||||
@@ -322,12 +324,14 @@ private fun SetupUnlockScreenDialogs(
|
||||
) {
|
||||
when (dialogState) {
|
||||
is SetupUnlockState.DialogState.Loading -> BitwardenLoadingDialog(
|
||||
text = dialogState.title(),
|
||||
visibilityState = LoadingDialogState.Shown(text = dialogState.title),
|
||||
)
|
||||
|
||||
is SetupUnlockState.DialogState.Error -> BitwardenBasicDialog(
|
||||
title = dialogState.title(),
|
||||
message = dialogState.message(),
|
||||
visibilityState = BasicDialogState.Shown(
|
||||
title = dialogState.title,
|
||||
message = dialogState.message,
|
||||
),
|
||||
onDismissRequest = onDismissRequest,
|
||||
)
|
||||
|
||||
|
||||
@@ -65,12 +65,8 @@ class SetupUnlockViewModel @Inject constructor(
|
||||
SetupUnlockAction.EnableBiometricsClick -> handleEnableBiometricsClick()
|
||||
SetupUnlockAction.SetUpLaterClick -> handleSetUpLaterClick()
|
||||
SetupUnlockAction.DismissDialog -> handleDismissDialog()
|
||||
SetupUnlockAction.UnlockWithBiometricToggleDisabled -> {
|
||||
handleUnlockWithBiometricToggleDisabled()
|
||||
}
|
||||
|
||||
is SetupUnlockAction.UnlockWithBiometricToggleEnabled -> {
|
||||
handleUnlockWithBiometricToggleEnabled(action)
|
||||
is SetupUnlockAction.UnlockWithBiometricToggle -> {
|
||||
handleUnlockWithBiometricToggle(action)
|
||||
}
|
||||
|
||||
is SetupUnlockAction.UnlockWithPinToggle -> handleUnlockWithPinToggle(action)
|
||||
@@ -131,23 +127,23 @@ class SetupUnlockViewModel @Inject constructor(
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleUnlockWithBiometricToggleDisabled() {
|
||||
settingsRepository.clearBiometricsKey()
|
||||
mutableStateFlow.update { it.copy(isUnlockWithBiometricsEnabled = false) }
|
||||
}
|
||||
|
||||
private fun handleUnlockWithBiometricToggleEnabled(
|
||||
action: SetupUnlockAction.UnlockWithBiometricToggleEnabled,
|
||||
private fun handleUnlockWithBiometricToggle(
|
||||
action: SetupUnlockAction.UnlockWithBiometricToggle,
|
||||
) {
|
||||
mutableStateFlow.update {
|
||||
it.copy(
|
||||
dialogState = SetupUnlockState.DialogState.Loading(R.string.saving.asText()),
|
||||
isUnlockWithBiometricsEnabled = true,
|
||||
)
|
||||
}
|
||||
viewModelScope.launch {
|
||||
val result = settingsRepository.setupBiometricsKey(cipher = action.cipher)
|
||||
sendAction(SetupUnlockAction.Internal.BiometricsKeyResultReceive(result = result))
|
||||
if (action.isEnabled) {
|
||||
mutableStateFlow.update {
|
||||
it.copy(
|
||||
dialogState = SetupUnlockState.DialogState.Loading(R.string.saving.asText()),
|
||||
isUnlockWithBiometricsEnabled = true,
|
||||
)
|
||||
}
|
||||
viewModelScope.launch {
|
||||
val result = settingsRepository.setupBiometricsKey()
|
||||
sendAction(SetupUnlockAction.Internal.BiometricsKeyResultReceive(result))
|
||||
}
|
||||
} else {
|
||||
settingsRepository.clearBiometricsKey()
|
||||
mutableStateFlow.update { it.copy(isUnlockWithBiometricsEnabled = false) }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -276,15 +272,10 @@ sealed class SetupUnlockEvent {
|
||||
*/
|
||||
sealed class SetupUnlockAction {
|
||||
/**
|
||||
* User toggled the unlock with biometrics switch to off.
|
||||
* User toggled the unlock with biometrics switch.
|
||||
*/
|
||||
data object UnlockWithBiometricToggleDisabled : SetupUnlockAction()
|
||||
|
||||
/**
|
||||
* User toggled the unlock with biometrics switch to on.
|
||||
*/
|
||||
data class UnlockWithBiometricToggleEnabled(
|
||||
val cipher: Cipher,
|
||||
data class UnlockWithBiometricToggle(
|
||||
val isEnabled: Boolean,
|
||||
) : SetupUnlockAction()
|
||||
|
||||
/**
|
||||
|
||||
@@ -3,7 +3,6 @@ package com.x8bit.bitwarden.ui.auth.feature.accountsetup.handlers
|
||||
import com.x8bit.bitwarden.ui.auth.feature.accountsetup.SetupUnlockAction
|
||||
import com.x8bit.bitwarden.ui.auth.feature.accountsetup.SetupUnlockViewModel
|
||||
import com.x8bit.bitwarden.ui.platform.components.toggle.UnlockWithPinState
|
||||
import javax.crypto.Cipher
|
||||
|
||||
/**
|
||||
* A collection of handler functions for managing actions within the context of the Setup Unlock
|
||||
@@ -15,7 +14,7 @@ data class SetupUnlockHandler(
|
||||
val onUnlockWithPinToggle: (UnlockWithPinState) -> Unit,
|
||||
val onContinueClick: () -> Unit,
|
||||
val onSetUpLaterClick: () -> Unit,
|
||||
val unlockWithBiometricToggle: (cipher: Cipher) -> Unit,
|
||||
val unlockWithBiometricToggle: () -> Unit,
|
||||
) {
|
||||
@Suppress("UndocumentedPublicClass")
|
||||
companion object {
|
||||
@@ -26,7 +25,9 @@ data class SetupUnlockHandler(
|
||||
fun create(viewModel: SetupUnlockViewModel): SetupUnlockHandler =
|
||||
SetupUnlockHandler(
|
||||
onDisableBiometrics = {
|
||||
viewModel.trySendAction(SetupUnlockAction.UnlockWithBiometricToggleDisabled)
|
||||
viewModel.trySendAction(
|
||||
SetupUnlockAction.UnlockWithBiometricToggle(isEnabled = false),
|
||||
)
|
||||
},
|
||||
onEnableBiometrics = {
|
||||
viewModel.trySendAction(SetupUnlockAction.EnableBiometricsClick)
|
||||
@@ -38,7 +39,7 @@ data class SetupUnlockHandler(
|
||||
onSetUpLaterClick = { viewModel.trySendAction(SetupUnlockAction.SetUpLaterClick) },
|
||||
unlockWithBiometricToggle = {
|
||||
viewModel.trySendAction(
|
||||
SetupUnlockAction.UnlockWithBiometricToggleEnabled(cipher = it),
|
||||
SetupUnlockAction.UnlockWithBiometricToggle(isEnabled = true),
|
||||
)
|
||||
},
|
||||
)
|
||||
|
||||
@@ -35,9 +35,11 @@ import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.x8bit.bitwarden.R
|
||||
import com.x8bit.bitwarden.ui.auth.feature.checkemail.handlers.rememberCheckEmailHandler
|
||||
import com.x8bit.bitwarden.ui.platform.base.util.ClickableTextHighlight
|
||||
import com.x8bit.bitwarden.ui.platform.base.util.EventsEffect
|
||||
import com.x8bit.bitwarden.ui.platform.base.util.createAnnotatedString
|
||||
import com.x8bit.bitwarden.ui.platform.base.util.createClickableAnnotatedString
|
||||
import com.x8bit.bitwarden.ui.platform.base.util.standardHorizontalMargin
|
||||
import com.x8bit.bitwarden.ui.platform.base.util.toAnnotatedString
|
||||
import com.x8bit.bitwarden.ui.platform.components.appbar.BitwardenTopAppBar
|
||||
import com.x8bit.bitwarden.ui.platform.components.button.BitwardenFilledButton
|
||||
import com.x8bit.bitwarden.ui.platform.components.button.BitwardenTextButton
|
||||
@@ -150,13 +152,18 @@ private fun CheckEmailContent(
|
||||
)
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
|
||||
val descriptionAnnotatedString = R.string.we_sent_an_email_to.toAnnotatedString(
|
||||
args = arrayOf(email),
|
||||
emphasisHighlightStyle = SpanStyle(
|
||||
val descriptionAnnotatedString = createAnnotatedString(
|
||||
mainString = stringResource(
|
||||
id = R.string.we_sent_an_email_to,
|
||||
email,
|
||||
),
|
||||
highlights = listOf(email),
|
||||
highlightStyle = SpanStyle(
|
||||
color = BitwardenTheme.colorScheme.text.primary,
|
||||
fontSize = BitwardenTheme.typography.bodyMedium.fontSize,
|
||||
fontWeight = FontWeight.Bold,
|
||||
),
|
||||
tag = "EMAIL",
|
||||
)
|
||||
Text(
|
||||
text = descriptionAnnotatedString,
|
||||
@@ -234,14 +241,18 @@ private fun CheckEmailLegacyContent(
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
val descriptionAnnotatedString =
|
||||
R.string.follow_the_instructions_in_the_email_sent_to_x_to_continue_creating_your_account.toAnnotatedString(
|
||||
val descriptionAnnotatedString = createAnnotatedString(
|
||||
mainString = stringResource(
|
||||
id = R.string.follow_the_instructions_in_the_email_sent_to_x_to_continue_creating_your_account,
|
||||
email,
|
||||
emphasisHighlightStyle = SpanStyle(
|
||||
),
|
||||
highlights = listOf(email),
|
||||
highlightStyle = SpanStyle(
|
||||
color = BitwardenTheme.colorScheme.text.primary,
|
||||
fontSize = BitwardenTheme.typography.bodyMedium.fontSize,
|
||||
fontWeight = FontWeight.Bold,
|
||||
),
|
||||
tag = "EMAIL",
|
||||
)
|
||||
Text(
|
||||
text = descriptionAnnotatedString,
|
||||
@@ -265,17 +276,34 @@ private fun CheckEmailLegacyContent(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
val goBackAnnotatedString = createClickableAnnotatedString(
|
||||
mainString = stringResource(
|
||||
id = R.string.no_email_go_back_to_edit_your_email_address,
|
||||
),
|
||||
highlights = listOf(
|
||||
ClickableTextHighlight(
|
||||
textToHighlight = stringResource(id = R.string.go_back),
|
||||
onTextClick = onChangeEmailClick,
|
||||
),
|
||||
),
|
||||
)
|
||||
Text(
|
||||
text = R.string.no_email_go_back_to_edit_your_email_address.toAnnotatedString {
|
||||
onChangeEmailClick()
|
||||
},
|
||||
text = goBackAnnotatedString,
|
||||
)
|
||||
Spacer(modifier = Modifier.height(32.dp))
|
||||
val logInAnnotatedString = createClickableAnnotatedString(
|
||||
mainString = stringResource(
|
||||
id = R.string.or_log_in_you_may_already_have_an_account,
|
||||
),
|
||||
highlights = listOf(
|
||||
ClickableTextHighlight(
|
||||
textToHighlight = stringResource(id = R.string.log_in),
|
||||
onTextClick = onLoginClick,
|
||||
),
|
||||
),
|
||||
)
|
||||
Text(
|
||||
text = R.string.or_log_in_you_may_already_have_an_account
|
||||
.toAnnotatedString {
|
||||
onLoginClick()
|
||||
},
|
||||
text = logInAnnotatedString,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user