mirror of
https://github.com/bitwarden/android.git
synced 2026-05-10 16:45:43 -05:00
Compare commits
140 Commits
v2024.10.0
...
v2024.10.1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1c10a94109 | ||
|
|
6f535c0abe | ||
|
|
bdb6136d36 | ||
|
|
2d9451cc34 | ||
|
|
6217532237 | ||
|
|
ef1e8403e1 | ||
|
|
24c8406ed8 | ||
|
|
51c87625cb | ||
|
|
fa248243b6 | ||
|
|
f1d7d1a530 | ||
|
|
79ebd2ba33 | ||
|
|
a23fc319de | ||
|
|
c5a266dfc0 | ||
|
|
fca00d38f5 | ||
|
|
65380095f0 | ||
|
|
002fd06b72 | ||
|
|
5d85060260 | ||
|
|
4dfee643a0 | ||
|
|
7aab846244 | ||
|
|
c704cd2eca | ||
|
|
09c11f4890 | ||
|
|
df6e842201 | ||
|
|
b82614e5fa | ||
|
|
27beb25bf7 | ||
|
|
d1f13e49a4 | ||
|
|
36a718753d | ||
|
|
be0ebb9b3f | ||
|
|
4fc01c77d1 | ||
|
|
258f25cd37 | ||
|
|
98c3ced191 | ||
|
|
c26a7cdf28 | ||
|
|
083578ec2b | ||
|
|
f73ce842fc | ||
|
|
56ad1ef05b | ||
|
|
5faa30e2f2 | ||
|
|
a9b6f296d8 | ||
|
|
655beb9dd6 | ||
|
|
0d6a8513b2 | ||
|
|
ab9d57b4f2 | ||
|
|
c382227b6a | ||
|
|
cf3624264e | ||
|
|
62cfd5e746 | ||
|
|
1446e43c46 | ||
|
|
43dc2f8116 | ||
|
|
8eb408b140 | ||
|
|
970a1e14cd | ||
|
|
736912bd6c | ||
|
|
ec47cb9ee2 | ||
|
|
690de93e63 | ||
|
|
9adb106a12 | ||
|
|
499ab2d2d0 | ||
|
|
12afbea83e | ||
|
|
efbf84238d | ||
|
|
2b87cdac9e | ||
|
|
8eab74d458 | ||
|
|
b465cc5078 | ||
|
|
9b5c88e990 | ||
|
|
4756040c4a | ||
|
|
bde47d7919 | ||
|
|
86db9bd3fa | ||
|
|
cd9f4e8723 | ||
|
|
879c2b9107 | ||
|
|
cdb03f5649 | ||
|
|
ba8e3a6c51 | ||
|
|
028242c4be | ||
|
|
3296477932 | ||
|
|
c3af26d83f | ||
|
|
3e9e45ba2f | ||
|
|
22c0745993 | ||
|
|
537281f6c3 | ||
|
|
79d2a00bf8 | ||
|
|
57d79cd51c | ||
|
|
57082ff7c1 | ||
|
|
8a30f14dea | ||
|
|
2af96988ab | ||
|
|
ccb52ae6c5 | ||
|
|
cda4e47414 | ||
|
|
5e7dc26837 | ||
|
|
b5658fda42 | ||
|
|
1539c2032e | ||
|
|
94791b4256 | ||
|
|
49d9a46917 | ||
|
|
e7450171cd | ||
|
|
641a48fe44 | ||
|
|
bc057932a0 | ||
|
|
0cb8e369ae | ||
|
|
3d4c901039 | ||
|
|
e62dc5dd21 | ||
|
|
f8592f4e17 | ||
|
|
c4467f0cba | ||
|
|
8d578a9b57 | ||
|
|
73a802a483 | ||
|
|
60fce08c7e | ||
|
|
8ae6433906 | ||
|
|
83652c9699 | ||
|
|
a5cf4f49d7 | ||
|
|
78d14547e4 | ||
|
|
29f00421bb | ||
|
|
8501db0eb2 | ||
|
|
c1e9759dae | ||
|
|
8f24597bad | ||
|
|
954a9acf92 | ||
|
|
3dfe6adc05 | ||
|
|
1d84479cf3 | ||
|
|
c8dcafe737 | ||
|
|
567c2ffb94 | ||
|
|
488ec095bc | ||
|
|
32f2bfb29f | ||
|
|
20383f06a8 | ||
|
|
fd6b276cc8 | ||
|
|
e6eb626d85 | ||
|
|
569ffc3583 | ||
|
|
e2e5042be5 | ||
|
|
0c83a1099f | ||
|
|
36a5fee048 | ||
|
|
01ab047d9c | ||
|
|
4fd81ed3b8 | ||
|
|
8e092ef860 | ||
|
|
9e4119fe32 | ||
|
|
757baf0290 | ||
|
|
53b1bec42b | ||
|
|
e63c4806f1 | ||
|
|
2224708fb1 | ||
|
|
b3e885bcb1 | ||
|
|
10bbab971f | ||
|
|
8ec743736a | ||
|
|
2f05355487 | ||
|
|
b7c48c2e26 | ||
|
|
1e9583b3be | ||
|
|
75819cce3c | ||
|
|
d60c534e06 | ||
|
|
ad338a8fd6 | ||
|
|
290377af74 | ||
|
|
24195ddb90 | ||
|
|
73c5571d6b | ||
|
|
5beec9d687 | ||
|
|
9d19a73fd6 | ||
|
|
72cb9918ac | ||
|
|
aa6762dc22 | ||
|
|
b696964cb7 |
81
.github/workflows/build.yml
vendored
81
.github/workflows/build.yml
vendored
@@ -33,17 +33,17 @@ env:
|
||||
jobs:
|
||||
build:
|
||||
name: Build
|
||||
runs-on: ubuntu-22.04
|
||||
runs-on: ubuntu-24.04
|
||||
|
||||
steps:
|
||||
- name: Check out repo
|
||||
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
|
||||
uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1
|
||||
|
||||
- name: Validate Gradle wrapper
|
||||
uses: gradle/actions/wrapper-validation@d156388eb19639ec20ade50009f3d199ce1e2808 # v4.1.0
|
||||
|
||||
- name: Cache Gradle files
|
||||
uses: actions/cache@0c45773b623bea8c8e75f6c82b208c3cf94ea4f9 # v4.0.2
|
||||
uses: actions/cache@3624ceb22c1c5a301c8db4169662070a689d9ea8 # v4.1.1
|
||||
with:
|
||||
path: |
|
||||
~/.gradle/caches
|
||||
@@ -53,7 +53,7 @@ jobs:
|
||||
${{ runner.os }}-gradle-v2-
|
||||
|
||||
- name: Cache build output
|
||||
uses: actions/cache@0c45773b623bea8c8e75f6c82b208c3cf94ea4f9 # v4.0.2
|
||||
uses: actions/cache@3624ceb22c1c5a301c8db4169662070a689d9ea8 # v4.1.1
|
||||
with:
|
||||
path: |
|
||||
${{ github.workspace }}/build-cache
|
||||
@@ -62,13 +62,13 @@ jobs:
|
||||
${{ runner.os }}-build-
|
||||
|
||||
- name: Configure JDK
|
||||
uses: actions/setup-java@2dfa2011c5b2a0f1489bf9e433881c92c1631f88 # v4.3.0
|
||||
uses: actions/setup-java@b36c23c0d998641eff861008f374ee103c25ac73 # v4.4.0
|
||||
with:
|
||||
distribution: "temurin"
|
||||
java-version: ${{ env.JAVA_VERSION }}
|
||||
|
||||
- name: Configure Ruby
|
||||
uses: ruby/setup-ruby@f321cf5a4d1533575411f8752cf25b86478b0442 # v1.193.0
|
||||
uses: ruby/setup-ruby@f26937343756480a8cb3ae1f623b9c8d89ed6984 # v1.196.0
|
||||
with:
|
||||
bundler-cache: true
|
||||
|
||||
@@ -84,11 +84,18 @@ jobs:
|
||||
- name: Build
|
||||
run: bundle exec fastlane assembleDebugApks
|
||||
|
||||
- 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/
|
||||
|
||||
publish_playstore:
|
||||
name: Publish Play Store artifacts
|
||||
needs:
|
||||
- build
|
||||
runs-on: ubuntu-22.04
|
||||
runs-on: ubuntu-24.04
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
@@ -96,10 +103,10 @@ jobs:
|
||||
artifact: ["apk", "aab"]
|
||||
steps:
|
||||
- name: Check out repo
|
||||
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
|
||||
uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1
|
||||
|
||||
- name: Configure Ruby
|
||||
uses: ruby/setup-ruby@f321cf5a4d1533575411f8752cf25b86478b0442 # v1.193.0
|
||||
uses: ruby/setup-ruby@f26937343756480a8cb3ae1f623b9c8d89ed6984 # v1.196.0
|
||||
with:
|
||||
bundler-cache: true
|
||||
|
||||
@@ -153,7 +160,7 @@ jobs:
|
||||
uses: gradle/actions/wrapper-validation@d156388eb19639ec20ade50009f3d199ce1e2808 # v4.1.0
|
||||
|
||||
- name: Cache Gradle files
|
||||
uses: actions/cache@0c45773b623bea8c8e75f6c82b208c3cf94ea4f9 # v4.0.2
|
||||
uses: actions/cache@3624ceb22c1c5a301c8db4169662070a689d9ea8 # v4.1.1
|
||||
with:
|
||||
path: |
|
||||
~/.gradle/caches
|
||||
@@ -163,7 +170,7 @@ jobs:
|
||||
${{ runner.os }}-gradle-v2-
|
||||
|
||||
- name: Cache build output
|
||||
uses: actions/cache@0c45773b623bea8c8e75f6c82b208c3cf94ea4f9 # v4.0.2
|
||||
uses: actions/cache@3624ceb22c1c5a301c8db4169662070a689d9ea8 # v4.1.1
|
||||
with:
|
||||
path: |
|
||||
${{ github.workspace }}/build-cache
|
||||
@@ -172,7 +179,7 @@ jobs:
|
||||
${{ runner.os }}-build-
|
||||
|
||||
- name: Configure JDK
|
||||
uses: actions/setup-java@2dfa2011c5b2a0f1489bf9e433881c92c1631f88 # v4.3.0
|
||||
uses: actions/setup-java@b36c23c0d998641eff861008f374ee103c25ac73 # v4.4.0
|
||||
with:
|
||||
distribution: "temurin"
|
||||
java-version: ${{ env.JAVA_VERSION }}
|
||||
@@ -237,7 +244,7 @@ jobs:
|
||||
|
||||
- name: Upload release Play Store .aab artifact
|
||||
if: ${{ (matrix.variant == 'prod') && (matrix.artifact == 'aab') }}
|
||||
uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0
|
||||
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
|
||||
with:
|
||||
name: com.x8bit.bitwarden.aab
|
||||
path: app/build/outputs/bundle/standardRelease/com.x8bit.bitwarden-standard-release.aab
|
||||
@@ -245,7 +252,7 @@ jobs:
|
||||
|
||||
- name: Upload beta Play Store .aab artifact
|
||||
if: ${{ (matrix.variant == 'prod') && (matrix.artifact == 'aab') }}
|
||||
uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.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-standard-beta.aab
|
||||
@@ -253,7 +260,7 @@ jobs:
|
||||
|
||||
- name: Upload release .apk artifact
|
||||
if: ${{ (matrix.variant == 'prod') && (matrix.artifact == 'apk') }}
|
||||
uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.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-standard-release.apk
|
||||
@@ -261,7 +268,7 @@ jobs:
|
||||
|
||||
- name: Upload beta .apk artifact
|
||||
if: ${{ (matrix.variant == 'prod') && (matrix.artifact == 'apk') }}
|
||||
uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.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-standard-beta.apk
|
||||
@@ -270,7 +277,7 @@ jobs:
|
||||
# When building variants other than 'prod'
|
||||
- name: Upload debug .apk artifact
|
||||
if: ${{ (matrix.variant != 'prod') && (matrix.artifact == 'apk') }}
|
||||
uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.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-standard-debug.apk
|
||||
@@ -308,7 +315,7 @@ jobs:
|
||||
|
||||
- name: Upload .apk SHA file for release
|
||||
if: ${{ (matrix.variant == 'prod') && (matrix.artifact == 'apk') }}
|
||||
uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0
|
||||
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
|
||||
with:
|
||||
name: com.x8bit.bitwarden.apk-sha256.txt
|
||||
path: ./com.x8bit.bitwarden.apk-sha256.txt
|
||||
@@ -316,7 +323,7 @@ jobs:
|
||||
|
||||
- name: Upload .apk SHA file for beta
|
||||
if: ${{ (matrix.variant == 'prod') && (matrix.artifact == 'apk') }}
|
||||
uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.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
|
||||
@@ -324,7 +331,7 @@ jobs:
|
||||
|
||||
- name: Upload .aab SHA file for release
|
||||
if: ${{ (matrix.variant == 'prod') && (matrix.artifact == 'aab') }}
|
||||
uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0
|
||||
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
|
||||
with:
|
||||
name: com.x8bit.bitwarden.aab-sha256.txt
|
||||
path: ./com.x8bit.bitwarden.aab-sha256.txt
|
||||
@@ -332,7 +339,7 @@ jobs:
|
||||
|
||||
- name: Upload .aab SHA file for beta
|
||||
if: ${{ (matrix.variant == 'prod') && (matrix.artifact == 'aab') }}
|
||||
uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.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
|
||||
@@ -340,18 +347,18 @@ jobs:
|
||||
|
||||
- name: Upload .apk SHA file for debug
|
||||
if: ${{ (matrix.variant != 'prod') && (matrix.artifact == 'apk') }}
|
||||
uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.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
|
||||
if-no-files-found: error
|
||||
|
||||
- name: Install Firebase app distribution plugin
|
||||
if: ${{ matrix.variant == 'prod' && github.ref_name == 'main' && (inputs.distribute-to-firebase || github.event_name == 'push') }}
|
||||
if: ${{ matrix.variant == 'prod' && (inputs.distribute-to-firebase || github.event_name == 'push') }}
|
||||
run: bundle exec fastlane add_plugin firebase_app_distribution
|
||||
|
||||
- name: Publish release artifacts to Firebase
|
||||
if: ${{ matrix.variant == 'prod' && matrix.artifact == 'apk' && github.ref_name == 'main' && (inputs.distribute-to-firebase || github.event_name == 'push') }}
|
||||
if: ${{ matrix.variant == 'prod' && matrix.artifact == 'apk' && (inputs.distribute-to-firebase || github.event_name == 'push') }}
|
||||
env:
|
||||
APP_PLAY_FIREBASE_CREDS_PATH: ${{ github.workspace }}/secrets/app_play_prod_firebase-creds.json
|
||||
run: |
|
||||
@@ -360,7 +367,7 @@ jobs:
|
||||
service_credentials_file:${{ env.APP_PLAY_FIREBASE_CREDS_PATH }}
|
||||
|
||||
- name: Publish beta artifacts to Firebase
|
||||
if: ${{ (matrix.variant == 'prod' && matrix.artifact == 'apk') && github.ref_name == 'main' && (inputs.distribute-to-firebase || github.event_name == 'push') }}
|
||||
if: ${{ (matrix.variant == 'prod' && matrix.artifact == 'apk') && (inputs.distribute-to-firebase || github.event_name == 'push') }}
|
||||
env:
|
||||
APP_PLAY_FIREBASE_CREDS_PATH: ${{ github.workspace }}/secrets/app_play_prod_firebase-creds.json
|
||||
run: |
|
||||
@@ -381,13 +388,13 @@ jobs:
|
||||
name: Publish F-Droid artifacts
|
||||
needs:
|
||||
- build
|
||||
runs-on: ubuntu-22.04
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- name: Check out repo
|
||||
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
|
||||
uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1
|
||||
|
||||
- name: Configure Ruby
|
||||
uses: ruby/setup-ruby@f321cf5a4d1533575411f8752cf25b86478b0442 # v1.193.0
|
||||
uses: ruby/setup-ruby@f26937343756480a8cb3ae1f623b9c8d89ed6984 # v1.196.0
|
||||
with:
|
||||
bundler-cache: true
|
||||
|
||||
@@ -427,7 +434,7 @@ jobs:
|
||||
uses: gradle/actions/wrapper-validation@d156388eb19639ec20ade50009f3d199ce1e2808 # v4.1.0
|
||||
|
||||
- name: Cache Gradle files
|
||||
uses: actions/cache@0c45773b623bea8c8e75f6c82b208c3cf94ea4f9 # v4.0.2
|
||||
uses: actions/cache@3624ceb22c1c5a301c8db4169662070a689d9ea8 # v4.1.1
|
||||
with:
|
||||
path: |
|
||||
~/.gradle/caches
|
||||
@@ -437,7 +444,7 @@ jobs:
|
||||
${{ runner.os }}-gradle-v2-
|
||||
|
||||
- name: Cache build output
|
||||
uses: actions/cache@0c45773b623bea8c8e75f6c82b208c3cf94ea4f9 # v4.0.2
|
||||
uses: actions/cache@3624ceb22c1c5a301c8db4169662070a689d9ea8 # v4.1.1
|
||||
with:
|
||||
path: |
|
||||
${{ github.workspace }}/build-cache
|
||||
@@ -446,7 +453,7 @@ jobs:
|
||||
${{ runner.os }}-build-
|
||||
|
||||
- name: Configure JDK
|
||||
uses: actions/setup-java@2dfa2011c5b2a0f1489bf9e433881c92c1631f88 # v4.3.0
|
||||
uses: actions/setup-java@b36c23c0d998641eff861008f374ee103c25ac73 # v4.4.0
|
||||
with:
|
||||
distribution: "temurin"
|
||||
java-version: ${{ env.JAVA_VERSION }}
|
||||
@@ -481,7 +488,7 @@ jobs:
|
||||
keyPassword:"${{ env.FDROID_BETA_KEY_PASSWORD }}"
|
||||
|
||||
- name: Upload F-Droid .apk artifact
|
||||
uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.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-release.apk
|
||||
@@ -493,14 +500,14 @@ jobs:
|
||||
> ./com.x8bit.bitwarden-fdroid.apk-sha256.txt
|
||||
|
||||
- name: Upload F-Droid SHA file
|
||||
uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.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@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.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-fdroid-beta.apk
|
||||
@@ -512,18 +519,18 @@ jobs:
|
||||
> ./com.x8bit.bitwarden.beta-fdroid.apk-sha256.txt
|
||||
|
||||
- name: Upload F-Droid Beta SHA file
|
||||
uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0
|
||||
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
|
||||
with:
|
||||
name: com.x8bit.bitwarden.beta-fdroid.apk-sha256.txt
|
||||
path: ./com.x8bit.bitwarden.beta-fdroid.apk-sha256.txt
|
||||
if-no-files-found: error
|
||||
|
||||
- name: Install Firebase app distribution plugin
|
||||
if: ${{ github.ref_name == 'main' && inputs.distribute_to_firebase }}
|
||||
if: ${{ inputs.distribute_to_firebase || github.event_name == 'push' }}
|
||||
run: bundle exec fastlane add_plugin firebase_app_distribution
|
||||
|
||||
- name: Publish release F-Droid artifacts to Firebase
|
||||
if: ${{ github.ref_name == 'main' && inputs.distribute_to_firebase }}
|
||||
if: ${{ inputs.distribute_to_firebase || github.event_name == 'push' }}
|
||||
env:
|
||||
APP_FDROID_FIREBASE_CREDS_PATH: ${{ github.workspace }}/secrets/app_fdroid_firebase-creds.json
|
||||
run: |
|
||||
|
||||
7
.github/workflows/crowdin-pull.yml
vendored
7
.github/workflows/crowdin-pull.yml
vendored
@@ -1,4 +1,3 @@
|
||||
---
|
||||
name: Crowdin Sync
|
||||
|
||||
on:
|
||||
@@ -10,12 +9,12 @@ on:
|
||||
jobs:
|
||||
crowdin-sync:
|
||||
name: Autosync
|
||||
runs-on: ubuntu-22.04
|
||||
runs-on: ubuntu-24.04
|
||||
env:
|
||||
_CROWDIN_PROJECT_ID: "269690"
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
|
||||
uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1
|
||||
|
||||
- name: Login to Azure - CI Subscription
|
||||
uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0
|
||||
@@ -30,7 +29,7 @@ jobs:
|
||||
secrets: "crowdin-api-token, github-gpg-private-key, github-gpg-private-key-passphrase"
|
||||
|
||||
- name: Download translations
|
||||
uses: crowdin/github-action@91d52b545f82cb88e86c3002a443de22df77fa16 # v2.1.3
|
||||
uses: crowdin/github-action@95d6e895e871c3c7acf0cfb962f296baa41e63c6 # v2.2.0
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
CROWDIN_API_TOKEN: ${{ steps.retrieve-secrets.outputs.crowdin-api-token }}
|
||||
|
||||
6
.github/workflows/crowdin-push.yml
vendored
6
.github/workflows/crowdin-push.yml
vendored
@@ -9,12 +9,12 @@ on:
|
||||
jobs:
|
||||
crowdin-push:
|
||||
name: Crowdin Push
|
||||
runs-on: ubuntu-22.04
|
||||
runs-on: ubuntu-24.04
|
||||
env:
|
||||
_CROWDIN_PROJECT_ID: "269690"
|
||||
steps:
|
||||
- name: Check out repo
|
||||
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
|
||||
uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1
|
||||
|
||||
- name: Log in to Azure
|
||||
uses: Azure/login@cb79c773a3cfa27f31f25eb3f677781210c9ce3d # v1.6.1
|
||||
@@ -29,7 +29,7 @@ jobs:
|
||||
secrets: "crowdin-api-token"
|
||||
|
||||
- name: Upload sources
|
||||
uses: crowdin/github-action@91d52b545f82cb88e86c3002a443de22df77fa16 # v2.1.3
|
||||
uses: crowdin/github-action@95d6e895e871c3c7acf0cfb962f296baa41e63c6 # v2.2.0
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
CROWDIN_API_TOKEN: ${{ steps.retrieve-secrets.outputs.crowdin-api-token }}
|
||||
|
||||
16
.github/workflows/scan.yml
vendored
16
.github/workflows/scan.yml
vendored
@@ -9,6 +9,8 @@ on:
|
||||
- "hotfix-rc"
|
||||
pull_request_target:
|
||||
types: [opened, synchronize]
|
||||
merge_group:
|
||||
types: [checks_requested]
|
||||
|
||||
jobs:
|
||||
check-run:
|
||||
@@ -17,7 +19,7 @@ jobs:
|
||||
|
||||
sast:
|
||||
name: SAST scan
|
||||
runs-on: ubuntu-22.04
|
||||
runs-on: ubuntu-24.04
|
||||
needs: check-run
|
||||
permissions:
|
||||
contents: read
|
||||
@@ -26,12 +28,12 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Check out repo
|
||||
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
|
||||
uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
|
||||
- name: Scan with Checkmarx
|
||||
uses: checkmarx/ast-github-action@9fda5a4a2c297608117a5a56af424502a9192e57 # 2.0.34
|
||||
uses: checkmarx/ast-github-action@f0869bd1a37fddc06499a096101e6c900e815d81 # 2.0.36
|
||||
env:
|
||||
INCREMENTAL: "${{ contains(github.event_name, 'pull_request') && '--sast-incremental' || '' }}"
|
||||
with:
|
||||
@@ -46,13 +48,13 @@ jobs:
|
||||
--output-path . ${{ env.INCREMENTAL }}
|
||||
|
||||
- name: Upload Checkmarx results to GitHub
|
||||
uses: github/codeql-action/upload-sarif@294a9d92911152fe08befb9ec03e240add280cb3 # v3.26.8
|
||||
uses: github/codeql-action/upload-sarif@f779452ac5af1c261dce0346a8f964149f49322b # v3.26.13
|
||||
with:
|
||||
sarif_file: cx_result.sarif
|
||||
|
||||
quality:
|
||||
name: Quality scan
|
||||
runs-on: ubuntu-22.04
|
||||
runs-on: ubuntu-24.04
|
||||
needs: check-run
|
||||
permissions:
|
||||
contents: read
|
||||
@@ -60,13 +62,13 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Check out repo
|
||||
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
|
||||
uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1
|
||||
with:
|
||||
fetch-depth: 0
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
|
||||
- name: Scan with SonarCloud
|
||||
uses: sonarsource/sonarcloud-github-action@eb211723266fe8e83102bac7361f0a05c3ac1d1b # v3.0.0
|
||||
uses: sonarsource/sonarcloud-github-action@383f7e52eae3ab0510c3cb0e7d9d150bbaeab838 # v3.1.0
|
||||
env:
|
||||
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
23
.github/workflows/test.yml
vendored
23
.github/workflows/test.yml
vendored
@@ -8,6 +8,8 @@ on:
|
||||
- "hotfix-rc"
|
||||
pull_request_target:
|
||||
types: [opened, synchronize]
|
||||
merge_group:
|
||||
type: [checks_requested]
|
||||
workflow_dispatch:
|
||||
|
||||
env:
|
||||
@@ -21,7 +23,7 @@ jobs:
|
||||
|
||||
test:
|
||||
name: Test
|
||||
runs-on: ubuntu-22.04
|
||||
runs-on: ubuntu-24.04
|
||||
needs: check-run
|
||||
permissions:
|
||||
contents: read
|
||||
@@ -31,7 +33,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Check out repo
|
||||
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
|
||||
uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
|
||||
@@ -39,7 +41,7 @@ jobs:
|
||||
uses: gradle/actions/wrapper-validation@d156388eb19639ec20ade50009f3d199ce1e2808 # v4.1.0
|
||||
|
||||
- name: Cache Gradle files
|
||||
uses: actions/cache@0c45773b623bea8c8e75f6c82b208c3cf94ea4f9 # v4.0.2
|
||||
uses: actions/cache@3624ceb22c1c5a301c8db4169662070a689d9ea8 # v4.1.1
|
||||
with:
|
||||
path: |
|
||||
~/.gradle/caches
|
||||
@@ -49,7 +51,7 @@ jobs:
|
||||
${{ runner.os }}-gradle-v2-
|
||||
|
||||
- name: Cache build output
|
||||
uses: actions/cache@0c45773b623bea8c8e75f6c82b208c3cf94ea4f9 # v4.0.2
|
||||
uses: actions/cache@3624ceb22c1c5a301c8db4169662070a689d9ea8 # v4.1.1
|
||||
with:
|
||||
path: |
|
||||
${{ github.workspace }}/build-cache
|
||||
@@ -58,12 +60,12 @@ jobs:
|
||||
${{ runner.os }}-build-
|
||||
|
||||
- name: Configure Ruby
|
||||
uses: ruby/setup-ruby@f321cf5a4d1533575411f8752cf25b86478b0442 # v1.193.0
|
||||
uses: ruby/setup-ruby@f26937343756480a8cb3ae1f623b9c8d89ed6984 # v1.196.0
|
||||
with:
|
||||
bundler-cache: true
|
||||
|
||||
- name: Configure JDK
|
||||
uses: actions/setup-java@2dfa2011c5b2a0f1489bf9e433881c92c1631f88 # v4.3.0
|
||||
uses: actions/setup-java@b36c23c0d998641eff861008f374ee103c25ac73 # v4.4.0
|
||||
with:
|
||||
distribution: "temurin"
|
||||
java-version: ${{ env.JAVA_VERSION }}
|
||||
@@ -78,8 +80,15 @@ jobs:
|
||||
run: |
|
||||
bundle exec fastlane check
|
||||
|
||||
- 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
|
||||
uses: codecov/codecov-action@e28ff129e5465c2c0dcc6f003fc735cb6ae0c673 # v4.5.0
|
||||
uses: codecov/codecov-action@b9fd7d16f6d7d1b5d2bec1a2887e65ceed900238 # v4.6.0
|
||||
with:
|
||||
file: app/build/reports/kover/reportStandardDebug.xml
|
||||
env:
|
||||
|
||||
30
Gemfile.lock
30
Gemfile.lock
@@ -10,20 +10,20 @@ GEM
|
||||
artifactory (3.0.17)
|
||||
atomos (0.1.3)
|
||||
aws-eventstream (1.3.0)
|
||||
aws-partitions (1.975.0)
|
||||
aws-sdk-core (3.205.0)
|
||||
aws-partitions (1.989.0)
|
||||
aws-sdk-core (3.209.1)
|
||||
aws-eventstream (~> 1, >= 1.3.0)
|
||||
aws-partitions (~> 1, >= 1.651.0)
|
||||
aws-sigv4 (~> 1.9)
|
||||
jmespath (~> 1, >= 1.6.1)
|
||||
aws-sdk-kms (1.91.0)
|
||||
aws-sdk-core (~> 3, >= 3.205.0)
|
||||
aws-sdk-kms (1.94.0)
|
||||
aws-sdk-core (~> 3, >= 3.207.0)
|
||||
aws-sigv4 (~> 1.5)
|
||||
aws-sdk-s3 (1.162.0)
|
||||
aws-sdk-core (~> 3, >= 3.205.0)
|
||||
aws-sdk-s3 (1.167.0)
|
||||
aws-sdk-core (~> 3, >= 3.207.0)
|
||||
aws-sdk-kms (~> 1)
|
||||
aws-sigv4 (~> 1.5)
|
||||
aws-sigv4 (1.9.1)
|
||||
aws-sigv4 (1.10.0)
|
||||
aws-eventstream (~> 1, >= 1.0.2)
|
||||
babosa (1.0.4)
|
||||
base64 (0.2.0)
|
||||
@@ -39,8 +39,8 @@ GEM
|
||||
domain_name (0.6.20240107)
|
||||
dotenv (2.8.1)
|
||||
emoji_regex (3.2.3)
|
||||
excon (0.111.0)
|
||||
faraday (1.10.3)
|
||||
excon (0.112.0)
|
||||
faraday (1.10.4)
|
||||
faraday-em_http (~> 1.0)
|
||||
faraday-em_synchrony (~> 1.0)
|
||||
faraday-excon (~> 1.1)
|
||||
@@ -66,10 +66,10 @@ GEM
|
||||
faraday-patron (1.0.0)
|
||||
faraday-rack (1.0.0)
|
||||
faraday-retry (1.0.3)
|
||||
faraday_middleware (1.2.0)
|
||||
faraday_middleware (1.2.1)
|
||||
faraday (~> 1.0)
|
||||
fastimage (2.3.1)
|
||||
fastlane (2.222.0)
|
||||
fastlane (2.224.0)
|
||||
CFPropertyList (>= 2.3, < 4.0.0)
|
||||
addressable (>= 2.8, < 3.0.0)
|
||||
artifactory (~> 3.0)
|
||||
@@ -160,7 +160,7 @@ GEM
|
||||
httpclient (2.8.3)
|
||||
jmespath (1.6.2)
|
||||
json (2.7.2)
|
||||
jwt (2.9.0)
|
||||
jwt (2.9.3)
|
||||
base64
|
||||
mini_magick (4.13.2)
|
||||
mini_mime (1.1.5)
|
||||
@@ -179,7 +179,7 @@ GEM
|
||||
trailblazer-option (>= 0.1.1, < 0.2.0)
|
||||
uber (< 0.2.0)
|
||||
retriable (3.1.2)
|
||||
rexml (3.3.7)
|
||||
rexml (3.3.8)
|
||||
rouge (2.0.7)
|
||||
ruby2_keywords (0.0.5)
|
||||
rubyzip (2.3.2)
|
||||
@@ -205,13 +205,13 @@ GEM
|
||||
uber (0.1.0)
|
||||
unicode-display_width (2.6.0)
|
||||
word_wrap (1.0.0)
|
||||
xcodeproj (1.25.0)
|
||||
xcodeproj (1.25.1)
|
||||
CFPropertyList (>= 2.3.3, < 4.0)
|
||||
atomos (~> 0.1.3)
|
||||
claide (>= 1.0.2, < 2.0)
|
||||
colored2 (~> 3.1)
|
||||
nanaimo (~> 0.3.0)
|
||||
rexml (>= 3.3.2, < 4.0)
|
||||
rexml (>= 3.3.6, < 4.0)
|
||||
xcpretty (0.3.0)
|
||||
rouge (~> 2.0.7)
|
||||
xcpretty-travis-formatter (1.0.1)
|
||||
|
||||
@@ -28,7 +28,7 @@
|
||||
2. Create a `user.properties` file in the root directory of the project and add the following properties:
|
||||
|
||||
- `gitHubToken`: A "classic" Github Personal Access Token (PAT) with the `read:packages` scope (ex: `gitHubToken=gph_xx...xx`). These can be generated by going to the [Github tokens page](https://github.com/settings/tokens). See [the Github Packages user documentation concerning authentication](https://docs.github.com/en/packages/working-with-a-github-packages-registry/working-with-the-gradle-registry#authenticating-to-github-packages) for more details.
|
||||
- `localSdk`: A boolean value to determine if the SDK should be loaded from the local maven artifactory (ex: `localSdk=true`). This is particularly useful when developing new SDK capabilities. Review [Linking SDK to clients](https://contributing-docs.pages.dev/getting-started/sdk/#linking-sdk-to-clients) for more details.
|
||||
- `localSdk`: A boolean value to determine if the SDK should be loaded from the local maven artifactory (ex: `localSdk=true`). This is particularly useful when developing new SDK capabilities. Review [Linking SDK to clients](https://contributing.bitwarden.com/getting-started/sdk/#linking-the-sdk-to-clients) for more details.
|
||||
|
||||
3. Setup the code style formatter:
|
||||
|
||||
@@ -171,6 +171,11 @@ The following is a list of all third-party dependencies included as part of the
|
||||
- Purpose: A networking layer interface.
|
||||
- License: Apache 2.0
|
||||
|
||||
- **Timber**
|
||||
- https://github.com/JakeWharton/timber
|
||||
- Purpose: Extensible logging library for Android.
|
||||
- License: Apache 2.0
|
||||
|
||||
- **zxcvbn4j**
|
||||
- https://github.com/nulab/zxcvbn4j
|
||||
- Purpose: Password strength estimation.
|
||||
|
||||
@@ -75,6 +75,7 @@ android {
|
||||
isMinifyEnabled = false
|
||||
|
||||
buildConfigField(type = "boolean", name = "HAS_DEBUG_MENU", value = "true")
|
||||
buildConfigField(type = "boolean", name = "HAS_LOGS_ENABLED", value = "true")
|
||||
}
|
||||
|
||||
// Beta and Release variants are identical except beta has a different package name
|
||||
@@ -88,6 +89,7 @@ android {
|
||||
)
|
||||
|
||||
buildConfigField(type = "boolean", name = "HAS_DEBUG_MENU", value = "false")
|
||||
buildConfigField(type = "boolean", name = "HAS_LOGS_ENABLED", value = "false")
|
||||
}
|
||||
release {
|
||||
isDebuggable = false
|
||||
@@ -98,6 +100,7 @@ android {
|
||||
)
|
||||
|
||||
buildConfigField(type = "boolean", name = "HAS_DEBUG_MENU", value = "false")
|
||||
buildConfigField(type = "boolean", name = "HAS_LOGS_ENABLED", value = "false")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -200,6 +203,7 @@ dependencies {
|
||||
implementation(platform(libs.square.retrofit.bom))
|
||||
implementation(libs.square.retrofit)
|
||||
implementation(libs.square.retrofit.kotlinx.serialization)
|
||||
implementation(libs.timber)
|
||||
implementation(libs.zxing.zxing.core)
|
||||
|
||||
// For now we are restricted to running Compose tests for debug builds only
|
||||
|
||||
Binary file not shown.
9
app/src/beta/res/values/manifest.xml
Normal file
9
app/src/beta/res/values/manifest.xml
Normal file
@@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<!-- For beta variant, we don't have a matching variant of the Bitwarden Authenticator app.
|
||||
Therefore, we leave the known app cert null here so that no clients can connect to
|
||||
AuthenticatorBridgeService in the beta variant. If later another variant of the
|
||||
Bitwarden Authenticator app is added, a SHA-256 digest of that variant's APK can be added here.
|
||||
-->
|
||||
<string-array name="known_authenticator_app_certs" />
|
||||
</resources>
|
||||
7
app/src/debug/res/values/manifest.xml
Normal file
7
app/src/debug/res/values/manifest.xml
Normal file
@@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string-array name="known_authenticator_app_certs">
|
||||
<!-- This is the SHA-256 digest for the Authenticator App debug variant:-->
|
||||
<item>13144ab52af797a88c2fe292674461ef1715e0e1e4f5f538f63f1c174696f476</item>
|
||||
</string-array>
|
||||
</resources>
|
||||
@@ -1,16 +0,0 @@
|
||||
package com.x8bit.bitwarden.data.platform.manager
|
||||
|
||||
import com.x8bit.bitwarden.data.platform.datasource.disk.legacy.LegacyAppCenterMigrator
|
||||
import com.x8bit.bitwarden.data.platform.repository.SettingsRepository
|
||||
|
||||
/**
|
||||
* CrashLogsManager implementation for F-droid flavor builds.
|
||||
*/
|
||||
class CrashLogsManagerImpl(
|
||||
settingsRepository: SettingsRepository,
|
||||
legacyAppCenterMigrator: LegacyAppCenterMigrator,
|
||||
) : CrashLogsManager {
|
||||
override var isEnabled: Boolean = true
|
||||
|
||||
override fun trackNonFatalException(e: Exception) = Unit
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package com.x8bit.bitwarden.data.platform.manager
|
||||
|
||||
import com.x8bit.bitwarden.BuildConfig
|
||||
import com.x8bit.bitwarden.data.platform.datasource.disk.legacy.LegacyAppCenterMigrator
|
||||
import com.x8bit.bitwarden.data.platform.repository.SettingsRepository
|
||||
import com.x8bit.bitwarden.data.platform.repository.model.Environment
|
||||
import timber.log.Timber
|
||||
|
||||
/**
|
||||
* [LogsManager] implementation for F-droid flavor builds.
|
||||
*/
|
||||
class LogsManagerImpl(
|
||||
settingsRepository: SettingsRepository,
|
||||
legacyAppCenterMigrator: LegacyAppCenterMigrator,
|
||||
) : LogsManager {
|
||||
init {
|
||||
if (BuildConfig.HAS_LOGS_ENABLED) {
|
||||
Timber.plant(Timber.DebugTree())
|
||||
}
|
||||
}
|
||||
|
||||
override var isEnabled: Boolean = false
|
||||
|
||||
override fun setUserData(userId: String?, environmentType: Environment.Type) = Unit
|
||||
|
||||
override fun trackNonFatalException(throwable: Throwable) = Unit
|
||||
}
|
||||
@@ -16,6 +16,20 @@
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||
|
||||
<!-- Protect access to AuthenticatorBridgeService using this custom permission.
|
||||
|
||||
Note that each build type uses a different value for knownCerts.
|
||||
|
||||
This in effect means that the only application that can connect to the debug/release/etc
|
||||
variant AuthenticatorBridgeService is the debug/release/etc variant Bitwarden Authenticator
|
||||
app. -->
|
||||
<permission
|
||||
android:name="${applicationId}.permission.AUTHENTICATOR_BRIDGE_SERVICE"
|
||||
android:knownCerts="@array/known_authenticator_app_certs"
|
||||
android:label="Bitwarden Bridge"
|
||||
android:protectionLevel="signature|knownSigner"
|
||||
tools:targetApi="s" />
|
||||
|
||||
<application
|
||||
android:name=".BitwardenApplication"
|
||||
android:allowBackup="false"
|
||||
@@ -75,6 +89,20 @@
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
|
||||
<data android:scheme="otpauth" />
|
||||
<data android:host="totp" />
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<data android:scheme="bitwarden" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<activity
|
||||
@@ -277,6 +305,11 @@
|
||||
android:name="android.content.APP_RESTRICTIONS"
|
||||
android:resource="@xml/app_restrictions" />
|
||||
|
||||
<service
|
||||
android:name="com.x8bit.bitwarden.data.platform.service.AuthenticatorBridgeService"
|
||||
android:exported="true"
|
||||
android:permission="${applicationId}.permission.AUTHENTICATOR_BRIDGE_SERVICE" />
|
||||
|
||||
</application>
|
||||
|
||||
<queries>
|
||||
|
||||
80
app/src/main/assets/fido2_privileged_community.json
Normal file
80
app/src/main/assets/fido2_privileged_community.json
Normal file
@@ -0,0 +1,80 @@
|
||||
{
|
||||
"apps": [
|
||||
{
|
||||
"type": "android",
|
||||
"info": {
|
||||
"package_name": "org.chromium.chrome",
|
||||
"signatures": [
|
||||
{
|
||||
"build": "release",
|
||||
"cert_fingerprint_sha256": "A8:56:48:50:79:BC:B3:57:BF:BE:69:BA:19:A9:BA:43:CD:0A:D9:AB:22:67:52:C7:80:B6:88:8A:FD:48:21:6B"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "android",
|
||||
"info": {
|
||||
"package_name": "org.cromite.cromite",
|
||||
"signatures": [
|
||||
{
|
||||
"build": "release",
|
||||
"cert_fingerprint_sha256": "63:3F:A4:1D:82:11:D6:D0:91:6A:81:9B:89:66:8C:6D:E9:2E:64:23:2D:A6:7F:9D:16:FD:81:C3:B7:E9:23:FF"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "android",
|
||||
"info": {
|
||||
"package_name": "org.mozilla.fennec_fdroid",
|
||||
"signatures": [
|
||||
{
|
||||
"build": "release",
|
||||
"cert_fingerprint_sha256": "06:66:53:58:EF:D8:BA:05:BE:23:6A:47:A1:2C:B0:95:8D:7D:75:DD:93:9D:77:C2:B3:1F:53:98:53:7E:BD:C5"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "android",
|
||||
"info": {
|
||||
"package_name": "us.spotco.fennec_dos",
|
||||
"signatures": [
|
||||
{
|
||||
"build": "release",
|
||||
"cert_fingerprint_sha256": "26:0E:0A:49:67:8C:78:B7:0C:02:D6:53:7A:DD:3B:6D:C0:A1:71:71:BB:DE:8C:E7:5F:D4:02:6A:8A:3E:18:D2"
|
||||
},
|
||||
{
|
||||
"build": "release",
|
||||
"cert_fingerprint_sha256": "FF:81:F5:BE:56:39:65:94:EE:E7:0F:EF:28:32:25:6E:15:21:41:22:E2:BA:9C:ED:D2:60:05:FF:D4:BC:AA:A8"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "android",
|
||||
"info": {
|
||||
"package_name": "us.spotco.mulch",
|
||||
"signatures": [
|
||||
{
|
||||
"build": "release",
|
||||
"cert_fingerprint_sha256": "26:0E:0A:49:67:8C:78:B7:0C:02:D6:53:7A:DD:3B:6D:C0:A1:71:71:BB:DE:8C:E7:5F:D4:02:6A:8A:3E:18:D2"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "android",
|
||||
"info": {
|
||||
"package_name": "io.github.forkmaintainers.iceraven",
|
||||
"signatures": [
|
||||
{
|
||||
"build": "release",
|
||||
"cert_fingerprint_sha256": "9C:0D:22:37:9F:48:7B:70:A4:F9:F8:BE:C0:17:3C:F9:1A:16:44:F0:8F:93:38:5B:5B:78:2C:E3:76:60:BA:81"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -475,7 +475,102 @@
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "android",
|
||||
"info": {
|
||||
"package_name": "com.talonsec.talon",
|
||||
"signatures": [
|
||||
{
|
||||
"build": "release",
|
||||
"cert_fingerprint_sha256": "A3:66:03:44:A6:F6:AF:CA:81:8C:BF:43:96:A2:3C:CF:D5:ED:7A:78:1B:B4:A3:D1:85:03:01:E2:F4:6D:23:83"
|
||||
},
|
||||
{
|
||||
"build": "release",
|
||||
"cert_fingerprint_sha256": "E2:A5:64:74:EA:23:7B:06:67:B6:F5:2C:DC:E9:04:5E:24:88:3B:AE:D0:82:59:9A:A2:DF:0B:60:3A:CF:6A:3B"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "android",
|
||||
"info": {
|
||||
"package_name": "com.talonsec.talon_beta",
|
||||
"signatures": [
|
||||
{
|
||||
"build": "release",
|
||||
"cert_fingerprint_sha256": "F5:86:62:7A:32:C8:9F:E6:7E:00:6D:B1:8C:34:31:9E:01:7F:B3:B2:BE:D6:9D:01:01:B7:F9:43:E7:7C:48:AE"
|
||||
},
|
||||
{
|
||||
"build": "release",
|
||||
"cert_fingerprint_sha256": "9A:A1:25:D5:E5:5E:3F:B0:DE:96:72:D9:A9:5D:04:65:3F:49:4A:1E:C3:EE:76:1E:94:C4:4E:5D:2F:65:8E:2F"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "android",
|
||||
"info": {
|
||||
"package_name": "com.duckduckgo.mobile.android.debug",
|
||||
"signatures": [
|
||||
{
|
||||
"build": "release",
|
||||
"cert_fingerprint_sha256": "C4:F0:9E:2B:D7:25:AD:F5:AD:92:0B:A2:80:27:66:AC:16:4A:C1:53:B3:EA:9E:08:48:B0:57:98:37:F7:6A:29"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "android",
|
||||
"info": {
|
||||
"package_name": "com.duckduckgo.mobile.android",
|
||||
"signatures": [
|
||||
{
|
||||
"build": "release",
|
||||
"cert_fingerprint_sha256": "BB:7B:B3:1C:57:3C:46:A1:DA:7F:C5:C5:28:A6:AC:F4:32:10:84:56:FE:EC:50:81:0C:7F:33:69:4E:B3:D2:D4"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "android",
|
||||
"info": {
|
||||
"package_name": "com.naver.whale",
|
||||
"signatures": [
|
||||
{
|
||||
"build": "release",
|
||||
"cert_fingerprint_sha256": "0B:8B:85:23:BB:4A:EF:FA:34:6E:4B:DD:4F:BF:7D:19:34:50:56:9A:A1:4A:AA:D4:AD:FD:94:A3:F7:B2:27:BB"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "android",
|
||||
"info": {
|
||||
"package_name": "com.fido.fido2client",
|
||||
"signatures": [
|
||||
{
|
||||
"build": "release",
|
||||
"cert_fingerprint_sha256": "FC:98:DA:E6:3A:D3:96:26:C8:C6:7F:BE:83:F2:F0:6F:74:93:2A:9C:D1:46:B9:2C:EC:FC:6A:04:7A:90:43:86"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "android",
|
||||
"info": {
|
||||
"package_name": "com.heytap.browser",
|
||||
"signatures": [
|
||||
{
|
||||
"build": "release",
|
||||
"cert_fingerprint_sha256": "AF:F8:A7:49:CF:0E:7D:75:44:65:D0:FB:FA:7B:8D:0C:64:5E:22:5C:10:C6:E2:32:AD:A0:D9:74:88:36:B8:E5"
|
||||
},
|
||||
{
|
||||
"build": "release",
|
||||
"cert_fingerprint_sha256": "A8:FE:A4:CA:FB:93:32:DA:26:B8:E6:81:08:17:C1:DA:90:A5:03:0E:35:A6:0A:79:E0:6C:90:97:AA:C6:A4:42"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ package com.x8bit.bitwarden
|
||||
import android.app.Application
|
||||
import com.x8bit.bitwarden.data.auth.manager.AuthRequestNotificationManager
|
||||
import com.x8bit.bitwarden.data.platform.annotation.OmitFromCoverage
|
||||
import com.x8bit.bitwarden.data.platform.manager.CrashLogsManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.LogsManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.NetworkConfigManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.event.OrganizationEventManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.restriction.RestrictionManager
|
||||
@@ -19,10 +19,10 @@ class BitwardenApplication : Application() {
|
||||
// Inject classes here that must be triggered on startup but are not otherwise consumed by
|
||||
// other callers.
|
||||
@Inject
|
||||
lateinit var networkConfigManager: NetworkConfigManager
|
||||
lateinit var logsManager: LogsManager
|
||||
|
||||
@Inject
|
||||
lateinit var crashLogsManager: CrashLogsManager
|
||||
lateinit var networkConfigManager: NetworkConfigManager
|
||||
|
||||
@Inject
|
||||
lateinit var authRequestNotificationManager: AuthRequestNotificationManager
|
||||
|
||||
@@ -5,6 +5,7 @@ import android.os.Parcelable
|
||||
import androidx.lifecycle.SavedStateHandle
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.bitwarden.vault.CipherView
|
||||
import com.x8bit.bitwarden.data.auth.manager.AddTotpItemFromAuthenticatorManager
|
||||
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.EmailTokenResult
|
||||
import com.x8bit.bitwarden.data.auth.util.getCompleteRegistrationDataIntentOrNull
|
||||
@@ -23,6 +24,7 @@ import com.x8bit.bitwarden.data.platform.manager.model.CompleteRegistrationData
|
||||
import com.x8bit.bitwarden.data.platform.manager.model.SpecialCircumstance
|
||||
import com.x8bit.bitwarden.data.platform.repository.EnvironmentRepository
|
||||
import com.x8bit.bitwarden.data.platform.repository.SettingsRepository
|
||||
import com.x8bit.bitwarden.data.platform.util.isAddTotpLoginItemFromAuthenticator
|
||||
import com.x8bit.bitwarden.data.vault.manager.model.VaultStateEvent
|
||||
import com.x8bit.bitwarden.data.vault.repository.VaultRepository
|
||||
import com.x8bit.bitwarden.ui.platform.base.BaseViewModel
|
||||
@@ -30,8 +32,11 @@ import com.x8bit.bitwarden.ui.platform.base.util.Text
|
||||
import com.x8bit.bitwarden.ui.platform.base.util.asText
|
||||
import com.x8bit.bitwarden.ui.platform.feature.settings.appearance.model.AppTheme
|
||||
import com.x8bit.bitwarden.ui.platform.manager.intent.IntentManager
|
||||
import com.x8bit.bitwarden.ui.platform.util.isAccountSecurityShortcut
|
||||
import com.x8bit.bitwarden.ui.platform.util.isMyVaultShortcut
|
||||
import com.x8bit.bitwarden.ui.platform.util.isPasswordGeneratorShortcut
|
||||
import com.x8bit.bitwarden.ui.vault.model.TotpData
|
||||
import com.x8bit.bitwarden.ui.vault.util.getTotpDataOrNull
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
@@ -56,12 +61,13 @@ private const val SPECIAL_CIRCUMSTANCE_KEY = "special-circumstance"
|
||||
class MainViewModel @Inject constructor(
|
||||
accessibilitySelectionManager: AccessibilitySelectionManager,
|
||||
autofillSelectionManager: AutofillSelectionManager,
|
||||
private val addTotpItemFromAuthenticatorManager: AddTotpItemFromAuthenticatorManager,
|
||||
private val specialCircumstanceManager: SpecialCircumstanceManager,
|
||||
private val garbageCollectionManager: GarbageCollectionManager,
|
||||
private val fido2CredentialManager: Fido2CredentialManager,
|
||||
private val intentManager: IntentManager,
|
||||
settingsRepository: SettingsRepository,
|
||||
vaultRepository: VaultRepository,
|
||||
private val vaultRepository: VaultRepository,
|
||||
private val authRepository: AuthRepository,
|
||||
private val environmentRepository: EnvironmentRepository,
|
||||
private val savedStateHandle: SavedStateHandle,
|
||||
@@ -223,7 +229,7 @@ class MainViewModel @Inject constructor(
|
||||
)
|
||||
}
|
||||
|
||||
@Suppress("LongMethod")
|
||||
@Suppress("LongMethod", "CyclomaticComplexMethod")
|
||||
private fun handleIntent(
|
||||
intent: Intent,
|
||||
isFirstIntent: Boolean,
|
||||
@@ -232,14 +238,39 @@ class MainViewModel @Inject constructor(
|
||||
val autofillSaveItem = intent.getAutofillSaveItemOrNull()
|
||||
val autofillSelectionData = intent.getAutofillSelectionDataOrNull()
|
||||
val shareData = intentManager.getShareDataFromIntent(intent)
|
||||
val totpData: TotpData? =
|
||||
// First grab TOTP URI directly from the intent data:
|
||||
intent.getTotpDataOrNull()
|
||||
?: run {
|
||||
// Then check to see if the intent is coming from the Authenticator app:
|
||||
if (intent.isAddTotpLoginItemFromAuthenticator()) {
|
||||
addTotpItemFromAuthenticatorManager.pendingAddTotpLoginItemData.also {
|
||||
// Clear pending add TOTP data so it is only handled once:
|
||||
addTotpItemFromAuthenticatorManager.pendingAddTotpLoginItemData = null
|
||||
}
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
val hasGeneratorShortcut = intent.isPasswordGeneratorShortcut
|
||||
val hasVaultShortcut = intent.isMyVaultShortcut
|
||||
val hasAccountSecurityShortcut = intent.isAccountSecurityShortcut
|
||||
val fido2CredentialRequestData = intent.getFido2CredentialRequestOrNull()
|
||||
val completeRegistrationData = intent.getCompleteRegistrationDataIntentOrNull()
|
||||
val fido2CredentialAssertionRequest = intent.getFido2AssertionRequestOrNull()
|
||||
val fido2GetCredentialsRequest = intent.getFido2GetCredentialsRequestOrNull()
|
||||
when {
|
||||
passwordlessRequestData != null -> {
|
||||
authRepository.activeUserId?.let {
|
||||
if (it != passwordlessRequestData.userId &&
|
||||
!vaultRepository.isVaultUnlocked(it)
|
||||
) {
|
||||
// We only switch the account here if the current user's vault is not
|
||||
// unlocked, otherwise prompt the user to allow us to change the account
|
||||
// in the LoginApprovalScreen
|
||||
authRepository.switchAccount(passwordlessRequestData.userId)
|
||||
}
|
||||
}
|
||||
specialCircumstanceManager.specialCircumstance =
|
||||
SpecialCircumstance.PasswordlessRequest(
|
||||
passwordlessRequestData = passwordlessRequestData,
|
||||
@@ -270,6 +301,11 @@ class MainViewModel @Inject constructor(
|
||||
)
|
||||
}
|
||||
|
||||
totpData != null -> {
|
||||
specialCircumstanceManager.specialCircumstance =
|
||||
SpecialCircumstance.AddTotpLoginItem(data = totpData)
|
||||
}
|
||||
|
||||
shareData != null -> {
|
||||
specialCircumstanceManager.specialCircumstance =
|
||||
SpecialCircumstance.ShareNewSend(
|
||||
@@ -320,6 +356,11 @@ class MainViewModel @Inject constructor(
|
||||
hasVaultShortcut -> {
|
||||
specialCircumstanceManager.specialCircumstance = SpecialCircumstance.VaultShortcut
|
||||
}
|
||||
|
||||
hasAccountSecurityShortcut -> {
|
||||
specialCircumstanceManager.specialCircumstance =
|
||||
SpecialCircumstance.AccountSecurityShortcut
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -306,4 +306,19 @@ interface AuthDiskSource {
|
||||
* if any exists.
|
||||
*/
|
||||
fun getOnboardingStatusFlow(userId: String): Flow<OnboardingStatus?>
|
||||
|
||||
/**
|
||||
* Gets the show import logins flag for the given [userId].
|
||||
*/
|
||||
fun getShowImportLogins(userId: String): Boolean?
|
||||
|
||||
/**
|
||||
* Stores the show import logins flag for the given [userId].
|
||||
*/
|
||||
fun storeShowImportLogins(userId: String, showImportLogins: Boolean?)
|
||||
|
||||
/**
|
||||
* Emits updates that track [getShowImportLogins]. This will replay the last known value,
|
||||
*/
|
||||
fun getShowImportLoginsFlow(userId: String): Flow<Boolean?>
|
||||
}
|
||||
|
||||
@@ -45,6 +45,7 @@ private const val SHOULD_TRUST_DEVICE_KEY = "shouldTrustDevice"
|
||||
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"
|
||||
|
||||
/**
|
||||
* Primary implementation of [AuthDiskSource].
|
||||
@@ -72,6 +73,7 @@ class AuthDiskSourceImpl(
|
||||
mutableMapOf<String, MutableSharedFlow<AccountTokensJson?>>()
|
||||
private val mutableOnboardingStatusFlowMap =
|
||||
mutableMapOf<String, MutableSharedFlow<OnboardingStatus?>>()
|
||||
private val mutableShowImportLoginsFlowMap = mutableMapOf<String, MutableSharedFlow<Boolean?>>()
|
||||
private val mutableUserStateFlow = bufferedMutableSharedFlow<UserStateJson?>(replay = 1)
|
||||
|
||||
override var userState: UserStateJson?
|
||||
@@ -143,9 +145,11 @@ class AuthDiskSourceImpl(
|
||||
storeShouldUseKeyConnector(userId = userId, shouldUseKeyConnector = null)
|
||||
storeIsTdeLoginComplete(userId = userId, isTdeLoginComplete = null)
|
||||
storeAuthenticatorSyncUnlockKey(userId = userId, authenticatorSyncUnlockKey = null)
|
||||
storeShowImportLogins(userId = userId, showImportLogins = null)
|
||||
|
||||
// Do not remove the DeviceKey or PendingAuthRequest on logout, these are persisted
|
||||
// indefinitely unless the TDE flow explicitly removes them.
|
||||
// Do not remove OnboardingStatus we want to keep track of this even after logout.
|
||||
}
|
||||
|
||||
override fun getAuthenticatorSyncUnlockKey(userId: String): String? =
|
||||
@@ -437,6 +441,22 @@ class AuthDiskSourceImpl(
|
||||
.onSubscription { emit(getOnboardingStatus(userId = userId)) }
|
||||
}
|
||||
|
||||
override fun getShowImportLogins(userId: String): Boolean? {
|
||||
return getBoolean(SHOW_IMPORT_LOGINS_KEY.appendIdentifier(userId))
|
||||
}
|
||||
|
||||
override fun storeShowImportLogins(userId: String, showImportLogins: Boolean?) {
|
||||
putBoolean(
|
||||
key = SHOW_IMPORT_LOGINS_KEY.appendIdentifier(userId),
|
||||
value = showImportLogins,
|
||||
)
|
||||
getMutableShowImportLoginsFlow(userId = userId).tryEmit(showImportLogins)
|
||||
}
|
||||
|
||||
override fun getShowImportLoginsFlow(userId: String): Flow<Boolean?> =
|
||||
getMutableShowImportLoginsFlow(userId)
|
||||
.onSubscription { emit(getShowImportLogins(userId)) }
|
||||
|
||||
private fun generateAndStoreUniqueAppId(): String =
|
||||
UUID
|
||||
.randomUUID()
|
||||
@@ -480,6 +500,12 @@ class AuthDiskSourceImpl(
|
||||
bufferedMutableSharedFlow(replay = 1)
|
||||
}
|
||||
|
||||
private fun getMutableShowImportLoginsFlow(
|
||||
userId: String,
|
||||
): MutableSharedFlow<Boolean?> = mutableShowImportLoginsFlowMap.getOrPut(userId) {
|
||||
bufferedMutableSharedFlow(replay = 1)
|
||||
}
|
||||
|
||||
private fun migrateAccountTokens() {
|
||||
userState
|
||||
?.accounts
|
||||
|
||||
@@ -96,9 +96,17 @@ sealed class GetTokenResponseJson {
|
||||
@Serializable
|
||||
data class Invalid(
|
||||
@SerialName("ErrorModel")
|
||||
val errorModel: ErrorModel,
|
||||
val errorModel: ErrorModel?,
|
||||
@SerialName("errorModel")
|
||||
val legacyErrorModel: LegacyErrorModel?,
|
||||
) : GetTokenResponseJson() {
|
||||
|
||||
/**
|
||||
* The error message returned from the server, or null.
|
||||
*/
|
||||
val errorMessage: String?
|
||||
get() = errorModel?.errorMessage ?: legacyErrorModel?.errorMessage
|
||||
|
||||
/**
|
||||
* The error body of an invalid request containing a message.
|
||||
*/
|
||||
@@ -107,6 +115,18 @@ sealed class GetTokenResponseJson {
|
||||
@SerialName("Message")
|
||||
val errorMessage: String,
|
||||
)
|
||||
|
||||
/**
|
||||
* The legacy error body of an invalid request containing a message.
|
||||
*
|
||||
* This model is used to support older versions of the error response model that used
|
||||
* lower-case keys.
|
||||
*/
|
||||
@Serializable
|
||||
data class LegacyErrorModel(
|
||||
@SerialName("message")
|
||||
val errorMessage: String,
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
package com.x8bit.bitwarden.data.auth.manager
|
||||
|
||||
import com.x8bit.bitwarden.ui.vault.model.TotpData
|
||||
|
||||
/**
|
||||
* Manager for keeping track of requests from the Bitwarden Authenticator app to add a TOTP
|
||||
* item.
|
||||
*/
|
||||
interface AddTotpItemFromAuthenticatorManager {
|
||||
|
||||
/**
|
||||
* Current pending [TotpData] to be added from the Authenticator app.
|
||||
*/
|
||||
var pendingAddTotpLoginItemData: TotpData?
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package com.x8bit.bitwarden.data.auth.manager
|
||||
|
||||
import com.x8bit.bitwarden.ui.vault.model.TotpData
|
||||
|
||||
/**
|
||||
* Default in memory implementation for [AddTotpItemFromAuthenticatorManager].
|
||||
*/
|
||||
class AddTotpItemFromAuthenticatorManagerImpl : AddTotpItemFromAuthenticatorManager {
|
||||
|
||||
override var pendingAddTotpLoginItemData: TotpData? = null
|
||||
}
|
||||
@@ -7,6 +7,8 @@ import com.x8bit.bitwarden.data.auth.datasource.network.service.AuthRequestsServ
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.service.DevicesService
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.service.NewAuthRequestService
|
||||
import com.x8bit.bitwarden.data.auth.datasource.sdk.AuthSdkSource
|
||||
import com.x8bit.bitwarden.data.auth.manager.AddTotpItemFromAuthenticatorManager
|
||||
import com.x8bit.bitwarden.data.auth.manager.AddTotpItemFromAuthenticatorManagerImpl
|
||||
import com.x8bit.bitwarden.data.auth.manager.AuthRequestManager
|
||||
import com.x8bit.bitwarden.data.auth.manager.AuthRequestManagerImpl
|
||||
import com.x8bit.bitwarden.data.auth.manager.AuthRequestNotificationManager
|
||||
@@ -124,4 +126,9 @@ object AuthManagerModule {
|
||||
vaultSdkSource = vaultSdkSource,
|
||||
dispatcherManager = dispatcherManager,
|
||||
)
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun providesAddTotpItemFromAuthenticatorManager(): AddTotpItemFromAuthenticatorManager =
|
||||
AddTotpItemFromAuthenticatorManagerImpl()
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ val AuthRequestType.isSso: Boolean
|
||||
AuthRequestType.OTHER_DEVICE -> false
|
||||
AuthRequestType.SSO_OTHER_DEVICE,
|
||||
AuthRequestType.SSO_ADMIN_APPROVAL,
|
||||
-> true
|
||||
-> true
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -21,7 +21,7 @@ fun AuthRequestType.toAuthRequestTypeJson(): AuthRequestTypeJson =
|
||||
when (this) {
|
||||
AuthRequestType.OTHER_DEVICE,
|
||||
AuthRequestType.SSO_OTHER_DEVICE,
|
||||
-> AuthRequestTypeJson.LOGIN_WITH_DEVICE
|
||||
-> AuthRequestTypeJson.LOGIN_WITH_DEVICE
|
||||
|
||||
AuthRequestType.SSO_ADMIN_APPROVAL -> AuthRequestTypeJson.ADMIN_APPROVAL
|
||||
}
|
||||
|
||||
@@ -212,6 +212,7 @@ interface AuthRepository : AuthenticatorProvider, AuthRequestManager {
|
||||
password: String?,
|
||||
twoFactorData: TwoFactorDataModel,
|
||||
captchaToken: String?,
|
||||
orgIdentifier: String?,
|
||||
): LoginResult
|
||||
|
||||
/**
|
||||
@@ -392,4 +393,9 @@ interface AuthRepository : AuthenticatorProvider, AuthRequestManager {
|
||||
* Update the value of the onboarding status for the user.
|
||||
*/
|
||||
fun setOnboardingStatus(userId: String, status: OnboardingStatus?)
|
||||
|
||||
/**
|
||||
* Update the value of the showImportLogins status for the user.
|
||||
*/
|
||||
fun setShowImportLogins(showImportLogins: Boolean)
|
||||
}
|
||||
|
||||
@@ -92,15 +92,20 @@ import com.x8bit.bitwarden.data.auth.repository.util.userSwitchingChangesFlow
|
||||
import com.x8bit.bitwarden.data.auth.util.KdfParamsConstants.DEFAULT_PBKDF2_ITERATIONS
|
||||
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.manager.FeatureFlagManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.FirstTimeActionManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.LogsManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.PolicyManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.PushManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.dispatcher.DispatcherManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.model.FirstTimeState
|
||||
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.util.bufferedMutableSharedFlow
|
||||
import com.x8bit.bitwarden.data.platform.repository.util.toEnvironmentUrls
|
||||
import com.x8bit.bitwarden.data.platform.util.asFailure
|
||||
import com.x8bit.bitwarden.data.platform.util.asSuccess
|
||||
import com.x8bit.bitwarden.data.platform.util.flatMap
|
||||
@@ -151,6 +156,7 @@ class AuthRepositoryImpl(
|
||||
private val authSdkSource: AuthSdkSource,
|
||||
private val vaultSdkSource: VaultSdkSource,
|
||||
private val authDiskSource: AuthDiskSource,
|
||||
private val configDiskSource: ConfigDiskSource,
|
||||
private val environmentRepository: EnvironmentRepository,
|
||||
private val settingsRepository: SettingsRepository,
|
||||
private val vaultRepository: VaultRepository,
|
||||
@@ -160,6 +166,8 @@ class AuthRepositoryImpl(
|
||||
private val userLogoutManager: UserLogoutManager,
|
||||
private val policyManager: PolicyManager,
|
||||
private val featureFlagManager: FeatureFlagManager,
|
||||
firstTimeActionManager: FirstTimeActionManager,
|
||||
logsManager: LogsManager,
|
||||
pushManager: PushManager,
|
||||
dispatcherManager: DispatcherManager,
|
||||
) : AuthRepository,
|
||||
@@ -254,6 +262,7 @@ class AuthRepositoryImpl(
|
||||
authDiskSource.userOrganizationsListFlow,
|
||||
authDiskSource.userKeyConnectorStateFlow,
|
||||
authDiskSource.onboardingStatusChangesFlow,
|
||||
firstTimeActionManager.firstTimeStateFlow,
|
||||
vaultRepository.vaultUnlockDataStateFlow,
|
||||
mutableHasPendingAccountAdditionStateFlow,
|
||||
// Ignore the data in the merge, but trigger an update when they emit.
|
||||
@@ -267,8 +276,9 @@ class AuthRepositoryImpl(
|
||||
val userOrganizationsList = array[2] as List<UserOrganizations>
|
||||
val userIsUsingKeyConnectorList = array[3] as List<UserKeyConnectorState>
|
||||
val onboardingStatus = array[4] as OnboardingStatus?
|
||||
val vaultState = array[5] as List<VaultUnlockData>
|
||||
val hasPendingAccountAddition = array[6] as Boolean
|
||||
val firstTimeState = array[5] as FirstTimeState
|
||||
val vaultState = array[6] as List<VaultUnlockData>
|
||||
val hasPendingAccountAddition = array[7] as Boolean
|
||||
userStateJson?.toUserState(
|
||||
vaultState = vaultState,
|
||||
userAccountTokens = userAccountTokens,
|
||||
@@ -279,6 +289,7 @@ class AuthRepositoryImpl(
|
||||
isBiometricsEnabledProvider = ::isBiometricsEnabled,
|
||||
vaultUnlockTypeProvider = ::getVaultUnlockType,
|
||||
isDeviceTrustedProvider = ::isDeviceTrusted,
|
||||
firstTimeState = firstTimeState,
|
||||
)
|
||||
}
|
||||
.filterNot { mutableHasPendingAccountDeletionStateFlow.value }
|
||||
@@ -298,6 +309,7 @@ class AuthRepositoryImpl(
|
||||
isBiometricsEnabledProvider = ::isBiometricsEnabled,
|
||||
vaultUnlockTypeProvider = ::getVaultUnlockType,
|
||||
isDeviceTrustedProvider = ::isDeviceTrusted,
|
||||
firstTimeState = firstTimeActionManager.currentOrDefaultUserFirstTimeState,
|
||||
),
|
||||
)
|
||||
|
||||
@@ -354,6 +366,24 @@ class AuthRepositoryImpl(
|
||||
featureFlagManager.getFeatureFlag(FlagKey.OnboardingCarousel)
|
||||
|
||||
init {
|
||||
combine(
|
||||
mutableHasPendingAccountAdditionStateFlow,
|
||||
authDiskSource.userStateFlow,
|
||||
environmentRepository.environmentStateFlow,
|
||||
) { hasPendingAddition, userState, environment ->
|
||||
logsManager.setUserData(
|
||||
userId = userState?.activeUserId.takeUnless { hasPendingAddition },
|
||||
environmentType = userState
|
||||
?.activeAccount
|
||||
?.settings
|
||||
?.environmentUrlData
|
||||
?.toEnvironmentUrls()
|
||||
?.type
|
||||
.takeUnless { hasPendingAddition }
|
||||
?: environment.type,
|
||||
)
|
||||
}
|
||||
.launchIn(unconfinedScope)
|
||||
pushManager
|
||||
.syncOrgKeysFlow
|
||||
.onEach {
|
||||
@@ -628,6 +658,7 @@ class AuthRepositoryImpl(
|
||||
password: String?,
|
||||
twoFactorData: TwoFactorDataModel,
|
||||
captchaToken: String?,
|
||||
orgIdentifier: String?,
|
||||
): LoginResult = identityTokenAuthModel
|
||||
?.let {
|
||||
loginCommon(
|
||||
@@ -637,6 +668,7 @@ class AuthRepositoryImpl(
|
||||
twoFactorData = twoFactorData,
|
||||
captchaToken = captchaToken ?: twoFactorResponse?.captchaToken,
|
||||
deviceData = twoFactorDeviceData,
|
||||
orgIdentifier = orgIdentifier,
|
||||
)
|
||||
}
|
||||
?: LoginResult.Error(errorMessage = null)
|
||||
@@ -998,7 +1030,7 @@ class AuthRepositoryImpl(
|
||||
ForcePasswordResetReason.ADMIN_FORCE_PASSWORD_RESET,
|
||||
ForcePasswordResetReason.WEAK_MASTER_PASSWORD_ON_LOGIN,
|
||||
null,
|
||||
-> {
|
||||
-> {
|
||||
authSdkSource
|
||||
.makeRegisterKeys(
|
||||
email = activeAccount.profile.email,
|
||||
@@ -1048,7 +1080,7 @@ class AuthRepositoryImpl(
|
||||
is VaultUnlockResult.AuthenticationError,
|
||||
VaultUnlockResult.InvalidStateError,
|
||||
VaultUnlockResult.GenericError,
|
||||
-> {
|
||||
-> {
|
||||
IllegalStateException("Failed to unlock vault").asFailure()
|
||||
}
|
||||
}
|
||||
@@ -1295,6 +1327,11 @@ class AuthRepositoryImpl(
|
||||
authDiskSource.storeOnboardingStatus(userId = userId, onboardingStatus = status)
|
||||
}
|
||||
|
||||
override fun setShowImportLogins(showImportLogins: Boolean) {
|
||||
val userId: String = activeUserId ?: return
|
||||
authDiskSource.storeShowImportLogins(userId = userId, showImportLogins = showImportLogins)
|
||||
}
|
||||
|
||||
@Suppress("CyclomaticComplexMethod")
|
||||
private suspend fun validatePasswordAgainstPolicy(
|
||||
password: String,
|
||||
@@ -1459,7 +1496,12 @@ class AuthRepositoryImpl(
|
||||
captchaToken = captchaToken,
|
||||
)
|
||||
.fold(
|
||||
onFailure = { LoginResult.Error(errorMessage = null) },
|
||||
onFailure = {
|
||||
when (configDiskSource.serverConfig?.isOfficialBitwardenServer) {
|
||||
false -> LoginResult.UnofficialServerError
|
||||
else -> LoginResult.Error(errorMessage = null)
|
||||
}
|
||||
},
|
||||
onSuccess = { loginResponse ->
|
||||
when (loginResponse) {
|
||||
is GetTokenResponseJson.CaptchaRequired -> LoginResult.CaptchaRequired(
|
||||
@@ -1482,7 +1524,7 @@ class AuthRepositoryImpl(
|
||||
)
|
||||
|
||||
is GetTokenResponseJson.Invalid -> LoginResult.Error(
|
||||
errorMessage = loginResponse.errorModel.errorMessage,
|
||||
errorMessage = loginResponse.errorMessage,
|
||||
)
|
||||
}
|
||||
},
|
||||
|
||||
@@ -13,7 +13,10 @@ import com.x8bit.bitwarden.data.auth.manager.TrustedDeviceManager
|
||||
import com.x8bit.bitwarden.data.auth.manager.UserLogoutManager
|
||||
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
|
||||
import com.x8bit.bitwarden.data.auth.repository.AuthRepositoryImpl
|
||||
import com.x8bit.bitwarden.data.platform.datasource.disk.ConfigDiskSource
|
||||
import com.x8bit.bitwarden.data.platform.manager.FeatureFlagManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.FirstTimeActionManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.LogsManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.PolicyManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.PushManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.dispatcher.DispatcherManager
|
||||
@@ -45,6 +48,7 @@ object AuthRepositoryModule {
|
||||
authSdkSource: AuthSdkSource,
|
||||
vaultSdkSource: VaultSdkSource,
|
||||
authDiskSource: AuthDiskSource,
|
||||
configDiskSource: ConfigDiskSource,
|
||||
dispatcherManager: DispatcherManager,
|
||||
environmentRepository: EnvironmentRepository,
|
||||
settingsRepository: SettingsRepository,
|
||||
@@ -56,6 +60,8 @@ object AuthRepositoryModule {
|
||||
pushManager: PushManager,
|
||||
policyManager: PolicyManager,
|
||||
featureFlagManager: FeatureFlagManager,
|
||||
firstTimeActionManager: FirstTimeActionManager,
|
||||
logsManager: LogsManager,
|
||||
): AuthRepository = AuthRepositoryImpl(
|
||||
accountsService = accountsService,
|
||||
devicesService = devicesService,
|
||||
@@ -64,6 +70,7 @@ object AuthRepositoryModule {
|
||||
authSdkSource = authSdkSource,
|
||||
vaultSdkSource = vaultSdkSource,
|
||||
authDiskSource = authDiskSource,
|
||||
configDiskSource = configDiskSource,
|
||||
haveIBeenPwnedService = haveIBeenPwnedService,
|
||||
dispatcherManager = dispatcherManager,
|
||||
environmentRepository = environmentRepository,
|
||||
@@ -76,5 +83,7 @@ object AuthRepositoryModule {
|
||||
pushManager = pushManager,
|
||||
policyManager = policyManager,
|
||||
featureFlagManager = featureFlagManager,
|
||||
firstTimeActionManager = firstTimeActionManager,
|
||||
logsManager = logsManager,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -23,4 +23,9 @@ sealed class LoginResult {
|
||||
* There was an error logging in.
|
||||
*/
|
||||
data class Error(val errorMessage: String?) : LoginResult()
|
||||
|
||||
/**
|
||||
* There was an error while logging into an unofficial Bitwarden server.
|
||||
*/
|
||||
data object UnofficialServerError : LoginResult()
|
||||
}
|
||||
|
||||
@@ -11,5 +11,5 @@ fun VaultUnlockError.toLoginErrorResult(): LoginResult.Error = when (this) {
|
||||
is VaultUnlockResult.AuthenticationError -> LoginResult.Error(this.message)
|
||||
VaultUnlockResult.GenericError,
|
||||
VaultUnlockResult.InvalidStateError,
|
||||
-> LoginResult.Error(errorMessage = null)
|
||||
-> LoginResult.Error(errorMessage = null)
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
package com.x8bit.bitwarden.data.auth.repository.model
|
||||
|
||||
import com.x8bit.bitwarden.data.vault.datasource.network.model.SyncResponseJson
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ package com.x8bit.bitwarden.data.auth.repository.model
|
||||
|
||||
import com.x8bit.bitwarden.data.auth.datasource.disk.model.OnboardingStatus
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.UserState.Account
|
||||
import com.x8bit.bitwarden.data.platform.manager.model.FirstTimeState
|
||||
import com.x8bit.bitwarden.data.platform.repository.model.Environment
|
||||
|
||||
/**
|
||||
@@ -29,6 +30,9 @@ data class UserState(
|
||||
val activeAccount: Account
|
||||
get() = accounts.first { it.userId == activeUserId }
|
||||
|
||||
val activeUserFirstTimeState: FirstTimeState
|
||||
get() = activeAccount.firstTimeState
|
||||
|
||||
/**
|
||||
* Basic account information about a given user.
|
||||
*
|
||||
@@ -71,6 +75,7 @@ data class UserState(
|
||||
val vaultUnlockType: VaultUnlockType = VaultUnlockType.MASTER_PASSWORD,
|
||||
val isUsingKeyConnector: Boolean,
|
||||
val onboardingStatus: OnboardingStatus,
|
||||
val firstTimeState: FirstTimeState,
|
||||
) {
|
||||
/**
|
||||
* Indicates that the user does or does not have a means to manually unlock the vault.
|
||||
|
||||
@@ -176,13 +176,16 @@ val AuthDiskSource.activeUserIdChangesFlow: Flow<String?>
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
val AuthDiskSource.onboardingStatusChangesFlow: Flow<OnboardingStatus?>
|
||||
get() = activeUserIdChangesFlow
|
||||
.flatMapLatest { activeUserId ->
|
||||
activeUserId
|
||||
?.let { this.getOnboardingStatusFlow(userId = it) }
|
||||
?: flowOf(null)
|
||||
}
|
||||
.distinctUntilChanged()
|
||||
.flatMapLatest { activeUserId ->
|
||||
activeUserId
|
||||
?.let { this.getOnboardingStatusFlow(userId = it) }
|
||||
?: flowOf(null)
|
||||
}
|
||||
.distinctUntilChanged()
|
||||
|
||||
/**
|
||||
* Returns the current [OnboardingStatus] of the active user.
|
||||
*/
|
||||
val AuthDiskSource.currentOnboardingStatus: OnboardingStatus?
|
||||
get() = this
|
||||
.userState
|
||||
|
||||
@@ -8,6 +8,7 @@ import com.x8bit.bitwarden.data.auth.repository.model.UserKeyConnectorState
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.UserOrganizations
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.UserState
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.VaultUnlockType
|
||||
import com.x8bit.bitwarden.data.platform.manager.model.FirstTimeState
|
||||
import com.x8bit.bitwarden.data.platform.repository.util.toEnvironmentUrlsOrDefault
|
||||
import com.x8bit.bitwarden.data.vault.datasource.network.model.OrganizationType
|
||||
import com.x8bit.bitwarden.data.vault.datasource.network.model.SyncResponseJson
|
||||
@@ -111,6 +112,7 @@ fun UserStateJson.toUserState(
|
||||
userIsUsingKeyConnectorList: List<UserKeyConnectorState>,
|
||||
hasPendingAccountAddition: Boolean,
|
||||
onboardingStatus: OnboardingStatus?,
|
||||
firstTimeState: FirstTimeState,
|
||||
isBiometricsEnabledProvider: (userId: String) -> Boolean,
|
||||
vaultUnlockTypeProvider: (userId: String) -> VaultUnlockType,
|
||||
isDeviceTrustedProvider: (userId: String) -> Boolean,
|
||||
@@ -137,9 +139,6 @@ fun UserStateJson.toUserState(
|
||||
it.role == OrganizationType.ADMIN ||
|
||||
it.shouldManageResetPassword
|
||||
}
|
||||
val needsMasterPassword = decryptionOptions?.hasMasterPassword == false &&
|
||||
hasManageResetPasswordPermission &&
|
||||
keyConnectorOptions == null
|
||||
val trustedDevice = trustedDeviceOptions?.let {
|
||||
UserState.TrustedDevice(
|
||||
isDeviceTrusted = isDeviceTrustedProvider(userId),
|
||||
@@ -148,7 +147,14 @@ fun UserStateJson.toUserState(
|
||||
hasResetPasswordPermission = it.hasManageResetPasswordPermission,
|
||||
)
|
||||
}
|
||||
|
||||
// If a user does not have a Master Password we want to check if they have another
|
||||
// method for unlocking the vault. In the case of a TDE user we check if they
|
||||
// have the reset password permission via their organization(S). If the user does
|
||||
// not belong to a TDE or we check to see if they user key connector.
|
||||
val tdeUserNeedsMasterPassword =
|
||||
hasManageResetPasswordPermission.takeIf { trustedDevice != null }
|
||||
val needsMasterPassword = decryptionOptions?.hasMasterPassword == false &&
|
||||
(tdeUserNeedsMasterPassword ?: (keyConnectorOptions == null))
|
||||
UserState.Account(
|
||||
userId = userId,
|
||||
name = profile.name,
|
||||
@@ -176,6 +182,7 @@ fun UserStateJson.toUserState(
|
||||
// If the user exists with no onboarding status we can assume they have been
|
||||
// using the app prior to the release of the onboarding flow.
|
||||
onboardingStatus = onboardingStatus ?: OnboardingStatus.COMPLETE,
|
||||
firstTimeState = firstTimeState,
|
||||
)
|
||||
},
|
||||
hasPendingAccountAddition = hasPendingAccountAddition,
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
package com.x8bit.bitwarden.data.autofill.accessibility.manager
|
||||
|
||||
import android.net.Uri
|
||||
import android.util.Log
|
||||
import android.view.accessibility.AccessibilityNodeInfo
|
||||
import com.x8bit.bitwarden.BuildConfig
|
||||
import com.x8bit.bitwarden.data.autofill.accessibility.util.getKnownUsernameFieldNull
|
||||
import com.x8bit.bitwarden.data.autofill.accessibility.util.isUsername
|
||||
import timber.log.Timber
|
||||
|
||||
private const val MAX_NODE_COUNT: Int = 100
|
||||
|
||||
@@ -90,7 +89,6 @@ class AccessibilityNodeInfoManagerImpl : AccessibilityNodeInfoManager {
|
||||
?.let { allNodes.getOrNull(index = allNodes.indexOf(element = it) - 1) }
|
||||
|
||||
private fun log(message: String) {
|
||||
if (!BuildConfig.DEBUG) return
|
||||
Log.i("AccessibilityNodeInfoManager", message)
|
||||
Timber.i(message)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,7 +21,6 @@ import com.x8bit.bitwarden.data.autofill.processor.AutofillProcessor
|
||||
import com.x8bit.bitwarden.data.autofill.processor.AutofillProcessorImpl
|
||||
import com.x8bit.bitwarden.data.autofill.provider.AutofillCipherProvider
|
||||
import com.x8bit.bitwarden.data.autofill.provider.AutofillCipherProviderImpl
|
||||
import com.x8bit.bitwarden.data.platform.manager.CrashLogsManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.PolicyManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.ciphermatching.CipherMatchingManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.clipboard.BitwardenClipboardManager
|
||||
@@ -121,7 +120,6 @@ object AutofillModule {
|
||||
policyManager: PolicyManager,
|
||||
saveInfoBuilder: SaveInfoBuilder,
|
||||
settingsRepository: SettingsRepository,
|
||||
crashLogsManager: CrashLogsManager,
|
||||
): AutofillProcessor =
|
||||
AutofillProcessorImpl(
|
||||
dispatcherManager = dispatcherManager,
|
||||
@@ -131,7 +129,6 @@ object AutofillModule {
|
||||
policyManager = policyManager,
|
||||
saveInfoBuilder = saveInfoBuilder,
|
||||
settingsRepository = settingsRepository,
|
||||
crashLogsManager = crashLogsManager,
|
||||
)
|
||||
|
||||
@Singleton
|
||||
|
||||
@@ -2,6 +2,8 @@ package com.x8bit.bitwarden.data.autofill.fido2.manager
|
||||
|
||||
import androidx.credentials.provider.CallingAppInfo
|
||||
import com.bitwarden.fido.ClientData
|
||||
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.datasource.network.model.DigitalAssetLinkResponseJson
|
||||
@@ -24,11 +26,13 @@ import com.x8bit.bitwarden.data.vault.datasource.sdk.model.AuthenticateFido2Cred
|
||||
import com.x8bit.bitwarden.data.vault.datasource.sdk.model.RegisterFido2CredentialRequest
|
||||
import com.x8bit.bitwarden.data.vault.datasource.sdk.util.toAndroidAttestationResponse
|
||||
import com.x8bit.bitwarden.data.vault.datasource.sdk.util.toAndroidFido2PublicKeyCredential
|
||||
import com.x8bit.bitwarden.ui.platform.base.util.toHostOrPathOrNull
|
||||
import kotlinx.serialization.SerializationException
|
||||
import kotlinx.serialization.encodeToString
|
||||
import kotlinx.serialization.json.Json
|
||||
|
||||
private const val ALLOW_LIST_FILE_NAME = "fido2_privileged_allow_list.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].
|
||||
@@ -65,11 +69,23 @@ class Fido2CredentialManagerImpl(
|
||||
.packageName,
|
||||
)
|
||||
}
|
||||
val origin = fido2CredentialRequest
|
||||
val assetLinkUrl = fido2CredentialRequest
|
||||
.origin
|
||||
?: getOriginUrlFromAttestationOptionsOrNull(fido2CredentialRequest.requestJson)
|
||||
?: return Fido2RegisterCredentialResult.Error
|
||||
|
||||
val origin = Origin.Android(
|
||||
UnverifiedAssetLink(
|
||||
packageName = fido2CredentialRequest.packageName,
|
||||
sha256CertFingerprint = fido2CredentialRequest
|
||||
.callingAppInfo
|
||||
.getSignatureFingerprintAsHexString()
|
||||
?: return Fido2RegisterCredentialResult.Error,
|
||||
host = assetLinkUrl.toHostOrPathOrNull()
|
||||
?: return Fido2RegisterCredentialResult.Error,
|
||||
assetLinkUrl = assetLinkUrl,
|
||||
),
|
||||
)
|
||||
return vaultSdkSource
|
||||
.registerFido2Credential(
|
||||
request = RegisterFido2CredentialRequest(
|
||||
@@ -157,7 +173,16 @@ class Fido2CredentialManagerImpl(
|
||||
.authenticateFido2Credential(
|
||||
request = AuthenticateFido2CredentialRequest(
|
||||
userId = userId,
|
||||
origin = origin,
|
||||
origin = Origin.Android(
|
||||
UnverifiedAssetLink(
|
||||
callingAppInfo.packageName,
|
||||
callingAppInfo.getSignatureFingerprintAsHexString()
|
||||
?: return Fido2CredentialAssertionResult.Error,
|
||||
origin.toHostOrPathOrNull()
|
||||
?: return Fido2CredentialAssertionResult.Error,
|
||||
origin,
|
||||
),
|
||||
),
|
||||
requestJson = """{"publicKey": ${request.requestJson}}""",
|
||||
clientData = clientData,
|
||||
selectedCipherView = selectedCipherView,
|
||||
@@ -179,7 +204,8 @@ class Fido2CredentialManagerImpl(
|
||||
callingAppInfo: CallingAppInfo,
|
||||
relyingPartyId: String,
|
||||
): Fido2ValidateOriginResult {
|
||||
return digitalAssetLinkService.getDigitalAssetLinkForRp(relyingParty = relyingPartyId)
|
||||
return digitalAssetLinkService
|
||||
.getDigitalAssetLinkForRp(relyingParty = relyingPartyId)
|
||||
.onFailure {
|
||||
return Fido2ValidateOriginResult.Error.AssetLinkNotFound
|
||||
}
|
||||
@@ -191,7 +217,8 @@ class Fido2CredentialManagerImpl(
|
||||
?: return Fido2ValidateOriginResult.Error.ApplicationNotFound
|
||||
}
|
||||
.map { matchingStatements ->
|
||||
callingAppInfo.getSignatureFingerprintAsHexString()
|
||||
callingAppInfo
|
||||
.getSignatureFingerprintAsHexString()
|
||||
?.let { certificateFingerprint ->
|
||||
matchingStatements
|
||||
.filterMatchingAppSignaturesOrNull(
|
||||
@@ -212,9 +239,46 @@ class Fido2CredentialManagerImpl(
|
||||
|
||||
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(ALLOW_LIST_FILE_NAME)
|
||||
.readAsset(fileName)
|
||||
.map { allowList ->
|
||||
callingAppInfo.validatePrivilegedApp(
|
||||
allowList = allowList,
|
||||
|
||||
@@ -10,6 +10,7 @@ import kotlinx.parcelize.Parcelize
|
||||
*/
|
||||
@Parcelize
|
||||
data class Fido2CredentialAssertionRequest(
|
||||
val userId: String,
|
||||
val cipherId: String?,
|
||||
val credentialId: String?,
|
||||
val requestJson: String,
|
||||
|
||||
@@ -14,6 +14,7 @@ import kotlinx.parcelize.Parcelize
|
||||
data class Fido2GetCredentialsRequest(
|
||||
val candidateQueryData: Bundle,
|
||||
val id: String,
|
||||
val userId: String,
|
||||
val requestJson: String,
|
||||
val clientDataHash: ByteArray? = null,
|
||||
val packageName: String,
|
||||
|
||||
@@ -10,11 +10,13 @@ sealed class Fido2GetCredentialsResult {
|
||||
/**
|
||||
* Indicates credentials were successfully queried.
|
||||
*
|
||||
* @param userId ID of the user whose credentials were queried.
|
||||
* @param options Original request options provided by the relying party.
|
||||
* @param credentials Collection of [Fido2CredentialAutofillView]s matching the original request
|
||||
* parameters. This may be an empty list if no matching values were found.
|
||||
*/
|
||||
data class Success(
|
||||
val userId: String,
|
||||
val options: BeginGetPublicKeyCredentialOption,
|
||||
val credentials: List<Fido2CredentialAutofillView>,
|
||||
) : Fido2GetCredentialsResult()
|
||||
|
||||
@@ -161,6 +161,7 @@ class Fido2ProviderProcessorImpl(
|
||||
title = context.getString(R.string.unlock),
|
||||
pendingIntent = intentManager.createFido2UnlockPendingIntent(
|
||||
action = UNLOCK_ACCOUNT_INTENT,
|
||||
userId = userState.activeUserId,
|
||||
requestCode = requestCode.getAndIncrement(),
|
||||
),
|
||||
)
|
||||
@@ -209,13 +210,14 @@ class Fido2ProviderProcessorImpl(
|
||||
.getPasskeyAssertionOptionsOrNull(requestJson = option.requestJson)
|
||||
?.relyingPartyId
|
||||
?: throw GetCredentialUnknownException("Invalid data.")
|
||||
buildCredentialEntries(relyingPartyId, option)
|
||||
buildCredentialEntries(userId, relyingPartyId, option)
|
||||
} else {
|
||||
throw GetCredentialUnsupportedException("Unsupported option.")
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun buildCredentialEntries(
|
||||
userId: String,
|
||||
relyingPartyId: String,
|
||||
option: BeginGetPublicKeyCredentialOption,
|
||||
): List<CredentialEntry> {
|
||||
@@ -236,12 +238,16 @@ class Fido2ProviderProcessorImpl(
|
||||
result
|
||||
.fido2CredentialAutofillViews
|
||||
.filter { it.rpId == relyingPartyId }
|
||||
.toCredentialEntries(option)
|
||||
.toCredentialEntries(
|
||||
userId = userId,
|
||||
option = option,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun List<Fido2CredentialAutofillView>.toCredentialEntries(
|
||||
userId: String,
|
||||
option: BeginGetPublicKeyCredentialOption,
|
||||
): List<CredentialEntry> =
|
||||
this
|
||||
@@ -253,6 +259,7 @@ class Fido2ProviderProcessorImpl(
|
||||
pendingIntent = intentManager
|
||||
.createFido2GetCredentialPendingIntent(
|
||||
action = GET_PASSKEY_INTENT,
|
||||
userId = userId,
|
||||
credentialId = it.credentialId.toString(),
|
||||
cipherId = it.cipherId,
|
||||
requestCode = requestCode.getAndIncrement(),
|
||||
|
||||
@@ -64,7 +64,11 @@ fun Intent.getFido2AssertionRequestOrNull(): Fido2CredentialAssertionRequest? {
|
||||
val cipherId = getStringExtra(EXTRA_KEY_CIPHER_ID)
|
||||
?: return null
|
||||
|
||||
val userId: String = getStringExtra(EXTRA_KEY_USER_ID)
|
||||
?: return null
|
||||
|
||||
return Fido2CredentialAssertionRequest(
|
||||
userId = userId,
|
||||
cipherId = cipherId,
|
||||
credentialId = credentialId,
|
||||
requestJson = option.requestJson,
|
||||
@@ -95,9 +99,13 @@ fun Intent.getFido2GetCredentialsRequestOrNull(): Fido2GetCredentialsRequest? {
|
||||
.callingAppInfo
|
||||
?: return null
|
||||
|
||||
val userId: String = getStringExtra(EXTRA_KEY_USER_ID)
|
||||
?: return null
|
||||
|
||||
return Fido2GetCredentialsRequest(
|
||||
candidateQueryData = option.candidateQueryData,
|
||||
id = option.id,
|
||||
userId = userId,
|
||||
requestJson = option.requestJson,
|
||||
clientDataHash = option.clientDataHash,
|
||||
packageName = callingAppInfo.packageName,
|
||||
|
||||
@@ -48,7 +48,7 @@ sealed class AutofillCipher {
|
||||
val number: String,
|
||||
) : AutofillCipher() {
|
||||
override val iconRes: Int
|
||||
@DrawableRes get() = R.drawable.ic_card_item
|
||||
@DrawableRes get() = R.drawable.ic_payment_card
|
||||
|
||||
override val isTotpEnabled: Boolean
|
||||
get() = false
|
||||
@@ -67,6 +67,6 @@ sealed class AutofillCipher {
|
||||
val username: String,
|
||||
) : AutofillCipher() {
|
||||
override val iconRes: Int
|
||||
@DrawableRes get() = R.drawable.ic_login_item
|
||||
@DrawableRes get() = R.drawable.ic_globe
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,7 +13,6 @@ import com.x8bit.bitwarden.data.autofill.model.AutofillRequest
|
||||
import com.x8bit.bitwarden.data.autofill.parser.AutofillParser
|
||||
import com.x8bit.bitwarden.data.autofill.util.createAutofillSavedItemIntentSender
|
||||
import com.x8bit.bitwarden.data.autofill.util.toAutofillSaveItem
|
||||
import com.x8bit.bitwarden.data.platform.manager.CrashLogsManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.PolicyManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.dispatcher.DispatcherManager
|
||||
import com.x8bit.bitwarden.data.platform.repository.SettingsRepository
|
||||
@@ -21,6 +20,7 @@ import com.x8bit.bitwarden.data.vault.datasource.network.model.PolicyTypeJson
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.launch
|
||||
import timber.log.Timber
|
||||
|
||||
/**
|
||||
* The default implementation of [AutofillProcessor]. Its purpose is to handle autofill related
|
||||
@@ -35,7 +35,6 @@ class AutofillProcessorImpl(
|
||||
private val parser: AutofillParser,
|
||||
private val saveInfoBuilder: SaveInfoBuilder,
|
||||
private val settingsRepository: SettingsRepository,
|
||||
private val crashLogsManager: CrashLogsManager,
|
||||
) : AutofillProcessor {
|
||||
|
||||
/**
|
||||
@@ -146,7 +145,7 @@ class AutofillProcessorImpl(
|
||||
} catch (e: RuntimeException) {
|
||||
// This is to catch any TransactionTooLargeExceptions that could occur here.
|
||||
// These exceptions get wrapped as a RuntimeException.
|
||||
crashLogsManager.trackNonFatalException(e)
|
||||
Timber.e(e, "Autofill Error")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -68,6 +68,12 @@ interface SettingsDiskSource {
|
||||
*/
|
||||
val hasUserLoggedInOrCreatedAccountFlow: Flow<Boolean?>
|
||||
|
||||
/**
|
||||
* The instant when the last database scheme change was applied. `null` if no scheme changes
|
||||
* have been applied yet.
|
||||
*/
|
||||
var lastDatabaseSchemeChangeInstant: Instant?
|
||||
|
||||
/**
|
||||
* Clears all the settings data for the given user.
|
||||
*/
|
||||
|
||||
@@ -35,6 +35,7 @@ private const val INITIAL_AUTOFILL_DIALOG_SHOWN = "addSitePromptShown"
|
||||
private const val HAS_USER_LOGGED_IN_OR_CREATED_AN_ACCOUNT_KEY = "hasUserLoggedInOrCreatedAccount"
|
||||
private const val SHOW_AUTOFILL_SETTING_BADGE = "showAutofillSettingBadge"
|
||||
private const val SHOW_UNLOCK_SETTING_BADGE = "showUnlockSettingBadge"
|
||||
private const val LAST_SCHEME_CHANGE_INSTANT = "lastDatabaseSchemeChangeInstant"
|
||||
|
||||
/**
|
||||
* Primary implementation of [SettingsDiskSource].
|
||||
@@ -151,6 +152,10 @@ class SettingsDiskSourceImpl(
|
||||
get() = mutableHasUserLoggedInOrCreatedAccountFlow
|
||||
.onSubscription { emit(getBoolean(HAS_USER_LOGGED_IN_OR_CREATED_AN_ACCOUNT_KEY)) }
|
||||
|
||||
override var lastDatabaseSchemeChangeInstant: Instant?
|
||||
get() = getLong(LAST_SCHEME_CHANGE_INSTANT)?.let { Instant.ofEpochMilli(it) }
|
||||
set(value) = putLong(LAST_SCHEME_CHANGE_INSTANT, value?.toEpochMilli())
|
||||
|
||||
override fun clearData(userId: String) {
|
||||
storeVaultTimeoutInMinutes(userId = userId, vaultTimeoutInMinutes = null)
|
||||
storeVaultTimeoutAction(userId = userId, vaultTimeoutAction = null)
|
||||
@@ -167,6 +172,8 @@ class SettingsDiskSourceImpl(
|
||||
// The following are intentionally not cleared so they can be
|
||||
// restored after logging out and back in:
|
||||
// - screen capture allowed
|
||||
// - show autofill setting badge
|
||||
// - show unlock setting badge
|
||||
}
|
||||
|
||||
override fun getAccountBiometricIntegrityValidity(
|
||||
|
||||
@@ -26,8 +26,10 @@ import com.x8bit.bitwarden.data.platform.datasource.disk.legacy.LegacySecureStor
|
||||
import com.x8bit.bitwarden.data.platform.datasource.disk.legacy.LegacySecureStorageImpl
|
||||
import com.x8bit.bitwarden.data.platform.datasource.disk.legacy.LegacySecureStorageMigrator
|
||||
import com.x8bit.bitwarden.data.platform.datasource.disk.legacy.LegacySecureStorageMigratorImpl
|
||||
import com.x8bit.bitwarden.data.platform.manager.DatabaseSchemeManager
|
||||
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.callback.DatabaseSchemeCallback
|
||||
import com.x8bit.bitwarden.data.vault.datasource.disk.convertor.ZonedDateTimeTypeConverter
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
@@ -35,6 +37,7 @@ import dagger.hilt.InstallIn
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import kotlinx.serialization.json.Json
|
||||
import java.time.Clock
|
||||
import javax.inject.Singleton
|
||||
|
||||
/**
|
||||
@@ -68,7 +71,11 @@ object PlatformDiskModule {
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideEventDatabase(app: Application): PlatformDatabase =
|
||||
fun provideEventDatabase(
|
||||
app: Application,
|
||||
databaseSchemeManager: DatabaseSchemeManager,
|
||||
clock: Clock,
|
||||
): PlatformDatabase =
|
||||
Room
|
||||
.databaseBuilder(
|
||||
context = app,
|
||||
@@ -77,6 +84,12 @@ object PlatformDiskModule {
|
||||
)
|
||||
.fallbackToDestructiveMigration()
|
||||
.addTypeConverter(ZonedDateTimeTypeConverter())
|
||||
.addCallback(
|
||||
DatabaseSchemeCallback(
|
||||
databaseSchemeManager = databaseSchemeManager,
|
||||
clock = clock,
|
||||
),
|
||||
)
|
||||
.build()
|
||||
|
||||
@Provides
|
||||
|
||||
@@ -19,4 +19,10 @@ data class ServerConfig(
|
||||
|
||||
@SerialName("serverData")
|
||||
val serverData: ConfigResponseJson,
|
||||
)
|
||||
) {
|
||||
/**
|
||||
* Whether the server is an official Bitwarden server or not.
|
||||
*/
|
||||
val isOfficialBitwardenServer: Boolean
|
||||
get() = serverData.server == null
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ import retrofit2.Call
|
||||
import retrofit2.Callback
|
||||
import retrofit2.HttpException
|
||||
import retrofit2.Response
|
||||
import timber.log.Timber
|
||||
import java.lang.reflect.Type
|
||||
|
||||
/**
|
||||
@@ -19,6 +20,7 @@ private const val NO_CONTENT_RESPONSE_CODE: Int = 204
|
||||
/**
|
||||
* A [Call] for wrapping a network request into a [Result].
|
||||
*/
|
||||
@Suppress("TooManyFunctions")
|
||||
class ResultCall<T>(
|
||||
private val backingCall: Call<T>,
|
||||
private val successType: Type,
|
||||
@@ -34,7 +36,7 @@ class ResultCall<T>(
|
||||
}
|
||||
|
||||
override fun onFailure(call: Call<T>, t: Throwable) {
|
||||
callback.onResponse(this@ResultCall, Response.success(t.asFailure()))
|
||||
callback.onResponse(this@ResultCall, Response.success(t.toFailure()))
|
||||
}
|
||||
},
|
||||
)
|
||||
@@ -44,9 +46,9 @@ class ResultCall<T>(
|
||||
try {
|
||||
Response.success(backingCall.execute().toResult())
|
||||
} catch (ioException: IOException) {
|
||||
Response.success(ioException.asFailure())
|
||||
Response.success(ioException.toFailure())
|
||||
} catch (runtimeException: RuntimeException) {
|
||||
Response.success(runtimeException.asFailure())
|
||||
Response.success(runtimeException.toFailure())
|
||||
}
|
||||
|
||||
override fun isCanceled(): Boolean = backingCall.isCanceled
|
||||
@@ -62,9 +64,14 @@ class ResultCall<T>(
|
||||
*/
|
||||
fun executeForResult(): Result<T> = requireNotNull(execute().body())
|
||||
|
||||
private fun Throwable.toFailure(): Result<T> =
|
||||
this
|
||||
.also { Timber.w(it, "Network Error: ${backingCall.request().url}") }
|
||||
.asFailure()
|
||||
|
||||
private fun Response<T>.toResult(): Result<T> =
|
||||
if (!this.isSuccessful) {
|
||||
HttpException(this).asFailure()
|
||||
HttpException(this).toFailure()
|
||||
} else {
|
||||
val body = this.body()
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
@@ -76,7 +83,7 @@ class ResultCall<T>(
|
||||
// We allow null for 204's, just return null.
|
||||
this.code() == NO_CONTENT_RESPONSE_CODE -> (null as T).asSuccess()
|
||||
// All other null bodies result in an error.
|
||||
else -> IllegalStateException("Unexpected null body!").asFailure()
|
||||
else -> IllegalStateException("Unexpected null body!").toFailure()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -58,7 +58,9 @@ object PlatformNetworkModule {
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun providesAuthTokenInterceptor(): AuthTokenInterceptor = AuthTokenInterceptor()
|
||||
fun providesAuthTokenInterceptor(
|
||||
authDiskSource: AuthDiskSource,
|
||||
): AuthTokenInterceptor = AuthTokenInterceptor(authDiskSource = authDiskSource)
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.x8bit.bitwarden.data.platform.datasource.network.interceptor
|
||||
|
||||
import com.x8bit.bitwarden.data.auth.datasource.disk.AuthDiskSource
|
||||
import com.x8bit.bitwarden.data.platform.datasource.network.util.HEADER_KEY_AUTHORIZATION
|
||||
import com.x8bit.bitwarden.data.platform.datasource.network.util.HEADER_VALUE_BEARER_PREFIX
|
||||
import okhttp3.Interceptor
|
||||
@@ -11,11 +12,20 @@ import javax.inject.Singleton
|
||||
* Interceptor responsible for adding the auth token(Bearer) to API requests.
|
||||
*/
|
||||
@Singleton
|
||||
class AuthTokenInterceptor : Interceptor {
|
||||
class AuthTokenInterceptor(
|
||||
private val authDiskSource: AuthDiskSource,
|
||||
) : Interceptor {
|
||||
/**
|
||||
* The auth token to be added to API requests.
|
||||
*
|
||||
* Note: This is done on demand to ensure that no race conditions can exist when retrieving the
|
||||
* token.
|
||||
*/
|
||||
var authToken: String? = null
|
||||
private val authToken: String?
|
||||
get() = authDiskSource
|
||||
.userState
|
||||
?.activeUserId
|
||||
?.let { userId -> authDiskSource.getAccountTokens(userId = userId)?.accessToken }
|
||||
|
||||
private val missingTokenMessage = "Auth token is missing!"
|
||||
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
package com.x8bit.bitwarden.data.platform.datasource.network.retrofit
|
||||
|
||||
import android.util.Log
|
||||
import com.x8bit.bitwarden.BuildConfig
|
||||
import com.x8bit.bitwarden.data.platform.datasource.network.authenticator.RefreshAuthenticator
|
||||
import com.x8bit.bitwarden.data.platform.datasource.network.core.ResultCallAdapterFactory
|
||||
import com.x8bit.bitwarden.data.platform.datasource.network.interceptor.AuthTokenInterceptor
|
||||
@@ -15,8 +13,7 @@ import okhttp3.OkHttpClient
|
||||
import okhttp3.logging.HttpLoggingInterceptor
|
||||
import retrofit2.Retrofit
|
||||
import retrofit2.converter.kotlinx.serialization.asConverterFactory
|
||||
|
||||
private const val MAX_LOG_MESSAGE_LENGTH: Int = 4000
|
||||
import timber.log.Timber
|
||||
|
||||
/**
|
||||
* Primary implementation of [Retrofits].
|
||||
@@ -79,20 +76,10 @@ class RetrofitsImpl(
|
||||
|
||||
//region Helper properties and functions
|
||||
private val loggingInterceptor: HttpLoggingInterceptor by lazy {
|
||||
HttpLoggingInterceptor { message ->
|
||||
message.chunked(size = MAX_LOG_MESSAGE_LENGTH).forEach { chunk ->
|
||||
Log.d("BitwardenNetworkClient", chunk)
|
||||
}
|
||||
}
|
||||
HttpLoggingInterceptor { message -> Timber.tag("BitwardenNetworkClient").d(message) }
|
||||
.apply {
|
||||
redactHeader(name = HEADER_KEY_AUTHORIZATION)
|
||||
setLevel(
|
||||
if (BuildConfig.DEBUG) {
|
||||
HttpLoggingInterceptor.Level.BODY
|
||||
} else {
|
||||
HttpLoggingInterceptor.Level.NONE
|
||||
},
|
||||
)
|
||||
setLevel(HttpLoggingInterceptor.Level.BODY)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
package com.x8bit.bitwarden.data.platform.datasource.sdk
|
||||
|
||||
import android.util.Log
|
||||
import com.bitwarden.sdk.Client
|
||||
import com.x8bit.bitwarden.BuildConfig
|
||||
import com.x8bit.bitwarden.data.platform.manager.SdkClientManager
|
||||
import timber.log.Timber
|
||||
|
||||
/**
|
||||
* Base class for simplifying sdk interactions.
|
||||
@@ -27,9 +26,5 @@ abstract class BaseSdkSource(
|
||||
protected inline fun <T, R> T.runCatchingWithLogs(
|
||||
block: T.() -> R,
|
||||
): Result<R> = runCatching(block = block)
|
||||
.onFailure {
|
||||
if (BuildConfig.DEBUG) {
|
||||
Log.w(this@BaseSdkSource::class.java.simpleName, it)
|
||||
}
|
||||
}
|
||||
.onFailure { Timber.w(it) }
|
||||
}
|
||||
|
||||
@@ -55,9 +55,9 @@ class BiometricsEncryptionManagerImpl(
|
||||
}
|
||||
val cipher = try {
|
||||
Cipher.getInstance(CIPHER_TRANSFORMATION)
|
||||
} catch (e: NoSuchAlgorithmException) {
|
||||
} catch (_: NoSuchAlgorithmException) {
|
||||
return null
|
||||
} catch (e: NoSuchPaddingException) {
|
||||
} catch (_: NoSuchPaddingException) {
|
||||
return null
|
||||
}
|
||||
// This should never fail to initialize / return false because the cipher is newly generated
|
||||
@@ -116,20 +116,20 @@ class BiometricsEncryptionManagerImpl(
|
||||
KeyProperties.KEY_ALGORITHM_AES,
|
||||
ENCRYPTION_KEYSTORE_NAME,
|
||||
)
|
||||
} catch (e: NoSuchAlgorithmException) {
|
||||
} catch (_: NoSuchAlgorithmException) {
|
||||
return null
|
||||
} catch (e: NoSuchProviderException) {
|
||||
} catch (_: NoSuchProviderException) {
|
||||
return null
|
||||
} catch (e: IllegalArgumentException) {
|
||||
} catch (_: IllegalArgumentException) {
|
||||
return null
|
||||
}
|
||||
|
||||
try {
|
||||
keyGen.init(keyGenParameterSpec)
|
||||
keyGen.generateKey()
|
||||
} catch (e: InvalidAlgorithmParameterException) {
|
||||
} catch (_: InvalidAlgorithmParameterException) {
|
||||
return null
|
||||
} catch (e: ProviderException) {
|
||||
} catch (_: ProviderException) {
|
||||
return null
|
||||
}
|
||||
|
||||
@@ -142,29 +142,29 @@ class BiometricsEncryptionManagerImpl(
|
||||
private fun getSecretKeyOrNull(): SecretKey? {
|
||||
try {
|
||||
keystore.load(null)
|
||||
} catch (e: IllegalArgumentException) {
|
||||
} catch (_: IllegalArgumentException) {
|
||||
// keystore could not be loaded because [param] is unrecognized.
|
||||
return null
|
||||
} catch (e: IOException) {
|
||||
} catch (_: IOException) {
|
||||
// keystore data format is invalid or the password is incorrect.
|
||||
return null
|
||||
} catch (e: NoSuchAlgorithmException) {
|
||||
} catch (_: NoSuchAlgorithmException) {
|
||||
// keystore integrity could not be checked due to missing algorithm.
|
||||
return null
|
||||
} catch (e: CertificateException) {
|
||||
} catch (_: CertificateException) {
|
||||
// keystore certificates could not be loaded
|
||||
return null
|
||||
}
|
||||
|
||||
return try {
|
||||
keystore.getKey(ENCRYPTION_KEY_NAME, null) as? SecretKey
|
||||
} catch (e: KeyStoreException) {
|
||||
} catch (_: KeyStoreException) {
|
||||
// keystore was not loaded
|
||||
null
|
||||
} catch (e: NoSuchAlgorithmException) {
|
||||
} catch (_: NoSuchAlgorithmException) {
|
||||
// keystore algorithm cannot be found
|
||||
null
|
||||
} catch (e: UnrecoverableKeyException) {
|
||||
} catch (_: UnrecoverableKeyException) {
|
||||
// key could not be recovered
|
||||
null
|
||||
}
|
||||
@@ -181,15 +181,15 @@ class BiometricsEncryptionManagerImpl(
|
||||
try {
|
||||
cipher.init(Cipher.ENCRYPT_MODE, secretKey)
|
||||
true
|
||||
} catch (e: KeyPermanentlyInvalidatedException) {
|
||||
} catch (_: KeyPermanentlyInvalidatedException) {
|
||||
// Biometric has changed
|
||||
settingsDiskSource.systemBiometricIntegritySource = null
|
||||
false
|
||||
} catch (e: UnrecoverableKeyException) {
|
||||
} catch (_: UnrecoverableKeyException) {
|
||||
// Biometric was disabled and re-enabled
|
||||
settingsDiskSource.systemBiometricIntegritySource = null
|
||||
false
|
||||
} catch (e: InvalidKeyException) {
|
||||
} catch (_: InvalidKeyException) {
|
||||
// Fallback for old Bitwarden users without a key
|
||||
createIntegrityValues(userId)
|
||||
true
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
package com.x8bit.bitwarden.data.platform.manager
|
||||
|
||||
import java.time.Instant
|
||||
|
||||
/**
|
||||
* Manager for tracking changes to database scheme(s).
|
||||
*/
|
||||
interface DatabaseSchemeManager {
|
||||
|
||||
/**
|
||||
* The instant of the last database schema change performed on the database, if any.
|
||||
*
|
||||
* There is only a single scheme change instant tracked for all database schemes. It is expected
|
||||
* that a scheme change to any database will update this value and trigger a sync.
|
||||
*/
|
||||
var lastDatabaseSchemeChangeInstant: Instant?
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package com.x8bit.bitwarden.data.platform.manager
|
||||
|
||||
import com.x8bit.bitwarden.data.platform.datasource.disk.SettingsDiskSource
|
||||
import java.time.Instant
|
||||
|
||||
/**
|
||||
* Primary implementation of [DatabaseSchemeManager].
|
||||
*/
|
||||
class DatabaseSchemeManagerImpl(
|
||||
val settingsDiskSource: SettingsDiskSource,
|
||||
) : DatabaseSchemeManager {
|
||||
override var lastDatabaseSchemeChangeInstant: Instant?
|
||||
get() = settingsDiskSource.lastDatabaseSchemeChangeInstant
|
||||
set(value) {
|
||||
settingsDiskSource.lastDatabaseSchemeChangeInstant = value
|
||||
}
|
||||
}
|
||||
@@ -3,10 +3,12 @@ package com.x8bit.bitwarden.data.platform.manager
|
||||
import com.x8bit.bitwarden.data.platform.datasource.disk.model.ServerConfig
|
||||
import com.x8bit.bitwarden.data.platform.manager.model.FlagKey
|
||||
import com.x8bit.bitwarden.data.platform.repository.ServerConfigRepository
|
||||
import com.x8bit.bitwarden.data.platform.util.isServerVersionAtLeast
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.map
|
||||
|
||||
private const val CIPHER_KEY_ENCRYPTION_KEY = "enableCipherKeyEncryption"
|
||||
private const val CIPHER_KEY_ENC_MIN_SERVER_VERSION = "2024.2.0"
|
||||
|
||||
/**
|
||||
* Primary implementation of [FeatureFlagManager].
|
||||
@@ -16,7 +18,13 @@ class FeatureFlagManagerImpl(
|
||||
) : FeatureFlagManager {
|
||||
|
||||
override val sdkFeatureFlags: Map<String, Boolean>
|
||||
get() = mapOf(CIPHER_KEY_ENCRYPTION_KEY to true)
|
||||
get() = mapOf(
|
||||
CIPHER_KEY_ENCRYPTION_KEY to
|
||||
isServerVersionAtLeast(
|
||||
serverConfigRepository.serverConfigStateFlow.value,
|
||||
CIPHER_KEY_ENC_MIN_SERVER_VERSION,
|
||||
),
|
||||
)
|
||||
|
||||
override fun <T : Any> getFeatureFlagFlow(key: FlagKey<T>): Flow<T> =
|
||||
serverConfigRepository
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
package com.x8bit.bitwarden.data.platform.manager
|
||||
|
||||
import com.x8bit.bitwarden.data.platform.manager.model.FirstTimeState
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
|
||||
/**
|
||||
* Manager for compiling the state of all first time actions and related information such
|
||||
* as counts of notifications to show, etc.
|
||||
*/
|
||||
interface FirstTimeActionManager {
|
||||
|
||||
/**
|
||||
* Returns an observable count of the number of settings items that have a badge to display
|
||||
* for the current active user.
|
||||
*/
|
||||
val allSettingsBadgeCountFlow: StateFlow<Int>
|
||||
|
||||
/**
|
||||
* Returns an observable count of the number of security settings items that have a badge to
|
||||
* display for the current active user.
|
||||
*/
|
||||
val allSecuritySettingsBadgeCountFlow: StateFlow<Int>
|
||||
|
||||
/**
|
||||
* Returns an observable count of the number of autofill settings items that have a badge to
|
||||
* display for the current active user.
|
||||
*/
|
||||
val allAutofillSettingsBadgeCountFlow: StateFlow<Int>
|
||||
|
||||
/**
|
||||
* Returns an observable count of the number of vault settings items that have a badge to
|
||||
* display for the current active user.
|
||||
*/
|
||||
val allVaultSettingsBadgeCountFlow: StateFlow<Int>
|
||||
|
||||
/**
|
||||
* Returns a [Flow] that emits every time the active user's first time state is changed.
|
||||
*/
|
||||
val firstTimeStateFlow: Flow<FirstTimeState>
|
||||
|
||||
/**
|
||||
* Get the current [FirstTimeState] of the active user if available, otherwise return
|
||||
* a default configuration.
|
||||
*/
|
||||
val currentOrDefaultUserFirstTimeState: FirstTimeState
|
||||
}
|
||||
@@ -0,0 +1,186 @@
|
||||
package com.x8bit.bitwarden.data.platform.manager
|
||||
|
||||
import com.x8bit.bitwarden.data.auth.datasource.disk.AuthDiskSource
|
||||
import com.x8bit.bitwarden.data.auth.repository.util.activeUserIdChangesFlow
|
||||
import com.x8bit.bitwarden.data.platform.datasource.disk.SettingsDiskSource
|
||||
import com.x8bit.bitwarden.data.platform.manager.dispatcher.DispatcherManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.model.FirstTimeState
|
||||
import com.x8bit.bitwarden.data.platform.manager.model.FlagKey
|
||||
import com.x8bit.bitwarden.data.vault.datasource.disk.VaultDiskSource
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.filterNotNull
|
||||
import kotlinx.coroutines.flow.flatMapLatest
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.onStart
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import javax.inject.Inject
|
||||
|
||||
/**
|
||||
* Implementation of [FirstTimeActionManager]
|
||||
*/
|
||||
class FirstTimeActionManagerImpl @Inject constructor(
|
||||
dispatcherManager: DispatcherManager,
|
||||
private val authDiskSource: AuthDiskSource,
|
||||
private val settingsDiskSource: SettingsDiskSource,
|
||||
private val vaultDiskSource: VaultDiskSource,
|
||||
private val featureFlagManager: FeatureFlagManager,
|
||||
) : FirstTimeActionManager {
|
||||
|
||||
private val unconfinedScope = CoroutineScope(dispatcherManager.unconfined)
|
||||
|
||||
override val allSettingsBadgeCountFlow: StateFlow<Int>
|
||||
get() = combine(
|
||||
listOf(
|
||||
allSecuritySettingsBadgeCountFlow,
|
||||
allAutofillSettingsBadgeCountFlow,
|
||||
allVaultSettingsBadgeCountFlow,
|
||||
),
|
||||
) {
|
||||
it.sum()
|
||||
}
|
||||
.stateIn(
|
||||
scope = unconfinedScope,
|
||||
started = SharingStarted.Lazily,
|
||||
initialValue = 0,
|
||||
)
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
override val allSecuritySettingsBadgeCountFlow: StateFlow<Int>
|
||||
get() = authDiskSource
|
||||
.activeUserIdChangesFlow
|
||||
.filterNotNull()
|
||||
.flatMapLatest {
|
||||
// can be expanded to support multiple security settings
|
||||
settingsDiskSource.getShowUnlockSettingBadgeFlow(userId = it)
|
||||
.map { showUnlockBadge ->
|
||||
listOfNotNull(showUnlockBadge)
|
||||
}
|
||||
.map { list ->
|
||||
list.count { badgeOnValue -> badgeOnValue }
|
||||
}
|
||||
}
|
||||
.stateIn(
|
||||
scope = unconfinedScope,
|
||||
started = SharingStarted.Lazily,
|
||||
initialValue = 0,
|
||||
)
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
override val allAutofillSettingsBadgeCountFlow: StateFlow<Int>
|
||||
get() = authDiskSource
|
||||
.activeUserIdChangesFlow
|
||||
.filterNotNull()
|
||||
.flatMapLatest {
|
||||
// Can be expanded to support multiple autofill settings
|
||||
settingsDiskSource.getShowAutoFillSettingBadgeFlow(userId = it)
|
||||
.map { showAutofillBadge ->
|
||||
listOfNotNull(showAutofillBadge)
|
||||
}
|
||||
.map { list ->
|
||||
list.count { showBadge -> showBadge }
|
||||
}
|
||||
}
|
||||
.stateIn(
|
||||
scope = unconfinedScope,
|
||||
started = SharingStarted.Lazily,
|
||||
initialValue = 0,
|
||||
)
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
override val allVaultSettingsBadgeCountFlow: StateFlow<Int>
|
||||
get() = authDiskSource
|
||||
.activeUserIdChangesFlow
|
||||
.filterNotNull()
|
||||
.flatMapLatest {
|
||||
combine(
|
||||
getShowImportLoginsFlowInternal(userId = it),
|
||||
featureFlagManager.getFeatureFlagFlow(FlagKey.ImportLoginsFlow),
|
||||
) { showImportLogins, importLoginsEnabled ->
|
||||
val shouldShowImportLogins = showImportLogins && importLoginsEnabled
|
||||
listOf(shouldShowImportLogins)
|
||||
}
|
||||
.map { list ->
|
||||
list.count { showImportLogins -> showImportLogins }
|
||||
}
|
||||
}
|
||||
.stateIn(
|
||||
scope = unconfinedScope,
|
||||
started = SharingStarted.Lazily,
|
||||
initialValue = 0,
|
||||
)
|
||||
|
||||
/**
|
||||
* Returns a [Flow] that emits every time the active user's first time state is changed.
|
||||
*/
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
override val firstTimeStateFlow: Flow<FirstTimeState>
|
||||
get() = authDiskSource
|
||||
.activeUserIdChangesFlow
|
||||
.filterNotNull()
|
||||
.flatMapLatest { activeUserId ->
|
||||
combine(
|
||||
listOf(
|
||||
getShowImportLoginsFlowInternal(userId = activeUserId),
|
||||
settingsDiskSource.getShowUnlockSettingBadgeFlow(userId = activeUserId),
|
||||
settingsDiskSource.getShowAutoFillSettingBadgeFlow(userId = activeUserId),
|
||||
),
|
||||
) {
|
||||
FirstTimeState(
|
||||
showImportLoginsCard = it[0],
|
||||
showSetupUnlockCard = it[1],
|
||||
showSetupAutofillCard = it[2],
|
||||
)
|
||||
}
|
||||
}
|
||||
.onStart {
|
||||
emit(
|
||||
FirstTimeState(
|
||||
showImportLoginsCard = null,
|
||||
showSetupUnlockCard = null,
|
||||
showSetupAutofillCard = null,
|
||||
),
|
||||
)
|
||||
}
|
||||
.distinctUntilChanged()
|
||||
|
||||
/**
|
||||
* Internal implementation to get a flow of the showImportLogins value which takes
|
||||
* into account if the vault is empty.
|
||||
*/
|
||||
private fun getShowImportLoginsFlowInternal(userId: String): Flow<Boolean> {
|
||||
return authDiskSource.getShowImportLoginsFlow(userId)
|
||||
.combine(
|
||||
vaultDiskSource.getCiphers(userId),
|
||||
) { showImportLogins, ciphers ->
|
||||
showImportLogins ?: true && ciphers.isEmpty()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current [FirstTimeState] of the active user if available, otherwise return
|
||||
* a default configuration.
|
||||
*/
|
||||
override val currentOrDefaultUserFirstTimeState: FirstTimeState
|
||||
get() =
|
||||
authDiskSource
|
||||
.userState
|
||||
?.activeUserId
|
||||
?.let {
|
||||
FirstTimeState(
|
||||
showImportLoginsCard = authDiskSource.getShowImportLogins(it),
|
||||
showSetupUnlockCard = settingsDiskSource.getShowUnlockSettingBadge(it),
|
||||
showSetupAutofillCard = settingsDiskSource.getShowAutoFillSettingBadge(it),
|
||||
)
|
||||
}
|
||||
?: FirstTimeState(
|
||||
showImportLoginsCard = null,
|
||||
showSetupUnlockCard = null,
|
||||
showSetupAutofillCard = null,
|
||||
)
|
||||
}
|
||||
@@ -1,17 +1,24 @@
|
||||
package com.x8bit.bitwarden.data.platform.manager
|
||||
|
||||
import com.x8bit.bitwarden.data.platform.repository.model.Environment
|
||||
|
||||
/**
|
||||
* Implementations of this interface provide a way to enable or disable the collection of crash
|
||||
* logs, giving control over whether crash logs are generated and stored.
|
||||
*/
|
||||
interface CrashLogsManager {
|
||||
interface LogsManager {
|
||||
/**
|
||||
* Gets or sets whether the collection of crash logs is enabled.
|
||||
*/
|
||||
var isEnabled: Boolean
|
||||
|
||||
/**
|
||||
* Tracks an exception if logs are enabled.
|
||||
* Tracks a [Throwable] if logs are enabled.
|
||||
*/
|
||||
fun trackNonFatalException(e: Exception)
|
||||
fun trackNonFatalException(throwable: Throwable)
|
||||
|
||||
/**
|
||||
* Tracks the current user data.
|
||||
*/
|
||||
fun setUserData(userId: String?, environmentType: Environment.Type)
|
||||
}
|
||||
@@ -1,9 +1,7 @@
|
||||
package com.x8bit.bitwarden.data.platform.manager
|
||||
|
||||
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.AuthState
|
||||
import com.x8bit.bitwarden.data.platform.datasource.network.authenticator.RefreshAuthenticator
|
||||
import com.x8bit.bitwarden.data.platform.datasource.network.interceptor.AuthTokenInterceptor
|
||||
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
|
||||
@@ -18,10 +16,8 @@ private const val ENVIRONMENT_DEBOUNCE_TIMEOUT_MS: Long = 500L
|
||||
/**
|
||||
* Primary implementation of [NetworkConfigManager].
|
||||
*/
|
||||
@Suppress("LongParameterList")
|
||||
class NetworkConfigManagerImpl(
|
||||
authRepository: AuthRepository,
|
||||
private val authTokenInterceptor: AuthTokenInterceptor,
|
||||
environmentRepository: EnvironmentRepository,
|
||||
serverConfigRepository: ServerConfigRepository,
|
||||
private val baseUrlInterceptors: BaseUrlInterceptors,
|
||||
@@ -32,17 +28,6 @@ class NetworkConfigManagerImpl(
|
||||
private val collectionScope = CoroutineScope(dispatcherManager.unconfined)
|
||||
|
||||
init {
|
||||
authRepository
|
||||
.authStateFlow
|
||||
.onEach { authState ->
|
||||
authTokenInterceptor.authToken = when (authState) {
|
||||
is AuthState.Authenticated -> authState.accessToken
|
||||
is AuthState.Unauthenticated -> null
|
||||
is AuthState.Uninitialized -> null
|
||||
}
|
||||
}
|
||||
.launchIn(collectionScope)
|
||||
|
||||
@Suppress("OPT_IN_USAGE")
|
||||
environmentRepository
|
||||
.environmentStateFlow
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package com.x8bit.bitwarden.data.platform.manager
|
||||
|
||||
import com.x8bit.bitwarden.data.auth.datasource.disk.AuthDiskSource
|
||||
import com.x8bit.bitwarden.data.auth.repository.util.activeUserIdChangesFlow
|
||||
import com.x8bit.bitwarden.data.platform.datasource.disk.PushDiskSource
|
||||
import com.x8bit.bitwarden.data.platform.datasource.network.model.PushTokenRequest
|
||||
import com.x8bit.bitwarden.data.platform.datasource.network.service.PushService
|
||||
@@ -21,7 +22,6 @@ import com.x8bit.bitwarden.data.platform.util.decodeFromStringOrNull
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.flow.SharedFlow
|
||||
import kotlinx.coroutines.flow.asSharedFlow
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.mapNotNull
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
@@ -100,9 +100,8 @@ class PushManagerImpl @Inject constructor(
|
||||
|
||||
init {
|
||||
authDiskSource
|
||||
.userStateFlow
|
||||
.mapNotNull { it?.activeUserId }
|
||||
.distinctUntilChanged()
|
||||
.activeUserIdChangesFlow
|
||||
.mapNotNull { it }
|
||||
.onEach { registerStoredPushTokenIfNecessary() }
|
||||
.launchIn(unconfinedScope)
|
||||
}
|
||||
@@ -129,7 +128,7 @@ class PushManagerImpl @Inject constructor(
|
||||
when (val type = notification.notificationType) {
|
||||
NotificationType.AUTH_REQUEST,
|
||||
NotificationType.AUTH_REQUEST_RESPONSE,
|
||||
-> {
|
||||
-> {
|
||||
json
|
||||
.decodeFromString<NotificationPayload.PasswordlessRequestNotification>(
|
||||
string = notification.payload,
|
||||
@@ -156,25 +155,20 @@ class PushManagerImpl @Inject constructor(
|
||||
|
||||
NotificationType.SYNC_CIPHER_CREATE,
|
||||
NotificationType.SYNC_CIPHER_UPDATE,
|
||||
-> {
|
||||
-> {
|
||||
json
|
||||
.decodeFromString<NotificationPayload.SyncCipherNotification>(
|
||||
string = notification.payload,
|
||||
)
|
||||
.takeIf { isLoggedIn(userId) && it.userMatchesNotification(userId) }
|
||||
?.takeIf {
|
||||
it.cipherId != null &&
|
||||
it.revisionDate != null &&
|
||||
it.organizationId != null &&
|
||||
it.collectionIds != null
|
||||
}
|
||||
?.takeIf { it.cipherId != null && it.revisionDate != null }
|
||||
?.let {
|
||||
mutableSyncCipherUpsertSharedFlow.tryEmit(
|
||||
SyncCipherUpsertData(
|
||||
cipherId = requireNotNull(it.cipherId),
|
||||
revisionDate = requireNotNull(it.revisionDate),
|
||||
organizationId = requireNotNull(it.organizationId),
|
||||
collectionIds = requireNotNull(it.collectionIds),
|
||||
organizationId = it.organizationId,
|
||||
collectionIds = it.collectionIds,
|
||||
isUpdate = type == NotificationType.SYNC_CIPHER_UPDATE,
|
||||
),
|
||||
)
|
||||
@@ -183,7 +177,7 @@ class PushManagerImpl @Inject constructor(
|
||||
|
||||
NotificationType.SYNC_CIPHER_DELETE,
|
||||
NotificationType.SYNC_LOGIN_DELETE,
|
||||
-> {
|
||||
-> {
|
||||
json
|
||||
.decodeFromString<NotificationPayload.SyncCipherNotification>(
|
||||
string = notification.payload,
|
||||
@@ -196,13 +190,13 @@ class PushManagerImpl @Inject constructor(
|
||||
NotificationType.SYNC_CIPHERS,
|
||||
NotificationType.SYNC_SETTINGS,
|
||||
NotificationType.SYNC_VAULT,
|
||||
-> {
|
||||
-> {
|
||||
mutableFullSyncSharedFlow.tryEmit(Unit)
|
||||
}
|
||||
|
||||
NotificationType.SYNC_FOLDER_CREATE,
|
||||
NotificationType.SYNC_FOLDER_UPDATE,
|
||||
-> {
|
||||
-> {
|
||||
json
|
||||
.decodeFromString<NotificationPayload.SyncFolderNotification>(
|
||||
string = notification.payload,
|
||||
@@ -238,7 +232,7 @@ class PushManagerImpl @Inject constructor(
|
||||
|
||||
NotificationType.SYNC_SEND_CREATE,
|
||||
NotificationType.SYNC_SEND_UPDATE,
|
||||
-> {
|
||||
-> {
|
||||
json
|
||||
.decodeFromString<NotificationPayload.SyncSendNotification>(
|
||||
string = notification.payload,
|
||||
@@ -339,6 +333,6 @@ class PushManagerImpl @Inject constructor(
|
||||
): Boolean = authDiskSource.getAccountTokens(userId)?.isLoggedIn == true
|
||||
}
|
||||
|
||||
private fun NotificationPayload.userMatchesNotification(userId: String?): Boolean {
|
||||
private fun NotificationPayload.userMatchesNotification(userId: String): Boolean {
|
||||
return this.userId != null && this.userId == userId
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ class SpecialCircumstanceManagerImpl(
|
||||
) : SpecialCircumstanceManager {
|
||||
private val mutableSpecialCircumstanceFlow = MutableStateFlow<SpecialCircumstance?>(null)
|
||||
private val unconfinedScope = CoroutineScope(dispatcherManager.unconfined)
|
||||
|
||||
init {
|
||||
authRepository
|
||||
.userStateFlow
|
||||
|
||||
@@ -7,8 +7,10 @@ import com.x8bit.bitwarden.data.platform.manager.ResourceCacheManager
|
||||
import com.x8bit.bitwarden.data.platform.repository.SettingsRepository
|
||||
import com.x8bit.bitwarden.data.platform.util.firstWithTimeoutOrNull
|
||||
import com.x8bit.bitwarden.data.platform.util.getDomainOrNull
|
||||
import com.x8bit.bitwarden.data.platform.util.getHostOrNull
|
||||
import com.x8bit.bitwarden.data.platform.util.getHostWithPortOrNull
|
||||
import com.x8bit.bitwarden.data.platform.util.getWebHostFromAndroidUriOrNull
|
||||
import com.x8bit.bitwarden.data.platform.util.hasPort
|
||||
import com.x8bit.bitwarden.data.platform.util.isAndroidApp
|
||||
import com.x8bit.bitwarden.data.platform.util.regexOrNull
|
||||
import com.x8bit.bitwarden.data.vault.repository.VaultRepository
|
||||
@@ -186,6 +188,7 @@ private fun checkForCipherMatch(
|
||||
* @param matchingDomains The set of domains that match the domain of [matchUri].
|
||||
* @param matchUri The uri that this [LoginUriView] is being matched to.
|
||||
*/
|
||||
@Suppress("CyclomaticComplexMethod")
|
||||
private fun LoginUriView.checkForMatch(
|
||||
resourceCacheManager: ResourceCacheManager,
|
||||
defaultUriMatchType: UriMatchType,
|
||||
@@ -210,9 +213,15 @@ private fun LoginUriView.checkForMatch(
|
||||
UriMatchType.EXACT -> exactIfTrue(loginViewUri == matchUri)
|
||||
|
||||
UriMatchType.HOST -> {
|
||||
val loginUriHost = loginViewUri.getHostWithPortOrNull()
|
||||
val matchUriHost = matchUri.getHostWithPortOrNull()
|
||||
exactIfTrue(matchUriHost != null && loginUriHost == matchUriHost)
|
||||
if (loginViewUri.hasPort() && matchUri.hasPort()) {
|
||||
val loginUriHost = loginViewUri.getHostWithPortOrNull()
|
||||
val matchUriHost = matchUri.getHostWithPortOrNull()
|
||||
exactIfTrue(matchUriHost != null && loginUriHost == matchUriHost)
|
||||
} else {
|
||||
val loginUriHost = loginViewUri.getHostOrNull()
|
||||
val matchUriHost = matchUri.getHostOrNull()
|
||||
exactIfTrue(matchUriHost != null && loginUriHost == matchUriHost)
|
||||
}
|
||||
}
|
||||
|
||||
UriMatchType.NEVER -> MatchResult.NONE
|
||||
|
||||
@@ -4,13 +4,13 @@ import android.app.Application
|
||||
import android.content.Context
|
||||
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.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.AuthTokenInterceptor
|
||||
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
|
||||
@@ -20,13 +20,15 @@ import com.x8bit.bitwarden.data.platform.manager.AssetManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.AssetManagerImpl
|
||||
import com.x8bit.bitwarden.data.platform.manager.BiometricsEncryptionManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.BiometricsEncryptionManagerImpl
|
||||
import com.x8bit.bitwarden.data.platform.processor.AuthenticatorBridgeProcessor
|
||||
import com.x8bit.bitwarden.data.platform.processor.AuthenticatorBridgeProcessorImpl
|
||||
import com.x8bit.bitwarden.data.platform.manager.CrashLogsManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.CrashLogsManagerImpl
|
||||
import com.x8bit.bitwarden.data.platform.manager.DatabaseSchemeManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.DatabaseSchemeManagerImpl
|
||||
import com.x8bit.bitwarden.data.platform.manager.DebugMenuFeatureFlagManagerImpl
|
||||
import com.x8bit.bitwarden.data.platform.manager.FeatureFlagManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.FeatureFlagManagerImpl
|
||||
import com.x8bit.bitwarden.data.platform.manager.FirstTimeActionManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.FirstTimeActionManagerImpl
|
||||
import com.x8bit.bitwarden.data.platform.manager.LogsManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.LogsManagerImpl
|
||||
import com.x8bit.bitwarden.data.platform.manager.NetworkConfigManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.NetworkConfigManagerImpl
|
||||
import com.x8bit.bitwarden.data.platform.manager.NetworkConnectionManager
|
||||
@@ -51,11 +53,14 @@ import com.x8bit.bitwarden.data.platform.manager.garbage.GarbageCollectionManage
|
||||
import com.x8bit.bitwarden.data.platform.manager.garbage.GarbageCollectionManagerImpl
|
||||
import com.x8bit.bitwarden.data.platform.manager.restriction.RestrictionManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.restriction.RestrictionManagerImpl
|
||||
import com.x8bit.bitwarden.data.platform.processor.AuthenticatorBridgeProcessor
|
||||
import com.x8bit.bitwarden.data.platform.processor.AuthenticatorBridgeProcessorImpl
|
||||
import com.x8bit.bitwarden.data.platform.repository.AuthenticatorBridgeRepository
|
||||
import com.x8bit.bitwarden.data.platform.repository.DebugMenuRepository
|
||||
import com.x8bit.bitwarden.data.platform.repository.EnvironmentRepository
|
||||
import com.x8bit.bitwarden.data.platform.repository.ServerConfigRepository
|
||||
import com.x8bit.bitwarden.data.platform.repository.SettingsRepository
|
||||
import com.x8bit.bitwarden.data.vault.datasource.disk.VaultDiskSource
|
||||
import com.x8bit.bitwarden.data.vault.repository.VaultRepository
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
@@ -82,10 +87,14 @@ object PlatformManagerModule {
|
||||
@Singleton
|
||||
fun provideAuthenticatorBridgeProcessor(
|
||||
authenticatorBridgeRepository: AuthenticatorBridgeRepository,
|
||||
addTotpItemFromAuthenticatorManager: AddTotpItemFromAuthenticatorManager,
|
||||
@ApplicationContext context: Context,
|
||||
dispatcherManager: DispatcherManager,
|
||||
featureFlagManager: FeatureFlagManager,
|
||||
): AuthenticatorBridgeProcessor = AuthenticatorBridgeProcessorImpl(
|
||||
authenticatorBridgeRepository = authenticatorBridgeRepository,
|
||||
addTotpItemFromAuthenticatorManager = addTotpItemFromAuthenticatorManager,
|
||||
context = context,
|
||||
dispatcherManager = dispatcherManager,
|
||||
featureFlagManager = featureFlagManager,
|
||||
)
|
||||
@@ -185,7 +194,6 @@ object PlatformManagerModule {
|
||||
@Singleton
|
||||
fun provideNetworkConfigManager(
|
||||
authRepository: AuthRepository,
|
||||
authTokenInterceptor: AuthTokenInterceptor,
|
||||
environmentRepository: EnvironmentRepository,
|
||||
serverConfigRepository: ServerConfigRepository,
|
||||
baseUrlInterceptors: BaseUrlInterceptors,
|
||||
@@ -194,7 +202,6 @@ object PlatformManagerModule {
|
||||
): NetworkConfigManager =
|
||||
NetworkConfigManagerImpl(
|
||||
authRepository = authRepository,
|
||||
authTokenInterceptor = authTokenInterceptor,
|
||||
environmentRepository = environmentRepository,
|
||||
serverConfigRepository = serverConfigRepository,
|
||||
baseUrlInterceptors = baseUrlInterceptors,
|
||||
@@ -238,10 +245,10 @@ object PlatformManagerModule {
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideCrashLogsManager(
|
||||
fun provideLogsManager(
|
||||
legacyAppCenterMigrator: LegacyAppCenterMigrator,
|
||||
settingsRepository: SettingsRepository,
|
||||
): CrashLogsManager = CrashLogsManagerImpl(
|
||||
): LogsManager = LogsManagerImpl(
|
||||
settingsRepository = settingsRepository,
|
||||
legacyAppCenterMigrator = legacyAppCenterMigrator,
|
||||
)
|
||||
@@ -276,4 +283,28 @@ object PlatformManagerModule {
|
||||
fun provideResourceCacheManager(
|
||||
@ApplicationContext context: Context,
|
||||
): ResourceCacheManager = ResourceCacheManagerImpl(context = context)
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideFirstTimeActionManager(
|
||||
authDiskSource: AuthDiskSource,
|
||||
settingsDiskSource: SettingsDiskSource,
|
||||
vaultDiskSource: VaultDiskSource,
|
||||
dispatcherManager: DispatcherManager,
|
||||
featureFlagManager: FeatureFlagManager,
|
||||
): FirstTimeActionManager = FirstTimeActionManagerImpl(
|
||||
authDiskSource = authDiskSource,
|
||||
settingsDiskSource = settingsDiskSource,
|
||||
vaultDiskSource = vaultDiskSource,
|
||||
dispatcherManager = dispatcherManager,
|
||||
featureFlagManager = featureFlagManager,
|
||||
)
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideDatabaseSchemeManager(
|
||||
settingsDiskSource: SettingsDiskSource,
|
||||
): DatabaseSchemeManager = DatabaseSchemeManagerImpl(
|
||||
settingsDiskSource = settingsDiskSource,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
package com.x8bit.bitwarden.data.platform.manager.model
|
||||
|
||||
/**
|
||||
* Model to encapsulate different states for a user's first time experience.
|
||||
*/
|
||||
data class FirstTimeState(
|
||||
val showImportLoginsCard: Boolean,
|
||||
val showSetupUnlockCard: Boolean,
|
||||
val showSetupAutofillCard: Boolean,
|
||||
) {
|
||||
/**
|
||||
* Constructs a [FirstTimeState] accepting nullable values. If a value is null, the default
|
||||
* is used.
|
||||
*/
|
||||
constructor(
|
||||
showImportLoginsCard: Boolean? = null,
|
||||
showSetupUnlockCard: Boolean? = null,
|
||||
showSetupAutofillCard: Boolean? = null,
|
||||
) : this(
|
||||
showImportLoginsCard = showImportLoginsCard ?: true,
|
||||
showSetupUnlockCard = showSetupUnlockCard ?: false,
|
||||
showSetupAutofillCard = showSetupAutofillCard ?: false,
|
||||
)
|
||||
}
|
||||
@@ -30,6 +30,8 @@ sealed class FlagKey<out T : Any> {
|
||||
EmailVerification,
|
||||
OnboardingFlow,
|
||||
OnboardingCarousel,
|
||||
ImportLoginsFlow,
|
||||
SshKeyCipherItems,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -70,6 +72,24 @@ sealed class FlagKey<out T : Any> {
|
||||
override val isRemotelyConfigured: Boolean = false
|
||||
}
|
||||
|
||||
/**
|
||||
* Data object holding the feature flag key for the import logins feature.
|
||||
*/
|
||||
data object ImportLoginsFlow : FlagKey<Boolean>() {
|
||||
override val keyName: String = "import-logins-flow"
|
||||
override val defaultValue: Boolean = false
|
||||
override val isRemotelyConfigured: Boolean = false
|
||||
}
|
||||
|
||||
/**
|
||||
* Data object holding the feature flag key for the SSH key cipher items feature.
|
||||
*/
|
||||
data object SshKeyCipherItems : FlagKey<Boolean>() {
|
||||
override val keyName: String = "ssh-key-vault-item"
|
||||
override val defaultValue: Boolean = false
|
||||
override val isRemotelyConfigured: Boolean = true
|
||||
}
|
||||
|
||||
/**
|
||||
* Data object holding the key for a [Boolean] flag to be used in tests.
|
||||
*/
|
||||
|
||||
@@ -7,6 +7,7 @@ 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
|
||||
import com.x8bit.bitwarden.ui.platform.manager.intent.IntentManager
|
||||
import com.x8bit.bitwarden.ui.vault.model.TotpData
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
/**
|
||||
@@ -14,6 +15,14 @@ import kotlinx.parcelize.Parcelize
|
||||
* of navigation that is counter to what otherwise may happen based on the state of the app.
|
||||
*/
|
||||
sealed class SpecialCircumstance : Parcelable {
|
||||
/**
|
||||
* The app was launched in order to add a new TOTP to a cipher.
|
||||
*/
|
||||
@Parcelize
|
||||
data class AddTotpLoginItem(
|
||||
val data: TotpData,
|
||||
) : SpecialCircumstance()
|
||||
|
||||
/**
|
||||
* The app was launched in order to create/share a new Send using the given [data].
|
||||
*/
|
||||
@@ -89,6 +98,12 @@ sealed class SpecialCircumstance : Parcelable {
|
||||
@Parcelize
|
||||
data object VaultShortcut : SpecialCircumstance()
|
||||
|
||||
/**
|
||||
* The app was launched via deeplink to the account security screen.
|
||||
*/
|
||||
@Parcelize
|
||||
data object AccountSecurityShortcut : SpecialCircumstance()
|
||||
|
||||
/**
|
||||
* A subset of [SpecialCircumstance] that are only relevant in a pre-login state and should be
|
||||
* cleared after a successful login.
|
||||
|
||||
@@ -6,6 +6,7 @@ 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
|
||||
import com.x8bit.bitwarden.data.platform.manager.model.SpecialCircumstance
|
||||
import com.x8bit.bitwarden.ui.vault.model.TotpData
|
||||
|
||||
/**
|
||||
* Returns [AutofillSaveItem] when contained in the given [SpecialCircumstance].
|
||||
@@ -13,16 +14,7 @@ import com.x8bit.bitwarden.data.platform.manager.model.SpecialCircumstance
|
||||
fun SpecialCircumstance.toAutofillSaveItemOrNull(): AutofillSaveItem? =
|
||||
when (this) {
|
||||
is SpecialCircumstance.AutofillSave -> this.autofillSaveItem
|
||||
is SpecialCircumstance.AutofillSelection -> null
|
||||
is SpecialCircumstance.PasswordlessRequest -> null
|
||||
is SpecialCircumstance.ShareNewSend -> null
|
||||
SpecialCircumstance.GeneratorShortcut -> null
|
||||
SpecialCircumstance.VaultShortcut -> null
|
||||
is SpecialCircumstance.Fido2Save -> null
|
||||
is SpecialCircumstance.Fido2Assertion -> null
|
||||
is SpecialCircumstance.Fido2GetCredentials -> null
|
||||
is SpecialCircumstance.RegistrationEvent.CompleteRegistration -> null
|
||||
SpecialCircumstance.RegistrationEvent.ExpiredRegistrationLink -> null
|
||||
else -> null
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -30,17 +22,8 @@ fun SpecialCircumstance.toAutofillSaveItemOrNull(): AutofillSaveItem? =
|
||||
*/
|
||||
fun SpecialCircumstance.toAutofillSelectionDataOrNull(): AutofillSelectionData? =
|
||||
when (this) {
|
||||
is SpecialCircumstance.AutofillSave -> null
|
||||
is SpecialCircumstance.AutofillSelection -> this.autofillSelectionData
|
||||
is SpecialCircumstance.PasswordlessRequest -> null
|
||||
is SpecialCircumstance.ShareNewSend -> null
|
||||
SpecialCircumstance.GeneratorShortcut -> null
|
||||
SpecialCircumstance.VaultShortcut -> null
|
||||
is SpecialCircumstance.Fido2Save -> null
|
||||
is SpecialCircumstance.Fido2Assertion -> null
|
||||
is SpecialCircumstance.Fido2GetCredentials -> null
|
||||
is SpecialCircumstance.RegistrationEvent.CompleteRegistration -> null
|
||||
SpecialCircumstance.RegistrationEvent.ExpiredRegistrationLink -> null
|
||||
else -> null
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -69,3 +52,12 @@ fun SpecialCircumstance.toFido2GetCredentialsRequestOrNull(): Fido2GetCredential
|
||||
is SpecialCircumstance.Fido2GetCredentials -> this.fido2GetCredentialsRequest
|
||||
else -> null
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the [TotpData] when contained in the given [SpecialCircumstance].
|
||||
*/
|
||||
fun SpecialCircumstance.toTotpDataOrNull(): TotpData? =
|
||||
when (this) {
|
||||
is SpecialCircumstance.AddTotpLoginItem -> this.data
|
||||
else -> null
|
||||
}
|
||||
|
||||
@@ -1,23 +1,28 @@
|
||||
package com.x8bit.bitwarden.data.platform.processor
|
||||
|
||||
import android.content.Intent
|
||||
import android.content.Context
|
||||
import android.os.Build
|
||||
import android.os.IInterface
|
||||
import android.os.RemoteCallbackList
|
||||
import androidx.core.net.toUri
|
||||
import com.bitwarden.authenticatorbridge.IAuthenticatorBridgeService
|
||||
import com.bitwarden.authenticatorbridge.IAuthenticatorBridgeServiceCallback
|
||||
import com.bitwarden.authenticatorbridge.model.EncryptedAddTotpLoginItemData
|
||||
import com.bitwarden.authenticatorbridge.model.SymmetricEncryptionKeyData
|
||||
import com.bitwarden.authenticatorbridge.model.SymmetricEncryptionKeyFingerprintData
|
||||
import com.bitwarden.authenticatorbridge.util.AUTHENTICATOR_BRIDGE_SDK_VERSION
|
||||
import com.bitwarden.authenticatorbridge.util.decrypt
|
||||
import com.bitwarden.authenticatorbridge.util.encrypt
|
||||
import com.bitwarden.authenticatorbridge.util.toFingerprint
|
||||
import com.bitwarden.authenticatorbridge.util.toSymmetricEncryptionKeyData
|
||||
import com.x8bit.bitwarden.data.auth.manager.AddTotpItemFromAuthenticatorManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.FeatureFlagManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.dispatcher.DispatcherManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.model.FlagKey
|
||||
import com.x8bit.bitwarden.data.platform.repository.AuthenticatorBridgeRepository
|
||||
import com.x8bit.bitwarden.data.platform.util.createAddTotpItemFromAuthenticatorIntent
|
||||
import com.x8bit.bitwarden.data.platform.util.isBuildVersionBelow
|
||||
import com.x8bit.bitwarden.ui.vault.util.getTotpDataOrNull
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@@ -26,10 +31,13 @@ import kotlinx.coroutines.launch
|
||||
*/
|
||||
class AuthenticatorBridgeProcessorImpl(
|
||||
private val authenticatorBridgeRepository: AuthenticatorBridgeRepository,
|
||||
private val addTotpItemFromAuthenticatorManager: AddTotpItemFromAuthenticatorManager,
|
||||
private val featureFlagManager: FeatureFlagManager,
|
||||
dispatcherManager: DispatcherManager,
|
||||
context: Context,
|
||||
) : AuthenticatorBridgeProcessor {
|
||||
|
||||
private val applicationContext = context.applicationContext
|
||||
private val callbacks by lazy { RemoteCallbackList<IAuthenticatorBridgeServiceCallback>() }
|
||||
private val scope by lazy { CoroutineScope(dispatcherManager.default) }
|
||||
|
||||
@@ -101,13 +109,18 @@ class AuthenticatorBridgeProcessorImpl(
|
||||
}
|
||||
}
|
||||
|
||||
override fun createAddTotpLoginItemIntent(): Intent {
|
||||
// TODO: BITAU-112
|
||||
return Intent()
|
||||
}
|
||||
|
||||
override fun setPendingAddTotpLoginItemData(data: EncryptedAddTotpLoginItemData?) {
|
||||
// TODO: BITAU-112
|
||||
override fun startAddTotpLoginItemFlow(data: EncryptedAddTotpLoginItemData): Boolean {
|
||||
val symmetricEncryptionKey = symmetricEncryptionKeyData ?: return false
|
||||
val intent = createAddTotpItemFromAuthenticatorIntent(context = applicationContext)
|
||||
val totpData = data.decrypt(symmetricEncryptionKey)
|
||||
.getOrNull()
|
||||
?.totpUri
|
||||
?.toUri()
|
||||
?.getTotpDataOrNull()
|
||||
?: return false
|
||||
addTotpItemFromAuthenticatorManager.pendingAddTotpLoginItemData = totpData
|
||||
applicationContext.startActivity(intent)
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@ package com.x8bit.bitwarden.data.platform.repository
|
||||
import com.bitwarden.authenticatorbridge.model.SharedAccountData
|
||||
import com.x8bit.bitwarden.data.auth.datasource.disk.AuthDiskSource
|
||||
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
|
||||
import com.x8bit.bitwarden.data.platform.datasource.disk.SettingsDiskSource
|
||||
import com.x8bit.bitwarden.data.vault.datasource.disk.VaultDiskSource
|
||||
import com.x8bit.bitwarden.data.vault.datasource.sdk.VaultSdkSource
|
||||
import com.x8bit.bitwarden.data.vault.repository.VaultRepository
|
||||
@@ -22,7 +21,6 @@ class AuthenticatorBridgeRepositoryImpl(
|
||||
private val vaultRepository: VaultRepository,
|
||||
private val vaultDiskSource: VaultDiskSource,
|
||||
private val vaultSdkSource: VaultSdkSource,
|
||||
private val settingsDiskSource: SettingsDiskSource,
|
||||
) : AuthenticatorBridgeRepository {
|
||||
|
||||
override val authenticatorSyncSymmetricKey: ByteArray?
|
||||
@@ -75,7 +73,7 @@ class AuthenticatorBridgeRepositoryImpl(
|
||||
is VaultUnlockResult.AuthenticationError,
|
||||
VaultUnlockResult.GenericError,
|
||||
VaultUnlockResult.InvalidStateError,
|
||||
-> {
|
||||
-> {
|
||||
// Not being able to unlock the user's vault with the
|
||||
// decrypted unlock key is an unexpected case, but if it does
|
||||
// happen we omit the account from list of shared accounts
|
||||
@@ -97,8 +95,8 @@ class AuthenticatorBridgeRepositoryImpl(
|
||||
val totpUris = vaultDiskSource
|
||||
.getCiphers(userId)
|
||||
.first()
|
||||
// Filter out any ciphers without a totp item:
|
||||
.filter { it.login?.totp != null }
|
||||
// Filter out any ciphers without a totp item and also deleted ciphers:
|
||||
.filter { it.login?.totp != null && it.deletedDate == null }
|
||||
.mapNotNull {
|
||||
// Decrypt each cipher and take just totp codes:
|
||||
vaultSdkSource
|
||||
@@ -111,9 +109,6 @@ class AuthenticatorBridgeRepositoryImpl(
|
||||
?.totp
|
||||
}
|
||||
|
||||
val lastSyncTime =
|
||||
settingsDiskSource.getLastSyncTime(userId) ?: return@mapNotNull null
|
||||
|
||||
// Lock the user's vault if we unlocked it for this operation:
|
||||
if (!isVaultAlreadyUnlocked) {
|
||||
vaultRepository.lockVault(userId)
|
||||
@@ -124,7 +119,6 @@ class AuthenticatorBridgeRepositoryImpl(
|
||||
name = account.name,
|
||||
email = account.email,
|
||||
environmentLabel = account.environment.label,
|
||||
lastSyncTime = lastSyncTime,
|
||||
totpUris = totpUris,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -37,4 +37,11 @@ interface DebugMenuRepository {
|
||||
* Resets the onboarding status to NOT_STARTED for the current active user, if applicable.
|
||||
*/
|
||||
fun resetOnboardingStatusForCurrentUser()
|
||||
|
||||
/**
|
||||
* Manipulates the state to force showing the onboarding carousel.
|
||||
*
|
||||
* @param userStateUpdateTrigger A passable lambda to trigger a user state update.
|
||||
*/
|
||||
fun modifyStateToShowOnboardingCarousel(userStateUpdateTrigger: () -> Unit)
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import com.x8bit.bitwarden.BuildConfig
|
||||
import com.x8bit.bitwarden.data.auth.datasource.disk.AuthDiskSource
|
||||
import com.x8bit.bitwarden.data.auth.datasource.disk.model.OnboardingStatus
|
||||
import com.x8bit.bitwarden.data.platform.datasource.disk.FeatureFlagOverrideDiskSource
|
||||
import com.x8bit.bitwarden.data.platform.datasource.disk.SettingsDiskSource
|
||||
import com.x8bit.bitwarden.data.platform.manager.getFlagValueOrDefault
|
||||
import com.x8bit.bitwarden.data.platform.manager.model.FlagKey
|
||||
import com.x8bit.bitwarden.data.platform.repository.util.bufferedMutableSharedFlow
|
||||
@@ -16,6 +17,7 @@ import kotlinx.coroutines.flow.onSubscription
|
||||
class DebugMenuRepositoryImpl(
|
||||
private val featureFlagOverrideDiskSource: FeatureFlagOverrideDiskSource,
|
||||
private val serverConfigRepository: ServerConfigRepository,
|
||||
private val settingsDiskSource: SettingsDiskSource,
|
||||
private val authDiskSource: AuthDiskSource,
|
||||
) : DebugMenuRepository {
|
||||
|
||||
@@ -53,4 +55,11 @@ class DebugMenuRepositoryImpl(
|
||||
onboardingStatus = OnboardingStatus.NOT_STARTED,
|
||||
)
|
||||
}
|
||||
|
||||
override fun modifyStateToShowOnboardingCarousel(
|
||||
userStateUpdateTrigger: () -> Unit,
|
||||
) {
|
||||
settingsDiskSource.hasUserLoggedInOrCreatedAccount = false
|
||||
userStateUpdateTrigger.invoke()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -160,24 +160,6 @@ interface SettingsRepository {
|
||||
*/
|
||||
val isScreenCaptureAllowedStateFlow: StateFlow<Boolean>
|
||||
|
||||
/**
|
||||
* Returns an observable count of the number of settings items that have a badge to display
|
||||
* for the current active user.
|
||||
*/
|
||||
val allSettingsBadgeCountFlow: StateFlow<Int>
|
||||
|
||||
/**
|
||||
* Returns an observable count of the number of security settings items that have a badge to
|
||||
* display for the current active user.
|
||||
*/
|
||||
val allSecuritySettingsBadgeCountFlow: StateFlow<Int>
|
||||
|
||||
/**
|
||||
* Returns an observable count of the number of autofill settings items that have a badge to
|
||||
* display for the current active user.
|
||||
*/
|
||||
val allAutofillSettingsBadgeCountFlow: StateFlow<Int>
|
||||
|
||||
/**
|
||||
* Disables autofill if it is currently enabled.
|
||||
*/
|
||||
|
||||
@@ -6,7 +6,6 @@ import com.x8bit.bitwarden.BuildConfig
|
||||
import com.x8bit.bitwarden.data.auth.datasource.disk.AuthDiskSource
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.PolicyInformation
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.UserFingerprintResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.util.activeUserIdChangesFlow
|
||||
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
|
||||
@@ -30,8 +29,6 @@ import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.filterNotNull
|
||||
import kotlinx.coroutines.flow.flatMapLatest
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
@@ -340,60 +337,6 @@ class SettingsRepositoryImpl(
|
||||
?: DEFAULT_IS_SCREEN_CAPTURE_ALLOWED,
|
||||
)
|
||||
|
||||
override val allSettingsBadgeCountFlow: StateFlow<Int>
|
||||
get() = combine(
|
||||
allSecuritySettingsBadgeCountFlow,
|
||||
allAutofillSettingsBadgeCountFlow,
|
||||
transform = ::sumSettingsBadgeCount,
|
||||
)
|
||||
.stateIn(
|
||||
scope = unconfinedScope,
|
||||
started = SharingStarted.Lazily,
|
||||
initialValue = 0,
|
||||
)
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
override val allSecuritySettingsBadgeCountFlow: StateFlow<Int>
|
||||
get() = authDiskSource
|
||||
.activeUserIdChangesFlow
|
||||
.filterNotNull()
|
||||
.flatMapLatest {
|
||||
// can be expanded to support multiple security settings
|
||||
getShowUnlockBadgeFlow(userId = it)
|
||||
.map { showUnlockBadge ->
|
||||
listOf(showUnlockBadge)
|
||||
}
|
||||
.map { list ->
|
||||
list.count { badgeOnValue -> badgeOnValue }
|
||||
}
|
||||
}
|
||||
.stateIn(
|
||||
scope = unconfinedScope,
|
||||
started = SharingStarted.Lazily,
|
||||
initialValue = 0,
|
||||
)
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
override val allAutofillSettingsBadgeCountFlow: StateFlow<Int>
|
||||
get() = authDiskSource
|
||||
.activeUserIdChangesFlow
|
||||
.filterNotNull()
|
||||
.flatMapLatest {
|
||||
// Can be expanded to support multiple autofill settings
|
||||
getShowAutofillBadgeFlow(userId = it)
|
||||
.map { showAutofillBadge ->
|
||||
listOf(showAutofillBadge)
|
||||
}
|
||||
.map { list ->
|
||||
list.count { showBadge -> showBadge }
|
||||
}
|
||||
}
|
||||
.stateIn(
|
||||
scope = unconfinedScope,
|
||||
started = SharingStarted.Lazily,
|
||||
initialValue = 0,
|
||||
)
|
||||
|
||||
init {
|
||||
policyManager
|
||||
.getActivePoliciesFlow(type = PolicyTypeJson.MAXIMUM_VAULT_TIMEOUT)
|
||||
@@ -660,10 +603,6 @@ class SettingsRepositoryImpl(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// helper function to sum badge counts from different settings sub-menus.
|
||||
private fun sumSettingsBadgeCount(autoFillBadgeCount: Int, securityBadgeCount: Int) =
|
||||
autoFillBadgeCount + securityBadgeCount
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -48,14 +48,12 @@ object PlatformRepositoryModule {
|
||||
vaultRepository: VaultRepository,
|
||||
vaultDiskSource: VaultDiskSource,
|
||||
vaultSdkSource: VaultSdkSource,
|
||||
settingsDiskSource: SettingsDiskSource,
|
||||
): AuthenticatorBridgeRepository = AuthenticatorBridgeRepositoryImpl(
|
||||
authRepository = authRepository,
|
||||
authDiskSource = authDiskSource,
|
||||
vaultRepository = vaultRepository,
|
||||
vaultDiskSource = vaultDiskSource,
|
||||
vaultSdkSource = vaultSdkSource,
|
||||
settingsDiskSource = settingsDiskSource,
|
||||
)
|
||||
|
||||
@Provides
|
||||
@@ -117,9 +115,11 @@ object PlatformRepositoryModule {
|
||||
featureFlagOverrideDiskSource: FeatureFlagOverrideDiskSource,
|
||||
serverConfigRepository: ServerConfigRepository,
|
||||
authDiskSource: AuthDiskSource,
|
||||
settingsDiskSource: SettingsDiskSource,
|
||||
): DebugMenuRepository = DebugMenuRepositoryImpl(
|
||||
featureFlagOverrideDiskSource = featureFlagOverrideDiskSource,
|
||||
serverConfigRepository = serverConfigRepository,
|
||||
authDiskSource = authDiskSource,
|
||||
settingsDiskSource = settingsDiskSource,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -125,11 +125,11 @@ fun EnvironmentUrlDataJson.toEnvironmentUrls(): Environment =
|
||||
when (this) {
|
||||
EnvironmentUrlDataJson.DEFAULT_US,
|
||||
EnvironmentUrlDataJson.DEFAULT_LEGACY_US,
|
||||
-> Environment.Us
|
||||
-> Environment.Us
|
||||
|
||||
EnvironmentUrlDataJson.DEFAULT_EU,
|
||||
EnvironmentUrlDataJson.DEFAULT_LEGACY_EU,
|
||||
-> Environment.Eu
|
||||
-> Environment.Eu
|
||||
|
||||
else -> Environment.SelfHosted(environmentUrlData = this)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
@file:OmitFromCoverage
|
||||
|
||||
package com.x8bit.bitwarden.data.platform.util
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.Intent.FLAG_ACTIVITY_NEW_TASK
|
||||
import android.content.Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED
|
||||
import android.content.Intent.FLAG_ACTIVITY_SINGLE_TOP
|
||||
import com.x8bit.bitwarden.MainActivity
|
||||
import com.x8bit.bitwarden.data.auth.manager.AddTotpItemFromAuthenticatorManager
|
||||
import com.x8bit.bitwarden.data.platform.annotation.OmitFromCoverage
|
||||
|
||||
private const val ADD_TOTP_ITEM_FROM_AUTHENTICATOR_KEY = "add-totp-item-from-authenticator-key"
|
||||
|
||||
/**
|
||||
* Creates an intent for launching add TOTP item flow from the Authenticator app.
|
||||
*/
|
||||
fun createAddTotpItemFromAuthenticatorIntent(
|
||||
context: Context,
|
||||
): Intent =
|
||||
Intent(
|
||||
context,
|
||||
MainActivity::class.java,
|
||||
)
|
||||
.apply {
|
||||
putExtra(
|
||||
ADD_TOTP_ITEM_FROM_AUTHENTICATOR_KEY,
|
||||
true,
|
||||
)
|
||||
addFlags(FLAG_ACTIVITY_NEW_TASK)
|
||||
addFlags(FLAG_ACTIVITY_SINGLE_TOP)
|
||||
addFlags(FLAG_ACTIVITY_RESET_TASK_IF_NEEDED)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the Intent was started by the Authenticator app to add a TOTP item. The TOTP
|
||||
* item can be found in [AddTotpItemFromAuthenticatorManager].
|
||||
*/
|
||||
fun Intent.isAddTotpLoginItemFromAuthenticator(): Boolean =
|
||||
getBooleanExtra(ADD_TOTP_ITEM_FROM_AUTHENTICATOR_KEY, false)
|
||||
@@ -0,0 +1,51 @@
|
||||
package com.x8bit.bitwarden.data.platform.util
|
||||
|
||||
import com.x8bit.bitwarden.data.platform.datasource.disk.model.ServerConfig
|
||||
import kotlin.text.split
|
||||
import kotlin.text.toIntOrNull
|
||||
|
||||
private const val VERSION_SEPARATOR = "."
|
||||
private const val SUFFIX_SEPARATOR = "-"
|
||||
|
||||
/**
|
||||
* Checks if the server version is greater than another provided version, returns true if it is.
|
||||
*/
|
||||
fun isServerVersionAtLeast(serverConfig: ServerConfig?, version: String): Boolean {
|
||||
val serverVersion = serverConfig
|
||||
?.serverData
|
||||
?.version
|
||||
|
||||
if (serverVersion.isNullOrEmpty() || version.isEmpty()) {
|
||||
return false
|
||||
}
|
||||
|
||||
val serverVersionParts = getVersionComponents(serverVersion)
|
||||
val otherVersionParts = getVersionComponents(version)
|
||||
|
||||
if (serverVersionParts.isNullOrEmpty() || otherVersionParts.isNullOrEmpty()) {
|
||||
return false
|
||||
}
|
||||
|
||||
// Must iterate through all indices to establish if versions are equal
|
||||
for (i in serverVersionParts.indices) {
|
||||
val serverPart = serverVersionParts.getOrNull(i)?.toIntOrNull() ?: 0
|
||||
val otherPart = otherVersionParts.getOrNull(i)?.toIntOrNull() ?: 0
|
||||
|
||||
if (serverPart > otherPart) {
|
||||
return true
|
||||
} else if (serverPart < otherPart) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// Versions are equal
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts the version components from a version string, disregarding any suffixes.
|
||||
*/
|
||||
private fun getVersionComponents(version: String?): List<String>? {
|
||||
val versionComponents = version?.split(SUFFIX_SEPARATOR)?.first()
|
||||
return versionComponents?.split(VERSION_SEPARATOR)
|
||||
}
|
||||
@@ -58,6 +58,21 @@ fun String.getDomainOrNull(resourceCacheManager: ResourceCacheManager): String?
|
||||
.toUriOrNull()
|
||||
?.parseDomainOrNull(resourceCacheManager = resourceCacheManager)
|
||||
|
||||
/**
|
||||
* Returns `true` if the [String] uri has a port, `false` otherwise.
|
||||
*/
|
||||
@OmitFromCoverage
|
||||
fun String.hasPort(): Boolean {
|
||||
val uri = this.toUriOrNull() ?: return false
|
||||
return uri.port != -1
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract the host from this [String] if possible, otherwise return null.
|
||||
*/
|
||||
@OmitFromCoverage
|
||||
fun String.getHostOrNull(): String? = this.toUriOrNull()?.host
|
||||
|
||||
/**
|
||||
* Extract the host with optional port from this [String] if possible, otherwise return null.
|
||||
*/
|
||||
|
||||
@@ -121,7 +121,7 @@ private fun parseDomainNameOrNullInternal(
|
||||
val tldRange: IntRange? = when (largestMatch) {
|
||||
is SuffixMatchType.Exception,
|
||||
is SuffixMatchType.Normal,
|
||||
-> {
|
||||
-> {
|
||||
host.findLastSubstringIndicesOrNull(largestMatch.partialDomain)
|
||||
}
|
||||
|
||||
|
||||
@@ -4,17 +4,20 @@ import android.app.Application
|
||||
import android.content.SharedPreferences
|
||||
import androidx.room.Room
|
||||
import com.x8bit.bitwarden.data.platform.datasource.di.UnencryptedPreferences
|
||||
import com.x8bit.bitwarden.data.platform.manager.DatabaseSchemeManager
|
||||
import com.x8bit.bitwarden.data.tools.generator.datasource.disk.GeneratorDiskSource
|
||||
import com.x8bit.bitwarden.data.tools.generator.datasource.disk.GeneratorDiskSourceImpl
|
||||
import com.x8bit.bitwarden.data.tools.generator.datasource.disk.PasswordHistoryDiskSource
|
||||
import com.x8bit.bitwarden.data.tools.generator.datasource.disk.PasswordHistoryDiskSourceImpl
|
||||
import com.x8bit.bitwarden.data.tools.generator.datasource.disk.dao.PasswordHistoryDao
|
||||
import com.x8bit.bitwarden.data.tools.generator.datasource.disk.database.PasswordHistoryDatabase
|
||||
import com.x8bit.bitwarden.data.vault.datasource.disk.callback.DatabaseSchemeCallback
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import kotlinx.serialization.json.Json
|
||||
import java.time.Clock
|
||||
import javax.inject.Singleton
|
||||
|
||||
/**
|
||||
@@ -45,13 +48,23 @@ object GeneratorDiskModule {
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun providePasswordHistoryDatabase(app: Application): PasswordHistoryDatabase {
|
||||
fun providePasswordHistoryDatabase(
|
||||
app: Application,
|
||||
databaseSchemeManager: DatabaseSchemeManager,
|
||||
clock: Clock,
|
||||
): PasswordHistoryDatabase {
|
||||
return Room
|
||||
.databaseBuilder(
|
||||
context = app,
|
||||
klass = PasswordHistoryDatabase::class.java,
|
||||
name = "passcode_history_database",
|
||||
)
|
||||
.addCallback(
|
||||
DatabaseSchemeCallback(
|
||||
databaseSchemeManager = databaseSchemeManager,
|
||||
clock = clock,
|
||||
),
|
||||
)
|
||||
.build()
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
package com.x8bit.bitwarden.data.vault.datasource.disk.callback
|
||||
|
||||
import androidx.room.RoomDatabase
|
||||
import androidx.sqlite.db.SupportSQLiteDatabase
|
||||
import com.x8bit.bitwarden.data.platform.manager.DatabaseSchemeManager
|
||||
import java.time.Clock
|
||||
|
||||
/**
|
||||
* A [RoomDatabase.Callback] for tracking database scheme changes.
|
||||
*/
|
||||
class DatabaseSchemeCallback(
|
||||
private val databaseSchemeManager: DatabaseSchemeManager,
|
||||
private val clock: Clock,
|
||||
) : RoomDatabase.Callback() {
|
||||
override fun onDestructiveMigration(db: SupportSQLiteDatabase) {
|
||||
databaseSchemeManager.lastDatabaseSchemeChangeInstant = clock.instant()
|
||||
}
|
||||
}
|
||||
@@ -2,9 +2,11 @@ package com.x8bit.bitwarden.data.vault.datasource.disk.di
|
||||
|
||||
import android.app.Application
|
||||
import androidx.room.Room
|
||||
import com.x8bit.bitwarden.data.platform.manager.DatabaseSchemeManager
|
||||
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.disk.VaultDiskSourceImpl
|
||||
import com.x8bit.bitwarden.data.vault.datasource.disk.callback.DatabaseSchemeCallback
|
||||
import com.x8bit.bitwarden.data.vault.datasource.disk.convertor.ZonedDateTimeTypeConverter
|
||||
import com.x8bit.bitwarden.data.vault.datasource.disk.dao.CiphersDao
|
||||
import com.x8bit.bitwarden.data.vault.datasource.disk.dao.CollectionsDao
|
||||
@@ -17,6 +19,7 @@ import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import kotlinx.serialization.json.Json
|
||||
import java.time.Clock
|
||||
import javax.inject.Singleton
|
||||
|
||||
/**
|
||||
@@ -28,7 +31,11 @@ class VaultDiskModule {
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideVaultDatabase(app: Application): VaultDatabase =
|
||||
fun provideVaultDatabase(
|
||||
app: Application,
|
||||
databaseSchemeManager: DatabaseSchemeManager,
|
||||
clock: Clock,
|
||||
): VaultDatabase =
|
||||
Room
|
||||
.databaseBuilder(
|
||||
context = app,
|
||||
@@ -36,6 +43,7 @@ class VaultDiskModule {
|
||||
name = "vault_database",
|
||||
)
|
||||
.fallbackToDestructiveMigration()
|
||||
.addCallback(DatabaseSchemeCallback(databaseSchemeManager, clock))
|
||||
.addTypeConverter(ZonedDateTimeTypeConverter())
|
||||
.build()
|
||||
|
||||
|
||||
@@ -3,7 +3,6 @@ package com.x8bit.bitwarden.data.vault.datasource.sdk
|
||||
import com.bitwarden.core.DateTime
|
||||
import com.bitwarden.core.DerivePinKeyResponse
|
||||
import com.bitwarden.core.InitOrgCryptoRequest
|
||||
import com.bitwarden.core.InitUserCryptoMethod
|
||||
import com.bitwarden.core.InitUserCryptoRequest
|
||||
import com.bitwarden.core.UpdatePasswordResponse
|
||||
import com.bitwarden.crypto.Kdf
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
package com.x8bit.bitwarden.data.vault.datasource.sdk.model
|
||||
|
||||
import com.bitwarden.fido.ClientData
|
||||
import com.bitwarden.fido.Origin
|
||||
import com.bitwarden.vault.CipherView
|
||||
|
||||
/**
|
||||
* Models a FIDO 2 authentication request to the Bitwarden SDK.
|
||||
*
|
||||
* @param userId User whom the credential is being authenticated for.
|
||||
* @param origin Origin of the Relying Party. This can either be a Relying Party's URL or their
|
||||
* application fingerprint.
|
||||
* @param origin Origin of the Relying Party WebAuthn Request.
|
||||
* @param requestJson Authentication request JSON received from the OS.
|
||||
* @param clientData Metadata containing either privileged application certificate hash or Android
|
||||
* package name of the Relying Party.
|
||||
@@ -18,7 +18,7 @@ import com.bitwarden.vault.CipherView
|
||||
*/
|
||||
data class AuthenticateFido2CredentialRequest(
|
||||
val userId: String,
|
||||
val origin: String,
|
||||
val origin: Origin,
|
||||
val requestJson: String,
|
||||
val clientData: ClientData,
|
||||
val selectedCipherView: CipherView,
|
||||
|
||||
@@ -9,6 +9,7 @@ import com.x8bit.bitwarden.data.autofill.util.isActiveWithFido2Credentials
|
||||
import com.x8bit.bitwarden.data.platform.annotation.OmitFromCoverage
|
||||
import com.x8bit.bitwarden.data.vault.datasource.sdk.VaultSdkSource
|
||||
import com.x8bit.bitwarden.data.vault.repository.VaultRepository
|
||||
import com.x8bit.bitwarden.data.vault.repository.model.SyncVaultDataResult
|
||||
|
||||
/**
|
||||
* Primary implementation of [Fido2CredentialStore].
|
||||
@@ -24,7 +25,12 @@ class Fido2CredentialStoreImpl(
|
||||
* Return all active ciphers that contain FIDO 2 credentials.
|
||||
*/
|
||||
override suspend fun allCredentials(): List<CipherView> {
|
||||
vaultRepository.sync()
|
||||
val syncResult = vaultRepository.syncForResult()
|
||||
if (syncResult is SyncVaultDataResult.Error) {
|
||||
syncResult.throwable
|
||||
?.let { throw it }
|
||||
?: throw IllegalStateException("Sync failed.")
|
||||
}
|
||||
return vaultRepository.ciphersStateFlow.value.data
|
||||
?.filter { it.isActiveWithFido2Credentials }
|
||||
?: emptyList()
|
||||
@@ -40,7 +46,12 @@ class Fido2CredentialStoreImpl(
|
||||
override suspend fun findCredentials(ids: List<ByteArray>?, ripId: String): List<CipherView> {
|
||||
val userId = getActiveUserIdOrThrow()
|
||||
|
||||
vaultRepository.sync()
|
||||
val syncResult = vaultRepository.syncForResult()
|
||||
if (syncResult is SyncVaultDataResult.Error) {
|
||||
syncResult.throwable
|
||||
?.let { throw it }
|
||||
?: throw IllegalStateException("Sync failed.")
|
||||
}
|
||||
|
||||
val ciphersWithFido2Credentials = vaultRepository.ciphersStateFlow.value.data
|
||||
?.filter { it.isActiveWithFido2Credentials }
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package com.x8bit.bitwarden.data.vault.datasource.sdk.model
|
||||
|
||||
import com.bitwarden.fido.ClientData
|
||||
import com.bitwarden.fido.Origin
|
||||
import com.bitwarden.vault.CipherView
|
||||
|
||||
/**
|
||||
@@ -18,7 +19,7 @@ import com.bitwarden.vault.CipherView
|
||||
*/
|
||||
data class RegisterFido2CredentialRequest(
|
||||
val userId: String,
|
||||
val origin: String,
|
||||
val origin: Origin,
|
||||
val requestJson: String,
|
||||
val clientData: ClientData,
|
||||
val selectedCipherView: CipherView,
|
||||
|
||||
@@ -489,7 +489,7 @@ class VaultLockManagerImpl(
|
||||
// User no longer active or engaging with the app.
|
||||
CheckTimeoutReason.APP_BACKGROUNDED,
|
||||
CheckTimeoutReason.USER_CHANGED,
|
||||
-> {
|
||||
-> {
|
||||
handleTimeoutActionWithDelay(
|
||||
userId = userId,
|
||||
vaultTimeoutAction = vaultTimeoutAction,
|
||||
|
||||
@@ -24,6 +24,7 @@ import com.x8bit.bitwarden.data.vault.repository.model.ExportVaultDataResult
|
||||
import com.x8bit.bitwarden.data.vault.repository.model.GenerateTotpResult
|
||||
import com.x8bit.bitwarden.data.vault.repository.model.RemovePasswordSendResult
|
||||
import com.x8bit.bitwarden.data.vault.repository.model.SendData
|
||||
import com.x8bit.bitwarden.data.vault.repository.model.SyncVaultDataResult
|
||||
import com.x8bit.bitwarden.data.vault.repository.model.TotpCodeResult
|
||||
import com.x8bit.bitwarden.data.vault.repository.model.UpdateFolderResult
|
||||
import com.x8bit.bitwarden.data.vault.repository.model.UpdateSendResult
|
||||
@@ -116,6 +117,12 @@ interface VaultRepository : CipherManager, VaultLockManager {
|
||||
*/
|
||||
fun syncIfNecessary()
|
||||
|
||||
/**
|
||||
* Syncs the vault data for the current user. This is an explicit request to sync and will
|
||||
* return the result of the sync as a [SyncVaultDataResult].
|
||||
*/
|
||||
suspend fun syncForResult(): SyncVaultDataResult
|
||||
|
||||
/**
|
||||
* Flow that represents the data for a specific vault item as found by ID. This may emit `null`
|
||||
* if the item cannot be found.
|
||||
|
||||
@@ -21,6 +21,7 @@ import com.x8bit.bitwarden.data.auth.repository.util.toUpdatedUserStateJson
|
||||
import com.x8bit.bitwarden.data.auth.repository.util.userSwitchingChangesFlow
|
||||
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.dispatcher.DispatcherManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.model.SyncCipherDeleteData
|
||||
@@ -65,6 +66,7 @@ import com.x8bit.bitwarden.data.vault.repository.model.ExportVaultDataResult
|
||||
import com.x8bit.bitwarden.data.vault.repository.model.GenerateTotpResult
|
||||
import com.x8bit.bitwarden.data.vault.repository.model.RemovePasswordSendResult
|
||||
import com.x8bit.bitwarden.data.vault.repository.model.SendData
|
||||
import com.x8bit.bitwarden.data.vault.repository.model.SyncVaultDataResult
|
||||
import com.x8bit.bitwarden.data.vault.repository.model.TotpCodeResult
|
||||
import com.x8bit.bitwarden.data.vault.repository.model.UpdateFolderResult
|
||||
import com.x8bit.bitwarden.data.vault.repository.model.UpdateSendResult
|
||||
@@ -83,9 +85,11 @@ import com.x8bit.bitwarden.data.vault.repository.util.toEncryptedSdkSend
|
||||
import com.x8bit.bitwarden.data.vault.repository.util.toEncryptedSdkSendList
|
||||
import com.x8bit.bitwarden.ui.vault.feature.vault.model.VaultFilterType
|
||||
import com.x8bit.bitwarden.ui.vault.feature.vault.util.toFilteredList
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
@@ -135,6 +139,7 @@ class VaultRepositoryImpl(
|
||||
private val vaultLockManager: VaultLockManager,
|
||||
private val totpCodeManager: TotpCodeManager,
|
||||
private val userLogoutManager: UserLogoutManager,
|
||||
private val databaseSchemeManager: DatabaseSchemeManager,
|
||||
pushManager: PushManager,
|
||||
private val clock: Clock,
|
||||
dispatcherManager: DispatcherManager,
|
||||
@@ -312,7 +317,6 @@ class VaultRepositoryImpl(
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("LongMethod")
|
||||
override fun sync() {
|
||||
val userId = activeUserId ?: return
|
||||
if (!syncJob.isCompleted) return
|
||||
@@ -321,74 +325,7 @@ class VaultRepositoryImpl(
|
||||
mutableFoldersStateFlow.updateToPendingOrLoading()
|
||||
mutableCollectionsStateFlow.updateToPendingOrLoading()
|
||||
mutableSendDataStateFlow.updateToPendingOrLoading()
|
||||
syncJob = ioScope.launch {
|
||||
val lastSyncInstant = settingsDiskSource
|
||||
.getLastSyncTime(userId = userId)
|
||||
?.toEpochMilli()
|
||||
?: 0
|
||||
|
||||
syncService
|
||||
.getAccountRevisionDateMillis()
|
||||
.fold(
|
||||
onSuccess = { serverRevisionDate ->
|
||||
if (serverRevisionDate < lastSyncInstant) {
|
||||
// We can skip the actual sync call if there is no new data
|
||||
vaultDiskSource.resyncVaultData(userId)
|
||||
settingsDiskSource.storeLastSyncTime(
|
||||
userId = userId,
|
||||
lastSyncTime = clock.instant(),
|
||||
)
|
||||
return@launch
|
||||
}
|
||||
},
|
||||
onFailure = {
|
||||
updateVaultStateFlowsToError(it)
|
||||
return@launch
|
||||
},
|
||||
)
|
||||
|
||||
syncService
|
||||
.sync()
|
||||
.fold(
|
||||
onSuccess = { syncResponse ->
|
||||
val localSecurityStamp =
|
||||
authDiskSource.userState?.activeAccount?.profile?.stamp
|
||||
val serverSecurityStamp = syncResponse.profile.securityStamp
|
||||
|
||||
// Log the user out if the stamps do not match
|
||||
localSecurityStamp?.let {
|
||||
if (serverSecurityStamp != localSecurityStamp) {
|
||||
userLogoutManager.softLogout(userId = userId, isExpired = true)
|
||||
return@launch
|
||||
}
|
||||
}
|
||||
|
||||
// Update user information with additional information from sync response
|
||||
authDiskSource.userState = authDiskSource
|
||||
.userState
|
||||
?.toUpdatedUserStateJson(
|
||||
syncResponse = syncResponse,
|
||||
)
|
||||
|
||||
unlockVaultForOrganizationsIfNecessary(syncResponse = syncResponse)
|
||||
storeProfileData(syncResponse = syncResponse)
|
||||
// Treat absent network policies as known empty data to
|
||||
// distinguish between unknown null data.
|
||||
authDiskSource.storePolicies(
|
||||
userId = userId,
|
||||
policies = syncResponse.policies.orEmpty(),
|
||||
)
|
||||
settingsDiskSource.storeLastSyncTime(
|
||||
userId = userId,
|
||||
lastSyncTime = clock.instant(),
|
||||
)
|
||||
vaultDiskSource.replaceVaultData(userId = userId, vault = syncResponse)
|
||||
},
|
||||
onFailure = { throwable ->
|
||||
updateVaultStateFlowsToError(throwable)
|
||||
},
|
||||
)
|
||||
}
|
||||
syncJob = ioScope.launch { syncInternal(userId) }
|
||||
}
|
||||
|
||||
@Suppress("MagicNumber")
|
||||
@@ -396,15 +333,32 @@ class VaultRepositoryImpl(
|
||||
val userId = activeUserId ?: return
|
||||
val currentInstant = clock.instant()
|
||||
val lastSyncInstant = settingsDiskSource.getLastSyncTime(userId = userId)
|
||||
val lastDatabaseSchemeChangeInstant = databaseSchemeManager.lastDatabaseSchemeChangeInstant
|
||||
|
||||
// Sync if we have never done so or the last time was at last 30 minutes ago
|
||||
// Sync if we have never done so, the last time was at last 30 minutes ago, or the database
|
||||
// scheme changed since the last sync.
|
||||
if (lastSyncInstant == null ||
|
||||
currentInstant.isAfter(lastSyncInstant.plus(30, ChronoUnit.MINUTES))
|
||||
currentInstant.isAfter(lastSyncInstant.plus(30, ChronoUnit.MINUTES)) ||
|
||||
lastDatabaseSchemeChangeInstant?.isAfter(lastSyncInstant) == true
|
||||
) {
|
||||
sync()
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun syncForResult(): SyncVaultDataResult {
|
||||
val userId = activeUserId
|
||||
?: return SyncVaultDataResult.Error(throwable = null)
|
||||
syncJob = ioScope
|
||||
.async { syncInternal(userId) }
|
||||
.also {
|
||||
return try {
|
||||
it.await()
|
||||
} catch (e: CancellationException) {
|
||||
SyncVaultDataResult.Error(throwable = e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun getVaultItemStateFlow(itemId: String): StateFlow<DataState<CipherView?>> =
|
||||
vaultDataStateFlow
|
||||
.map { dataState ->
|
||||
@@ -1355,6 +1309,86 @@ class VaultRepositoryImpl(
|
||||
.onSuccess { vaultDiskSource.saveFolder(userId, it) }
|
||||
}
|
||||
//endregion Push Notification helpers
|
||||
|
||||
@Suppress("LongMethod")
|
||||
private suspend fun syncInternal(userId: String): SyncVaultDataResult {
|
||||
val lastSyncInstant = settingsDiskSource
|
||||
.getLastSyncTime(userId = userId)
|
||||
?.toEpochMilli()
|
||||
?: 0
|
||||
|
||||
val lastDatabaseSchemeChangeInstant = databaseSchemeManager
|
||||
.lastDatabaseSchemeChangeInstant
|
||||
?.toEpochMilli()
|
||||
?: 0
|
||||
|
||||
syncService
|
||||
.getAccountRevisionDateMillis()
|
||||
.fold(
|
||||
onSuccess = { serverRevisionDate ->
|
||||
if (serverRevisionDate < lastSyncInstant &&
|
||||
lastDatabaseSchemeChangeInstant < lastSyncInstant
|
||||
) {
|
||||
// We can skip the actual sync call if there is no new data or database
|
||||
// scheme changes since the last sync.
|
||||
vaultDiskSource.resyncVaultData(userId)
|
||||
settingsDiskSource.storeLastSyncTime(
|
||||
userId = userId,
|
||||
lastSyncTime = clock.instant(),
|
||||
)
|
||||
return SyncVaultDataResult.Success
|
||||
}
|
||||
},
|
||||
onFailure = {
|
||||
updateVaultStateFlowsToError(it)
|
||||
return SyncVaultDataResult.Error(it)
|
||||
},
|
||||
)
|
||||
|
||||
syncService
|
||||
.sync()
|
||||
.fold(
|
||||
onSuccess = { syncResponse ->
|
||||
val localSecurityStamp =
|
||||
authDiskSource.userState?.activeAccount?.profile?.stamp
|
||||
val serverSecurityStamp = syncResponse.profile.securityStamp
|
||||
|
||||
// Log the user out if the stamps do not match
|
||||
localSecurityStamp?.let {
|
||||
if (serverSecurityStamp != localSecurityStamp) {
|
||||
userLogoutManager.softLogout(userId = userId, isExpired = true)
|
||||
return SyncVaultDataResult.Error(throwable = null)
|
||||
}
|
||||
}
|
||||
|
||||
// Update user information with additional information from sync response
|
||||
authDiskSource.userState = authDiskSource
|
||||
.userState
|
||||
?.toUpdatedUserStateJson(
|
||||
syncResponse = syncResponse,
|
||||
)
|
||||
|
||||
unlockVaultForOrganizationsIfNecessary(syncResponse = syncResponse)
|
||||
storeProfileData(syncResponse = syncResponse)
|
||||
// Treat absent network policies as known empty data to
|
||||
// distinguish between unknown null data.
|
||||
authDiskSource.storePolicies(
|
||||
userId = userId,
|
||||
policies = syncResponse.policies.orEmpty(),
|
||||
)
|
||||
settingsDiskSource.storeLastSyncTime(
|
||||
userId = userId,
|
||||
lastSyncTime = clock.instant(),
|
||||
)
|
||||
vaultDiskSource.replaceVaultData(userId = userId, vault = syncResponse)
|
||||
return SyncVaultDataResult.Success
|
||||
},
|
||||
onFailure = { throwable ->
|
||||
updateVaultStateFlowsToError(throwable)
|
||||
return SyncVaultDataResult.Error(throwable)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun <T> Throwable.toNetworkOrErrorState(data: T?): DataState<T> =
|
||||
|
||||
@@ -3,6 +3,7 @@ package com.x8bit.bitwarden.data.vault.repository.di
|
||||
import com.x8bit.bitwarden.data.auth.datasource.disk.AuthDiskSource
|
||||
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.dispatcher.DispatcherManager
|
||||
import com.x8bit.bitwarden.data.vault.datasource.disk.VaultDiskSource
|
||||
@@ -49,6 +50,7 @@ object VaultRepositoryModule {
|
||||
totpCodeManager: TotpCodeManager,
|
||||
pushManager: PushManager,
|
||||
userLogoutManager: UserLogoutManager,
|
||||
databaseSchemeManager: DatabaseSchemeManager,
|
||||
clock: Clock,
|
||||
): VaultRepository = VaultRepositoryImpl(
|
||||
syncService = syncService,
|
||||
@@ -66,6 +68,7 @@ object VaultRepositoryModule {
|
||||
totpCodeManager = totpCodeManager,
|
||||
pushManager = pushManager,
|
||||
userLogoutManager = userLogoutManager,
|
||||
databaseSchemeManager = databaseSchemeManager,
|
||||
clock = clock,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
package com.x8bit.bitwarden.data.vault.repository.model
|
||||
|
||||
/**
|
||||
* Represents the result of a sync operation.
|
||||
*/
|
||||
sealed class SyncVaultDataResult {
|
||||
/**
|
||||
* Indicates a successful sync operation.
|
||||
*/
|
||||
data object Success : SyncVaultDataResult()
|
||||
|
||||
/**
|
||||
* Indicates a failed sync operation.
|
||||
*
|
||||
* @property throwable The exception that caused the failure, if any.
|
||||
*/
|
||||
data class Error(val throwable: Throwable?) : SyncVaultDataResult()
|
||||
}
|
||||
@@ -1,29 +1,79 @@
|
||||
package com.x8bit.bitwarden.ui.auth.feature.accountsetup
|
||||
|
||||
import androidx.lifecycle.SavedStateHandle
|
||||
import androidx.navigation.NavController
|
||||
import androidx.navigation.NavGraphBuilder
|
||||
import androidx.navigation.NavOptions
|
||||
import androidx.navigation.NavType
|
||||
import androidx.navigation.navArgument
|
||||
import com.x8bit.bitwarden.data.platform.annotation.OmitFromCoverage
|
||||
import com.x8bit.bitwarden.ui.platform.base.util.composableWithPushTransitions
|
||||
import com.x8bit.bitwarden.ui.platform.base.util.composableWithSlideTransitions
|
||||
|
||||
/**
|
||||
* Route name for [SetupAutoFillScreen].
|
||||
* Route constant for navigating to the [SetupAutoFillScreen].
|
||||
*/
|
||||
const val SETUP_AUTO_FILL_ROUTE = "setup_auto_fill"
|
||||
private const val SETUP_AUTO_FILL_PREFIX = "setup_auto_fill"
|
||||
private const val SETUP_AUTO_FILL_AS_ROOT_PREFIX = "${SETUP_AUTO_FILL_PREFIX}_as_root"
|
||||
private const val SETUP_AUTO_FILL_NAV_ARG = "isInitialSetup"
|
||||
private const val SETUP_AUTO_FILL_ROUTE = "$SETUP_AUTO_FILL_PREFIX/{$SETUP_AUTO_FILL_NAV_ARG}"
|
||||
const val SETUP_AUTO_FILL_AS_ROOT_ROUTE =
|
||||
"$SETUP_AUTO_FILL_AS_ROOT_PREFIX/{$SETUP_AUTO_FILL_NAV_ARG}"
|
||||
|
||||
/**
|
||||
* Arguments for the [SetupAutoFillScreen] using [SavedStateHandle].
|
||||
*/
|
||||
@OmitFromCoverage
|
||||
data class SetupAutoFillScreenArgs(val isInitialSetup: Boolean) {
|
||||
constructor(savedStateHandle: SavedStateHandle) : this(
|
||||
isInitialSetup = requireNotNull(savedStateHandle[SETUP_AUTO_FILL_NAV_ARG]),
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Navigate to the setup auto-fill screen.
|
||||
*/
|
||||
fun NavController.navigateToSetupAutoFillScreen(navOptions: NavOptions? = null) {
|
||||
this.navigate(SETUP_AUTO_FILL_ROUTE, navOptions)
|
||||
this.navigate("$SETUP_AUTO_FILL_PREFIX/false", navOptions)
|
||||
}
|
||||
|
||||
/**
|
||||
* Navigate to the setup auto-fill screen as the root.
|
||||
*/
|
||||
fun NavController.navigateToSetupAutoFillAsRootScreen(navOptions: NavOptions? = null) {
|
||||
this.navigate("$SETUP_AUTO_FILL_AS_ROOT_PREFIX/true", navOptions)
|
||||
}
|
||||
|
||||
/**
|
||||
* Add the setup auto-fil screen to the nav graph.
|
||||
*/
|
||||
fun NavGraphBuilder.setupAutoFillDestination() {
|
||||
composableWithPushTransitions(
|
||||
fun NavGraphBuilder.setupAutoFillDestination(onNavigateBack: () -> Unit) {
|
||||
composableWithSlideTransitions(
|
||||
route = SETUP_AUTO_FILL_ROUTE,
|
||||
arguments = setupAutofillNavArgs,
|
||||
) {
|
||||
SetupAutoFillScreen()
|
||||
SetupAutoFillScreen(onNavigateBack = onNavigateBack)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add the setup autofil screen to the root nav graph.
|
||||
*/
|
||||
fun NavGraphBuilder.setupAutoFillDestinationAsRoot() {
|
||||
composableWithPushTransitions(
|
||||
route = SETUP_AUTO_FILL_AS_ROOT_ROUTE,
|
||||
arguments = setupAutofillNavArgs,
|
||||
) {
|
||||
SetupAutoFillScreen(
|
||||
onNavigateBack = {
|
||||
// No-Op
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private val setupAutofillNavArgs = listOf(
|
||||
navArgument(SETUP_AUTO_FILL_NAV_ARG) {
|
||||
type = NavType.BoolType
|
||||
},
|
||||
)
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
package com.x8bit.bitwarden.ui.auth.feature.accountsetup
|
||||
|
||||
import android.os.Parcelable
|
||||
import androidx.lifecycle.SavedStateHandle
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.x8bit.bitwarden.data.auth.datasource.disk.model.OnboardingStatus
|
||||
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
|
||||
@@ -10,20 +12,31 @@ import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.parcelize.Parcelize
|
||||
import javax.inject.Inject
|
||||
|
||||
private const val KEY_STATE = "state"
|
||||
|
||||
/**
|
||||
* View model for the Auto-fill setup screen.
|
||||
*/
|
||||
@HiltViewModel
|
||||
class SetupAutoFillViewModel @Inject constructor(
|
||||
savedStateHandle: SavedStateHandle,
|
||||
private val settingsRepository: SettingsRepository,
|
||||
private val authRepository: AuthRepository,
|
||||
) :
|
||||
BaseViewModel<SetupAutoFillState, SetupAutoFillEvent, SetupAutoFillAction>(
|
||||
initialState = run {
|
||||
// We load the state from the savedStateHandle for testing purposes.
|
||||
initialState = savedStateHandle[KEY_STATE] ?: run {
|
||||
val userId = requireNotNull(authRepository.userStateFlow.value).activeUserId
|
||||
SetupAutoFillState(userId = userId, dialogState = null, autofillEnabled = false)
|
||||
val isInitialSetup = SetupAutoFillScreenArgs(savedStateHandle).isInitialSetup
|
||||
SetupAutoFillState(
|
||||
userId = userId,
|
||||
dialogState = null,
|
||||
autofillEnabled = false,
|
||||
isInitialSetup = isInitialSetup,
|
||||
)
|
||||
},
|
||||
) {
|
||||
|
||||
@@ -48,9 +61,15 @@ class SetupAutoFillViewModel @Inject constructor(
|
||||
is SetupAutoFillAction.Internal.AutofillEnabledUpdateReceive -> {
|
||||
handleAutofillEnabledUpdateReceive(action)
|
||||
}
|
||||
|
||||
SetupAutoFillAction.CloseClick -> handleCloseClick()
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleCloseClick() {
|
||||
sendEvent(SetupAutoFillEvent.NavigateBack)
|
||||
}
|
||||
|
||||
private fun handleAutofillEnabledUpdateReceive(
|
||||
action: SetupAutoFillAction.Internal.AutofillEnabledUpdateReceive,
|
||||
) {
|
||||
@@ -83,7 +102,11 @@ class SetupAutoFillViewModel @Inject constructor(
|
||||
}
|
||||
|
||||
private fun handleContinueClick() {
|
||||
updateOnboardingStatusToNextStep()
|
||||
if (state.isInitialSetup) {
|
||||
updateOnboardingStatusToNextStep()
|
||||
} else {
|
||||
sendEvent(SetupAutoFillEvent.NavigateBack)
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleAutofillServiceChanged(action: SetupAutoFillAction.AutofillServiceChanged) {
|
||||
@@ -105,24 +128,28 @@ class SetupAutoFillViewModel @Inject constructor(
|
||||
/**
|
||||
* UI State for the Auto-fill setup screen.
|
||||
*/
|
||||
@Parcelize
|
||||
data class SetupAutoFillState(
|
||||
val userId: String,
|
||||
val dialogState: SetupAutoFillDialogState?,
|
||||
val autofillEnabled: Boolean,
|
||||
)
|
||||
val isInitialSetup: Boolean,
|
||||
) : Parcelable
|
||||
|
||||
/**
|
||||
* Dialog states for the Auto-fill setup screen.
|
||||
*/
|
||||
sealed class SetupAutoFillDialogState {
|
||||
sealed class SetupAutoFillDialogState : Parcelable {
|
||||
/**
|
||||
* Represents the turn on later dialog.
|
||||
*/
|
||||
@Parcelize
|
||||
data object TurnOnLaterDialog : SetupAutoFillDialogState()
|
||||
|
||||
/**
|
||||
* Represents the autofill fallback dialog.
|
||||
*/
|
||||
@Parcelize
|
||||
data object AutoFillFallbackDialog : SetupAutoFillDialogState()
|
||||
}
|
||||
|
||||
@@ -135,6 +162,11 @@ sealed class SetupAutoFillEvent {
|
||||
* Navigate to the autofill settings screen.
|
||||
*/
|
||||
data object NavigateToAutofillSettings : SetupAutoFillEvent()
|
||||
|
||||
/**
|
||||
* Navigate back.
|
||||
*/
|
||||
data object NavigateBack : SetupAutoFillEvent()
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -173,6 +205,11 @@ sealed class SetupAutoFillAction {
|
||||
*/
|
||||
data object AutoFillServiceFallback : SetupAutoFillAction()
|
||||
|
||||
/**
|
||||
* The user has clicked the close button.
|
||||
*/
|
||||
data object CloseClick : SetupAutoFillAction()
|
||||
|
||||
/**
|
||||
* Internal actions not send through UI.
|
||||
*/
|
||||
|
||||
@@ -15,11 +15,11 @@ import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TopAppBarDefaults
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
@@ -38,6 +38,7 @@ 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
|
||||
@@ -46,6 +47,7 @@ import com.x8bit.bitwarden.ui.platform.components.dialog.BitwardenTwoButtonDialo
|
||||
import com.x8bit.bitwarden.ui.platform.components.image.BitwardenGifImage
|
||||
import com.x8bit.bitwarden.ui.platform.components.scaffold.BitwardenScaffold
|
||||
import com.x8bit.bitwarden.ui.platform.components.toggle.BitwardenWideSwitch
|
||||
import com.x8bit.bitwarden.ui.platform.components.util.rememberVectorPainter
|
||||
import com.x8bit.bitwarden.ui.platform.composition.LocalIntentManager
|
||||
import com.x8bit.bitwarden.ui.platform.manager.intent.IntentManager
|
||||
import com.x8bit.bitwarden.ui.platform.theme.BitwardenTheme
|
||||
@@ -58,6 +60,7 @@ import com.x8bit.bitwarden.ui.platform.util.isPortrait
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun SetupAutoFillScreen(
|
||||
onNavigateBack: () -> Unit,
|
||||
intentManager: IntentManager = LocalIntentManager.current,
|
||||
viewModel: SetupAutoFillViewModel = hiltViewModel(),
|
||||
) {
|
||||
@@ -71,6 +74,8 @@ fun SetupAutoFillScreen(
|
||||
handler.sendAutoFillServiceFallback.invoke()
|
||||
}
|
||||
}
|
||||
|
||||
SetupAutoFillEvent.NavigateBack -> onNavigateBack()
|
||||
}
|
||||
}
|
||||
when (state.dialogState) {
|
||||
@@ -106,14 +111,32 @@ fun SetupAutoFillScreen(
|
||||
.nestedScroll(scrollBehavior.nestedScrollConnection),
|
||||
topBar = {
|
||||
BitwardenTopAppBar(
|
||||
title = stringResource(id = R.string.account_setup),
|
||||
title = stringResource(
|
||||
id = if (state.isInitialSetup) {
|
||||
R.string.account_setup
|
||||
} else {
|
||||
R.string.turn_on_autofill
|
||||
},
|
||||
),
|
||||
scrollBehavior = scrollBehavior,
|
||||
navigationIcon = null,
|
||||
navigationIcon = if (state.isInitialSetup) {
|
||||
null
|
||||
} else {
|
||||
NavigationIcon(
|
||||
navigationIcon = rememberVectorPainter(id = R.drawable.ic_close),
|
||||
navigationIconContentDescription = stringResource(id = R.string.close),
|
||||
onNavigationIconClick = remember(viewModel) {
|
||||
{
|
||||
viewModel.trySendAction(SetupAutoFillAction.CloseClick)
|
||||
}
|
||||
},
|
||||
)
|
||||
},
|
||||
)
|
||||
},
|
||||
) { innerPadding ->
|
||||
SetupAutoFillContent(
|
||||
autofillEnabled = state.autofillEnabled,
|
||||
state = state,
|
||||
onAutofillServiceChanged = { handler.onAutofillServiceChanged(it) },
|
||||
onContinueClick = handler.onContinueClick,
|
||||
onTurnOnLaterClick = handler.onTurnOnLaterClick,
|
||||
@@ -128,7 +151,7 @@ fun SetupAutoFillScreen(
|
||||
@Suppress("LongMethod")
|
||||
@Composable
|
||||
private fun SetupAutoFillContent(
|
||||
autofillEnabled: Boolean,
|
||||
state: SetupAutoFillState,
|
||||
onAutofillServiceChanged: (Boolean) -> Unit,
|
||||
onContinueClick: () -> Unit,
|
||||
onTurnOnLaterClick: () -> Unit,
|
||||
@@ -148,7 +171,7 @@ private fun SetupAutoFillContent(
|
||||
label = stringResource(
|
||||
R.string.autofill_services,
|
||||
),
|
||||
isChecked = autofillEnabled,
|
||||
isChecked = state.autofillEnabled,
|
||||
onCheckedChange = onAutofillServiceChanged,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
@@ -163,13 +186,15 @@ private fun SetupAutoFillContent(
|
||||
.standardHorizontalMargin(),
|
||||
)
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
BitwardenTextButton(
|
||||
label = stringResource(R.string.turn_on_later),
|
||||
onClick = onTurnOnLaterClick,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.standardHorizontalMargin(),
|
||||
)
|
||||
if (state.isInitialSetup) {
|
||||
BitwardenTextButton(
|
||||
label = stringResource(R.string.turn_on_later),
|
||||
onClick = onTurnOnLaterClick,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.standardHorizontalMargin(),
|
||||
)
|
||||
}
|
||||
Spacer(modifier = Modifier.navigationBarsPadding())
|
||||
}
|
||||
}
|
||||
@@ -220,15 +245,15 @@ private fun OrderedHeaderContent() {
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(R.string.turn_on_autofill),
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
color = MaterialTheme.colorScheme.onSurface,
|
||||
style = BitwardenTheme.typography.titleMedium,
|
||||
color = BitwardenTheme.colorScheme.text.primary,
|
||||
textAlign = TextAlign.Center,
|
||||
)
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
Text(
|
||||
text = stringResource(R.string.use_autofill_to_log_into_your_accounts),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
style = BitwardenTheme.typography.bodyMedium,
|
||||
color = BitwardenTheme.colorScheme.text.primary,
|
||||
textAlign = TextAlign.Center,
|
||||
// Apply similar line breaks to design
|
||||
modifier = Modifier.sizeIn(maxWidth = 300.dp),
|
||||
@@ -241,7 +266,12 @@ private fun OrderedHeaderContent() {
|
||||
private fun SetupAutoFillContentDisabled_preview() {
|
||||
BitwardenTheme {
|
||||
SetupAutoFillContent(
|
||||
autofillEnabled = false,
|
||||
state = SetupAutoFillState(
|
||||
userId = "disputationi",
|
||||
dialogState = null,
|
||||
autofillEnabled = false,
|
||||
isInitialSetup = true,
|
||||
),
|
||||
onAutofillServiceChanged = {},
|
||||
onContinueClick = {},
|
||||
onTurnOnLaterClick = {},
|
||||
@@ -254,7 +284,12 @@ private fun SetupAutoFillContentDisabled_preview() {
|
||||
private fun SetupAutoFillContentEnabled_preview() {
|
||||
BitwardenTheme {
|
||||
SetupAutoFillContent(
|
||||
autofillEnabled = true,
|
||||
state = SetupAutoFillState(
|
||||
userId = "disputationi",
|
||||
dialogState = null,
|
||||
autofillEnabled = true,
|
||||
isInitialSetup = true,
|
||||
),
|
||||
onAutofillServiceChanged = {},
|
||||
onContinueClick = {},
|
||||
onTurnOnLaterClick = {},
|
||||
|
||||
@@ -8,10 +8,10 @@ import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TopAppBarDefaults
|
||||
@@ -86,13 +86,14 @@ private fun SetupCompleteContent(
|
||||
contentDescription = null,
|
||||
modifier = Modifier
|
||||
.align(CenterHorizontally)
|
||||
.standardHorizontalMargin(),
|
||||
.standardHorizontalMargin()
|
||||
.size(size = 100.dp),
|
||||
)
|
||||
Spacer(Modifier.height(24.dp))
|
||||
Text(
|
||||
text = stringResource(R.string.youre_all_set),
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
color = MaterialTheme.colorScheme.onSurface,
|
||||
style = BitwardenTheme.typography.titleMedium,
|
||||
color = BitwardenTheme.colorScheme.text.primary,
|
||||
textAlign = TextAlign.Center,
|
||||
modifier = Modifier
|
||||
.align(CenterHorizontally)
|
||||
@@ -101,8 +102,8 @@ private fun SetupCompleteContent(
|
||||
Spacer(Modifier.height(8.dp))
|
||||
Text(
|
||||
text = stringResource(R.string.what_bitwarden_has_to_offer),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
style = BitwardenTheme.typography.bodyMedium,
|
||||
color = BitwardenTheme.colorScheme.text.primary,
|
||||
textAlign = TextAlign.Center,
|
||||
modifier = Modifier
|
||||
.align(CenterHorizontally)
|
||||
|
||||
@@ -1,29 +1,88 @@
|
||||
package com.x8bit.bitwarden.ui.auth.feature.accountsetup
|
||||
|
||||
import androidx.lifecycle.SavedStateHandle
|
||||
import androidx.navigation.NavController
|
||||
import androidx.navigation.NavGraphBuilder
|
||||
import androidx.navigation.NavOptions
|
||||
import androidx.navigation.NavType
|
||||
import androidx.navigation.navArgument
|
||||
import com.x8bit.bitwarden.data.platform.annotation.OmitFromCoverage
|
||||
import com.x8bit.bitwarden.ui.platform.base.util.composableWithPushTransitions
|
||||
import com.x8bit.bitwarden.ui.platform.base.util.composableWithSlideTransitions
|
||||
|
||||
/**
|
||||
* Route for [SetupUnlockScreen]
|
||||
* Route constants for [SetupUnlockScreen]
|
||||
*/
|
||||
const val SETUP_UNLOCK_ROUTE = "setup_unlock"
|
||||
private const val SETUP_UNLOCK_PREFIX = "setup_unlock"
|
||||
private const val SETUP_UNLOCK_AS_ROOT_PREFIX = "${SETUP_UNLOCK_PREFIX}_as_root"
|
||||
private const val SETUP_UNLOCK_INITIAL_SETUP_ARG = "isInitialSetup"
|
||||
const val SETUP_UNLOCK_AS_ROOT_ROUTE = "$SETUP_UNLOCK_AS_ROOT_PREFIX/" +
|
||||
"{$SETUP_UNLOCK_INITIAL_SETUP_ARG}"
|
||||
private const val SETUP_UNLOCK_ROUTE = "$SETUP_UNLOCK_PREFIX/{$SETUP_UNLOCK_INITIAL_SETUP_ARG}"
|
||||
|
||||
/**
|
||||
* Class to retrieve setup unlock arguments from the [SavedStateHandle].
|
||||
*/
|
||||
@OmitFromCoverage
|
||||
data class SetupUnlockArgs(
|
||||
val isInitialSetup: Boolean,
|
||||
) {
|
||||
constructor(savedStateHandle: SavedStateHandle) : this(
|
||||
isInitialSetup = requireNotNull(savedStateHandle[SETUP_UNLOCK_INITIAL_SETUP_ARG]),
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Navigate to the setup unlock screen.
|
||||
*/
|
||||
fun NavController.navigateToSetupUnlockScreen(navOptions: NavOptions? = null) {
|
||||
this.navigate(SETUP_UNLOCK_ROUTE, navOptions)
|
||||
this.navigate("$SETUP_UNLOCK_PREFIX/false", navOptions)
|
||||
}
|
||||
|
||||
/**
|
||||
* Add the setup unlock screen to the nav graph.
|
||||
* Navigate to the setup unlock screen as root.
|
||||
*/
|
||||
fun NavGraphBuilder.setupUnlockDestination() {
|
||||
composableWithPushTransitions(
|
||||
fun NavController.navigateToSetupUnlockScreenAsRoot(navOptions: NavOptions? = null) {
|
||||
this.navigate("$SETUP_UNLOCK_AS_ROOT_PREFIX/true", navOptions)
|
||||
}
|
||||
|
||||
/**
|
||||
* Add the setup unlock screen to a nav graph.
|
||||
*/
|
||||
fun NavGraphBuilder.setupUnlockDestination(
|
||||
onNavigateBack: () -> Unit,
|
||||
) {
|
||||
composableWithSlideTransitions(
|
||||
route = SETUP_UNLOCK_ROUTE,
|
||||
arguments = setupUnlockArguments,
|
||||
) {
|
||||
SetupUnlockScreen()
|
||||
SetupUnlockScreen(
|
||||
onNavigateBack = onNavigateBack,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add the setup unlock screen to the root nav graph.
|
||||
*/
|
||||
fun NavGraphBuilder.setupUnlockDestinationAsRoot() {
|
||||
composableWithPushTransitions(
|
||||
route = SETUP_UNLOCK_AS_ROOT_ROUTE,
|
||||
arguments = setupUnlockArguments,
|
||||
) {
|
||||
SetupUnlockScreen(
|
||||
onNavigateBack = {
|
||||
// No-Op
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private val setupUnlockArguments = listOf(
|
||||
navArgument(
|
||||
name = SETUP_UNLOCK_INITIAL_SETUP_ARG,
|
||||
builder = {
|
||||
type = NavType.BoolType
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
@@ -16,7 +16,6 @@ import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TopAppBarDefaults
|
||||
import androidx.compose.material3.rememberTopAppBarState
|
||||
@@ -41,6 +40,7 @@ import com.x8bit.bitwarden.ui.auth.feature.accountsetup.handlers.SetupUnlockHand
|
||||
import com.x8bit.bitwarden.ui.platform.base.util.EventsEffect
|
||||
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
|
||||
@@ -54,16 +54,19 @@ import com.x8bit.bitwarden.ui.platform.components.toggle.BitwardenUnlockWithPinS
|
||||
import com.x8bit.bitwarden.ui.platform.components.util.rememberVectorPainter
|
||||
import com.x8bit.bitwarden.ui.platform.composition.LocalBiometricsManager
|
||||
import com.x8bit.bitwarden.ui.platform.manager.biometrics.BiometricsManager
|
||||
import com.x8bit.bitwarden.ui.platform.theme.BitwardenTheme
|
||||
import com.x8bit.bitwarden.ui.platform.util.isPortrait
|
||||
|
||||
/**
|
||||
* Top level composable for the setup unlock screen.
|
||||
*/
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Suppress("LongMethod")
|
||||
@Composable
|
||||
fun SetupUnlockScreen(
|
||||
viewModel: SetupUnlockViewModel = hiltViewModel(),
|
||||
biometricsManager: BiometricsManager = LocalBiometricsManager.current,
|
||||
onNavigateBack: () -> Unit,
|
||||
) {
|
||||
val state by viewModel.stateFlow.collectAsStateWithLifecycle()
|
||||
val handler = remember(viewModel) { SetupUnlockHandler.create(viewModel = viewModel) }
|
||||
@@ -83,6 +86,8 @@ fun SetupUnlockScreen(
|
||||
cipher = event.cipher,
|
||||
)
|
||||
}
|
||||
|
||||
SetupUnlockEvent.NavigateBack -> onNavigateBack()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -100,9 +105,27 @@ fun SetupUnlockScreen(
|
||||
.nestedScroll(scrollBehavior.nestedScrollConnection),
|
||||
topBar = {
|
||||
BitwardenTopAppBar(
|
||||
title = stringResource(id = R.string.account_setup),
|
||||
title = stringResource(
|
||||
id = if (state.isInitialSetup) {
|
||||
R.string.account_setup
|
||||
} else {
|
||||
R.string.set_up_unlock
|
||||
},
|
||||
),
|
||||
scrollBehavior = scrollBehavior,
|
||||
navigationIcon = null,
|
||||
navigationIcon = if (state.isInitialSetup) {
|
||||
null
|
||||
} else {
|
||||
NavigationIcon(
|
||||
navigationIcon = rememberVectorPainter(id = R.drawable.ic_close),
|
||||
navigationIconContentDescription = stringResource(id = R.string.close),
|
||||
onNavigationIconClick = remember(viewModel) {
|
||||
{
|
||||
viewModel.trySendAction(SetupUnlockAction.CloseClick)
|
||||
}
|
||||
},
|
||||
)
|
||||
},
|
||||
)
|
||||
},
|
||||
) { innerPadding ->
|
||||
@@ -138,7 +161,7 @@ private fun SetupUnlockScreenContent(
|
||||
|
||||
Spacer(modifier = Modifier.height(height = 24.dp))
|
||||
BitwardenUnlockWithBiometricsSwitch(
|
||||
isBiometricsSupported = biometricsManager.isBiometricsSupported,
|
||||
biometricSupportStatus = biometricsManager.biometricSupportStatus,
|
||||
isChecked = state.isUnlockWithBiometricsEnabled || showBiometricsPrompt,
|
||||
onDisableBiometrics = handler.onDisableBiometrics,
|
||||
onEnableBiometrics = handler.onEnableBiometrics,
|
||||
@@ -169,14 +192,16 @@ private fun SetupUnlockScreenContent(
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(height = 12.dp))
|
||||
SetUpLaterButton(
|
||||
onConfirmClick = handler.onSetUpLaterClick,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.standardHorizontalMargin(),
|
||||
)
|
||||
if (state.isInitialSetup) {
|
||||
SetUpLaterButton(
|
||||
onConfirmClick = handler.onSetUpLaterClick,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.standardHorizontalMargin(),
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(height = 12.dp))
|
||||
Spacer(modifier = Modifier.height(height = 12.dp))
|
||||
}
|
||||
Spacer(modifier = Modifier.navigationBarsPadding())
|
||||
}
|
||||
}
|
||||
@@ -227,8 +252,8 @@ private fun ColumnScope.SetupUnlockHeaderPortrait() {
|
||||
Spacer(modifier = Modifier.height(height = 24.dp))
|
||||
Text(
|
||||
text = stringResource(id = R.string.set_up_unlock),
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
color = MaterialTheme.colorScheme.onSurface,
|
||||
style = BitwardenTheme.typography.titleMedium,
|
||||
color = BitwardenTheme.colorScheme.text.primary,
|
||||
textAlign = TextAlign.Center,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
@@ -241,8 +266,8 @@ private fun ColumnScope.SetupUnlockHeaderPortrait() {
|
||||
text = stringResource(
|
||||
id = R.string.set_up_biometrics_or_choose_a_pin_code_to_quickly_access_your_vault_and_autofill_your_logins,
|
||||
),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurface,
|
||||
style = BitwardenTheme.typography.bodyMedium,
|
||||
color = BitwardenTheme.colorScheme.text.primary,
|
||||
textAlign = TextAlign.Center,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
@@ -273,8 +298,8 @@ private fun SetupUnlockHeaderLandscape(
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(id = R.string.set_up_unlock),
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
color = MaterialTheme.colorScheme.onSurface,
|
||||
style = BitwardenTheme.typography.titleMedium,
|
||||
color = BitwardenTheme.colorScheme.text.primary,
|
||||
textAlign = TextAlign.Center,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
)
|
||||
@@ -285,8 +310,8 @@ private fun SetupUnlockHeaderLandscape(
|
||||
text = stringResource(
|
||||
id = R.string.set_up_biometrics_or_choose_a_pin_code_to_quickly_access_your_vault_and_autofill_your_logins,
|
||||
),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurface,
|
||||
style = BitwardenTheme.typography.bodyMedium,
|
||||
color = BitwardenTheme.colorScheme.text.primary,
|
||||
textAlign = TextAlign.Center,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
)
|
||||
|
||||
@@ -25,6 +25,7 @@ private const val KEY_STATE = "state"
|
||||
/**
|
||||
* Models logic for the setup unlock screen.
|
||||
*/
|
||||
@Suppress("TooManyFunctions")
|
||||
@HiltViewModel
|
||||
class SetupUnlockViewModel @Inject constructor(
|
||||
savedStateHandle: SavedStateHandle,
|
||||
@@ -32,12 +33,15 @@ class SetupUnlockViewModel @Inject constructor(
|
||||
private val settingsRepository: SettingsRepository,
|
||||
private val biometricsEncryptionManager: BiometricsEncryptionManager,
|
||||
) : BaseViewModel<SetupUnlockState, SetupUnlockEvent, SetupUnlockAction>(
|
||||
// We load the state from the savedStateHandle for testing purposes.
|
||||
initialState = savedStateHandle[KEY_STATE] ?: run {
|
||||
val userId = requireNotNull(authRepository.userStateFlow.value).activeUserId
|
||||
val isBiometricsValid = biometricsEncryptionManager.isBiometricIntegrityValid(
|
||||
userId = userId,
|
||||
cipher = biometricsEncryptionManager.getOrCreateCipher(userId = userId),
|
||||
)
|
||||
// whether or not the user has completed the initial setup prior to this.
|
||||
val isInitialSetup = SetupUnlockArgs(savedStateHandle).isInitialSetup
|
||||
SetupUnlockState(
|
||||
userId = userId,
|
||||
isUnlockWithPasswordEnabled = authRepository
|
||||
@@ -49,6 +53,7 @@ class SetupUnlockViewModel @Inject constructor(
|
||||
isUnlockWithBiometricsEnabled = settingsRepository.isUnlockWithBiometricsEnabled &&
|
||||
isBiometricsValid,
|
||||
dialogState = null,
|
||||
isInitialSetup = isInitialSetup,
|
||||
)
|
||||
},
|
||||
) {
|
||||
@@ -64,11 +69,20 @@ class SetupUnlockViewModel @Inject constructor(
|
||||
|
||||
is SetupUnlockAction.UnlockWithPinToggle -> handleUnlockWithPinToggle(action)
|
||||
is SetupUnlockAction.Internal -> handleInternalActions(action)
|
||||
SetupUnlockAction.CloseClick -> handleCloseClick()
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleCloseClick() {
|
||||
sendEvent(SetupUnlockEvent.NavigateBack)
|
||||
}
|
||||
|
||||
private fun handleContinueClick() {
|
||||
updateOnboardingStatusToNextStep()
|
||||
if (state.isInitialSetup) {
|
||||
updateOnboardingStatusToNextStep()
|
||||
} else {
|
||||
sendEvent(SetupUnlockEvent.NavigateBack)
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleEnableBiometricsClick() {
|
||||
@@ -196,6 +210,7 @@ data class SetupUnlockState(
|
||||
val isUnlockWithPinEnabled: Boolean,
|
||||
val isUnlockWithBiometricsEnabled: Boolean,
|
||||
val dialogState: DialogState?,
|
||||
val isInitialSetup: Boolean,
|
||||
) : Parcelable {
|
||||
/**
|
||||
* Indicates whether the continue button should be enabled or disabled.
|
||||
@@ -237,6 +252,11 @@ sealed class SetupUnlockEvent {
|
||||
data class ShowBiometricsPrompt(
|
||||
val cipher: Cipher,
|
||||
) : SetupUnlockEvent()
|
||||
|
||||
/**
|
||||
* Navigates back to the previous screen.
|
||||
*/
|
||||
data object NavigateBack : SetupUnlockEvent()
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -277,6 +297,11 @@ sealed class SetupUnlockAction {
|
||||
*/
|
||||
data object DismissDialog : SetupUnlockAction()
|
||||
|
||||
/**
|
||||
* The user has clicked the close button.
|
||||
*/
|
||||
data object CloseClick : SetupUnlockAction()
|
||||
|
||||
/**
|
||||
* Models actions that can be sent by the view model itself.
|
||||
*/
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user