mirror of
https://github.com/bitwarden/android.git
synced 2026-05-12 23:01:13 -05:00
Compare commits
179 Commits
pm-6702/ad
...
v2024.9.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
853f25bf57 | ||
|
|
70e75b910c | ||
|
|
aaa541dd19 | ||
|
|
80c1c26f5c | ||
|
|
f349a72f72 | ||
|
|
21e1d8b5bc | ||
|
|
9213b4843f | ||
|
|
dad501d37e | ||
|
|
61b09fe8f1 | ||
|
|
415aafb5e9 | ||
|
|
251d13a832 | ||
|
|
40dd0e9776 | ||
|
|
ad154b6c91 | ||
|
|
caa0c613a5 | ||
|
|
6908111377 | ||
|
|
0f009943b5 | ||
|
|
4f34f6da21 | ||
|
|
3d0dd2996e | ||
|
|
774b828cb0 | ||
|
|
071a5330e6 | ||
|
|
86bbf13175 | ||
|
|
87e223bc59 | ||
|
|
190f92ec67 | ||
|
|
ee7d00247e | ||
|
|
f26374aae7 | ||
|
|
f68b4df9f9 | ||
|
|
05d6c2f61e | ||
|
|
8fe637d207 | ||
|
|
ad8702a34f | ||
|
|
bcdbea13bf | ||
|
|
37b5dc7de3 | ||
|
|
39cc24c13a | ||
|
|
d0c2bb5b7e | ||
|
|
5667a1cfd0 | ||
|
|
3238279290 | ||
|
|
73e158afd0 | ||
|
|
21b41fd4db | ||
|
|
7fd16c9e10 | ||
|
|
cb1e4f6b39 | ||
|
|
71d3ae9ee8 | ||
|
|
c8b1b71960 | ||
|
|
384be79838 | ||
|
|
cdf8b49d61 | ||
|
|
84f92f1b13 | ||
|
|
2068948035 | ||
|
|
f89b053d2e | ||
|
|
4b53358c67 | ||
|
|
2721f7293c | ||
|
|
1c2ccd5305 | ||
|
|
4f55d622cb | ||
|
|
74ae39a665 | ||
|
|
37f1da1ec2 | ||
|
|
503c966177 | ||
|
|
76cc9c8579 | ||
|
|
ed0494e159 | ||
|
|
fd8eda75b5 | ||
|
|
765a9ebd74 | ||
|
|
39df103f12 | ||
|
|
135a48b3c0 | ||
|
|
368ca49fd6 | ||
|
|
759e926588 | ||
|
|
3ecf1382b2 | ||
|
|
190ba792a1 | ||
|
|
d1f21d3585 | ||
|
|
4c1d55e9fe | ||
|
|
f544ccc3ef | ||
|
|
537c501b9b | ||
|
|
2d3a47f3d1 | ||
|
|
6521848a8d | ||
|
|
fe1f897d64 | ||
|
|
a5726fb72b | ||
|
|
8f30742908 | ||
|
|
98bcff5e06 | ||
|
|
19596ea4c3 | ||
|
|
8dce8cd576 | ||
|
|
b94a1adda9 | ||
|
|
8489693d84 | ||
|
|
647b3e921b | ||
|
|
4e69ed57e8 | ||
|
|
95240a7ce3 | ||
|
|
19facaf8fd | ||
|
|
bef05d5ed9 | ||
|
|
fa93985a2e | ||
|
|
b7544ef7f4 | ||
|
|
fa2d7e0218 | ||
|
|
c817253760 | ||
|
|
eb4e2ab31f | ||
|
|
705153ea21 | ||
|
|
47fe78536f | ||
|
|
3f78ad6d6d | ||
|
|
e468ec695b | ||
|
|
ae349183e8 | ||
|
|
e039d5c3fb | ||
|
|
b1ecf125d1 | ||
|
|
69ca7649e2 | ||
|
|
22958aa5ed | ||
|
|
7ebbe700f0 | ||
|
|
de48bb37d7 | ||
|
|
1dd19a8b2d | ||
|
|
733053b548 | ||
|
|
f10a2b15ba | ||
|
|
c02afb6714 | ||
|
|
b672418c45 | ||
|
|
c017c1b10c | ||
|
|
e3a4a7b153 | ||
|
|
36f13e44a3 | ||
|
|
2597c44117 | ||
|
|
ae685c73d2 | ||
|
|
2b5337c4ff | ||
|
|
7e5203efa5 | ||
|
|
17c579bfc2 | ||
|
|
3c39d8beac | ||
|
|
2a057bb1fb | ||
|
|
f778d7ecd1 | ||
|
|
4c983525d3 | ||
|
|
e32a9f303d | ||
|
|
522e3bb939 | ||
|
|
0676cf8826 | ||
|
|
5173dfd424 | ||
|
|
5bc31448b4 | ||
|
|
e9a7136a9a | ||
|
|
c36d0851ca | ||
|
|
ace5f19375 | ||
|
|
88b40cfd10 | ||
|
|
38e693f92c | ||
|
|
9dbb40f33b | ||
|
|
76a3265bbb | ||
|
|
666c165b6f | ||
|
|
9db09c18cc | ||
|
|
b7330392cc | ||
|
|
162da64567 | ||
|
|
09f497ca9b | ||
|
|
87d7143cc8 | ||
|
|
23bcfad717 | ||
|
|
f1f16cfee5 | ||
|
|
82d3b44712 | ||
|
|
b56a21b6e5 | ||
|
|
eb2ba8e598 | ||
|
|
91f039ecb6 | ||
|
|
0d6aeee870 | ||
|
|
a0a5070ac7 | ||
|
|
e7bd966e94 | ||
|
|
075956ce17 | ||
|
|
13b256d4e9 | ||
|
|
5761e9510a | ||
|
|
3b3b9ef33b | ||
|
|
17fd3ec0f0 | ||
|
|
43a6495b98 | ||
|
|
86dabea39f | ||
|
|
8d08b5f7c5 | ||
|
|
13c29c8296 | ||
|
|
eac5516a94 | ||
|
|
88b674f54c | ||
|
|
bcc24a2e25 | ||
|
|
e14f399e2d | ||
|
|
ad2c575b39 | ||
|
|
57c2e7ee4e | ||
|
|
55b57a605e | ||
|
|
397c78b4af | ||
|
|
9e372c29d1 | ||
|
|
82fd7f01f8 | ||
|
|
a15b84a5bf | ||
|
|
5f46423638 | ||
|
|
8aebd36465 | ||
|
|
b4f864d89c | ||
|
|
8c8db78da6 | ||
|
|
b18d9f53c6 | ||
|
|
7134d89352 | ||
|
|
5a7dc198dd | ||
|
|
7dbfcfdea2 | ||
|
|
b56ccd1bab | ||
|
|
f05828c87d | ||
|
|
48817f0fe4 | ||
|
|
3bed2581af | ||
|
|
acb125b2b9 | ||
|
|
72e5aedccd | ||
|
|
9148a750a5 | ||
|
|
d4600c5c83 | ||
|
|
8094b3fd22 |
5
.github/CODEOWNERS
vendored
5
.github/CODEOWNERS
vendored
@@ -5,7 +5,10 @@
|
||||
# https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/about-code-owners
|
||||
|
||||
# Default file owners.
|
||||
# * @bitwarden/tech-leads
|
||||
* @bitwarden/team-android @brian-livefront @david-livefront @dseverns-livefront @ahaisting-livefront
|
||||
|
||||
# Actions and workflow changes.
|
||||
.github/workflows @bitwarden/dept-development-mobile
|
||||
|
||||
# Auth
|
||||
# app/src/main/java/com/x8bit/bitwarden/data/auth @bitwarden/team-auth-dev
|
||||
|
||||
103
.github/workflows/build.yml
vendored
103
.github/workflows/build.yml
vendored
@@ -40,7 +40,7 @@ jobs:
|
||||
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
|
||||
|
||||
- name: Validate Gradle wrapper
|
||||
uses: gradle/actions/wrapper-validation@d9c87d481d55275bb5441eef3fe0e46805f9ef70 # v3.5.0
|
||||
uses: gradle/actions/wrapper-validation@d156388eb19639ec20ade50009f3d199ce1e2808 # v4.1.0
|
||||
|
||||
- name: Cache Gradle files
|
||||
uses: actions/cache@0c45773b623bea8c8e75f6c82b208c3cf94ea4f9 # v4.0.2
|
||||
@@ -62,13 +62,13 @@ jobs:
|
||||
${{ runner.os }}-build-
|
||||
|
||||
- name: Configure JDK
|
||||
uses: actions/setup-java@99b8673ff64fbf99d8d325f52d9a5bdedb8483e9 # v4.2.1
|
||||
uses: actions/setup-java@2dfa2011c5b2a0f1489bf9e433881c92c1631f88 # v4.3.0
|
||||
with:
|
||||
distribution: "temurin"
|
||||
java-version: ${{ env.JAVA_VERSION }}
|
||||
|
||||
- name: Configure Ruby
|
||||
uses: ruby/setup-ruby@a6e6f86333f0a2523ece813039b8b4be04560854 # v1.190.0
|
||||
uses: ruby/setup-ruby@f321cf5a4d1533575411f8752cf25b86478b0442 # v1.193.0
|
||||
with:
|
||||
bundler-cache: true
|
||||
|
||||
@@ -92,14 +92,14 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
variant: ["prod", "qa"]
|
||||
variant: ["prod", "dev"]
|
||||
artifact: ["apk", "aab"]
|
||||
steps:
|
||||
- name: Check out repo
|
||||
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
|
||||
|
||||
- name: Configure Ruby
|
||||
uses: ruby/setup-ruby@a6e6f86333f0a2523ece813039b8b4be04560854 # v1.190.0
|
||||
uses: ruby/setup-ruby@f321cf5a4d1533575411f8752cf25b86478b0442 # v1.193.0
|
||||
with:
|
||||
bundler-cache: true
|
||||
|
||||
@@ -150,7 +150,7 @@ jobs:
|
||||
--name app_play_prod_firebase-creds.json --file ${{ github.workspace }}/secrets/app_play_prod_firebase-creds.json --output none
|
||||
|
||||
- name: Validate Gradle wrapper
|
||||
uses: gradle/actions/wrapper-validation@d9c87d481d55275bb5441eef3fe0e46805f9ef70 # v3.5.0
|
||||
uses: gradle/actions/wrapper-validation@d156388eb19639ec20ade50009f3d199ce1e2808 # v4.1.0
|
||||
|
||||
- name: Cache Gradle files
|
||||
uses: actions/cache@0c45773b623bea8c8e75f6c82b208c3cf94ea4f9 # v4.0.2
|
||||
@@ -172,7 +172,7 @@ jobs:
|
||||
${{ runner.os }}-build-
|
||||
|
||||
- name: Configure JDK
|
||||
uses: actions/setup-java@99b8673ff64fbf99d8d325f52d9a5bdedb8483e9 # v4.2.1
|
||||
uses: actions/setup-java@2dfa2011c5b2a0f1489bf9e433881c92c1631f88 # v4.3.0
|
||||
with:
|
||||
distribution: "temurin"
|
||||
java-version: ${{ env.JAVA_VERSION }}
|
||||
@@ -230,14 +230,14 @@ jobs:
|
||||
keyAlias:bitwarden-beta \
|
||||
keyPassword:${{ env.PLAY_BETA_KEY_PASSWORD }}
|
||||
|
||||
- name: Generate QA Play Store APKs
|
||||
- name: Generate debug Play Store APKs
|
||||
if: ${{ (matrix.variant != 'prod') && (matrix.artifact == 'apk') }}
|
||||
run: |
|
||||
bundle exec fastlane assembleDebugApks
|
||||
|
||||
- name: Upload release Play Store .aab artifact
|
||||
if: ${{ (matrix.variant == 'prod') && (matrix.artifact == 'aab') }}
|
||||
uses: actions/upload-artifact@89ef406dd8d7e03cfd12d9e0a4a378f454709029 # v4.3.5
|
||||
uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0
|
||||
with:
|
||||
name: com.x8bit.bitwarden.aab
|
||||
path: app/build/outputs/bundle/standardRelease/com.x8bit.bitwarden-standard-release.aab
|
||||
@@ -245,7 +245,7 @@ jobs:
|
||||
|
||||
- name: Upload beta Play Store .aab artifact
|
||||
if: ${{ (matrix.variant == 'prod') && (matrix.artifact == 'aab') }}
|
||||
uses: actions/upload-artifact@89ef406dd8d7e03cfd12d9e0a4a378f454709029 # v4.3.5
|
||||
uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0
|
||||
with:
|
||||
name: com.x8bit.bitwarden.beta.aab
|
||||
path: app/build/outputs/bundle/standardBeta/com.x8bit.bitwarden-standard-beta.aab
|
||||
@@ -253,7 +253,7 @@ jobs:
|
||||
|
||||
- name: Upload release .apk artifact
|
||||
if: ${{ (matrix.variant == 'prod') && (matrix.artifact == 'apk') }}
|
||||
uses: actions/upload-artifact@89ef406dd8d7e03cfd12d9e0a4a378f454709029 # v4.3.5
|
||||
uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0
|
||||
with:
|
||||
name: com.x8bit.bitwarden.apk
|
||||
path: app/build/outputs/apk/standard/release/com.x8bit.bitwarden-standard-release.apk
|
||||
@@ -261,18 +261,18 @@ jobs:
|
||||
|
||||
- name: Upload beta .apk artifact
|
||||
if: ${{ (matrix.variant == 'prod') && (matrix.artifact == 'apk') }}
|
||||
uses: actions/upload-artifact@89ef406dd8d7e03cfd12d9e0a4a378f454709029 # v4.3.5
|
||||
uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0
|
||||
with:
|
||||
name: com.x8bit.bitwarden.beta.apk
|
||||
path: app/build/outputs/apk/standard/beta/com.x8bit.bitwarden-standard-beta.apk
|
||||
if-no-files-found: error
|
||||
|
||||
# When building variants other than 'prod'
|
||||
- name: Upload other .apk artifact
|
||||
- name: Upload debug .apk artifact
|
||||
if: ${{ (matrix.variant != 'prod') && (matrix.artifact == 'apk') }}
|
||||
uses: actions/upload-artifact@89ef406dd8d7e03cfd12d9e0a4a378f454709029 # v4.3.5
|
||||
uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0
|
||||
with:
|
||||
name: com.x8bit.bitwarden-${{ matrix.variant }}.apk
|
||||
name: com.x8bit.bitwarden.${{ matrix.variant }}.apk
|
||||
path: app/build/outputs/apk/standard/debug/com.x8bit.bitwarden-standard-debug.apk
|
||||
if-no-files-found: error
|
||||
|
||||
@@ -280,70 +280,70 @@ jobs:
|
||||
if: ${{ (matrix.variant == 'prod') && (matrix.artifact == 'apk') }}
|
||||
run: |
|
||||
sha256sum "app/build/outputs/apk/standard/release/com.x8bit.bitwarden-standard-release.apk" \
|
||||
> ./bw-android-apk-sha256.txt
|
||||
> ./com.x8bit.bitwarden.apk-sha256.txt
|
||||
|
||||
- name: Create checksum for beta .apk artifact
|
||||
if: ${{ (matrix.variant == 'prod') && (matrix.artifact == 'apk') }}
|
||||
run: |
|
||||
sha256sum "app/build/outputs/apk/standard/beta/com.x8bit.bitwarden-standard-beta.apk" \
|
||||
> ./bw-android-beta-apk-sha256.txt
|
||||
> ./com.x8bit.bitwarden.beta.apk-sha256.txt
|
||||
|
||||
- name: Create checksum for release .aab artifact
|
||||
if: ${{ (matrix.variant == 'prod') && (matrix.artifact == 'aab') }}
|
||||
run: |
|
||||
sha256sum "app/build/outputs/bundle/standardRelease/com.x8bit.bitwarden-standard-release.aab" \
|
||||
> ./bw-android-aab-sha256.txt
|
||||
> ./com.x8bit.bitwarden.aab-sha256.txt
|
||||
|
||||
- name: Create checksum for beta .aab artifact
|
||||
if: ${{ (matrix.variant == 'prod') && (matrix.artifact == 'aab') }}
|
||||
run: |
|
||||
sha256sum "app/build/outputs/bundle/standardBeta/com.x8bit.bitwarden-standard-beta.aab" \
|
||||
> ./bw-android-beta-aab-sha256.txt
|
||||
> ./com.x8bit.bitwarden.beta.aab-sha256.txt
|
||||
|
||||
- name: Create checksum for other .apk artifact
|
||||
- name: Create checksum for Debug .apk artifact
|
||||
if: ${{ (matrix.variant != 'prod') && (matrix.artifact == 'apk') }}
|
||||
run: |
|
||||
sha256sum "app/build/outputs/apk/standard/debug/com.x8bit.bitwarden-standard-debug.apk" \
|
||||
> ./bw-android-${{ matrix.variant }}-apk-sha256.txt
|
||||
> ./com.x8bit.bitwarden.${{ matrix.variant }}.apk-sha256.txt
|
||||
|
||||
- name: Upload .apk SHA file for release
|
||||
if: ${{ (matrix.variant == 'prod') && (matrix.artifact == 'apk') }}
|
||||
uses: actions/upload-artifact@89ef406dd8d7e03cfd12d9e0a4a378f454709029 # v4.3.5
|
||||
uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0
|
||||
with:
|
||||
name: bw-android-apk-sha256.txt
|
||||
path: ./bw-android-apk-sha256.txt
|
||||
name: com.x8bit.bitwarden.apk-sha256.txt
|
||||
path: ./com.x8bit.bitwarden.apk-sha256.txt
|
||||
if-no-files-found: error
|
||||
|
||||
- name: Upload .apk SHA file for beta
|
||||
if: ${{ (matrix.variant == 'prod') && (matrix.artifact == 'apk') }}
|
||||
uses: actions/upload-artifact@89ef406dd8d7e03cfd12d9e0a4a378f454709029 # v4.3.5
|
||||
uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0
|
||||
with:
|
||||
name: bw-android-beta-apk-sha256.txt
|
||||
path: ./bw-android-beta-apk-sha256.txt
|
||||
name: com.x8bit.bitwarden.beta.apk-sha256.txt
|
||||
path: ./com.x8bit.bitwarden.beta.apk-sha256.txt
|
||||
if-no-files-found: error
|
||||
|
||||
- name: Upload .aab SHA file for release
|
||||
if: ${{ (matrix.variant == 'prod') && (matrix.artifact == 'aab') }}
|
||||
uses: actions/upload-artifact@89ef406dd8d7e03cfd12d9e0a4a378f454709029 # v4.3.5
|
||||
uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0
|
||||
with:
|
||||
name: bw-android-aab-sha256.txt
|
||||
path: ./bw-android-aab-sha256.txt
|
||||
name: com.x8bit.bitwarden.aab-sha256.txt
|
||||
path: ./com.x8bit.bitwarden.aab-sha256.txt
|
||||
if-no-files-found: error
|
||||
|
||||
- name: Upload .aab SHA file for beta
|
||||
if: ${{ (matrix.variant == 'prod') && (matrix.artifact == 'aab') }}
|
||||
uses: actions/upload-artifact@89ef406dd8d7e03cfd12d9e0a4a378f454709029 # v4.3.5
|
||||
uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0
|
||||
with:
|
||||
name: bw-android-beta-aab-sha256.txt
|
||||
path: ./bw-android-beta-aab-sha256.txt
|
||||
name: com.x8bit.bitwarden.beta.aab-sha256.txt
|
||||
path: ./com.x8bit.bitwarden.beta.aab-sha256.txt
|
||||
if-no-files-found: error
|
||||
|
||||
- name: Upload .apk SHA file for other
|
||||
- name: Upload .apk SHA file for debug
|
||||
if: ${{ (matrix.variant != 'prod') && (matrix.artifact == 'apk') }}
|
||||
uses: actions/upload-artifact@89ef406dd8d7e03cfd12d9e0a4a378f454709029 # v4.3.5
|
||||
uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0
|
||||
with:
|
||||
name: bw-android-${{ matrix.variant }}-apk-sha256.txt
|
||||
path: ./bw-android-${{ matrix.variant }}-apk-sha256.txt
|
||||
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
|
||||
@@ -387,7 +387,7 @@ jobs:
|
||||
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
|
||||
|
||||
- name: Configure Ruby
|
||||
uses: ruby/setup-ruby@a6e6f86333f0a2523ece813039b8b4be04560854 # v1.190.0
|
||||
uses: ruby/setup-ruby@f321cf5a4d1533575411f8752cf25b86478b0442 # v1.193.0
|
||||
with:
|
||||
bundler-cache: true
|
||||
|
||||
@@ -424,7 +424,7 @@ jobs:
|
||||
--name app_fdroid_firebase-creds.json --file ${{ github.workspace }}/secrets/app_fdroid_firebase-creds.json --output none
|
||||
|
||||
- name: Validate Gradle wrapper
|
||||
uses: gradle/actions/wrapper-validation@d9c87d481d55275bb5441eef3fe0e46805f9ef70 # v3.5.0
|
||||
uses: gradle/actions/wrapper-validation@d156388eb19639ec20ade50009f3d199ce1e2808 # v4.1.0
|
||||
|
||||
- name: Cache Gradle files
|
||||
uses: actions/cache@0c45773b623bea8c8e75f6c82b208c3cf94ea4f9 # v4.0.2
|
||||
@@ -446,7 +446,7 @@ jobs:
|
||||
${{ runner.os }}-build-
|
||||
|
||||
- name: Configure JDK
|
||||
uses: actions/setup-java@99b8673ff64fbf99d8d325f52d9a5bdedb8483e9 # v4.2.1
|
||||
uses: actions/setup-java@2dfa2011c5b2a0f1489bf9e433881c92c1631f88 # v4.3.0
|
||||
with:
|
||||
distribution: "temurin"
|
||||
java-version: ${{ env.JAVA_VERSION }}
|
||||
@@ -469,7 +469,6 @@ jobs:
|
||||
keyAlias:bitwarden \
|
||||
keyPassword:"${{ env.FDROID_STORE_PASSWORD }}"
|
||||
|
||||
# Generate the F-Droid APK for publishing
|
||||
- name: Generate F-Droid Beta Artifacts
|
||||
env:
|
||||
FDROID_BETA_KEYSTORE_PASSWORD: ${{ secrets.FDROID_BETA_KEYSTORE_PASSWORD }}
|
||||
@@ -482,7 +481,7 @@ jobs:
|
||||
keyPassword:"${{ env.FDROID_BETA_KEY_PASSWORD }}"
|
||||
|
||||
- name: Upload F-Droid .apk artifact
|
||||
uses: actions/upload-artifact@89ef406dd8d7e03cfd12d9e0a4a378f454709029 # v4.3.5
|
||||
uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0
|
||||
with:
|
||||
name: com.x8bit.bitwarden-fdroid.apk
|
||||
path: app/build/outputs/apk/fdroid/release/com.x8bit.bitwarden-fdroid-release.apk
|
||||
@@ -491,32 +490,32 @@ jobs:
|
||||
- name: Create checksum for F-Droid artifact
|
||||
run: |
|
||||
sha256sum "app/build/outputs/apk/fdroid/release/com.x8bit.bitwarden-fdroid-release.apk" \
|
||||
> ./bw-fdroid-apk-sha256.txt
|
||||
> ./com.x8bit.bitwarden-fdroid.apk-sha256.txt
|
||||
|
||||
- name: Upload F-Droid SHA file
|
||||
uses: actions/upload-artifact@89ef406dd8d7e03cfd12d9e0a4a378f454709029 # v4.3.5
|
||||
uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0
|
||||
with:
|
||||
name: bw-fdroid-apk-sha256.txt
|
||||
path: ./bw-fdroid-apk-sha256.txt
|
||||
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@89ef406dd8d7e03cfd12d9e0a4a378f454709029 # v4.3.5
|
||||
uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0
|
||||
with:
|
||||
name: com.x8bit.bitwarden-fdroid-beta.apk
|
||||
name: com.x8bit.bitwarden.beta-fdroid.apk
|
||||
path: app/build/outputs/apk/fdroid/beta/com.x8bit.bitwarden-fdroid-beta.apk
|
||||
if-no-files-found: error
|
||||
|
||||
- name: Create checksum for F-Droid Beta artifact
|
||||
run: |
|
||||
sha256sum "app/build/outputs/apk/fdroid/beta/com.x8bit.bitwarden-fdroid-beta.apk" \
|
||||
> ./bw-fdroid-beta-apk-sha256.txt
|
||||
> ./com.x8bit.bitwarden.beta-fdroid.apk-sha256.txt
|
||||
|
||||
- name: Upload F-Droid Beta SHA file
|
||||
uses: actions/upload-artifact@89ef406dd8d7e03cfd12d9e0a4a378f454709029 # v4.3.5
|
||||
uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0
|
||||
with:
|
||||
name: bw-fdroid-beta-apk-sha256.txt
|
||||
path: ./bw-fdroid-beta-apk-sha256.txt
|
||||
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
|
||||
|
||||
2
.github/workflows/crowdin-pull.yml
vendored
2
.github/workflows/crowdin-pull.yml
vendored
@@ -30,7 +30,7 @@ jobs:
|
||||
secrets: "crowdin-api-token, github-gpg-private-key, github-gpg-private-key-passphrase"
|
||||
|
||||
- name: Download translations
|
||||
uses: crowdin/github-action@6ed209d411599a981ccb978df3be9dc9b8a81699 # v2.1.1
|
||||
uses: crowdin/github-action@91d52b545f82cb88e86c3002a443de22df77fa16 # v2.1.3
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
CROWDIN_API_TOKEN: ${{ steps.retrieve-secrets.outputs.crowdin-api-token }}
|
||||
|
||||
2
.github/workflows/crowdin-push.yml
vendored
2
.github/workflows/crowdin-push.yml
vendored
@@ -29,7 +29,7 @@ jobs:
|
||||
secrets: "crowdin-api-token"
|
||||
|
||||
- name: Upload sources
|
||||
uses: crowdin/github-action@6ed209d411599a981ccb978df3be9dc9b8a81699 # v2.1.1
|
||||
uses: crowdin/github-action@91d52b545f82cb88e86c3002a443de22df77fa16 # v2.1.3
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
CROWDIN_API_TOKEN: ${{ steps.retrieve-secrets.outputs.crowdin-api-token }}
|
||||
|
||||
6
.github/workflows/scan.yml
vendored
6
.github/workflows/scan.yml
vendored
@@ -31,7 +31,7 @@ jobs:
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
|
||||
- name: Scan with Checkmarx
|
||||
uses: checkmarx/ast-github-action@6c56658230f79c227a55120e9b24845d574d5225 # 2.0.31
|
||||
uses: checkmarx/ast-github-action@9fda5a4a2c297608117a5a56af424502a9192e57 # 2.0.34
|
||||
env:
|
||||
INCREMENTAL: "${{ contains(github.event_name, 'pull_request') && '--sast-incremental' || '' }}"
|
||||
with:
|
||||
@@ -46,7 +46,7 @@ jobs:
|
||||
--output-path . ${{ env.INCREMENTAL }}
|
||||
|
||||
- name: Upload Checkmarx results to GitHub
|
||||
uses: github/codeql-action/upload-sarif@afb54ba388a7dca6ecae48f608c4ff05ff4cc77a # v3.25.15
|
||||
uses: github/codeql-action/upload-sarif@294a9d92911152fe08befb9ec03e240add280cb3 # v3.26.8
|
||||
with:
|
||||
sarif_file: cx_result.sarif
|
||||
|
||||
@@ -66,7 +66,7 @@ jobs:
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
|
||||
- name: Scan with SonarCloud
|
||||
uses: sonarsource/sonarcloud-github-action@e44258b109568baa0df60ed515909fc6c72cba92 # v2.3.0
|
||||
uses: sonarsource/sonarcloud-github-action@eb211723266fe8e83102bac7361f0a05c3ac1d1b # v3.0.0
|
||||
env:
|
||||
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
6
.github/workflows/test.yml
vendored
6
.github/workflows/test.yml
vendored
@@ -36,7 +36,7 @@ jobs:
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
|
||||
- name: Validate Gradle wrapper
|
||||
uses: gradle/actions/wrapper-validation@d9c87d481d55275bb5441eef3fe0e46805f9ef70 # v3.5.0
|
||||
uses: gradle/actions/wrapper-validation@d156388eb19639ec20ade50009f3d199ce1e2808 # v4.1.0
|
||||
|
||||
- name: Cache Gradle files
|
||||
uses: actions/cache@0c45773b623bea8c8e75f6c82b208c3cf94ea4f9 # v4.0.2
|
||||
@@ -58,12 +58,12 @@ jobs:
|
||||
${{ runner.os }}-build-
|
||||
|
||||
- name: Configure Ruby
|
||||
uses: ruby/setup-ruby@a6e6f86333f0a2523ece813039b8b4be04560854 # v1.190.0
|
||||
uses: ruby/setup-ruby@f321cf5a4d1533575411f8752cf25b86478b0442 # v1.193.0
|
||||
with:
|
||||
bundler-cache: true
|
||||
|
||||
- name: Configure JDK
|
||||
uses: actions/setup-java@99b8673ff64fbf99d8d325f52d9a5bdedb8483e9 # v4.2.1
|
||||
uses: actions/setup-java@2dfa2011c5b2a0f1489bf9e433881c92c1631f88 # v4.3.0
|
||||
with:
|
||||
distribution: "temurin"
|
||||
java-version: ${{ env.JAVA_VERSION }}
|
||||
|
||||
32
Gemfile.lock
32
Gemfile.lock
@@ -10,17 +10,17 @@ GEM
|
||||
artifactory (3.0.17)
|
||||
atomos (0.1.3)
|
||||
aws-eventstream (1.3.0)
|
||||
aws-partitions (1.961.0)
|
||||
aws-sdk-core (3.201.3)
|
||||
aws-partitions (1.975.0)
|
||||
aws-sdk-core (3.205.0)
|
||||
aws-eventstream (~> 1, >= 1.3.0)
|
||||
aws-partitions (~> 1, >= 1.651.0)
|
||||
aws-sigv4 (~> 1.8)
|
||||
aws-sigv4 (~> 1.9)
|
||||
jmespath (~> 1, >= 1.6.1)
|
||||
aws-sdk-kms (1.88.0)
|
||||
aws-sdk-core (~> 3, >= 3.201.0)
|
||||
aws-sdk-kms (1.91.0)
|
||||
aws-sdk-core (~> 3, >= 3.205.0)
|
||||
aws-sigv4 (~> 1.5)
|
||||
aws-sdk-s3 (1.157.0)
|
||||
aws-sdk-core (~> 3, >= 3.201.0)
|
||||
aws-sdk-s3 (1.162.0)
|
||||
aws-sdk-core (~> 3, >= 3.205.0)
|
||||
aws-sdk-kms (~> 1)
|
||||
aws-sigv4 (~> 1.5)
|
||||
aws-sigv4 (1.9.1)
|
||||
@@ -134,7 +134,7 @@ GEM
|
||||
google-apis-core (>= 0.11.0, < 2.a)
|
||||
google-apis-storage_v1 (0.31.0)
|
||||
google-apis-core (>= 0.11.0, < 2.a)
|
||||
google-cloud-core (1.7.0)
|
||||
google-cloud-core (1.7.1)
|
||||
google-cloud-env (>= 1.0, < 3.a)
|
||||
google-cloud-errors (~> 1.0)
|
||||
google-cloud-env (1.6.0)
|
||||
@@ -155,12 +155,12 @@ GEM
|
||||
os (>= 0.9, < 2.0)
|
||||
signet (>= 0.16, < 2.a)
|
||||
highline (2.0.3)
|
||||
http-cookie (1.0.6)
|
||||
http-cookie (1.0.7)
|
||||
domain_name (~> 0.5)
|
||||
httpclient (2.8.3)
|
||||
jmespath (1.6.2)
|
||||
json (2.7.2)
|
||||
jwt (2.8.2)
|
||||
jwt (2.9.0)
|
||||
base64
|
||||
mini_magick (4.13.2)
|
||||
mini_mime (1.1.5)
|
||||
@@ -179,8 +179,7 @@ GEM
|
||||
trailblazer-option (>= 0.1.1, < 0.2.0)
|
||||
uber (< 0.2.0)
|
||||
retriable (3.1.2)
|
||||
rexml (3.2.9)
|
||||
strscan
|
||||
rexml (3.3.7)
|
||||
rouge (2.0.7)
|
||||
ruby2_keywords (0.0.5)
|
||||
rubyzip (2.3.2)
|
||||
@@ -193,11 +192,10 @@ GEM
|
||||
simctl (1.6.10)
|
||||
CFPropertyList
|
||||
naturally
|
||||
strscan (3.1.0)
|
||||
terminal-notifier (2.0.0)
|
||||
terminal-table (3.0.2)
|
||||
unicode-display_width (>= 1.1.1, < 3)
|
||||
time (0.3.0)
|
||||
time (0.4.0)
|
||||
date
|
||||
trailblazer-option (0.1.2)
|
||||
tty-cursor (0.7.1)
|
||||
@@ -205,15 +203,15 @@ GEM
|
||||
tty-spinner (0.9.3)
|
||||
tty-cursor (~> 0.7)
|
||||
uber (0.1.0)
|
||||
unicode-display_width (2.5.0)
|
||||
unicode-display_width (2.6.0)
|
||||
word_wrap (1.0.0)
|
||||
xcodeproj (1.24.0)
|
||||
xcodeproj (1.25.0)
|
||||
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.2.4)
|
||||
rexml (>= 3.3.2, < 4.0)
|
||||
xcpretty (0.3.0)
|
||||
rouge (~> 2.0.7)
|
||||
xcpretty-travis-formatter (1.0.1)
|
||||
|
||||
@@ -28,6 +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.
|
||||
|
||||
3. Setup the code style formatter:
|
||||
|
||||
|
||||
@@ -2,6 +2,8 @@ import com.google.firebase.crashlytics.buildtools.gradle.tasks.InjectMappingFile
|
||||
import com.google.firebase.crashlytics.buildtools.gradle.tasks.UploadMappingFileTask
|
||||
import com.google.gms.googleservices.GoogleServicesTask
|
||||
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
|
||||
import java.io.FileInputStream
|
||||
import java.util.Properties
|
||||
|
||||
plugins {
|
||||
alias(libs.plugins.android.application)
|
||||
@@ -20,6 +22,16 @@ plugins {
|
||||
alias(libs.plugins.sonarqube)
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads local user-specific build properties that are not checked into source control.
|
||||
*/
|
||||
val userProperties = Properties().apply {
|
||||
val buildPropertiesFile = File(rootDir, "user.properties")
|
||||
if (buildPropertiesFile.exists()) {
|
||||
FileInputStream(buildPropertiesFile).use { load(it) }
|
||||
}
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "com.x8bit.bitwarden"
|
||||
compileSdk = libs.versions.compileSdk.get().toInt()
|
||||
@@ -29,7 +41,7 @@ android {
|
||||
minSdk = libs.versions.minSdk.get().toInt()
|
||||
targetSdk = libs.versions.targetSdk.get().toInt()
|
||||
versionCode = 1
|
||||
versionName = "2024.06.00"
|
||||
versionName = "2024.9.0"
|
||||
|
||||
setProperty("archivesBaseName", "com.x8bit.bitwarden")
|
||||
|
||||
@@ -61,6 +73,8 @@ android {
|
||||
signingConfig = signingConfigs.getByName("debug")
|
||||
isDebuggable = true
|
||||
isMinifyEnabled = false
|
||||
|
||||
buildConfigField(type = "boolean", name = "HAS_DEBUG_MENU", value = "true")
|
||||
}
|
||||
|
||||
// Beta and Release variants are identical except beta has a different package name
|
||||
@@ -72,6 +86,8 @@ android {
|
||||
getDefaultProguardFile("proguard-android-optimize.txt"),
|
||||
"proguard-rules.pro"
|
||||
)
|
||||
|
||||
buildConfigField(type = "boolean", name = "HAS_DEBUG_MENU", value = "false")
|
||||
}
|
||||
release {
|
||||
isDebuggable = false
|
||||
@@ -80,6 +96,8 @@ android {
|
||||
getDefaultProguardFile("proguard-android-optimize.txt"),
|
||||
"proguard-rules.pro"
|
||||
)
|
||||
|
||||
buildConfigField(type = "boolean", name = "HAS_DEBUG_MENU", value = "false")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -124,11 +142,23 @@ kotlin {
|
||||
}
|
||||
}
|
||||
|
||||
configurations.all {
|
||||
resolutionStrategy.dependencySubstitution {
|
||||
if ((userProperties["localSdk"] as String?).toBoolean()) {
|
||||
substitute(module("com.bitwarden:sdk-android"))
|
||||
.using(module("com.bitwarden:sdk-android:LOCAL"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
fun standardImplementation(dependencyNotation: Any) {
|
||||
add("standardImplementation", dependencyNotation)
|
||||
}
|
||||
|
||||
// TODO: this should use a versioned AAR instead of referencing a local AAR BITAU-94
|
||||
implementation(files("libs/authenticatorbridge-0.1.0-SNAPSHOT-release.aar"))
|
||||
|
||||
implementation(libs.androidx.activity.compose)
|
||||
implementation(libs.androidx.appcompat)
|
||||
implementation(libs.androidx.autofill)
|
||||
@@ -213,6 +243,7 @@ kover {
|
||||
annotatedBy(
|
||||
// Compose previews
|
||||
"androidx.compose.ui.tooling.preview.Preview",
|
||||
"androidx.compose.ui.tooling.preview.PreviewScreenSizes",
|
||||
// Manually excluded classes/files/etc.
|
||||
"com.x8bit.bitwarden.data.platform.annotation.OmitFromCoverage",
|
||||
)
|
||||
@@ -302,4 +333,4 @@ tasks {
|
||||
getByName("sonar") {
|
||||
dependsOn("check")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
BIN
app/libs/authenticatorbridge-0.1.0-SNAPSHOT-release.aar
Normal file
BIN
app/libs/authenticatorbridge-0.1.0-SNAPSHOT-release.aar
Normal file
Binary file not shown.
@@ -62,18 +62,28 @@
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
|
||||
<data android:scheme="https" />
|
||||
<data android:host="bitwarden.com" />
|
||||
<data android:host="bitwarden.pw" />
|
||||
<data android:host="bitwarden.eu" />
|
||||
|
||||
<data android:host="vault.bitwarden.com" />
|
||||
<data android:host="vault.bitwarden.eu" />
|
||||
<data android:host="*.bitwarden.pw" />
|
||||
<data android:pathPattern="/redirect-connector.*" />
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="com.x8bit.bitwarden.fido2.ACTION_CREATE_PASSKEY" />
|
||||
<action android:name="com.x8bit.bitwarden.fido2.ACTION_GET_PASSKEY" />
|
||||
<action android:name="com.x8bit.bitwarden.fido2.ACTION_UNLOCK_ACCOUNT" />
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<activity
|
||||
android:name=".AccessibilityActivity"
|
||||
android:exported="false"
|
||||
android:launchMode="singleTop"
|
||||
android:noHistory="true"
|
||||
android:theme="@android:style/Theme.NoDisplay" />
|
||||
|
||||
<activity
|
||||
android:name=".AutofillTotpCopyActivity"
|
||||
android:exported="true"
|
||||
@@ -158,6 +168,26 @@
|
||||
</intent-filter>
|
||||
</service>
|
||||
|
||||
<!--
|
||||
The AccessibilityService name below refers to the legacy Xamarin app's service name. This
|
||||
must always match in order for the app to properly query if it is providing accessibility
|
||||
services.
|
||||
-->
|
||||
<!--suppress AndroidDomInspection -->
|
||||
<service
|
||||
android:name="com.x8bit.bitwarden.Accessibility.AccessibilityService"
|
||||
android:exported="true"
|
||||
android:label="@string/app_name"
|
||||
android:permission="android.permission.BIND_ACCESSIBILITY_SERVICE"
|
||||
tools:ignore="MissingClass">
|
||||
<intent-filter>
|
||||
<action android:name="android.accessibilityservice.AccessibilityService" />
|
||||
</intent-filter>
|
||||
<meta-data
|
||||
android:name="android.accessibilityservice"
|
||||
android:resource="@xml/accessibility_service" />
|
||||
</service>
|
||||
|
||||
<!--
|
||||
The CredentialProviderService name below refers to the legacy Xamarin app's service name.
|
||||
This must always match in order for the app to properly query if it is providing credential
|
||||
@@ -189,6 +219,60 @@
|
||||
android:value="true" />
|
||||
</service>
|
||||
|
||||
<!--
|
||||
The AutofillTileService name below refers to the legacy Xamarin app's service name.
|
||||
This must always match in order for the app to properly query if it is providing autofill
|
||||
tile services.
|
||||
-->
|
||||
<!--suppress AndroidDomInspection -->
|
||||
<service
|
||||
android:name="com.x8bit.bitwarden.AutofillTileService"
|
||||
android:exported="true"
|
||||
android:icon="@drawable/ic_notification"
|
||||
android:label="@string/autofill"
|
||||
android:permission="android.permission.BIND_QUICK_SETTINGS_TILE"
|
||||
tools:ignore="MissingClass">
|
||||
<intent-filter>
|
||||
<action android:name="android.service.quicksettings.action.QS_TILE" />
|
||||
</intent-filter>
|
||||
</service>
|
||||
|
||||
<!--
|
||||
The GeneratorTileService name below refers to the legacy Xamarin app's service name.
|
||||
This must always match in order for the app to properly query if it is providing generator
|
||||
tile services.
|
||||
-->
|
||||
<!--suppress AndroidDomInspection -->
|
||||
<service
|
||||
android:name="com.x8bit.bitwarden.GeneratorTileService"
|
||||
android:exported="true"
|
||||
android:icon="@drawable/ic_generator"
|
||||
android:label="@string/password_generator"
|
||||
android:permission="android.permission.BIND_QUICK_SETTINGS_TILE"
|
||||
tools:ignore="MissingClass">
|
||||
<intent-filter>
|
||||
<action android:name="android.service.quicksettings.action.QS_TILE" />
|
||||
</intent-filter>
|
||||
</service>
|
||||
|
||||
<!--
|
||||
The MyVaultTileService name below refers to the legacy Xamarin app's service name.
|
||||
This must always match in order for the app to properly query if it is providing vault
|
||||
tile services.
|
||||
-->
|
||||
<!--suppress AndroidDomInspection -->
|
||||
<service
|
||||
android:name="com.x8bit.bitwarden.MyVaultTileService"
|
||||
android:exported="true"
|
||||
android:icon="@drawable/ic_notification"
|
||||
android:label="@string/my_vault"
|
||||
android:permission="android.permission.BIND_QUICK_SETTINGS_TILE"
|
||||
tools:ignore="MissingClass">
|
||||
<intent-filter>
|
||||
<action android:name="android.service.quicksettings.action.QS_TILE" />
|
||||
</intent-filter>
|
||||
</service>
|
||||
|
||||
<meta-data
|
||||
android:name="android.content.APP_RESTRICTIONS"
|
||||
android:resource="@xml/app_restrictions" />
|
||||
@@ -199,6 +283,10 @@
|
||||
<intent>
|
||||
<action android:name="android.media.action.IMAGE_CAPTURE" />
|
||||
</intent>
|
||||
<intent>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.HOME" />
|
||||
</intent>
|
||||
</queries>
|
||||
|
||||
</manifest>
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
package com.x8bit.bitwarden
|
||||
|
||||
import android.os.Bundle
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import com.x8bit.bitwarden.data.platform.annotation.OmitFromCoverage
|
||||
|
||||
/**
|
||||
* An activity to be launched and then immediately closed so that the OS Shade can be collapsed
|
||||
* after the user clicks on the Autofill Quick Tile.
|
||||
*/
|
||||
@OmitFromCoverage
|
||||
class AccessibilityActivity : AppCompatActivity() {
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
finish()
|
||||
}
|
||||
}
|
||||
@@ -3,37 +3,62 @@ package com.x8bit.bitwarden
|
||||
import android.app.Service
|
||||
import android.content.Intent
|
||||
import android.os.Build
|
||||
import androidx.annotation.Keep
|
||||
import androidx.core.app.AppComponentFactory
|
||||
import com.x8bit.bitwarden.data.autofill.BitwardenAutofillService
|
||||
import com.x8bit.bitwarden.data.autofill.accessibility.BitwardenAccessibilityService
|
||||
import com.x8bit.bitwarden.data.autofill.fido2.BitwardenFido2ProviderService
|
||||
import com.x8bit.bitwarden.data.platform.annotation.OmitFromCoverage
|
||||
|
||||
private const val LEGACY_AUTOFILL_SERVICE_NAME = "com.x8bit.bitwarden.Autofill.AutofillService"
|
||||
private const val LEGACY_CREDENTIAL_SERVICE_NAME =
|
||||
"com.x8bit.bitwarden.Autofill.CredentialProviderService"
|
||||
import com.x8bit.bitwarden.data.tiles.BitwardenAutofillTileService
|
||||
import com.x8bit.bitwarden.data.tiles.BitwardenGeneratorTileService
|
||||
import com.x8bit.bitwarden.data.tiles.BitwardenVaultTileService
|
||||
|
||||
/**
|
||||
* A factory class that allows us to intercept when a manifest element is being instantiated
|
||||
* and modify various characteristics before initialization.
|
||||
*/
|
||||
@Suppress("unused")
|
||||
@Keep
|
||||
@OmitFromCoverage
|
||||
class BitwardenAppComponentFactory : AppComponentFactory() {
|
||||
/**
|
||||
* Used to intercept when the [BitwardenAutofillService] or [BitwardenFido2ProviderService] is
|
||||
* being instantiated and modify which service is created. This is required because the
|
||||
* [className] used in the manifest must match the legacy Xamarin app service name but the
|
||||
* service name in this app is different.
|
||||
* Used to intercept when certain legacy services are being instantiated and modify which
|
||||
* service is created. This is required because the [className] used in the manifest must match
|
||||
* the legacy Xamarin app service name but the service name in this app is different.
|
||||
*
|
||||
* Services currently being managed:
|
||||
* * [BitwardenAccessibilityService]
|
||||
* * [BitwardenAutofillService]
|
||||
* * [BitwardenAutofillTileService]
|
||||
* * [BitwardenFido2ProviderService]
|
||||
* * [BitwardenVaultTileService]
|
||||
* * [BitwardenGeneratorTileService]
|
||||
*/
|
||||
override fun instantiateServiceCompat(
|
||||
cl: ClassLoader,
|
||||
className: String,
|
||||
intent: Intent?,
|
||||
): Service = when (className) {
|
||||
LEGACY_ACCESSIBILITY_SERVICE_NAME -> {
|
||||
super.instantiateServiceCompat(
|
||||
cl,
|
||||
BitwardenAccessibilityService::class.java.name,
|
||||
intent,
|
||||
)
|
||||
}
|
||||
|
||||
LEGACY_AUTOFILL_SERVICE_NAME -> {
|
||||
super.instantiateServiceCompat(cl, BitwardenAutofillService::class.java.name, intent)
|
||||
}
|
||||
|
||||
LEGACY_AUTOFILL_TILE_SERVICE_NAME -> {
|
||||
super.instantiateServiceCompat(
|
||||
cl,
|
||||
BitwardenAutofillTileService::class.java.name,
|
||||
intent,
|
||||
)
|
||||
}
|
||||
|
||||
LEGACY_CREDENTIAL_SERVICE_NAME -> {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
|
||||
super.instantiateServiceCompat(
|
||||
@@ -48,6 +73,18 @@ class BitwardenAppComponentFactory : AppComponentFactory() {
|
||||
}
|
||||
}
|
||||
|
||||
LEGACY_VAULT_TILE_SERVICE_NAME -> {
|
||||
super.instantiateServiceCompat(cl, BitwardenVaultTileService::class.java.name, intent)
|
||||
}
|
||||
|
||||
LEGACY_GENERATOR_TILE_SERVICE_NAME -> {
|
||||
super.instantiateServiceCompat(
|
||||
cl,
|
||||
BitwardenGeneratorTileService::class.java.name,
|
||||
intent,
|
||||
)
|
||||
}
|
||||
|
||||
else -> super.instantiateServiceCompat(cl, className, intent)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
package com.x8bit.bitwarden
|
||||
|
||||
/**
|
||||
* The legacy name for the accessibility service.
|
||||
*/
|
||||
const val LEGACY_ACCESSIBILITY_SERVICE_NAME: String =
|
||||
"com.x8bit.bitwarden.Accessibility.AccessibilityService"
|
||||
|
||||
/**
|
||||
* The legacy name for the autofill service.
|
||||
*/
|
||||
const val LEGACY_AUTOFILL_SERVICE_NAME: String = "com.x8bit.bitwarden.Autofill.AutofillService"
|
||||
|
||||
/**
|
||||
* The legacy name for the accessibility autofill tile service.
|
||||
*/
|
||||
const val LEGACY_AUTOFILL_TILE_SERVICE_NAME: String = "com.x8bit.bitwarden.AutofillTileService"
|
||||
|
||||
/**
|
||||
* The legacy name for the credential service.
|
||||
*/
|
||||
const val LEGACY_CREDENTIAL_SERVICE_NAME: String =
|
||||
"com.x8bit.bitwarden.Autofill.CredentialProviderService"
|
||||
|
||||
/**
|
||||
* The legacy name for the generator tile service.
|
||||
*/
|
||||
const val LEGACY_GENERATOR_TILE_SERVICE_NAME: String = "com.x8bit.bitwarden.GeneratorTileService"
|
||||
|
||||
/**
|
||||
* The legacy name for the vault tile service.
|
||||
*/
|
||||
const val LEGACY_VAULT_TILE_SERVICE_NAME: String = "com.x8bit.bitwarden.MyVaultTileService"
|
||||
@@ -2,7 +2,10 @@ package com.x8bit.bitwarden
|
||||
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.view.KeyEvent
|
||||
import android.view.MotionEvent
|
||||
import android.view.WindowManager
|
||||
import android.widget.Toast
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.activity.viewModels
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
@@ -11,17 +14,20 @@ import androidx.compose.runtime.getValue
|
||||
import androidx.core.os.LocaleListCompat
|
||||
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.navigation.compose.rememberNavController
|
||||
import com.x8bit.bitwarden.data.autofill.accessibility.manager.AccessibilityActivityManager
|
||||
import com.x8bit.bitwarden.data.autofill.accessibility.manager.AccessibilityCompletionManager
|
||||
import com.x8bit.bitwarden.data.autofill.manager.AutofillActivityManager
|
||||
import com.x8bit.bitwarden.data.autofill.manager.AutofillCompletionManager
|
||||
import com.x8bit.bitwarden.data.platform.annotation.OmitFromCoverage
|
||||
import com.x8bit.bitwarden.data.platform.repository.SettingsRepository
|
||||
import com.x8bit.bitwarden.ui.platform.base.util.EventsEffect
|
||||
import com.x8bit.bitwarden.ui.platform.composition.LocalManagerProvider
|
||||
import com.x8bit.bitwarden.ui.platform.feature.debugmenu.manager.DebugMenuLaunchManager
|
||||
import com.x8bit.bitwarden.ui.platform.feature.debugmenu.navigateToDebugMenuScreen
|
||||
import com.x8bit.bitwarden.ui.platform.feature.rootnav.RootNavScreen
|
||||
import com.x8bit.bitwarden.ui.platform.theme.BitwardenTheme
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import javax.inject.Inject
|
||||
|
||||
/**
|
||||
@@ -33,22 +39,29 @@ class MainActivity : AppCompatActivity() {
|
||||
|
||||
private val mainViewModel: MainViewModel by viewModels()
|
||||
|
||||
@Inject
|
||||
lateinit var accessibilityActivityManager: AccessibilityActivityManager
|
||||
|
||||
@Inject
|
||||
lateinit var autofillActivityManager: AutofillActivityManager
|
||||
|
||||
@Inject
|
||||
lateinit var autofillCompletionManager: AutofillCompletionManager
|
||||
|
||||
@Inject
|
||||
lateinit var accessibilityCompletionManager: AccessibilityCompletionManager
|
||||
|
||||
@Inject
|
||||
lateinit var settingsRepository: SettingsRepository
|
||||
|
||||
@Inject
|
||||
lateinit var debugLaunchManager: DebugMenuLaunchManager
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
var shouldShowSplashScreen = true
|
||||
installSplashScreen().setKeepOnScreenCondition { shouldShowSplashScreen }
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
observeViewModelEvents()
|
||||
|
||||
if (savedInstanceState == null) {
|
||||
mainViewModel.trySendAction(
|
||||
MainAction.ReceiveFirstIntent(
|
||||
@@ -66,11 +79,33 @@ class MainActivity : AppCompatActivity() {
|
||||
}
|
||||
setContent {
|
||||
val state by mainViewModel.stateFlow.collectAsStateWithLifecycle()
|
||||
val navController = rememberNavController()
|
||||
EventsEffect(viewModel = mainViewModel) { event ->
|
||||
when (event) {
|
||||
is MainEvent.CompleteAccessibilityAutofill -> {
|
||||
handleCompleteAccessibilityAutofill(event)
|
||||
}
|
||||
|
||||
is MainEvent.CompleteAutofill -> handleCompleteAutofill(event)
|
||||
MainEvent.Recreate -> handleRecreate()
|
||||
MainEvent.NavigateToDebugMenu -> navController.navigateToDebugMenuScreen()
|
||||
is MainEvent.ShowToast -> {
|
||||
Toast
|
||||
.makeText(
|
||||
baseContext,
|
||||
event.message.invoke(resources),
|
||||
Toast.LENGTH_SHORT,
|
||||
)
|
||||
.show()
|
||||
}
|
||||
}
|
||||
}
|
||||
updateScreenCapture(isScreenCaptureAllowed = state.isScreenCaptureAllowed)
|
||||
LocalManagerProvider {
|
||||
BitwardenTheme(theme = state.theme) {
|
||||
RootNavScreen(
|
||||
onSplashScreenRemoved = { shouldShowSplashScreen = false },
|
||||
navController = navController,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -93,16 +128,27 @@ class MainActivity : AppCompatActivity() {
|
||||
currentFocus?.clearFocus()
|
||||
}
|
||||
|
||||
private fun observeViewModelEvents() {
|
||||
mainViewModel
|
||||
.eventFlow
|
||||
.onEach { event ->
|
||||
when (event) {
|
||||
is MainEvent.CompleteAutofill -> handleCompleteAutofill(event)
|
||||
MainEvent.Recreate -> handleRecreate()
|
||||
}
|
||||
}
|
||||
.launchIn(lifecycleScope)
|
||||
override fun dispatchTouchEvent(event: MotionEvent): Boolean = debugLaunchManager
|
||||
.actionOnInputEvent(event = event, action = ::sendOpenDebugMenuEvent)
|
||||
.takeIf { it }
|
||||
?: super.dispatchTouchEvent(event)
|
||||
|
||||
override fun dispatchKeyEvent(event: KeyEvent): Boolean = debugLaunchManager
|
||||
.actionOnInputEvent(event = event, action = ::sendOpenDebugMenuEvent)
|
||||
.takeIf { it }
|
||||
?: super.dispatchKeyEvent(event)
|
||||
|
||||
private fun sendOpenDebugMenuEvent() {
|
||||
mainViewModel.trySendAction(MainAction.OpenDebugMenu)
|
||||
}
|
||||
|
||||
private fun handleCompleteAccessibilityAutofill(
|
||||
event: MainEvent.CompleteAccessibilityAutofill,
|
||||
) {
|
||||
accessibilityCompletionManager.completeAccessibilityAutofill(
|
||||
activity = this,
|
||||
cipherView = event.cipherView,
|
||||
)
|
||||
}
|
||||
|
||||
private fun handleCompleteAutofill(event: MainEvent.CompleteAutofill) {
|
||||
|
||||
@@ -6,8 +6,10 @@ import androidx.lifecycle.SavedStateHandle
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.bitwarden.vault.CipherView
|
||||
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
|
||||
import com.x8bit.bitwarden.data.auth.util.getPasswordlessRequestDataIntentOrNull
|
||||
import com.x8bit.bitwarden.data.autofill.accessibility.manager.AccessibilitySelectionManager
|
||||
import com.x8bit.bitwarden.data.autofill.fido2.manager.Fido2CredentialManager
|
||||
import com.x8bit.bitwarden.data.autofill.fido2.util.getFido2AssertionRequestOrNull
|
||||
import com.x8bit.bitwarden.data.autofill.fido2.util.getFido2CredentialRequestOrNull
|
||||
@@ -17,11 +19,15 @@ import com.x8bit.bitwarden.data.autofill.util.getAutofillSaveItemOrNull
|
||||
import com.x8bit.bitwarden.data.autofill.util.getAutofillSelectionDataOrNull
|
||||
import com.x8bit.bitwarden.data.platform.manager.SpecialCircumstanceManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.garbage.GarbageCollectionManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.model.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.vault.manager.model.VaultStateEvent
|
||||
import com.x8bit.bitwarden.data.vault.repository.VaultRepository
|
||||
import com.x8bit.bitwarden.ui.platform.base.BaseViewModel
|
||||
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.isMyVaultShortcut
|
||||
@@ -30,10 +36,12 @@ import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.drop
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.parcelize.Parcelize
|
||||
import java.time.Clock
|
||||
import javax.inject.Inject
|
||||
@@ -46,14 +54,16 @@ private const val SPECIAL_CIRCUMSTANCE_KEY = "special-circumstance"
|
||||
@Suppress("LongParameterList", "TooManyFunctions")
|
||||
@HiltViewModel
|
||||
class MainViewModel @Inject constructor(
|
||||
accessibilitySelectionManager: AccessibilitySelectionManager,
|
||||
autofillSelectionManager: AutofillSelectionManager,
|
||||
private val specialCircumstanceManager: SpecialCircumstanceManager,
|
||||
private val garbageCollectionManager: GarbageCollectionManager,
|
||||
private val fido2CredentialManager: Fido2CredentialManager,
|
||||
private val intentManager: IntentManager,
|
||||
settingsRepository: SettingsRepository,
|
||||
private val vaultRepository: VaultRepository,
|
||||
vaultRepository: VaultRepository,
|
||||
private val authRepository: AuthRepository,
|
||||
private val environmentRepository: EnvironmentRepository,
|
||||
private val savedStateHandle: SavedStateHandle,
|
||||
private val clock: Clock,
|
||||
) : BaseViewModel<MainState, MainEvent, MainAction>(
|
||||
@@ -77,6 +87,12 @@ class MainViewModel @Inject constructor(
|
||||
.onEach { specialCircumstance = it }
|
||||
.launchIn(viewModelScope)
|
||||
|
||||
accessibilitySelectionManager
|
||||
.accessibilitySelectionFlow
|
||||
.map { MainAction.Internal.AccessibilitySelectionReceive(it) }
|
||||
.onEach(::sendAction)
|
||||
.launchIn(viewModelScope)
|
||||
|
||||
autofillSelectionManager
|
||||
.autofillSelectionFlow
|
||||
.onEach { trySendAction(MainAction.Internal.AutofillSelectionReceive(it)) }
|
||||
@@ -126,10 +142,27 @@ class MainViewModel @Inject constructor(
|
||||
}
|
||||
}
|
||||
.launchIn(viewModelScope)
|
||||
|
||||
// On app launch, mark all active users as having previously logged in.
|
||||
// This covers any users who are active prior to this value being recorded.
|
||||
viewModelScope.launch {
|
||||
val userState = authRepository
|
||||
.userStateFlow
|
||||
.first()
|
||||
userState
|
||||
?.accounts
|
||||
?.forEach {
|
||||
settingsRepository.storeUserHasLoggedInValue(it.userId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun handleAction(action: MainAction) {
|
||||
when (action) {
|
||||
is MainAction.Internal.AccessibilitySelectionReceive -> {
|
||||
handleAccessibilitySelectionReceive(action)
|
||||
}
|
||||
|
||||
is MainAction.Internal.AutofillSelectionReceive -> {
|
||||
handleAutofillSelectionReceive(action)
|
||||
}
|
||||
@@ -140,9 +173,20 @@ class MainViewModel @Inject constructor(
|
||||
is MainAction.Internal.VaultUnlockStateChange -> handleVaultUnlockStateChange()
|
||||
is MainAction.ReceiveFirstIntent -> handleFirstIntentReceived(action)
|
||||
is MainAction.ReceiveNewIntent -> handleNewIntentReceived(action)
|
||||
MainAction.OpenDebugMenu -> handleOpenDebugMenu()
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleOpenDebugMenu() {
|
||||
sendEvent(MainEvent.NavigateToDebugMenu)
|
||||
}
|
||||
|
||||
private fun handleAccessibilitySelectionReceive(
|
||||
action: MainAction.Internal.AccessibilitySelectionReceive,
|
||||
) {
|
||||
sendEvent(MainEvent.CompleteAccessibilityAutofill(cipherView = action.cipherView))
|
||||
}
|
||||
|
||||
private fun handleAutofillSelectionReceive(
|
||||
action: MainAction.Internal.AutofillSelectionReceive,
|
||||
) {
|
||||
@@ -206,14 +250,7 @@ class MainViewModel @Inject constructor(
|
||||
}
|
||||
|
||||
completeRegistrationData != null -> {
|
||||
if (authRepository.activeUserId != null) {
|
||||
authRepository.hasPendingAccountAddition = true
|
||||
}
|
||||
specialCircumstanceManager.specialCircumstance =
|
||||
SpecialCircumstance.CompleteRegistration(
|
||||
completeRegistrationData = completeRegistrationData,
|
||||
timestamp = clock.millis(),
|
||||
)
|
||||
handleCompleteRegistrationData(completeRegistrationData)
|
||||
}
|
||||
|
||||
autofillSaveItem != null -> {
|
||||
@@ -290,6 +327,49 @@ class MainViewModel @Inject constructor(
|
||||
sendEvent(MainEvent.Recreate)
|
||||
garbageCollectionManager.tryCollect()
|
||||
}
|
||||
|
||||
private fun handleCompleteRegistrationData(data: CompleteRegistrationData) {
|
||||
viewModelScope.launch {
|
||||
// Attempt to load the environment for the user if they have a pre-auth environment
|
||||
// saved.
|
||||
environmentRepository.loadEnvironmentForEmail(userEmail = data.email)
|
||||
// Determine if the token is still valid.
|
||||
val emailTokenResult = authRepository.validateEmailToken(
|
||||
email = data.email,
|
||||
token = data.verificationToken,
|
||||
)
|
||||
when (emailTokenResult) {
|
||||
is EmailTokenResult.Error -> {
|
||||
sendEvent(
|
||||
MainEvent.ShowToast(
|
||||
message = emailTokenResult
|
||||
.message
|
||||
?.asText()
|
||||
?: R.string.there_was_an_issue_validating_the_registration_token
|
||||
.asText(),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
EmailTokenResult.Expired -> {
|
||||
specialCircumstanceManager.specialCircumstance = SpecialCircumstance
|
||||
.RegistrationEvent
|
||||
.ExpiredRegistrationLink
|
||||
}
|
||||
|
||||
EmailTokenResult.Success -> {
|
||||
if (authRepository.activeUserId != null) {
|
||||
authRepository.hasPendingAccountAddition = true
|
||||
}
|
||||
specialCircumstanceManager.specialCircumstance =
|
||||
SpecialCircumstance.RegistrationEvent.CompleteRegistration(
|
||||
completeRegistrationData = data,
|
||||
timestamp = clock.millis(),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -315,10 +395,23 @@ sealed class MainAction {
|
||||
*/
|
||||
data class ReceiveNewIntent(val intent: Intent) : MainAction()
|
||||
|
||||
/**
|
||||
* Receive event to open the debug menu.
|
||||
*/
|
||||
data object OpenDebugMenu : MainAction()
|
||||
|
||||
/**
|
||||
* Actions for internal use by the ViewModel.
|
||||
*/
|
||||
sealed class Internal : MainAction() {
|
||||
/**
|
||||
* Indicates the user has manually selected the given [cipherView] for accessibility
|
||||
* autofill.
|
||||
*/
|
||||
data class AccessibilitySelectionReceive(
|
||||
val cipherView: CipherView,
|
||||
) : Internal()
|
||||
|
||||
/**
|
||||
* Indicates the user has manually selected the given [cipherView] for autofill.
|
||||
*/
|
||||
@@ -356,6 +449,12 @@ sealed class MainAction {
|
||||
* Represents events that are emitted by the [MainViewModel].
|
||||
*/
|
||||
sealed class MainEvent {
|
||||
/**
|
||||
* Event indicating that the user has chosen the given [cipherView] for accessibility autofill
|
||||
* and that the process is ready to complete.
|
||||
*/
|
||||
data class CompleteAccessibilityAutofill(val cipherView: CipherView) : MainEvent()
|
||||
|
||||
/**
|
||||
* Event indicating that the user has chosen the given [cipherView] for autofill and that the
|
||||
* process is ready to complete.
|
||||
@@ -366,4 +465,14 @@ sealed class MainEvent {
|
||||
* Event indicating that the UI should recreate itself.
|
||||
*/
|
||||
data object Recreate : MainEvent()
|
||||
|
||||
/**
|
||||
* Navigate to the debug menu.
|
||||
*/
|
||||
data object NavigateToDebugMenu : MainEvent()
|
||||
|
||||
/**
|
||||
* Show a toast with the given [message].
|
||||
*/
|
||||
data class ShowToast(val message: Text) : MainEvent()
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package com.x8bit.bitwarden.data.auth.datasource.disk
|
||||
|
||||
import com.x8bit.bitwarden.data.auth.datasource.disk.model.AccountTokensJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.disk.model.OnboardingStatus
|
||||
import com.x8bit.bitwarden.data.auth.datasource.disk.model.PendingAuthRequestJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.disk.model.UserStateJson
|
||||
import com.x8bit.bitwarden.data.vault.datasource.network.model.SyncResponseJson
|
||||
@@ -11,6 +12,13 @@ import kotlinx.coroutines.flow.Flow
|
||||
*/
|
||||
@Suppress("TooManyFunctions")
|
||||
interface AuthDiskSource {
|
||||
|
||||
/**
|
||||
* The currently persisted authenticator sync symmetric key. This key is used for
|
||||
* encrypting IPC traffic.
|
||||
*/
|
||||
var authenticatorSyncSymmetricKey: ByteArray?
|
||||
|
||||
/**
|
||||
* Retrieves a unique ID for the application that is stored locally. This will generate a new
|
||||
* one if it does not yet exist and it will only be reset for new installs or when clearing
|
||||
@@ -45,16 +53,43 @@ interface AuthDiskSource {
|
||||
*/
|
||||
fun clearData(userId: String)
|
||||
|
||||
/**
|
||||
* Get the authenticator sync unlock key. Null means there is no key, which means the user
|
||||
* has not enabled authenticator syncing
|
||||
*/
|
||||
fun getAuthenticatorSyncUnlockKey(userId: String): String?
|
||||
|
||||
/**
|
||||
* Store the authenticator sync unlock key. Storing a null key effectively disables
|
||||
* authenticator syncing.
|
||||
*/
|
||||
fun storeAuthenticatorSyncUnlockKey(userId: String, authenticatorSyncUnlockKey: String?)
|
||||
|
||||
/**
|
||||
* Retrieves the state indicating that the user should use a key connector.
|
||||
*/
|
||||
fun getShouldUseKeyConnector(userId: String): Boolean?
|
||||
|
||||
/**
|
||||
* Retrieves the state indicating that the user should use a key connector as a flow.
|
||||
*/
|
||||
fun getShouldUseKeyConnectorFlow(userId: String): Flow<Boolean?>
|
||||
|
||||
/**
|
||||
* Stores the boolean indicating that the user should use a key connector.
|
||||
*/
|
||||
fun storeShouldUseKeyConnector(userId: String, shouldUseKeyConnector: Boolean?)
|
||||
|
||||
/**
|
||||
* Retrieves the state indicating that the user has completed login with TDE.
|
||||
*/
|
||||
fun getIsTdeLoginComplete(userId: String): Boolean?
|
||||
|
||||
/**
|
||||
* Stores the boolean indicating that the user has completed login with TDE.
|
||||
*/
|
||||
fun storeIsTdeLoginComplete(userId: String, isTdeLoginComplete: Boolean?)
|
||||
|
||||
/**
|
||||
* Retrieves the state indicating that the user has chosen to trust this device.
|
||||
*
|
||||
@@ -255,4 +290,20 @@ interface AuthDiskSource {
|
||||
* Stores the [accountTokens] for the given [userId].
|
||||
*/
|
||||
fun storeAccountTokens(userId: String, accountTokens: AccountTokensJson?)
|
||||
|
||||
/**
|
||||
* Gets the onboarding status for the given [userId].
|
||||
*/
|
||||
fun getOnboardingStatus(userId: String): OnboardingStatus?
|
||||
|
||||
/**
|
||||
* Stores the [onboardingStatus] for the given [userId].
|
||||
*/
|
||||
fun storeOnboardingStatus(userId: String, onboardingStatus: OnboardingStatus?)
|
||||
|
||||
/**
|
||||
* Emits updates that track [getOnboardingStatus]. This will replay the last known value,
|
||||
* if any exists.
|
||||
*/
|
||||
fun getOnboardingStatusFlow(userId: String): Flow<OnboardingStatus?>
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package com.x8bit.bitwarden.data.auth.datasource.disk
|
||||
|
||||
import android.content.SharedPreferences
|
||||
import com.x8bit.bitwarden.data.auth.datasource.disk.model.AccountTokensJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.disk.model.OnboardingStatus
|
||||
import com.x8bit.bitwarden.data.auth.datasource.disk.model.PendingAuthRequestJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.disk.model.UserStateJson
|
||||
import com.x8bit.bitwarden.data.platform.datasource.disk.BaseEncryptedDiskSource
|
||||
@@ -18,6 +19,8 @@ import java.util.UUID
|
||||
|
||||
// These keys should be encrypted
|
||||
private const val ACCOUNT_TOKENS_KEY = "accountTokens"
|
||||
private const val AUTHENTICATOR_SYNC_SYMMETRIC_KEY = "authenticatorSyncSymmetric"
|
||||
private const val AUTHENTICATOR_SYNC_UNLOCK_KEY = "authenticatorSyncUnlock"
|
||||
private const val BIOMETRICS_UNLOCK_KEY = "userKeyBiometricUnlock"
|
||||
private const val USER_AUTO_UNLOCK_KEY_KEY = "userKeyAutoUnlock"
|
||||
private const val DEVICE_KEY_KEY = "deviceKey"
|
||||
@@ -39,7 +42,9 @@ private const val TWO_FACTOR_TOKEN_KEY = "twoFactorToken"
|
||||
private const val MASTER_PASSWORD_HASH_KEY = "keyHash"
|
||||
private const val POLICIES_KEY = "policies"
|
||||
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"
|
||||
|
||||
/**
|
||||
* Primary implementation of [AuthDiskSource].
|
||||
@@ -57,12 +62,16 @@ class AuthDiskSourceImpl(
|
||||
AuthDiskSource {
|
||||
|
||||
private val inMemoryPinProtectedUserKeys = mutableMapOf<String, String?>()
|
||||
private val mutableShouldUseKeyConnectorFlowMap =
|
||||
mutableMapOf<String, MutableSharedFlow<Boolean?>>()
|
||||
private val mutableOrganizationsFlowMap =
|
||||
mutableMapOf<String, MutableSharedFlow<List<SyncResponseJson.Profile.Organization>?>>()
|
||||
private val mutablePoliciesFlowMap =
|
||||
mutableMapOf<String, MutableSharedFlow<List<SyncResponseJson.Policy>?>>()
|
||||
private val mutableAccountTokensFlowMap =
|
||||
mutableMapOf<String, MutableSharedFlow<AccountTokensJson?>>()
|
||||
private val mutableOnboardingStatusFlowMap =
|
||||
mutableMapOf<String, MutableSharedFlow<OnboardingStatus?>>()
|
||||
private val mutableUserStateFlow = bufferedMutableSharedFlow<UserStateJson?>(replay = 1)
|
||||
|
||||
override var userState: UserStateJson?
|
||||
@@ -85,6 +94,14 @@ class AuthDiskSourceImpl(
|
||||
migrateAccountTokens()
|
||||
}
|
||||
|
||||
override var authenticatorSyncSymmetricKey: ByteArray?
|
||||
set(value) {
|
||||
val asString = value?.let { value.toString(Charsets.ISO_8859_1) }
|
||||
putEncryptedString(AUTHENTICATOR_SYNC_SYMMETRIC_KEY, asString)
|
||||
}
|
||||
get() = getEncryptedString(AUTHENTICATOR_SYNC_SYMMETRIC_KEY)
|
||||
?.toByteArray(Charsets.ISO_8859_1)
|
||||
|
||||
override val uniqueAppId: String
|
||||
get() = getString(key = UNIQUE_APP_ID_KEY) ?: generateAndStoreUniqueAppId()
|
||||
|
||||
@@ -124,11 +141,30 @@ class AuthDiskSourceImpl(
|
||||
storePolicies(userId = userId, policies = null)
|
||||
storeAccountTokens(userId = userId, accountTokens = null)
|
||||
storeShouldUseKeyConnector(userId = userId, shouldUseKeyConnector = null)
|
||||
storeIsTdeLoginComplete(userId = userId, isTdeLoginComplete = null)
|
||||
storeAuthenticatorSyncUnlockKey(userId = userId, authenticatorSyncUnlockKey = null)
|
||||
|
||||
// Do not remove the DeviceKey or PendingAuthRequest on logout, these are persisted
|
||||
// indefinitely unless the TDE flow explicitly removes them.
|
||||
}
|
||||
|
||||
override fun getAuthenticatorSyncUnlockKey(userId: String): String? =
|
||||
getEncryptedString(AUTHENTICATOR_SYNC_UNLOCK_KEY.appendIdentifier(userId))
|
||||
|
||||
override fun storeAuthenticatorSyncUnlockKey(
|
||||
userId: String,
|
||||
authenticatorSyncUnlockKey: String?,
|
||||
) {
|
||||
putEncryptedString(
|
||||
key = AUTHENTICATOR_SYNC_UNLOCK_KEY.appendIdentifier(userId),
|
||||
value = authenticatorSyncUnlockKey,
|
||||
)
|
||||
}
|
||||
|
||||
override fun getShouldUseKeyConnectorFlow(userId: String): Flow<Boolean?> =
|
||||
getMutableShouldUseKeyConnectorFlowMap(userId = userId)
|
||||
.onSubscription { emit(getShouldUseKeyConnector(userId = userId)) }
|
||||
|
||||
override fun getShouldUseKeyConnector(
|
||||
userId: String,
|
||||
): Boolean? = getBoolean(key = USES_KEY_CONNECTOR.appendIdentifier(userId))
|
||||
@@ -138,6 +174,15 @@ class AuthDiskSourceImpl(
|
||||
key = USES_KEY_CONNECTOR.appendIdentifier(userId),
|
||||
value = shouldUseKeyConnector,
|
||||
)
|
||||
getMutableShouldUseKeyConnectorFlowMap(userId = userId).tryEmit(shouldUseKeyConnector)
|
||||
}
|
||||
|
||||
override fun getIsTdeLoginComplete(
|
||||
userId: String,
|
||||
): Boolean? = getBoolean(key = TDE_LOGIN_COMPLETE.appendIdentifier(userId))
|
||||
|
||||
override fun storeIsTdeLoginComplete(userId: String, isTdeLoginComplete: Boolean?) {
|
||||
putBoolean(TDE_LOGIN_COMPLETE.appendIdentifier(userId), isTdeLoginComplete)
|
||||
}
|
||||
|
||||
override fun getShouldTrustDevice(
|
||||
@@ -373,6 +418,25 @@ class AuthDiskSourceImpl(
|
||||
getMutableAccountTokensFlow(userId = userId).tryEmit(accountTokens)
|
||||
}
|
||||
|
||||
override fun getOnboardingStatus(userId: String): OnboardingStatus? {
|
||||
return getString(key = ONBOARDING_STATUS_KEY.appendIdentifier(userId))?.let {
|
||||
json.decodeFromStringOrNull(it)
|
||||
}
|
||||
}
|
||||
|
||||
override fun storeOnboardingStatus(userId: String, onboardingStatus: OnboardingStatus?) {
|
||||
putString(
|
||||
key = ONBOARDING_STATUS_KEY.appendIdentifier(userId),
|
||||
value = onboardingStatus?.let { json.encodeToString(it) },
|
||||
)
|
||||
getMutableOnboardingStatusFlow(userId = userId).tryEmit(onboardingStatus)
|
||||
}
|
||||
|
||||
override fun getOnboardingStatusFlow(userId: String): Flow<OnboardingStatus?> {
|
||||
return getMutableOnboardingStatusFlow(userId = userId)
|
||||
.onSubscription { emit(getOnboardingStatus(userId = userId)) }
|
||||
}
|
||||
|
||||
private fun generateAndStoreUniqueAppId(): String =
|
||||
UUID
|
||||
.randomUUID()
|
||||
@@ -381,6 +445,20 @@ class AuthDiskSourceImpl(
|
||||
putString(key = UNIQUE_APP_ID_KEY, value = it)
|
||||
}
|
||||
|
||||
private fun getMutableOnboardingStatusFlow(
|
||||
userId: String,
|
||||
): MutableSharedFlow<OnboardingStatus?> =
|
||||
mutableOnboardingStatusFlowMap.getOrPut(userId) {
|
||||
bufferedMutableSharedFlow(replay = 1)
|
||||
}
|
||||
|
||||
private fun getMutableShouldUseKeyConnectorFlowMap(
|
||||
userId: String,
|
||||
): MutableSharedFlow<Boolean?> =
|
||||
mutableShouldUseKeyConnectorFlowMap.getOrPut(userId) {
|
||||
bufferedMutableSharedFlow(replay = 1)
|
||||
}
|
||||
|
||||
private fun getMutableOrganizationsFlow(
|
||||
userId: String,
|
||||
): MutableSharedFlow<List<SyncResponseJson.Profile.Organization>?> =
|
||||
|
||||
@@ -37,6 +37,7 @@ data class EnvironmentUrlDataJson(
|
||||
@SerialName("events")
|
||||
val events: String? = null,
|
||||
) {
|
||||
@Suppress("UndocumentedPublicClass")
|
||||
companion object {
|
||||
/**
|
||||
* Default [EnvironmentUrlDataJson] for the US region.
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
package com.x8bit.bitwarden.data.auth.datasource.disk.model
|
||||
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
/**
|
||||
* Describes the current status of a user in the account onboarding steps.
|
||||
*/
|
||||
@Serializable
|
||||
enum class OnboardingStatus {
|
||||
|
||||
/**
|
||||
* Onboarding has not yet started.
|
||||
*/
|
||||
@SerialName("notStarted")
|
||||
NOT_STARTED,
|
||||
|
||||
/**
|
||||
* The user is completing the account lock setup.
|
||||
*/
|
||||
@SerialName("accountLockSetup")
|
||||
ACCOUNT_LOCK_SETUP,
|
||||
|
||||
/**
|
||||
* The user is completing the auto fill service setup.
|
||||
*/
|
||||
@SerialName("autofillSetup")
|
||||
AUTOFILL_SETUP,
|
||||
|
||||
/**
|
||||
* The user is completing the final step of the onboarding process.
|
||||
*/
|
||||
@SerialName("finalStep")
|
||||
FINAL_STEP,
|
||||
|
||||
/**
|
||||
* The user has completed all onboarding steps.
|
||||
*/
|
||||
@SerialName("complete")
|
||||
COMPLETE,
|
||||
}
|
||||
@@ -2,7 +2,6 @@ package com.x8bit.bitwarden.data.auth.datasource.network.api
|
||||
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.CreateAccountKeysRequest
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.DeleteAccountRequestJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.KeyConnectorKeyRequestJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.ResetPasswordRequestJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.SetPasswordRequestJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.VerifyOtpRequestJson
|
||||
@@ -53,12 +52,6 @@ interface AuthenticatedAccountsApi {
|
||||
@HTTP(method = "POST", path = "/accounts/password", hasBody = true)
|
||||
suspend fun resetPassword(@Body body: ResetPasswordRequestJson): Result<Unit>
|
||||
|
||||
/**
|
||||
* Sets the key connector key.
|
||||
*/
|
||||
@POST("/accounts/set-key-connector-key")
|
||||
suspend fun setKeyConnectorKey(@Body body: KeyConnectorKeyRequestJson): Result<Unit>
|
||||
|
||||
/**
|
||||
* Sets the password.
|
||||
*/
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
package com.x8bit.bitwarden.data.auth.datasource.network.api
|
||||
|
||||
import androidx.annotation.Keep
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.KeyConnectorMasterKeyRequestJson
|
||||
import retrofit2.http.Body
|
||||
import retrofit2.http.POST
|
||||
import retrofit2.http.Url
|
||||
|
||||
/**
|
||||
* Defines raw calls specific for key connectors that use custom urls.
|
||||
*/
|
||||
@Keep
|
||||
interface AuthenticatedKeyConnectorApi {
|
||||
@POST
|
||||
suspend fun storeMasterKeyToKeyConnector(
|
||||
@Url url: String,
|
||||
@Body body: KeyConnectorMasterKeyRequestJson,
|
||||
): Result<Unit>
|
||||
}
|
||||
@@ -1,14 +1,17 @@
|
||||
package com.x8bit.bitwarden.data.auth.datasource.network.api
|
||||
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.KeyConnectorKeyRequestJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.PasswordHintRequestJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.ResendEmailRequestJson
|
||||
import com.x8bit.bitwarden.data.platform.datasource.network.util.HEADER_KEY_AUTHORIZATION
|
||||
import retrofit2.http.Body
|
||||
import retrofit2.http.Header
|
||||
import retrofit2.http.POST
|
||||
|
||||
/**
|
||||
* Defines raw calls under the /accounts API.
|
||||
*/
|
||||
interface AccountsApi {
|
||||
interface UnauthenticatedAccountsApi {
|
||||
@POST("/accounts/password-hint")
|
||||
suspend fun passwordHintRequest(
|
||||
@Body body: PasswordHintRequestJson,
|
||||
@@ -18,4 +21,10 @@ interface AccountsApi {
|
||||
suspend fun resendVerificationCodeEmail(
|
||||
@Body body: ResendEmailRequestJson,
|
||||
): Result<Unit>
|
||||
|
||||
@POST("/accounts/set-key-connector-key")
|
||||
suspend fun setKeyConnectorKey(
|
||||
@Body body: KeyConnectorKeyRequestJson,
|
||||
@Header(HEADER_KEY_AUTHORIZATION) bearerToken: String,
|
||||
): Result<Unit>
|
||||
}
|
||||
@@ -9,6 +9,7 @@ import com.x8bit.bitwarden.data.auth.datasource.network.model.RegisterFinishRequ
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.RegisterRequestJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.RegisterResponseJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.SendVerificationEmailRequestJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.VerifyEmailTokenRequestJson
|
||||
import kotlinx.serialization.json.JsonPrimitive
|
||||
import retrofit2.Call
|
||||
import retrofit2.http.Body
|
||||
@@ -22,7 +23,7 @@ import retrofit2.http.Query
|
||||
/**
|
||||
* Defines raw calls under the /identity API.
|
||||
*/
|
||||
interface IdentityApi {
|
||||
interface UnauthenticatedIdentityApi {
|
||||
|
||||
@POST("/connect/token")
|
||||
@Suppress("LongParameterList")
|
||||
@@ -79,4 +80,9 @@ interface IdentityApi {
|
||||
suspend fun sendVerificationEmail(
|
||||
@Body body: SendVerificationEmailRequestJson,
|
||||
): Result<JsonPrimitive?>
|
||||
|
||||
@POST("/accounts/register/verification-email-clicked")
|
||||
suspend fun verifyEmailToken(
|
||||
@Body body: VerifyEmailTokenRequestJson,
|
||||
): Result<Unit>
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package com.x8bit.bitwarden.data.auth.datasource.network.api
|
||||
|
||||
import androidx.annotation.Keep
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.KeyConnectorMasterKeyRequestJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.KeyConnectorMasterKeyResponseJson
|
||||
import com.x8bit.bitwarden.data.platform.datasource.network.util.HEADER_KEY_AUTHORIZATION
|
||||
import retrofit2.http.Body
|
||||
import retrofit2.http.GET
|
||||
import retrofit2.http.Header
|
||||
import retrofit2.http.POST
|
||||
import retrofit2.http.Url
|
||||
|
||||
/**
|
||||
* Defines raw calls specific for key connectors that use custom urls.
|
||||
*/
|
||||
@Keep
|
||||
interface UnauthenticatedKeyConnectorApi {
|
||||
@POST
|
||||
suspend fun storeMasterKeyToKeyConnector(
|
||||
@Url url: String,
|
||||
@Header(HEADER_KEY_AUTHORIZATION) bearerToken: String,
|
||||
@Body body: KeyConnectorMasterKeyRequestJson,
|
||||
): Result<Unit>
|
||||
|
||||
@GET
|
||||
suspend fun getMasterKeyFromKeyConnector(
|
||||
@Url url: String,
|
||||
@Header(HEADER_KEY_AUTHORIZATION) bearerToken: String,
|
||||
): Result<KeyConnectorMasterKeyResponseJson>
|
||||
}
|
||||
@@ -8,7 +8,7 @@ import retrofit2.http.POST
|
||||
/**
|
||||
* Defines raw calls under the /organizations API.
|
||||
*/
|
||||
interface OrganizationApi {
|
||||
interface UnauthenticatedOrganizationApi {
|
||||
/**
|
||||
* Checks for the claimed domain organization of an email for SSO purposes.
|
||||
*/
|
||||
@@ -36,8 +36,12 @@ object AuthNetworkModule {
|
||||
retrofits: Retrofits,
|
||||
json: Json,
|
||||
): AccountsService = AccountsServiceImpl(
|
||||
accountsApi = retrofits.unauthenticatedApiRetrofit.create(),
|
||||
unauthenticatedAccountsApi = retrofits.unauthenticatedApiRetrofit.create(),
|
||||
authenticatedAccountsApi = retrofits.authenticatedApiRetrofit.create(),
|
||||
unauthenticatedKeyConnectorApi = retrofits.createStaticRetrofit().create(),
|
||||
authenticatedKeyConnectorApi = retrofits
|
||||
.createStaticRetrofit(isAuthenticated = true)
|
||||
.create(),
|
||||
json = json,
|
||||
)
|
||||
|
||||
@@ -64,7 +68,7 @@ object AuthNetworkModule {
|
||||
retrofits: Retrofits,
|
||||
json: Json,
|
||||
): IdentityService = IdentityServiceImpl(
|
||||
api = retrofits.unauthenticatedIdentityRetrofit.create(),
|
||||
unauthenticatedIdentityApi = retrofits.unauthenticatedIdentityRetrofit.create(),
|
||||
json = json,
|
||||
)
|
||||
|
||||
@@ -93,6 +97,6 @@ object AuthNetworkModule {
|
||||
retrofits: Retrofits,
|
||||
): OrganizationService = OrganizationServiceImpl(
|
||||
authenticatedOrganizationApi = retrofits.authenticatedApiRetrofit.create(),
|
||||
organizationApi = retrofits.unauthenticatedApiRetrofit.create(),
|
||||
unauthenticatedOrganizationApi = retrofits.unauthenticatedApiRetrofit.create(),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -28,6 +28,7 @@ sealed class GetTokenResponseJson {
|
||||
* this token will be cached and used for future auth requests.
|
||||
* @property masterPasswordPolicyOptions The options available for a user's master password.
|
||||
* @property userDecryptionOptions The options available to a user for decryption.
|
||||
* @property keyConnectorUrl URL to the user's key connector.
|
||||
*/
|
||||
@Serializable
|
||||
data class Success(
|
||||
@@ -75,6 +76,9 @@ sealed class GetTokenResponseJson {
|
||||
|
||||
@SerialName("UserDecryptionOptions")
|
||||
val userDecryptionOptions: UserDecryptionOptionsJson?,
|
||||
|
||||
@SerialName("KeyConnectorUrl")
|
||||
val keyConnectorUrl: String?,
|
||||
) : GetTokenResponseJson()
|
||||
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
package com.x8bit.bitwarden.data.auth.datasource.network.model
|
||||
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
/**
|
||||
* Represents the request body used to store the master key in the cloud.
|
||||
*/
|
||||
@Serializable
|
||||
data class KeyConnectorMasterKeyRequestJson(
|
||||
@SerialName("Key") val masterKey: String,
|
||||
)
|
||||
@@ -0,0 +1,12 @@
|
||||
package com.x8bit.bitwarden.data.auth.datasource.network.model
|
||||
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
/**
|
||||
* Represents the response body used to retrieve the master key from the cloud.
|
||||
*/
|
||||
@Serializable
|
||||
data class KeyConnectorMasterKeyResponseJson(
|
||||
@SerialName("key") val masterKey: String,
|
||||
)
|
||||
@@ -1,25 +1,16 @@
|
||||
package com.x8bit.bitwarden.data.auth.datasource.network.model
|
||||
|
||||
import kotlinx.serialization.Contextual
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
import java.time.ZonedDateTime
|
||||
|
||||
/**
|
||||
* Response object returned when requesting organization domain SSO details.
|
||||
*
|
||||
* @property isSsoAvailable Whether or not SSO is available for this domain.
|
||||
* @property domainName The organization's domain name.
|
||||
* @property organizationIdentifier The organization's identifier.
|
||||
* @property isSsoRequired Whether or not SSO is required.
|
||||
* @property verifiedDate The date these details were verified.
|
||||
*/
|
||||
@Serializable
|
||||
data class OrganizationDomainSsoDetailsResponseJson(
|
||||
@SerialName("ssoAvailable") val isSsoAvailable: Boolean,
|
||||
@SerialName("domainName") val domainName: String,
|
||||
@SerialName("organizationIdentifier") val organizationIdentifier: String,
|
||||
@SerialName("ssoRequired") val isSsoRequired: Boolean,
|
||||
@Contextual
|
||||
@SerialName("verifiedDate") val verifiedDate: ZonedDateTime?,
|
||||
)
|
||||
|
||||
@@ -46,7 +46,6 @@ sealed class RegisterResponseJson {
|
||||
/**
|
||||
* Represents the json body of an invalid register request.
|
||||
*
|
||||
* @param message
|
||||
* @param validationErrors a map where each value is a list of error messages for each key.
|
||||
* The values in the array should be used for display to the user, since the keys tend to come
|
||||
* back as nonsense. (eg: empty string key)
|
||||
@@ -54,18 +53,17 @@ sealed class RegisterResponseJson {
|
||||
@Serializable
|
||||
data class Invalid(
|
||||
@SerialName("message")
|
||||
val message: String?,
|
||||
private val invalidMessage: String? = null,
|
||||
|
||||
@SerialName("Message")
|
||||
private val errorMessage: String? = null,
|
||||
|
||||
@SerialName("validationErrors")
|
||||
val validationErrors: Map<String, List<String>>?,
|
||||
) : RegisterResponseJson()
|
||||
|
||||
/**
|
||||
* A different register error with a message.
|
||||
*/
|
||||
@Serializable
|
||||
data class Error(
|
||||
@SerialName("Message")
|
||||
val message: String?,
|
||||
) : RegisterResponseJson()
|
||||
) : RegisterResponseJson() {
|
||||
/**
|
||||
* A generic error message.
|
||||
*/
|
||||
val message: String? get() = invalidMessage ?: errorMessage
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,7 +22,6 @@ sealed class SendVerificationEmailResponseJson {
|
||||
/**
|
||||
* Represents the json body of an invalid request.
|
||||
*
|
||||
* @param message
|
||||
* @param validationErrors a map where each value is a list of error messages for each key.
|
||||
* The values in the array should be used for display to the user, since the keys tend to come
|
||||
* back as nonsense. (eg: empty string key)
|
||||
@@ -30,18 +29,17 @@ sealed class SendVerificationEmailResponseJson {
|
||||
@Serializable
|
||||
data class Invalid(
|
||||
@SerialName("message")
|
||||
val message: String?,
|
||||
private val invalidMessage: String? = null,
|
||||
|
||||
@SerialName("Message")
|
||||
private val errorMessage: String? = null,
|
||||
|
||||
@SerialName("validationErrors")
|
||||
val validationErrors: Map<String, List<String>>?,
|
||||
) : SendVerificationEmailResponseJson()
|
||||
|
||||
/**
|
||||
* A different error with a message.
|
||||
*/
|
||||
@Serializable
|
||||
data class Error(
|
||||
@SerialName("Message")
|
||||
val message: String?,
|
||||
) : SendVerificationEmailResponseJson()
|
||||
) : SendVerificationEmailResponseJson() {
|
||||
/**
|
||||
* A generic error message.
|
||||
*/
|
||||
val message: String? get() = invalidMessage ?: errorMessage
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
package com.x8bit.bitwarden.data.auth.datasource.network.model
|
||||
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
/**
|
||||
* Models the request body for verify email token endpoint.
|
||||
*
|
||||
* @param email the email address of the user to verify.
|
||||
* @param token the provided email verification token.
|
||||
*/
|
||||
@Serializable
|
||||
data class VerifyEmailTokenRequestJson(
|
||||
@SerialName("email")
|
||||
val email: String,
|
||||
@SerialName("emailVerificationToken")
|
||||
val token: String,
|
||||
)
|
||||
@@ -0,0 +1,38 @@
|
||||
package com.x8bit.bitwarden.data.auth.datasource.network.model
|
||||
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
/**
|
||||
* Model the response of a verify email token request.
|
||||
*
|
||||
* A valid response will be a [VerifyEmailTokenResponseJson.Valid]
|
||||
*
|
||||
* an invalid response will be a [VerifyEmailTokenResponseJson.Invalid] with a message.
|
||||
*/
|
||||
@Serializable
|
||||
sealed class VerifyEmailTokenResponseJson {
|
||||
|
||||
/**
|
||||
* The token is confirmed as valid from the response.
|
||||
*/
|
||||
@Serializable
|
||||
data object Valid : VerifyEmailTokenResponseJson()
|
||||
|
||||
/**
|
||||
* The response is invalid.
|
||||
*
|
||||
* @property message The error message. Expected to explain the reason why the token is invalid.
|
||||
*/
|
||||
@Serializable
|
||||
data class Invalid(
|
||||
@SerialName("message")
|
||||
val message: String,
|
||||
) : VerifyEmailTokenResponseJson()
|
||||
|
||||
/**
|
||||
* The token has expired. This is special case of similar to [Invalid].
|
||||
*/
|
||||
@Serializable
|
||||
data object TokenExpired : VerifyEmailTokenResponseJson()
|
||||
}
|
||||
@@ -2,6 +2,7 @@ package com.x8bit.bitwarden.data.auth.datasource.network.service
|
||||
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.DeleteAccountResponseJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.KeyConnectorKeyRequestJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.KeyConnectorMasterKeyResponseJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.PasswordHintResponseJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.ResendEmailRequestJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.ResetPasswordRequestJson
|
||||
@@ -10,6 +11,7 @@ import com.x8bit.bitwarden.data.auth.datasource.network.model.SetPasswordRequest
|
||||
/**
|
||||
* Provides an API for querying accounts endpoints.
|
||||
*/
|
||||
@Suppress("TooManyFunctions")
|
||||
interface AccountsService {
|
||||
|
||||
/**
|
||||
@@ -57,11 +59,48 @@ interface AccountsService {
|
||||
|
||||
/**
|
||||
* Set the key connector key.
|
||||
*
|
||||
* This API requires the [accessToken] to be passed in manually because it occurs during the
|
||||
* login process.
|
||||
*/
|
||||
suspend fun setKeyConnectorKey(body: KeyConnectorKeyRequestJson): Result<Unit>
|
||||
suspend fun setKeyConnectorKey(
|
||||
accessToken: String,
|
||||
body: KeyConnectorKeyRequestJson,
|
||||
): Result<Unit>
|
||||
|
||||
/**
|
||||
* Set the password.
|
||||
*/
|
||||
suspend fun setPassword(body: SetPasswordRequestJson): Result<Unit>
|
||||
|
||||
/**
|
||||
* Retrieves the master key from the key connector.
|
||||
*
|
||||
* This API requires the [accessToken] to be passed in manually because it occurs during the
|
||||
* login process.
|
||||
*/
|
||||
suspend fun getMasterKeyFromKeyConnector(
|
||||
url: String,
|
||||
accessToken: String,
|
||||
): Result<KeyConnectorMasterKeyResponseJson>
|
||||
|
||||
/**
|
||||
* Stores the master key to the key connector.
|
||||
*/
|
||||
suspend fun storeMasterKeyToKeyConnector(
|
||||
url: String,
|
||||
masterKey: String,
|
||||
): Result<Unit>
|
||||
|
||||
/**
|
||||
* Stores the master key to the key connector.
|
||||
*
|
||||
* This API requires the [accessToken] to be passed in manually because it occurs during the
|
||||
* login process.
|
||||
*/
|
||||
suspend fun storeMasterKeyToKeyConnector(
|
||||
url: String,
|
||||
accessToken: String,
|
||||
masterKey: String,
|
||||
): Result<Unit>
|
||||
}
|
||||
|
||||
@@ -1,11 +1,15 @@
|
||||
package com.x8bit.bitwarden.data.auth.datasource.network.service
|
||||
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.api.AccountsApi
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.api.AuthenticatedAccountsApi
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.api.AuthenticatedKeyConnectorApi
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.api.UnauthenticatedAccountsApi
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.api.UnauthenticatedKeyConnectorApi
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.CreateAccountKeysRequest
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.DeleteAccountRequestJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.DeleteAccountResponseJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.KeyConnectorKeyRequestJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.KeyConnectorMasterKeyRequestJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.KeyConnectorMasterKeyResponseJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.PasswordHintRequestJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.PasswordHintResponseJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.ResendEmailRequestJson
|
||||
@@ -13,12 +17,19 @@ import com.x8bit.bitwarden.data.auth.datasource.network.model.ResetPasswordReque
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.SetPasswordRequestJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.VerifyOtpRequestJson
|
||||
import com.x8bit.bitwarden.data.platform.datasource.network.model.toBitwardenError
|
||||
import com.x8bit.bitwarden.data.platform.datasource.network.util.HEADER_VALUE_BEARER_PREFIX
|
||||
import com.x8bit.bitwarden.data.platform.datasource.network.util.parseErrorBodyOrNull
|
||||
import kotlinx.serialization.json.Json
|
||||
|
||||
/**
|
||||
* The default implementation of the [AccountsService].
|
||||
*/
|
||||
@Suppress("TooManyFunctions")
|
||||
class AccountsServiceImpl(
|
||||
private val accountsApi: AccountsApi,
|
||||
private val unauthenticatedAccountsApi: UnauthenticatedAccountsApi,
|
||||
private val authenticatedAccountsApi: AuthenticatedAccountsApi,
|
||||
private val unauthenticatedKeyConnectorApi: UnauthenticatedKeyConnectorApi,
|
||||
private val authenticatedKeyConnectorApi: AuthenticatedKeyConnectorApi,
|
||||
private val json: Json,
|
||||
) : AccountsService {
|
||||
|
||||
@@ -76,7 +87,7 @@ class AccountsServiceImpl(
|
||||
override suspend fun requestPasswordHint(
|
||||
email: String,
|
||||
): Result<PasswordHintResponseJson> =
|
||||
accountsApi
|
||||
unauthenticatedAccountsApi
|
||||
.passwordHintRequest(PasswordHintRequestJson(email))
|
||||
.map { PasswordHintResponseJson.Success }
|
||||
.recoverCatching { throwable ->
|
||||
@@ -90,7 +101,7 @@ class AccountsServiceImpl(
|
||||
}
|
||||
|
||||
override suspend fun resendVerificationCodeEmail(body: ResendEmailRequestJson): Result<Unit> =
|
||||
accountsApi.resendVerificationCodeEmail(body = body)
|
||||
unauthenticatedAccountsApi.resendVerificationCodeEmail(body = body)
|
||||
|
||||
override suspend fun resetPassword(body: ResetPasswordRequestJson): Result<Unit> {
|
||||
return if (body.currentPasswordHash == null) {
|
||||
@@ -101,10 +112,43 @@ class AccountsServiceImpl(
|
||||
}
|
||||
|
||||
override suspend fun setKeyConnectorKey(
|
||||
accessToken: String,
|
||||
body: KeyConnectorKeyRequestJson,
|
||||
): Result<Unit> = authenticatedAccountsApi.setKeyConnectorKey(body)
|
||||
): Result<Unit> = unauthenticatedAccountsApi.setKeyConnectorKey(
|
||||
body = body,
|
||||
bearerToken = "$HEADER_VALUE_BEARER_PREFIX$accessToken",
|
||||
)
|
||||
|
||||
override suspend fun setPassword(
|
||||
body: SetPasswordRequestJson,
|
||||
): Result<Unit> = authenticatedAccountsApi.setPassword(body)
|
||||
|
||||
override suspend fun getMasterKeyFromKeyConnector(
|
||||
url: String,
|
||||
accessToken: String,
|
||||
): Result<KeyConnectorMasterKeyResponseJson> =
|
||||
unauthenticatedKeyConnectorApi.getMasterKeyFromKeyConnector(
|
||||
url = "$url/user-keys",
|
||||
bearerToken = "$HEADER_VALUE_BEARER_PREFIX$accessToken",
|
||||
)
|
||||
|
||||
override suspend fun storeMasterKeyToKeyConnector(
|
||||
url: String,
|
||||
masterKey: String,
|
||||
): Result<Unit> =
|
||||
authenticatedKeyConnectorApi.storeMasterKeyToKeyConnector(
|
||||
url = "$url/user-keys",
|
||||
body = KeyConnectorMasterKeyRequestJson(masterKey = masterKey),
|
||||
)
|
||||
|
||||
override suspend fun storeMasterKeyToKeyConnector(
|
||||
url: String,
|
||||
accessToken: String,
|
||||
masterKey: String,
|
||||
): Result<Unit> =
|
||||
unauthenticatedKeyConnectorApi.storeMasterKeyToKeyConnector(
|
||||
url = "$url/user-keys",
|
||||
bearerToken = "$HEADER_VALUE_BEARER_PREFIX$accessToken",
|
||||
body = KeyConnectorMasterKeyRequestJson(masterKey = masterKey),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -10,6 +10,8 @@ import com.x8bit.bitwarden.data.auth.datasource.network.model.RegisterRequestJso
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.RegisterResponseJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.SendVerificationEmailRequestJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.TwoFactorDataModel
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.VerifyEmailTokenRequestJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.VerifyEmailTokenResponseJson
|
||||
|
||||
/**
|
||||
* Provides an API for querying identity endpoints.
|
||||
@@ -72,4 +74,12 @@ interface IdentityService {
|
||||
* Register a new account to Bitwarden using email verification flow.
|
||||
*/
|
||||
suspend fun registerFinish(body: RegisterFinishRequestJson): Result<RegisterResponseJson>
|
||||
|
||||
/**
|
||||
* Makes request to verify email registration token. If the token provided is
|
||||
* still valid will return success.
|
||||
*/
|
||||
suspend fun verifyEmailRegistrationToken(
|
||||
body: VerifyEmailTokenRequestJson,
|
||||
): Result<VerifyEmailTokenResponseJson>
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
package com.x8bit.bitwarden.data.auth.datasource.network.service
|
||||
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.api.IdentityApi
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.api.UnauthenticatedIdentityApi
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.GetTokenResponseJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.IdentityTokenAuthModel
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.PreLoginRequestJson
|
||||
@@ -12,6 +12,8 @@ import com.x8bit.bitwarden.data.auth.datasource.network.model.RegisterRequestJso
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.RegisterResponseJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.SendVerificationEmailRequestJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.TwoFactorDataModel
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.VerifyEmailTokenRequestJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.VerifyEmailTokenResponseJson
|
||||
import com.x8bit.bitwarden.data.platform.datasource.network.model.toBitwardenError
|
||||
import com.x8bit.bitwarden.data.platform.datasource.network.util.base64UrlEncode
|
||||
import com.x8bit.bitwarden.data.platform.datasource.network.util.executeForResult
|
||||
@@ -20,17 +22,17 @@ import com.x8bit.bitwarden.data.platform.util.DeviceModelProvider
|
||||
import kotlinx.serialization.json.Json
|
||||
|
||||
class IdentityServiceImpl(
|
||||
private val api: IdentityApi,
|
||||
private val unauthenticatedIdentityApi: UnauthenticatedIdentityApi,
|
||||
private val json: Json,
|
||||
private val deviceModelProvider: DeviceModelProvider = DeviceModelProvider(),
|
||||
) : IdentityService {
|
||||
|
||||
override suspend fun preLogin(email: String): Result<PreLoginResponseJson> =
|
||||
api.preLogin(PreLoginRequestJson(email = email))
|
||||
unauthenticatedIdentityApi.preLogin(PreLoginRequestJson(email = email))
|
||||
|
||||
@Suppress("MagicNumber")
|
||||
override suspend fun register(body: RegisterRequestJson): Result<RegisterResponseJson> =
|
||||
api
|
||||
unauthenticatedIdentityApi
|
||||
.register(body)
|
||||
.recoverCatching { throwable ->
|
||||
val bitwardenError = throwable.toBitwardenError()
|
||||
@@ -39,13 +41,8 @@ class IdentityServiceImpl(
|
||||
code = 400,
|
||||
json = json,
|
||||
)
|
||||
?: bitwardenError
|
||||
.parseErrorBodyOrNull<RegisterResponseJson.Invalid>(
|
||||
codes = listOf(400, 429),
|
||||
json = json,
|
||||
)
|
||||
?: bitwardenError.parseErrorBodyOrNull<RegisterResponseJson.Error>(
|
||||
code = 429,
|
||||
?: bitwardenError.parseErrorBodyOrNull<RegisterResponseJson.Invalid>(
|
||||
codes = listOf(400, 429),
|
||||
json = json,
|
||||
)
|
||||
?: throw throwable
|
||||
@@ -58,7 +55,7 @@ class IdentityServiceImpl(
|
||||
authModel: IdentityTokenAuthModel,
|
||||
captchaToken: String?,
|
||||
twoFactorData: TwoFactorDataModel?,
|
||||
): Result<GetTokenResponseJson> = api
|
||||
): Result<GetTokenResponseJson> = unauthenticatedIdentityApi
|
||||
.getToken(
|
||||
scope = "api offline_access",
|
||||
clientId = "mobile",
|
||||
@@ -94,14 +91,14 @@ class IdentityServiceImpl(
|
||||
|
||||
override suspend fun prevalidateSso(
|
||||
organizationIdentifier: String,
|
||||
): Result<PrevalidateSsoResponseJson> = api
|
||||
): Result<PrevalidateSsoResponseJson> = unauthenticatedIdentityApi
|
||||
.prevalidateSso(
|
||||
organizationIdentifier = organizationIdentifier,
|
||||
)
|
||||
|
||||
override fun refreshTokenSynchronously(
|
||||
refreshToken: String,
|
||||
): Result<RefreshTokenResponseJson> = api
|
||||
): Result<RefreshTokenResponseJson> = unauthenticatedIdentityApi
|
||||
.refreshTokenCall(
|
||||
clientId = "mobile",
|
||||
grantType = "refresh_token",
|
||||
@@ -113,7 +110,7 @@ class IdentityServiceImpl(
|
||||
override suspend fun registerFinish(
|
||||
body: RegisterFinishRequestJson,
|
||||
): Result<RegisterResponseJson> =
|
||||
api
|
||||
unauthenticatedIdentityApi
|
||||
.registerFinish(body)
|
||||
.recoverCatching { throwable ->
|
||||
val bitwardenError = throwable.toBitwardenError()
|
||||
@@ -122,18 +119,45 @@ class IdentityServiceImpl(
|
||||
codes = listOf(400, 429),
|
||||
json = json,
|
||||
)
|
||||
?: bitwardenError.parseErrorBodyOrNull<RegisterResponseJson.Error>(
|
||||
code = 429,
|
||||
json = json,
|
||||
)
|
||||
?: throw throwable
|
||||
}
|
||||
|
||||
override suspend fun sendVerificationEmail(
|
||||
body: SendVerificationEmailRequestJson,
|
||||
): Result<String?> {
|
||||
return api
|
||||
return unauthenticatedIdentityApi
|
||||
.sendVerificationEmail(body = body)
|
||||
.map { it?.content }
|
||||
}
|
||||
|
||||
override suspend fun verifyEmailRegistrationToken(
|
||||
body: VerifyEmailTokenRequestJson,
|
||||
): Result<VerifyEmailTokenResponseJson> = unauthenticatedIdentityApi
|
||||
.verifyEmailToken(
|
||||
body = body,
|
||||
)
|
||||
.map {
|
||||
VerifyEmailTokenResponseJson.Valid
|
||||
}
|
||||
.recoverCatching { throwable ->
|
||||
val bitwardenError = throwable.toBitwardenError()
|
||||
bitwardenError
|
||||
.parseErrorBodyOrNull<VerifyEmailTokenResponseJson.Invalid>(
|
||||
code = 400,
|
||||
json = json,
|
||||
)
|
||||
?.checkForExpiredMessage()
|
||||
?: throw throwable
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* If the message body contains text related to the token being expired, return
|
||||
* the TokenExpired type. Otherwise, return the original Invalid response.
|
||||
*/
|
||||
private fun VerifyEmailTokenResponseJson.Invalid.checkForExpiredMessage() =
|
||||
if (message.contains(other = "expired", ignoreCase = true)) {
|
||||
VerifyEmailTokenResponseJson.TokenExpired
|
||||
} else {
|
||||
this
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package com.x8bit.bitwarden.data.auth.datasource.network.service
|
||||
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.api.AuthenticatedOrganizationApi
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.api.OrganizationApi
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.api.UnauthenticatedOrganizationApi
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.OrganizationAutoEnrollStatusResponseJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.OrganizationDomainSsoDetailsRequestJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.OrganizationDomainSsoDetailsResponseJson
|
||||
@@ -13,7 +13,7 @@ import com.x8bit.bitwarden.data.auth.datasource.network.model.OrganizationResetP
|
||||
*/
|
||||
class OrganizationServiceImpl(
|
||||
private val authenticatedOrganizationApi: AuthenticatedOrganizationApi,
|
||||
private val organizationApi: OrganizationApi,
|
||||
private val unauthenticatedOrganizationApi: UnauthenticatedOrganizationApi,
|
||||
) : OrganizationService {
|
||||
override suspend fun organizationResetPasswordEnroll(
|
||||
organizationId: String,
|
||||
@@ -32,7 +32,7 @@ class OrganizationServiceImpl(
|
||||
|
||||
override suspend fun getOrganizationDomainSsoDetails(
|
||||
email: String,
|
||||
): Result<OrganizationDomainSsoDetailsResponseJson> = organizationApi
|
||||
): Result<OrganizationDomainSsoDetailsResponseJson> = unauthenticatedOrganizationApi
|
||||
.getClaimedDomainOrganizationDetails(
|
||||
body = OrganizationDomainSsoDetailsRequestJson(
|
||||
email = email,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package com.x8bit.bitwarden.data.auth.datasource.sdk
|
||||
|
||||
import com.bitwarden.core.AuthRequestResponse
|
||||
import com.bitwarden.core.KeyConnectorResponse
|
||||
import com.bitwarden.core.MasterPasswordPolicyOptions
|
||||
import com.bitwarden.core.RegisterKeyResponse
|
||||
import com.bitwarden.core.RegisterTdeKeyResponse
|
||||
@@ -37,6 +38,11 @@ interface AuthSdkSource {
|
||||
purpose: HashPurpose,
|
||||
): Result<String>
|
||||
|
||||
/**
|
||||
* Creates a set of encryption key information for use with a key connector.
|
||||
*/
|
||||
suspend fun makeKeyConnectorKeys(): Result<KeyConnectorResponse>
|
||||
|
||||
/**
|
||||
* Creates a set of encryption key information for registration.
|
||||
*/
|
||||
|
||||
@@ -2,6 +2,7 @@ package com.x8bit.bitwarden.data.auth.datasource.sdk
|
||||
|
||||
import com.bitwarden.core.AuthRequestResponse
|
||||
import com.bitwarden.core.FingerprintRequest
|
||||
import com.bitwarden.core.KeyConnectorResponse
|
||||
import com.bitwarden.core.MasterPasswordPolicyOptions
|
||||
import com.bitwarden.core.RegisterKeyResponse
|
||||
import com.bitwarden.core.RegisterTdeKeyResponse
|
||||
@@ -63,6 +64,13 @@ class AuthSdkSourceImpl(
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun makeKeyConnectorKeys(): Result<KeyConnectorResponse> =
|
||||
runCatchingWithLogs {
|
||||
getClient()
|
||||
.auth()
|
||||
.makeKeyConnectorKeys()
|
||||
}
|
||||
|
||||
override suspend fun makeRegisterKeys(
|
||||
email: String,
|
||||
password: String,
|
||||
|
||||
@@ -10,7 +10,7 @@ import com.x8bit.bitwarden.data.auth.manager.model.CreateAuthRequestResult
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
/**
|
||||
* A manager class for handling authentication fo logging in with remote device.
|
||||
* A manager class for handling authentication for logging in with remote device.
|
||||
*/
|
||||
interface AuthRequestManager {
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
package com.x8bit.bitwarden.data.auth.manager
|
||||
|
||||
import com.bitwarden.core.KeyConnectorResponse
|
||||
import com.bitwarden.crypto.Kdf
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.KdfTypeJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.KeyConnectorMasterKeyResponseJson
|
||||
|
||||
/**
|
||||
* Manager used to interface with a key connector.
|
||||
*/
|
||||
interface KeyConnectorManager {
|
||||
/**
|
||||
* Retrieves the master key from the key connector.
|
||||
*/
|
||||
suspend fun getMasterKeyFromKeyConnector(
|
||||
url: String,
|
||||
accessToken: String,
|
||||
): Result<KeyConnectorMasterKeyResponseJson>
|
||||
|
||||
/**
|
||||
* Migrates an existing user to use the key connector.
|
||||
*/
|
||||
@Suppress("LongParameterList")
|
||||
suspend fun migrateExistingUserToKeyConnector(
|
||||
userId: String,
|
||||
url: String,
|
||||
userKeyEncrypted: String,
|
||||
email: String,
|
||||
masterPassword: String,
|
||||
kdf: Kdf,
|
||||
): Result<Unit>
|
||||
|
||||
/**
|
||||
* Migrates a new user to use the key connector.
|
||||
*/
|
||||
@Suppress("LongParameterList")
|
||||
suspend fun migrateNewUserToKeyConnector(
|
||||
url: String,
|
||||
accessToken: String,
|
||||
kdfType: KdfTypeJson,
|
||||
kdfIterations: Int?,
|
||||
kdfMemory: Int?,
|
||||
kdfParallelism: Int?,
|
||||
organizationIdentifier: String,
|
||||
): Result<KeyConnectorResponse>
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
package com.x8bit.bitwarden.data.auth.manager
|
||||
|
||||
import com.bitwarden.core.KeyConnectorResponse
|
||||
import com.bitwarden.crypto.Kdf
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.KdfTypeJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.KeyConnectorKeyRequestJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.KeyConnectorMasterKeyResponseJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.service.AccountsService
|
||||
import com.x8bit.bitwarden.data.auth.datasource.sdk.AuthSdkSource
|
||||
import com.x8bit.bitwarden.data.platform.util.flatMap
|
||||
import com.x8bit.bitwarden.data.vault.datasource.sdk.VaultSdkSource
|
||||
|
||||
/**
|
||||
* The default implementation of the [KeyConnectorManager].
|
||||
*/
|
||||
class KeyConnectorManagerImpl(
|
||||
private val accountsService: AccountsService,
|
||||
private val authSdkSource: AuthSdkSource,
|
||||
private val vaultSdkSource: VaultSdkSource,
|
||||
) : KeyConnectorManager {
|
||||
override suspend fun getMasterKeyFromKeyConnector(
|
||||
url: String,
|
||||
accessToken: String,
|
||||
): Result<KeyConnectorMasterKeyResponseJson> =
|
||||
accountsService.getMasterKeyFromKeyConnector(
|
||||
url = url,
|
||||
accessToken = accessToken,
|
||||
)
|
||||
|
||||
override suspend fun migrateExistingUserToKeyConnector(
|
||||
userId: String,
|
||||
url: String,
|
||||
userKeyEncrypted: String,
|
||||
email: String,
|
||||
masterPassword: String,
|
||||
kdf: Kdf,
|
||||
): Result<Unit> =
|
||||
vaultSdkSource
|
||||
.deriveKeyConnector(
|
||||
userId = userId,
|
||||
userKeyEncrypted = userKeyEncrypted,
|
||||
email = email,
|
||||
password = masterPassword,
|
||||
kdf = kdf,
|
||||
)
|
||||
.flatMap { masterKey ->
|
||||
accountsService.storeMasterKeyToKeyConnector(url = url, masterKey = masterKey)
|
||||
}
|
||||
.flatMap { accountsService.convertToKeyConnector() }
|
||||
|
||||
override suspend fun migrateNewUserToKeyConnector(
|
||||
url: String,
|
||||
accessToken: String,
|
||||
kdfType: KdfTypeJson,
|
||||
kdfIterations: Int?,
|
||||
kdfMemory: Int?,
|
||||
kdfParallelism: Int?,
|
||||
organizationIdentifier: String,
|
||||
): Result<KeyConnectorResponse> =
|
||||
authSdkSource
|
||||
.makeKeyConnectorKeys()
|
||||
.flatMap { keyConnectorResponse ->
|
||||
accountsService
|
||||
.storeMasterKeyToKeyConnector(
|
||||
url = url,
|
||||
accessToken = accessToken,
|
||||
masterKey = keyConnectorResponse.masterKey,
|
||||
)
|
||||
.flatMap {
|
||||
accountsService.setKeyConnectorKey(
|
||||
accessToken = accessToken,
|
||||
body = KeyConnectorKeyRequestJson(
|
||||
userKey = keyConnectorResponse.encryptedUserKey,
|
||||
keys = KeyConnectorKeyRequestJson.Keys(
|
||||
publicKey = keyConnectorResponse.keys.public,
|
||||
encryptedPrivateKey = keyConnectorResponse.keys.private,
|
||||
),
|
||||
kdfType = kdfType,
|
||||
kdfIterations = kdfIterations,
|
||||
kdfMemory = kdfMemory,
|
||||
kdfParallelism = kdfParallelism,
|
||||
organizationIdentifier = organizationIdentifier,
|
||||
),
|
||||
)
|
||||
}
|
||||
.map { keyConnectorResponse }
|
||||
}
|
||||
}
|
||||
@@ -18,6 +18,7 @@ class TrustedDeviceManagerImpl(
|
||||
) : TrustedDeviceManager {
|
||||
override suspend fun trustThisDeviceIfNecessary(userId: String): Result<Boolean> =
|
||||
if (authDiskSource.getShouldTrustDevice(userId = userId) != true) {
|
||||
authDiskSource.storeIsTdeLoginComplete(userId = userId, isTdeLoginComplete = true)
|
||||
false.asSuccess()
|
||||
} else {
|
||||
vaultSdkSource
|
||||
@@ -51,6 +52,7 @@ class TrustedDeviceManagerImpl(
|
||||
userId = userId,
|
||||
previousUserState = requireNotNull(authDiskSource.userState),
|
||||
)
|
||||
authDiskSource.storeIsTdeLoginComplete(userId = userId, isTdeLoginComplete = true)
|
||||
}
|
||||
.also { authDiskSource.storeShouldTrustDevice(userId = userId, shouldTrustDevice = null) }
|
||||
.map { Unit }
|
||||
|
||||
@@ -14,15 +14,15 @@ interface UserLogoutManager {
|
||||
val logoutEventFlow: SharedFlow<LogoutEvent>
|
||||
|
||||
/**
|
||||
* Completely logs out the given [userId], removing all data.
|
||||
* If [isExpired] is true, a toast will be displayed
|
||||
* letting the user know the session has expired.
|
||||
* Completely logs out the given [userId], removing all data. If [isExpired] is true, a toast
|
||||
* will be displayed letting the user know the session has expired.
|
||||
*/
|
||||
fun logout(userId: String, isExpired: Boolean = false)
|
||||
|
||||
/**
|
||||
* Partially logs out the given [userId]. All data for the given [userId] will be removed with
|
||||
* the exception of basic account data.
|
||||
* the exception of basic account data. If [isExpired] is true, a toast will be displayed
|
||||
* letting the user know the session has expired.
|
||||
*/
|
||||
fun softLogout(userId: String)
|
||||
fun softLogout(userId: String, isExpired: Boolean = false)
|
||||
}
|
||||
|
||||
@@ -64,7 +64,10 @@ class UserLogoutManagerImpl(
|
||||
mutableLogoutEventFlow.tryEmit(LogoutEvent(loggedOutUserId = userId))
|
||||
}
|
||||
|
||||
override fun softLogout(userId: String) {
|
||||
override fun softLogout(userId: String, isExpired: Boolean) {
|
||||
if (isExpired) {
|
||||
showToast(message = R.string.login_expired)
|
||||
}
|
||||
authDiskSource.storeAccountTokens(
|
||||
userId = userId,
|
||||
accountTokens = null,
|
||||
@@ -74,7 +77,11 @@ class UserLogoutManagerImpl(
|
||||
val vaultTimeoutInMinutes = settingsDiskSource.getVaultTimeoutInMinutes(userId = userId)
|
||||
val vaultTimeoutAction = settingsDiskSource.getVaultTimeoutAction(userId = userId)
|
||||
|
||||
switchUserIfAvailable(currentUserId = userId, removeCurrentUserFromAccounts = false)
|
||||
switchUserIfAvailable(
|
||||
currentUserId = userId,
|
||||
removeCurrentUserFromAccounts = false,
|
||||
isExpired = isExpired,
|
||||
)
|
||||
|
||||
clearData(userId = userId)
|
||||
mutableLogoutEventFlow.tryEmit(LogoutEvent(loggedOutUserId = userId))
|
||||
|
||||
@@ -2,6 +2,7 @@ package com.x8bit.bitwarden.data.auth.manager.di
|
||||
|
||||
import android.content.Context
|
||||
import com.x8bit.bitwarden.data.auth.datasource.disk.AuthDiskSource
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.service.AccountsService
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.service.AuthRequestsService
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.service.DevicesService
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.service.NewAuthRequestService
|
||||
@@ -10,6 +11,8 @@ import com.x8bit.bitwarden.data.auth.manager.AuthRequestManager
|
||||
import com.x8bit.bitwarden.data.auth.manager.AuthRequestManagerImpl
|
||||
import com.x8bit.bitwarden.data.auth.manager.AuthRequestNotificationManager
|
||||
import com.x8bit.bitwarden.data.auth.manager.AuthRequestNotificationManagerImpl
|
||||
import com.x8bit.bitwarden.data.auth.manager.KeyConnectorManager
|
||||
import com.x8bit.bitwarden.data.auth.manager.KeyConnectorManagerImpl
|
||||
import com.x8bit.bitwarden.data.auth.manager.TrustedDeviceManager
|
||||
import com.x8bit.bitwarden.data.auth.manager.TrustedDeviceManagerImpl
|
||||
import com.x8bit.bitwarden.data.auth.manager.UserLogoutManager
|
||||
@@ -71,6 +74,19 @@ object AuthManagerModule {
|
||||
authDiskSource = authDiskSource,
|
||||
)
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideKeyConnectorManager(
|
||||
accountsService: AccountsService,
|
||||
authSdkSource: AuthSdkSource,
|
||||
vaultSdkSource: VaultSdkSource,
|
||||
): KeyConnectorManager =
|
||||
KeyConnectorManagerImpl(
|
||||
accountsService = accountsService,
|
||||
authSdkSource = authSdkSource,
|
||||
vaultSdkSource = vaultSdkSource,
|
||||
)
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideTrustedDeviceManager(
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
package com.x8bit.bitwarden.data.auth.repository
|
||||
|
||||
import com.x8bit.bitwarden.data.auth.datasource.disk.model.ForcePasswordResetReason
|
||||
import com.x8bit.bitwarden.data.auth.datasource.disk.model.OnboardingStatus
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.GetTokenResponseJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.TwoFactorDataModel
|
||||
import com.x8bit.bitwarden.data.auth.manager.AuthRequestManager
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.AuthState
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.BreachCountResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.DeleteAccountResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.EmailTokenResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.KnownDeviceResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.LoginResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.NewSsoUserResult
|
||||
@@ -16,6 +18,7 @@ import com.x8bit.bitwarden.data.auth.repository.model.PasswordStrengthResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.PolicyInformation
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.PrevalidateSsoResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.RegisterResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.RemovePasswordResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.RequestOtpResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.ResendEmailResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.ResetPasswordResult
|
||||
@@ -102,6 +105,11 @@ interface AuthRepository : AuthenticatorProvider, AuthRequestManager {
|
||||
*/
|
||||
var rememberedOrgIdentifier: String?
|
||||
|
||||
/**
|
||||
* The currently persisted state indicating whether the user has completed login via TDE.
|
||||
*/
|
||||
val tdeLoginComplete: Boolean?
|
||||
|
||||
/**
|
||||
* The currently persisted state indicating whether the user has trusted this device.
|
||||
*/
|
||||
@@ -267,6 +275,12 @@ interface AuthRepository : AuthenticatorProvider, AuthRequestManager {
|
||||
email: String,
|
||||
): PasswordHintResult
|
||||
|
||||
/**
|
||||
* Removes the users password from the account. This used used when migrating from master
|
||||
* password login to key connector login.
|
||||
*/
|
||||
suspend fun removePassword(masterPassword: String): RemovePasswordResult
|
||||
|
||||
/**
|
||||
* Resets the users password from the [currentPassword] (or null for account recovery resets),
|
||||
* to the [newPassword] and optional [passwordHint].
|
||||
@@ -365,4 +379,17 @@ interface AuthRepository : AuthenticatorProvider, AuthRequestManager {
|
||||
name: String,
|
||||
receiveMarketingEmails: Boolean,
|
||||
): SendVerificationEmailResult
|
||||
|
||||
/**
|
||||
* Validates the given [token] for the given [email]. Part of th new account registration flow.
|
||||
*/
|
||||
suspend fun validateEmailToken(
|
||||
email: String,
|
||||
token: String,
|
||||
): EmailTokenResult
|
||||
|
||||
/**
|
||||
* Update the value of the onboarding status for the user.
|
||||
*/
|
||||
fun setOnboardingStatus(userId: String, status: OnboardingStatus?)
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ import com.x8bit.bitwarden.data.auth.datasource.disk.AuthDiskSource
|
||||
import com.x8bit.bitwarden.data.auth.datasource.disk.model.AccountJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.disk.model.AccountTokensJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.disk.model.ForcePasswordResetReason
|
||||
import com.x8bit.bitwarden.data.auth.datasource.disk.model.OnboardingStatus
|
||||
import com.x8bit.bitwarden.data.auth.datasource.disk.model.UserStateJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.DeleteAccountResponseJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.DeviceDataModel
|
||||
@@ -26,6 +27,8 @@ import com.x8bit.bitwarden.data.auth.datasource.network.model.SetPasswordRequest
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.TrustedDeviceUserDecryptionOptionsJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.TwoFactorAuthMethod
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.TwoFactorDataModel
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.VerifyEmailTokenRequestJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.VerifyEmailTokenResponseJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.service.AccountsService
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.service.DevicesService
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.service.HaveIBeenPwnedService
|
||||
@@ -35,11 +38,13 @@ import com.x8bit.bitwarden.data.auth.datasource.sdk.AuthSdkSource
|
||||
import com.x8bit.bitwarden.data.auth.datasource.sdk.util.toInt
|
||||
import com.x8bit.bitwarden.data.auth.datasource.sdk.util.toKdfTypeJson
|
||||
import com.x8bit.bitwarden.data.auth.manager.AuthRequestManager
|
||||
import com.x8bit.bitwarden.data.auth.manager.KeyConnectorManager
|
||||
import com.x8bit.bitwarden.data.auth.manager.TrustedDeviceManager
|
||||
import com.x8bit.bitwarden.data.auth.manager.UserLogoutManager
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.AuthState
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.BreachCountResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.DeleteAccountResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.EmailTokenResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.KnownDeviceResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.LoginResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.NewSsoUserResult
|
||||
@@ -49,6 +54,7 @@ import com.x8bit.bitwarden.data.auth.repository.model.PasswordStrengthResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.PolicyInformation
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.PrevalidateSsoResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.RegisterResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.RemovePasswordResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.RequestOtpResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.ResendEmailResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.ResetPasswordResult
|
||||
@@ -56,6 +62,7 @@ import com.x8bit.bitwarden.data.auth.repository.model.SendVerificationEmailResul
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.SetPasswordResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.SwitchAccountResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.UserAccountTokens
|
||||
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.ValidatePasswordResult
|
||||
@@ -68,12 +75,17 @@ import com.x8bit.bitwarden.data.auth.repository.util.DuoCallbackTokenResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.util.SsoCallbackResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.util.WebAuthResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.util.activeUserIdChangesFlow
|
||||
import com.x8bit.bitwarden.data.auth.repository.util.currentOnboardingStatus
|
||||
import com.x8bit.bitwarden.data.auth.repository.util.onboardingStatusChangesFlow
|
||||
import com.x8bit.bitwarden.data.auth.repository.util.policyInformation
|
||||
import com.x8bit.bitwarden.data.auth.repository.util.toRemovedPasswordUserStateJson
|
||||
import com.x8bit.bitwarden.data.auth.repository.util.toSdkParams
|
||||
import com.x8bit.bitwarden.data.auth.repository.util.toUserState
|
||||
import com.x8bit.bitwarden.data.auth.repository.util.toUserStateJsonWithPassword
|
||||
import com.x8bit.bitwarden.data.auth.repository.util.userAccountTokens
|
||||
import com.x8bit.bitwarden.data.auth.repository.util.userAccountTokensFlow
|
||||
import com.x8bit.bitwarden.data.auth.repository.util.userKeyConnectorStateFlow
|
||||
import com.x8bit.bitwarden.data.auth.repository.util.userKeyConnectorStateList
|
||||
import com.x8bit.bitwarden.data.auth.repository.util.userOrganizationsList
|
||||
import com.x8bit.bitwarden.data.auth.repository.util.userOrganizationsListFlow
|
||||
import com.x8bit.bitwarden.data.auth.repository.util.userSwitchingChangesFlow
|
||||
@@ -92,6 +104,7 @@ import com.x8bit.bitwarden.data.platform.repository.util.bufferedMutableSharedFl
|
||||
import com.x8bit.bitwarden.data.platform.util.asFailure
|
||||
import com.x8bit.bitwarden.data.platform.util.asSuccess
|
||||
import com.x8bit.bitwarden.data.platform.util.flatMap
|
||||
import com.x8bit.bitwarden.data.vault.datasource.network.model.OrganizationType
|
||||
import com.x8bit.bitwarden.data.vault.datasource.network.model.PolicyTypeJson
|
||||
import com.x8bit.bitwarden.data.vault.datasource.network.model.SyncResponseJson
|
||||
import com.x8bit.bitwarden.data.vault.datasource.sdk.VaultSdkSource
|
||||
@@ -142,6 +155,7 @@ class AuthRepositoryImpl(
|
||||
private val settingsRepository: SettingsRepository,
|
||||
private val vaultRepository: VaultRepository,
|
||||
private val authRequestManager: AuthRequestManager,
|
||||
private val keyConnectorManager: KeyConnectorManager,
|
||||
private val trustedDeviceManager: TrustedDeviceManager,
|
||||
private val userLogoutManager: UserLogoutManager,
|
||||
private val policyManager: PolicyManager,
|
||||
@@ -238,6 +252,8 @@ class AuthRepositoryImpl(
|
||||
authDiskSource.userStateFlow,
|
||||
authDiskSource.userAccountTokensFlow,
|
||||
authDiskSource.userOrganizationsListFlow,
|
||||
authDiskSource.userKeyConnectorStateFlow,
|
||||
authDiskSource.onboardingStatusChangesFlow,
|
||||
vaultRepository.vaultUnlockDataStateFlow,
|
||||
mutableHasPendingAccountAdditionStateFlow,
|
||||
// Ignore the data in the merge, but trigger an update when they emit.
|
||||
@@ -249,13 +265,17 @@ class AuthRepositoryImpl(
|
||||
val userStateJson = array[0] as UserStateJson?
|
||||
val userAccountTokens = array[1] as List<UserAccountTokens>
|
||||
val userOrganizationsList = array[2] as List<UserOrganizations>
|
||||
val vaultState = array[3] as List<VaultUnlockData>
|
||||
val hasPendingAccountAddition = array[4] as Boolean
|
||||
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
|
||||
userStateJson?.toUserState(
|
||||
vaultState = vaultState,
|
||||
userAccountTokens = userAccountTokens,
|
||||
userOrganizationsList = userOrganizationsList,
|
||||
userIsUsingKeyConnectorList = userIsUsingKeyConnectorList,
|
||||
hasPendingAccountAddition = hasPendingAccountAddition,
|
||||
onboardingStatus = onboardingStatus,
|
||||
isBiometricsEnabledProvider = ::isBiometricsEnabled,
|
||||
vaultUnlockTypeProvider = ::getVaultUnlockType,
|
||||
isDeviceTrustedProvider = ::isDeviceTrusted,
|
||||
@@ -272,7 +292,9 @@ class AuthRepositoryImpl(
|
||||
vaultState = vaultRepository.vaultUnlockDataStateFlow.value,
|
||||
userAccountTokens = authDiskSource.userAccountTokens,
|
||||
userOrganizationsList = authDiskSource.userOrganizationsList,
|
||||
userIsUsingKeyConnectorList = authDiskSource.userKeyConnectorStateList,
|
||||
hasPendingAccountAddition = mutableHasPendingAccountAdditionStateFlow.value,
|
||||
onboardingStatus = authDiskSource.currentOnboardingStatus,
|
||||
isBiometricsEnabledProvider = ::isBiometricsEnabled,
|
||||
vaultUnlockTypeProvider = ::getVaultUnlockType,
|
||||
isDeviceTrustedProvider = ::isDeviceTrusted,
|
||||
@@ -300,6 +322,9 @@ class AuthRepositoryImpl(
|
||||
|
||||
override var rememberedOrgIdentifier: String? by authDiskSource::rememberedOrgIdentifier
|
||||
|
||||
override val tdeLoginComplete: Boolean?
|
||||
get() = activeUserId?.let { authDiskSource.getIsTdeLoginComplete(userId = it) }
|
||||
|
||||
override var shouldTrustDevice: Boolean
|
||||
get() = activeUserId?.let { authDiskSource.getShouldTrustDevice(userId = it) } ?: false
|
||||
set(value) {
|
||||
@@ -538,8 +563,7 @@ class AuthRepositoryImpl(
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
authDiskSource.storeUserKey(userId = userId, userKey = asymmetricalKey)
|
||||
settingsRepository.storeUserHasLoggedInValue(userId)
|
||||
vaultRepository.syncIfNecessary()
|
||||
return LoginResult.Success
|
||||
}
|
||||
@@ -757,6 +781,7 @@ class AuthRepositoryImpl(
|
||||
)
|
||||
.flatMap { registerKeyResponse ->
|
||||
if (emailVerificationToken == null) {
|
||||
// TODO PM-6675: Remove register call and service implementation
|
||||
identityService.register(
|
||||
body = RegisterRequestJson(
|
||||
email = email,
|
||||
@@ -815,10 +840,6 @@ class AuthRepositoryImpl(
|
||||
?: it.message,
|
||||
)
|
||||
}
|
||||
|
||||
is RegisterResponseJson.Error -> {
|
||||
RegisterResult.Error(it.message)
|
||||
}
|
||||
}
|
||||
},
|
||||
onFailure = { RegisterResult.Error(errorMessage = null) },
|
||||
@@ -837,6 +858,46 @@ class AuthRepositoryImpl(
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun removePassword(masterPassword: String): RemovePasswordResult {
|
||||
val activeAccount = authDiskSource
|
||||
.userState
|
||||
?.activeAccount
|
||||
?: return RemovePasswordResult.Error
|
||||
val profile = activeAccount.profile
|
||||
val userId = profile.userId
|
||||
val userKey = authDiskSource
|
||||
.getUserKey(userId = userId)
|
||||
?: return RemovePasswordResult.Error
|
||||
val keyConnectorUrl = organizations
|
||||
.find {
|
||||
it.shouldUseKeyConnector &&
|
||||
it.type != OrganizationType.OWNER &&
|
||||
it.type != OrganizationType.ADMIN
|
||||
}
|
||||
?.keyConnectorUrl
|
||||
?: return RemovePasswordResult.Error
|
||||
return keyConnectorManager
|
||||
.migrateExistingUserToKeyConnector(
|
||||
userId = userId,
|
||||
url = keyConnectorUrl,
|
||||
userKeyEncrypted = userKey,
|
||||
email = profile.email,
|
||||
masterPassword = masterPassword,
|
||||
kdf = profile.toSdkParams(),
|
||||
)
|
||||
.onSuccess {
|
||||
authDiskSource.userState = authDiskSource
|
||||
.userState
|
||||
?.toRemovedPasswordUserStateJson(userId = userId)
|
||||
vaultRepository.sync()
|
||||
settingsRepository.setDefaultsIfNecessary(userId = userId)
|
||||
}
|
||||
.fold(
|
||||
onFailure = { RemovePasswordResult.Error },
|
||||
onSuccess = { RemovePasswordResult.Success },
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun resetPassword(
|
||||
currentPassword: String?,
|
||||
newPassword: String,
|
||||
@@ -1205,6 +1266,35 @@ class AuthRepositoryImpl(
|
||||
},
|
||||
)
|
||||
|
||||
override suspend fun validateEmailToken(email: String, token: String): EmailTokenResult {
|
||||
return identityService
|
||||
.verifyEmailRegistrationToken(
|
||||
body = VerifyEmailTokenRequestJson(
|
||||
email = email,
|
||||
token = token,
|
||||
),
|
||||
)
|
||||
.fold(
|
||||
onSuccess = {
|
||||
when (val json = it) {
|
||||
VerifyEmailTokenResponseJson.Valid -> EmailTokenResult.Success
|
||||
is VerifyEmailTokenResponseJson.Invalid -> {
|
||||
EmailTokenResult.Error(json.message)
|
||||
}
|
||||
|
||||
VerifyEmailTokenResponseJson.TokenExpired -> EmailTokenResult.Expired
|
||||
}
|
||||
},
|
||||
onFailure = {
|
||||
EmailTokenResult.Error(message = null)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
override fun setOnboardingStatus(userId: String, status: OnboardingStatus?) {
|
||||
authDiskSource.storeOnboardingStatus(userId = userId, onboardingStatus = status)
|
||||
}
|
||||
|
||||
@Suppress("CyclomaticComplexMethod")
|
||||
private suspend fun validatePasswordAgainstPolicy(
|
||||
password: String,
|
||||
@@ -1413,30 +1503,42 @@ class AuthRepositoryImpl(
|
||||
previousUserState = authDiskSource.userState,
|
||||
environmentUrlData = environmentRepository.environment.environmentUrlData,
|
||||
)
|
||||
val userId = userStateJson.activeUserId
|
||||
val profile = userStateJson.activeAccount.profile
|
||||
val userId = profile.userId
|
||||
|
||||
checkForVaultUnlockError(
|
||||
onVaultUnlockError = { vaultUnlockError ->
|
||||
return@userStateTransaction vaultUnlockError.toLoginErrorResult()
|
||||
},
|
||||
) {
|
||||
val keyConnectorUrl = loginResponse
|
||||
.keyConnectorUrl
|
||||
?: loginResponse
|
||||
.userDecryptionOptions
|
||||
?.keyConnectorUserDecryptionOptions
|
||||
?.keyConnectorUrl
|
||||
val isDeviceUnlockAvailable = deviceData != null ||
|
||||
loginResponse.userDecryptionOptions?.trustedDeviceUserDecryptionOptions != null
|
||||
// if possible attempt to unlock the vault with trusted device data
|
||||
if (isDeviceUnlockAvailable) {
|
||||
unlockVaultWithTdeOnLoginSuccess(
|
||||
loginResponse = loginResponse,
|
||||
userStateJson = userStateJson,
|
||||
profile = profile,
|
||||
deviceData = deviceData,
|
||||
)
|
||||
} else if (keyConnectorUrl != null && orgIdentifier != null) {
|
||||
unlockVaultWithKeyConnectorOnLoginSuccess(
|
||||
profile = profile,
|
||||
keyConnectorUrl = keyConnectorUrl,
|
||||
orgIdentifier = orgIdentifier,
|
||||
loginResponse = loginResponse,
|
||||
)
|
||||
} else {
|
||||
password?.let {
|
||||
unlockVaultWithPasswordOnLoginSuccess(
|
||||
loginResponse = loginResponse,
|
||||
userStateJson = userStateJson,
|
||||
password = it,
|
||||
)
|
||||
}
|
||||
unlockVaultWithPasswordOnLoginSuccess(
|
||||
loginResponse = loginResponse,
|
||||
profile = profile,
|
||||
password = password,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1446,7 +1548,7 @@ class AuthRepositoryImpl(
|
||||
.hashPassword(
|
||||
email = email,
|
||||
password = it,
|
||||
kdf = userStateJson.activeAccount.profile.toSdkParams(),
|
||||
kdf = profile.toSdkParams(),
|
||||
purpose = HashPurpose.LOCAL_AUTHORIZATION,
|
||||
)
|
||||
.onSuccess { passwordHash ->
|
||||
@@ -1468,14 +1570,27 @@ class AuthRepositoryImpl(
|
||||
),
|
||||
)
|
||||
settingsRepository.hasUserLoggedInOrCreatedAccount = true
|
||||
|
||||
val shouldSetOnboardingStatus = featureFlagManager.getFeatureFlag(FlagKey.OnboardingFlow) &&
|
||||
!settingsRepository.getUserHasLoggedInValue(userId = userId)
|
||||
if (shouldSetOnboardingStatus) {
|
||||
setOnboardingStatus(
|
||||
userId = userId,
|
||||
status = OnboardingStatus.NOT_STARTED,
|
||||
)
|
||||
}
|
||||
|
||||
authDiskSource.userState = userStateJson
|
||||
loginResponse.key?.let {
|
||||
// Only set the value if it's present, since we may have set it already
|
||||
// when we completed the pending admin auth request.
|
||||
authDiskSource.storeUserKey(userId = userId, userKey = it)
|
||||
}
|
||||
authDiskSource.storePrivateKey(userId = userId, privateKey = loginResponse.privateKey)
|
||||
|
||||
loginResponse.privateKey?.let {
|
||||
// Only set the value if it's present, since we may have set it already
|
||||
// when we completed the key connector conversion.
|
||||
authDiskSource.storePrivateKey(userId = userId, privateKey = it)
|
||||
}
|
||||
// If the user just authenticated with a two-factor code and selected the option to
|
||||
// remember it, then the API response will return a token that will be used in place
|
||||
// of the two-factor code on the next login attempt.
|
||||
@@ -1494,6 +1609,7 @@ class AuthRepositoryImpl(
|
||||
resendEmailRequestJson = null
|
||||
twoFactorDeviceData = null
|
||||
settingsRepository.setDefaultsIfNecessary(userId = userId)
|
||||
settingsRepository.storeUserHasLoggedInValue(userId)
|
||||
vaultRepository.syncIfNecessary()
|
||||
hasPendingAccountAddition = false
|
||||
LoginResult.Success
|
||||
@@ -1524,12 +1640,89 @@ class AuthRepositoryImpl(
|
||||
return LoginResult.TwoFactorRequired
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempt to unlock the current user's vault with key connector data.
|
||||
*/
|
||||
private suspend fun unlockVaultWithKeyConnectorOnLoginSuccess(
|
||||
profile: AccountJson.Profile,
|
||||
keyConnectorUrl: String,
|
||||
orgIdentifier: String,
|
||||
loginResponse: GetTokenResponseJson.Success,
|
||||
): VaultUnlockResult? =
|
||||
if (loginResponse.userDecryptionOptions?.hasMasterPassword != false) {
|
||||
// This user has a master password, so we skip the key-connector logic as it is not
|
||||
// setup yet. The user can still unlock the vault with their master password.
|
||||
null
|
||||
} else if (loginResponse.key != null && loginResponse.privateKey != null) {
|
||||
// This is a returning user who should already have the key connector setup
|
||||
keyConnectorManager
|
||||
.getMasterKeyFromKeyConnector(
|
||||
url = keyConnectorUrl,
|
||||
accessToken = loginResponse.accessToken,
|
||||
)
|
||||
.map {
|
||||
unlockVault(
|
||||
accountProfile = profile,
|
||||
privateKey = loginResponse.privateKey,
|
||||
initUserCryptoMethod = InitUserCryptoMethod.KeyConnector(
|
||||
masterKey = it.masterKey,
|
||||
userKey = loginResponse.key,
|
||||
),
|
||||
)
|
||||
}
|
||||
.fold(
|
||||
// If the request failed, we want to abort the login process
|
||||
onFailure = { VaultUnlockResult.GenericError },
|
||||
onSuccess = { it },
|
||||
)
|
||||
} else {
|
||||
// This is a new user who needs to setup the key connector
|
||||
keyConnectorManager
|
||||
.migrateNewUserToKeyConnector(
|
||||
url = keyConnectorUrl,
|
||||
accessToken = loginResponse.accessToken,
|
||||
kdfType = loginResponse.kdfType,
|
||||
kdfIterations = loginResponse.kdfIterations,
|
||||
kdfMemory = loginResponse.kdfMemory,
|
||||
kdfParallelism = loginResponse.kdfParallelism,
|
||||
organizationIdentifier = orgIdentifier,
|
||||
)
|
||||
.map { keyConnectorResponse ->
|
||||
val result = unlockVault(
|
||||
accountProfile = profile,
|
||||
privateKey = keyConnectorResponse.keys.private,
|
||||
initUserCryptoMethod = InitUserCryptoMethod.KeyConnector(
|
||||
masterKey = keyConnectorResponse.masterKey,
|
||||
userKey = keyConnectorResponse.encryptedUserKey,
|
||||
),
|
||||
)
|
||||
if (result is VaultUnlockResult.Success) {
|
||||
// We now know that login/unlock was successful, so we store the userKey
|
||||
// and privateKey we now have since it didn't exist on the loginResponse
|
||||
authDiskSource.storeUserKey(
|
||||
userId = profile.userId,
|
||||
userKey = keyConnectorResponse.encryptedUserKey,
|
||||
)
|
||||
authDiskSource.storePrivateKey(
|
||||
userId = profile.userId,
|
||||
privateKey = keyConnectorResponse.keys.private,
|
||||
)
|
||||
}
|
||||
result
|
||||
}
|
||||
.fold(
|
||||
// If the request failed, we want to abort the login process
|
||||
onFailure = { VaultUnlockResult.GenericError },
|
||||
onSuccess = { it },
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempt to unlock the current user's vault with password data.
|
||||
*/
|
||||
private suspend fun unlockVaultWithPasswordOnLoginSuccess(
|
||||
loginResponse: GetTokenResponseJson.Success,
|
||||
userStateJson: UserStateJson,
|
||||
profile: AccountJson.Profile,
|
||||
password: String?,
|
||||
): VaultUnlockResult? {
|
||||
// Attempt to unlock the vault with password if possible.
|
||||
@@ -1537,7 +1730,7 @@ class AuthRepositoryImpl(
|
||||
val privateKey = loginResponse.privateKey ?: return null
|
||||
val key = loginResponse.key ?: return null
|
||||
return unlockVault(
|
||||
accountProfile = userStateJson.activeAccount.profile,
|
||||
accountProfile = profile,
|
||||
privateKey = privateKey,
|
||||
initUserCryptoMethod = InitUserCryptoMethod.Password(
|
||||
password = masterPassword,
|
||||
@@ -1551,7 +1744,7 @@ class AuthRepositoryImpl(
|
||||
*/
|
||||
private suspend fun unlockVaultWithTdeOnLoginSuccess(
|
||||
loginResponse: GetTokenResponseJson.Success,
|
||||
userStateJson: UserStateJson,
|
||||
profile: AccountJson.Profile,
|
||||
deviceData: DeviceDataModel?,
|
||||
): VaultUnlockResult? {
|
||||
// Attempt to unlock the vault with auth request if possible.
|
||||
@@ -1559,7 +1752,7 @@ class AuthRepositoryImpl(
|
||||
if (loginResponse.privateKey != null && loginResponse.key != null) {
|
||||
deviceData?.let { model ->
|
||||
return unlockVault(
|
||||
accountProfile = userStateJson.activeAccount.profile,
|
||||
accountProfile = profile,
|
||||
privateKey = loginResponse.privateKey,
|
||||
initUserCryptoMethod = InitUserCryptoMethod.AuthRequest(
|
||||
requestPrivateKey = model.privateKey,
|
||||
@@ -1588,7 +1781,7 @@ class AuthRepositoryImpl(
|
||||
loginResponse.privateKey?.let { privateKey ->
|
||||
unlockVaultWithTrustedDeviceUserDecryptionOptionsAndStoreKeys(
|
||||
options = options,
|
||||
userStateJson = userStateJson,
|
||||
profile = profile,
|
||||
privateKey = privateKey,
|
||||
)
|
||||
}
|
||||
@@ -1601,11 +1794,11 @@ class AuthRepositoryImpl(
|
||||
*/
|
||||
private suspend fun unlockVaultWithTrustedDeviceUserDecryptionOptionsAndStoreKeys(
|
||||
options: TrustedDeviceUserDecryptionOptionsJson,
|
||||
userStateJson: UserStateJson,
|
||||
profile: AccountJson.Profile,
|
||||
privateKey: String,
|
||||
): VaultUnlockResult? {
|
||||
var vaultUnlockResult: VaultUnlockResult? = null
|
||||
val userId = userStateJson.activeUserId
|
||||
val userId = profile.userId
|
||||
val deviceKey = authDiskSource.getDeviceKey(userId = userId)
|
||||
if (deviceKey == null) {
|
||||
// A null device key means this device is not trusted.
|
||||
@@ -1619,7 +1812,7 @@ class AuthRepositoryImpl(
|
||||
// For approved requests the key will always be present.
|
||||
val userKey = requireNotNull(request.key)
|
||||
vaultUnlockResult = unlockVault(
|
||||
accountProfile = userStateJson.activeAccount.profile,
|
||||
accountProfile = profile,
|
||||
privateKey = privateKey,
|
||||
initUserCryptoMethod = InitUserCryptoMethod.AuthRequest(
|
||||
requestPrivateKey = pendingRequest.requestPrivateKey,
|
||||
@@ -1646,7 +1839,7 @@ class AuthRepositoryImpl(
|
||||
}
|
||||
|
||||
vaultUnlockResult = unlockVault(
|
||||
accountProfile = userStateJson.activeAccount.profile,
|
||||
accountProfile = profile,
|
||||
privateKey = privateKey,
|
||||
initUserCryptoMethod = InitUserCryptoMethod.DeviceKey(
|
||||
deviceKey = deviceKey,
|
||||
|
||||
@@ -8,6 +8,7 @@ import com.x8bit.bitwarden.data.auth.datasource.network.service.IdentityService
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.service.OrganizationService
|
||||
import com.x8bit.bitwarden.data.auth.datasource.sdk.AuthSdkSource
|
||||
import com.x8bit.bitwarden.data.auth.manager.AuthRequestManager
|
||||
import com.x8bit.bitwarden.data.auth.manager.KeyConnectorManager
|
||||
import com.x8bit.bitwarden.data.auth.manager.TrustedDeviceManager
|
||||
import com.x8bit.bitwarden.data.auth.manager.UserLogoutManager
|
||||
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
|
||||
@@ -48,6 +49,7 @@ object AuthRepositoryModule {
|
||||
environmentRepository: EnvironmentRepository,
|
||||
settingsRepository: SettingsRepository,
|
||||
vaultRepository: VaultRepository,
|
||||
keyConnectorManager: KeyConnectorManager,
|
||||
authRequestManager: AuthRequestManager,
|
||||
trustedDeviceManager: TrustedDeviceManager,
|
||||
userLogoutManager: UserLogoutManager,
|
||||
@@ -67,6 +69,7 @@ object AuthRepositoryModule {
|
||||
environmentRepository = environmentRepository,
|
||||
settingsRepository = settingsRepository,
|
||||
vaultRepository = vaultRepository,
|
||||
keyConnectorManager = keyConnectorManager,
|
||||
authRequestManager = authRequestManager,
|
||||
trustedDeviceManager = trustedDeviceManager,
|
||||
userLogoutManager = userLogoutManager,
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
package com.x8bit.bitwarden.data.auth.repository.model
|
||||
|
||||
/**
|
||||
* Model the result of a request to validate a given email token.
|
||||
*/
|
||||
sealed class EmailTokenResult {
|
||||
|
||||
/**
|
||||
* The token is valid and the user can proceed with account creation.
|
||||
*/
|
||||
data object Success : EmailTokenResult()
|
||||
|
||||
/**
|
||||
* The token has expired and is no longer valid.
|
||||
*/
|
||||
data object Expired : EmailTokenResult()
|
||||
|
||||
/**
|
||||
* There was an error validating the token.
|
||||
*/
|
||||
data class Error(val message: String?) : EmailTokenResult()
|
||||
}
|
||||
@@ -37,4 +37,10 @@ data class JwtTokenDataJson(
|
||||
|
||||
@SerialName("amr")
|
||||
val authenticationMethodsReference: List<String>,
|
||||
)
|
||||
) {
|
||||
/**
|
||||
* Indicates that this is an external user. Mainly used for SSO users with a key connector.
|
||||
*/
|
||||
val isExternal: Boolean
|
||||
get() = authenticationMethodsReference.any { it == "external" }
|
||||
}
|
||||
|
||||
@@ -1,12 +1,21 @@
|
||||
package com.x8bit.bitwarden.data.auth.repository.model
|
||||
|
||||
import com.x8bit.bitwarden.data.vault.datasource.network.model.OrganizationType
|
||||
|
||||
/**
|
||||
* Represents an organization a user may be a member of.
|
||||
*
|
||||
* @property id The ID of the organization.
|
||||
* @property name The name of the organization (if applicable).
|
||||
* @property shouldManageResetPassword Indicates that this user has the permission to manage their
|
||||
* own password.
|
||||
* @property shouldUseKeyConnector Indicates that the organization uses a key connector.
|
||||
* @property role The user's role in the organization.
|
||||
*/
|
||||
data class Organization(
|
||||
val id: String,
|
||||
val name: String?,
|
||||
val shouldManageResetPassword: Boolean,
|
||||
val shouldUseKeyConnector: Boolean,
|
||||
val role: OrganizationType,
|
||||
)
|
||||
|
||||
@@ -47,7 +47,7 @@ sealed class PolicyInformation {
|
||||
/**
|
||||
* Represents a policy enforcing rules on the password generator.
|
||||
*
|
||||
* @property defaultType The default type of password to be generated.
|
||||
* @property overridePasswordType The default type of password to be generated.
|
||||
* @property minLength The minimum length of the password.
|
||||
* @property useUpper Whether the password requires upper case letters.
|
||||
* @property useLower Whether the password requires lower case letters.
|
||||
@@ -61,8 +61,8 @@ sealed class PolicyInformation {
|
||||
*/
|
||||
@Serializable
|
||||
data class PasswordGenerator(
|
||||
@SerialName("defaultType")
|
||||
val defaultType: String?,
|
||||
@SerialName("overridePasswordType")
|
||||
val overridePasswordType: String?,
|
||||
|
||||
@SerialName("minLength")
|
||||
val minLength: Int?,
|
||||
@@ -94,6 +94,7 @@ sealed class PolicyInformation {
|
||||
@SerialName("includeNumber")
|
||||
val includeNumber: Boolean?,
|
||||
) : PolicyInformation() {
|
||||
@Suppress("UndocumentedPublicClass")
|
||||
companion object {
|
||||
const val TYPE_PASSWORD: String = "password"
|
||||
const val TYPE_PASSPHRASE: String = "passphrase"
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
package com.x8bit.bitwarden.data.auth.repository.model
|
||||
|
||||
/**
|
||||
* Models result of removing a user's password.
|
||||
*/
|
||||
sealed class RemovePasswordResult {
|
||||
/**
|
||||
* The password was removed successfully.
|
||||
*/
|
||||
data object Success : RemovePasswordResult()
|
||||
|
||||
/**
|
||||
* There was an error removing the password.
|
||||
*/
|
||||
data object Error : RemovePasswordResult()
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package com.x8bit.bitwarden.data.auth.repository.model
|
||||
|
||||
/**
|
||||
* Associates [isUsingKeyConnector] with the given [userId].
|
||||
*/
|
||||
data class UserKeyConnectorState(
|
||||
val userId: String,
|
||||
val isUsingKeyConnector: Boolean?,
|
||||
)
|
||||
@@ -1,5 +1,6 @@
|
||||
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.repository.model.Environment
|
||||
|
||||
@@ -45,10 +46,12 @@ data class UserState(
|
||||
* they logged in using SSO and don't yet have one). NOTE: This should **not** be used to
|
||||
* determine whether a user has a master password. There are cases in which a user can both
|
||||
* not have a password but still not need one, such as TDE.
|
||||
* @property hasMasterPassword Indicates that the user does or does not have a master password.
|
||||
* @property organizations List of [Organization]s the user is associated with, if any.
|
||||
* @property isBiometricsEnabled Indicates that the biometrics mechanism for unlocking the
|
||||
* user's vault is enabled.
|
||||
* @property vaultUnlockType The mechanism by which the user's vault may be unlocked.
|
||||
* @property isUsingKeyConnector Indicates if the account is currently using a key connector.
|
||||
*/
|
||||
data class Account(
|
||||
val userId: String,
|
||||
@@ -61,16 +64,14 @@ data class UserState(
|
||||
val isVaultUnlocked: Boolean,
|
||||
val needsPasswordReset: Boolean,
|
||||
val needsMasterPassword: Boolean,
|
||||
val hasMasterPassword: Boolean,
|
||||
val trustedDevice: TrustedDevice?,
|
||||
val organizations: List<Organization>,
|
||||
val isBiometricsEnabled: Boolean,
|
||||
val vaultUnlockType: VaultUnlockType = VaultUnlockType.MASTER_PASSWORD,
|
||||
val isUsingKeyConnector: Boolean,
|
||||
val onboardingStatus: OnboardingStatus,
|
||||
) {
|
||||
/**
|
||||
* Indicates that the user does or does not have a master password.
|
||||
*/
|
||||
val hasMasterPassword: Boolean get() = trustedDevice?.hasMasterPassword != false
|
||||
|
||||
/**
|
||||
* Indicates that the user does or does not have a means to manually unlock the vault.
|
||||
*/
|
||||
@@ -86,7 +87,6 @@ data class UserState(
|
||||
*/
|
||||
data class TrustedDevice(
|
||||
val isDeviceTrusted: Boolean,
|
||||
val hasMasterPassword: Boolean,
|
||||
val hasAdminApproval: Boolean,
|
||||
val hasLoginApprovingDevice: Boolean,
|
||||
val hasResetPasswordPermission: Boolean,
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
package com.x8bit.bitwarden.data.auth.repository.util
|
||||
|
||||
import com.x8bit.bitwarden.data.auth.datasource.disk.AuthDiskSource
|
||||
import com.x8bit.bitwarden.data.auth.datasource.disk.model.OnboardingStatus
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.UserAccountTokens
|
||||
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.UserSwitchingData
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
@@ -9,6 +11,7 @@ import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.flatMapLatest
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
import kotlinx.coroutines.flow.map
|
||||
|
||||
/**
|
||||
@@ -100,6 +103,47 @@ val AuthDiskSource.userAccountTokensFlow: Flow<List<UserAccountTokens>>
|
||||
}
|
||||
.distinctUntilChanged()
|
||||
|
||||
/**
|
||||
* Returns the current list of [UserKeyConnectorState].
|
||||
*/
|
||||
val AuthDiskSource.userKeyConnectorStateList: List<UserKeyConnectorState>
|
||||
get() = this
|
||||
.userState
|
||||
?.accounts
|
||||
.orEmpty()
|
||||
.map { (userId, _) ->
|
||||
UserKeyConnectorState(
|
||||
userId = userId,
|
||||
isUsingKeyConnector = this.getShouldUseKeyConnector(userId = userId),
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a [Flow] that emits distinct updates to [UserKeyConnectorState].
|
||||
*/
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
val AuthDiskSource.userKeyConnectorStateFlow: Flow<List<UserKeyConnectorState>>
|
||||
get() = this
|
||||
.userStateFlow
|
||||
.flatMapLatest { userStateJson ->
|
||||
combine(
|
||||
userStateJson
|
||||
?.accounts
|
||||
.orEmpty()
|
||||
.map { (userId, _) ->
|
||||
this
|
||||
.getShouldUseKeyConnectorFlow(userId = userId)
|
||||
.map {
|
||||
UserKeyConnectorState(
|
||||
userId = userId,
|
||||
isUsingKeyConnector = it,
|
||||
)
|
||||
}
|
||||
},
|
||||
) { it.toList() }
|
||||
}
|
||||
.distinctUntilChanged()
|
||||
|
||||
/**
|
||||
* Returns a [Flow] that emits every time the active user is changed.
|
||||
*/
|
||||
@@ -125,3 +169,22 @@ val AuthDiskSource.activeUserIdChangesFlow: Flow<String?>
|
||||
.userStateFlow
|
||||
.map { it?.activeUserId }
|
||||
.distinctUntilChanged()
|
||||
|
||||
/**
|
||||
* Returns a [Flow] that emits every time the active user's onboarding status is changed
|
||||
*/
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
val AuthDiskSource.onboardingStatusChangesFlow: Flow<OnboardingStatus?>
|
||||
get() = activeUserIdChangesFlow
|
||||
.flatMapLatest { activeUserId ->
|
||||
activeUserId
|
||||
?.let { this.getOnboardingStatusFlow(userId = it) }
|
||||
?: flowOf(null)
|
||||
}
|
||||
.distinctUntilChanged()
|
||||
|
||||
val AuthDiskSource.currentOnboardingStatus: OnboardingStatus?
|
||||
get() = this
|
||||
.userState
|
||||
?.activeUserId
|
||||
?.let { this.getOnboardingStatus(userId = it) }
|
||||
|
||||
@@ -7,6 +7,11 @@ import com.x8bit.bitwarden.data.vault.datasource.network.model.PolicyTypeJson
|
||||
import com.x8bit.bitwarden.data.vault.datasource.network.model.SyncResponseJson
|
||||
import kotlinx.serialization.json.Json
|
||||
|
||||
private val JSON = Json {
|
||||
ignoreUnknownKeys = true
|
||||
explicitNulls = false
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps the given [SyncResponseJson.Profile.Organization] to an [Organization].
|
||||
*/
|
||||
@@ -14,6 +19,9 @@ fun SyncResponseJson.Profile.Organization.toOrganization(): Organization =
|
||||
Organization(
|
||||
id = this.id,
|
||||
name = this.name,
|
||||
shouldUseKeyConnector = this.shouldUseKeyConnector,
|
||||
role = this.type,
|
||||
shouldManageResetPassword = this.permissions.shouldManageResetPassword,
|
||||
)
|
||||
|
||||
/**
|
||||
@@ -28,21 +36,22 @@ fun List<SyncResponseJson.Profile.Organization>.toOrganizations(): List<Organiza
|
||||
*/
|
||||
val SyncResponseJson.Policy.policyInformation: PolicyInformation?
|
||||
get() = data?.toString()?.let {
|
||||
|
||||
when (type) {
|
||||
PolicyTypeJson.MASTER_PASSWORD -> {
|
||||
Json.decodeFromStringOrNull<PolicyInformation.MasterPassword>(it)
|
||||
JSON.decodeFromStringOrNull<PolicyInformation.MasterPassword>(it)
|
||||
}
|
||||
|
||||
PolicyTypeJson.PASSWORD_GENERATOR -> {
|
||||
Json.decodeFromStringOrNull<PolicyInformation.PasswordGenerator>(it)
|
||||
JSON.decodeFromStringOrNull<PolicyInformation.PasswordGenerator>(it)
|
||||
}
|
||||
|
||||
PolicyTypeJson.MAXIMUM_VAULT_TIMEOUT -> {
|
||||
Json.decodeFromStringOrNull<PolicyInformation.VaultTimeout>(it)
|
||||
JSON.decodeFromStringOrNull<PolicyInformation.VaultTimeout>(it)
|
||||
}
|
||||
|
||||
PolicyTypeJson.SEND_OPTIONS -> {
|
||||
Json.decodeFromStringOrNull<PolicyInformation.SendOptions>(it)
|
||||
JSON.decodeFromStringOrNull<PolicyInformation.SendOptions>(it)
|
||||
}
|
||||
|
||||
else -> null
|
||||
|
||||
@@ -1,17 +1,46 @@
|
||||
package com.x8bit.bitwarden.data.auth.repository.util
|
||||
|
||||
import com.x8bit.bitwarden.data.auth.datasource.disk.model.OnboardingStatus
|
||||
import com.x8bit.bitwarden.data.auth.datasource.disk.model.UserStateJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.UserDecryptionOptionsJson
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.UserAccountTokens
|
||||
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.repository.util.toEnvironmentUrlsOrDefault
|
||||
import com.x8bit.bitwarden.data.vault.datasource.network.model.OrganizationType
|
||||
import com.x8bit.bitwarden.data.vault.datasource.network.model.SyncResponseJson
|
||||
import com.x8bit.bitwarden.data.vault.repository.model.VaultUnlockData
|
||||
import com.x8bit.bitwarden.data.vault.repository.util.statusFor
|
||||
import com.x8bit.bitwarden.ui.platform.base.util.toHexColorRepresentation
|
||||
|
||||
/**
|
||||
* Updates the given [UserStateJson] with the data to indicate that the password has been removed.
|
||||
* The original will be returned if the [userId] does not match any accounts in the [UserStateJson].
|
||||
*/
|
||||
fun UserStateJson.toRemovedPasswordUserStateJson(
|
||||
userId: String,
|
||||
): UserStateJson {
|
||||
val account = this.accounts[userId] ?: return this
|
||||
val profile = account.profile
|
||||
val updatedUserDecryptionOptions = profile
|
||||
.userDecryptionOptions
|
||||
?.copy(hasMasterPassword = false)
|
||||
?: UserDecryptionOptionsJson(
|
||||
hasMasterPassword = false,
|
||||
trustedDeviceUserDecryptionOptions = null,
|
||||
keyConnectorUserDecryptionOptions = null,
|
||||
)
|
||||
val updatedProfile = profile.copy(userDecryptionOptions = updatedUserDecryptionOptions)
|
||||
val updatedAccount = account.copy(profile = updatedProfile)
|
||||
return this.copy(
|
||||
accounts = accounts
|
||||
.toMutableMap()
|
||||
.apply { replace(userId, updatedAccount) },
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the given [UserStateJson] with the data from the [syncResponse] to return a new
|
||||
* [UserStateJson]. The original will be returned if the sync response does not match any accounts
|
||||
@@ -74,12 +103,14 @@ fun UserStateJson.toUserStateJsonWithPassword(): UserStateJson {
|
||||
/**
|
||||
* Converts the given [UserStateJson] to a [UserState] using the given [vaultState].
|
||||
*/
|
||||
@Suppress("LongParameterList")
|
||||
@Suppress("LongParameterList", "LongMethod")
|
||||
fun UserStateJson.toUserState(
|
||||
vaultState: List<VaultUnlockData>,
|
||||
userAccountTokens: List<UserAccountTokens>,
|
||||
userOrganizationsList: List<UserOrganizations>,
|
||||
userIsUsingKeyConnectorList: List<UserKeyConnectorState>,
|
||||
hasPendingAccountAddition: Boolean,
|
||||
onboardingStatus: OnboardingStatus?,
|
||||
isBiometricsEnabledProvider: (userId: String) -> Boolean,
|
||||
vaultUnlockTypeProvider: (userId: String) -> VaultUnlockType,
|
||||
isDeviceTrustedProvider: (userId: String) -> Boolean,
|
||||
@@ -97,13 +128,21 @@ fun UserStateJson.toUserState(
|
||||
val decryptionOptions = profile.userDecryptionOptions
|
||||
val trustedDeviceOptions = decryptionOptions?.trustedDeviceUserDecryptionOptions
|
||||
val keyConnectorOptions = decryptionOptions?.keyConnectorUserDecryptionOptions
|
||||
val organizations = userOrganizationsList
|
||||
.find { it.userId == userId }
|
||||
?.organizations
|
||||
.orEmpty()
|
||||
val hasManageResetPasswordPermission = organizations.any {
|
||||
it.role == OrganizationType.OWNER ||
|
||||
it.role == OrganizationType.ADMIN ||
|
||||
it.shouldManageResetPassword
|
||||
}
|
||||
val needsMasterPassword = decryptionOptions?.hasMasterPassword == false &&
|
||||
trustedDeviceOptions?.hasManageResetPasswordPermission != false &&
|
||||
hasManageResetPasswordPermission &&
|
||||
keyConnectorOptions == null
|
||||
val trustedDevice = trustedDeviceOptions?.let {
|
||||
UserState.TrustedDevice(
|
||||
isDeviceTrusted = isDeviceTrustedProvider(userId),
|
||||
hasMasterPassword = decryptionOptions.hasMasterPassword,
|
||||
hasAdminApproval = it.hasAdminApproval,
|
||||
hasLoginApprovingDevice = it.hasLoginApprovingDevice,
|
||||
hasResetPasswordPermission = it.hasManageResetPasswordPermission,
|
||||
@@ -125,14 +164,18 @@ fun UserStateJson.toUserState(
|
||||
?.isLoggedIn == true,
|
||||
isVaultUnlocked = vaultUnlocked,
|
||||
needsPasswordReset = needsPasswordReset,
|
||||
organizations = userOrganizationsList
|
||||
.find { it.userId == userId }
|
||||
?.organizations
|
||||
.orEmpty(),
|
||||
organizations = organizations,
|
||||
isBiometricsEnabled = isBiometricsEnabledProvider(userId),
|
||||
vaultUnlockType = vaultUnlockTypeProvider(userId),
|
||||
needsMasterPassword = needsMasterPassword,
|
||||
hasMasterPassword = decryptionOptions?.hasMasterPassword != false,
|
||||
trustedDevice = trustedDevice,
|
||||
isUsingKeyConnector = userIsUsingKeyConnectorList
|
||||
.find { it.userId == userId }
|
||||
?.isUsingKeyConnector == true,
|
||||
// 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,
|
||||
)
|
||||
},
|
||||
hasPendingAccountAddition = hasPendingAccountAddition,
|
||||
|
||||
@@ -9,7 +9,11 @@ import com.x8bit.bitwarden.data.platform.manager.model.CompleteRegistrationData
|
||||
* The [CompleteRegistrationData] will be returned when present.
|
||||
*/
|
||||
fun Intent.getCompleteRegistrationDataIntentOrNull(): CompleteRegistrationData? {
|
||||
val sanitizedUriString = data.toString().replace("/#/", "/")
|
||||
val sanitizedUriString = data.toString().replace(
|
||||
oldValue = "/redirect-connector.html#",
|
||||
newValue = "/",
|
||||
ignoreCase = true,
|
||||
)
|
||||
val uri = runCatching { Uri.parse(sanitizedUriString) }.getOrNull() ?: return null
|
||||
uri.host ?: return null
|
||||
if (uri.path != "/finish-signup") return null
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
package com.x8bit.bitwarden.data.autofill.accessibility
|
||||
|
||||
import android.accessibilityservice.AccessibilityService
|
||||
import android.view.accessibility.AccessibilityEvent
|
||||
import androidx.annotation.Keep
|
||||
import com.x8bit.bitwarden.data.autofill.accessibility.processor.BitwardenAccessibilityProcessor
|
||||
import com.x8bit.bitwarden.data.platform.annotation.OmitFromCoverage
|
||||
import com.x8bit.bitwarden.data.tiles.BitwardenAutofillTileService
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import javax.inject.Inject
|
||||
|
||||
/**
|
||||
* The [AccessibilityService] implementation for the app. This is not used in the traditional
|
||||
* way, we use the [BitwardenAutofillTileService] to invoke this service in order to provide an
|
||||
* autofill fallback mechanism.
|
||||
*/
|
||||
@Keep
|
||||
@OmitFromCoverage
|
||||
@AndroidEntryPoint
|
||||
class BitwardenAccessibilityService : AccessibilityService() {
|
||||
@Inject
|
||||
lateinit var processor: BitwardenAccessibilityProcessor
|
||||
|
||||
override fun onAccessibilityEvent(event: AccessibilityEvent) {
|
||||
if (rootInActiveWindow?.packageName != event.packageName) return
|
||||
processor.processAccessibilityEvent(rootAccessibilityNodeInfo = rootInActiveWindow)
|
||||
}
|
||||
|
||||
override fun onInterrupt() = Unit
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
package com.x8bit.bitwarden.data.autofill.accessibility.di
|
||||
|
||||
import android.content.Context
|
||||
import android.content.pm.PackageManager
|
||||
import android.os.PowerManager
|
||||
import com.x8bit.bitwarden.data.autofill.accessibility.manager.AccessibilityAutofillManager
|
||||
import com.x8bit.bitwarden.data.autofill.accessibility.manager.AccessibilityAutofillManagerImpl
|
||||
import com.x8bit.bitwarden.data.autofill.accessibility.manager.AccessibilityCompletionManager
|
||||
import com.x8bit.bitwarden.data.autofill.accessibility.manager.AccessibilityCompletionManagerImpl
|
||||
import com.x8bit.bitwarden.data.autofill.accessibility.manager.AccessibilityEnabledManager
|
||||
import com.x8bit.bitwarden.data.autofill.accessibility.manager.AccessibilityEnabledManagerImpl
|
||||
import com.x8bit.bitwarden.data.autofill.accessibility.manager.AccessibilityNodeInfoManager
|
||||
import com.x8bit.bitwarden.data.autofill.accessibility.manager.AccessibilityNodeInfoManagerImpl
|
||||
import com.x8bit.bitwarden.data.autofill.accessibility.manager.AccessibilitySelectionManager
|
||||
import com.x8bit.bitwarden.data.autofill.accessibility.manager.AccessibilitySelectionManagerImpl
|
||||
import com.x8bit.bitwarden.data.autofill.accessibility.manager.LauncherPackageNameManager
|
||||
import com.x8bit.bitwarden.data.autofill.accessibility.manager.LauncherPackageNameManagerImpl
|
||||
import com.x8bit.bitwarden.data.autofill.accessibility.parser.AccessibilityParser
|
||||
import com.x8bit.bitwarden.data.autofill.accessibility.parser.AccessibilityParserImpl
|
||||
import com.x8bit.bitwarden.data.autofill.accessibility.processor.BitwardenAccessibilityProcessor
|
||||
import com.x8bit.bitwarden.data.autofill.accessibility.processor.BitwardenAccessibilityProcessorImpl
|
||||
import com.x8bit.bitwarden.data.autofill.manager.AutofillTotpManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.dispatcher.DispatcherManager
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import java.time.Clock
|
||||
import javax.inject.Singleton
|
||||
|
||||
/**
|
||||
* Provides dependencies within the accessibility package.
|
||||
*/
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
object AccessibilityModule {
|
||||
@Singleton
|
||||
@Provides
|
||||
fun providesAccessibilityCompletionManager(
|
||||
accessibilityAutofillManager: AccessibilityAutofillManager,
|
||||
totpManager: AutofillTotpManager,
|
||||
dispatcherManager: DispatcherManager,
|
||||
): AccessibilityCompletionManager =
|
||||
AccessibilityCompletionManagerImpl(
|
||||
accessibilityAutofillManager = accessibilityAutofillManager,
|
||||
totpManager = totpManager,
|
||||
dispatcherManager = dispatcherManager,
|
||||
)
|
||||
|
||||
@Singleton
|
||||
@Provides
|
||||
fun providesAccessibilityAutofillManager(): AccessibilityAutofillManager =
|
||||
AccessibilityAutofillManagerImpl()
|
||||
|
||||
@Singleton
|
||||
@Provides
|
||||
fun providesAccessibilityEnabledManager(): AccessibilityEnabledManager =
|
||||
AccessibilityEnabledManagerImpl()
|
||||
|
||||
@Singleton
|
||||
@Provides
|
||||
fun providesAccessibilityNodeInfoManager(): AccessibilityNodeInfoManager =
|
||||
AccessibilityNodeInfoManagerImpl()
|
||||
|
||||
@Singleton
|
||||
@Provides
|
||||
fun providesAccessibilityParser(
|
||||
accessibilityNodeInfoManager: AccessibilityNodeInfoManager,
|
||||
): AccessibilityParser = AccessibilityParserImpl(
|
||||
accessibilityNodeInfoManager = accessibilityNodeInfoManager,
|
||||
)
|
||||
|
||||
@Singleton
|
||||
@Provides
|
||||
fun providesAccessibilitySelectionManager(): AccessibilitySelectionManager =
|
||||
AccessibilitySelectionManagerImpl()
|
||||
|
||||
@Singleton
|
||||
@Provides
|
||||
fun providesBitwardenAccessibilityProcessor(
|
||||
@ApplicationContext context: Context,
|
||||
accessibilityParser: AccessibilityParser,
|
||||
accessibilityAutofillManager: AccessibilityAutofillManager,
|
||||
launcherPackageNameManager: LauncherPackageNameManager,
|
||||
powerManager: PowerManager,
|
||||
): BitwardenAccessibilityProcessor =
|
||||
BitwardenAccessibilityProcessorImpl(
|
||||
context = context,
|
||||
accessibilityParser = accessibilityParser,
|
||||
accessibilityAutofillManager = accessibilityAutofillManager,
|
||||
launcherPackageNameManager = launcherPackageNameManager,
|
||||
powerManager = powerManager,
|
||||
)
|
||||
|
||||
@Singleton
|
||||
@Provides
|
||||
fun providesLauncherPackageNameManager(
|
||||
clock: Clock,
|
||||
packageManager: PackageManager,
|
||||
): LauncherPackageNameManager =
|
||||
LauncherPackageNameManagerImpl(
|
||||
clockProvider = { clock },
|
||||
packageManager = packageManager,
|
||||
)
|
||||
|
||||
@Singleton
|
||||
@Provides
|
||||
fun providesPackageManager(
|
||||
@ApplicationContext context: Context,
|
||||
): PackageManager = context.packageManager
|
||||
|
||||
@Singleton
|
||||
@Provides
|
||||
fun providesPowerManager(
|
||||
@ApplicationContext context: Context,
|
||||
): PowerManager = context.getSystemService(PowerManager::class.java)
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
package com.x8bit.bitwarden.data.autofill.accessibility.di
|
||||
|
||||
import android.content.Context
|
||||
import androidx.lifecycle.LifecycleCoroutineScope
|
||||
import com.x8bit.bitwarden.data.autofill.accessibility.manager.AccessibilityActivityManager
|
||||
import com.x8bit.bitwarden.data.autofill.accessibility.manager.AccessibilityActivityManagerImpl
|
||||
import com.x8bit.bitwarden.data.autofill.accessibility.manager.AccessibilityEnabledManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.AppForegroundManager
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.android.components.ActivityComponent
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import dagger.hilt.android.scopes.ActivityScoped
|
||||
|
||||
/**
|
||||
* Provides dependencies within the accessibility package scoped to the activity.
|
||||
*/
|
||||
@Module
|
||||
@InstallIn(ActivityComponent::class)
|
||||
object ActivityAccessibilityModule {
|
||||
@ActivityScoped
|
||||
@Provides
|
||||
fun providesAccessibilityActivityManager(
|
||||
@ApplicationContext context: Context,
|
||||
accessibilityEnabledManager: AccessibilityEnabledManager,
|
||||
appForegroundManager: AppForegroundManager,
|
||||
lifecycleScope: LifecycleCoroutineScope,
|
||||
): AccessibilityActivityManager =
|
||||
AccessibilityActivityManagerImpl(
|
||||
context = context,
|
||||
accessibilityEnabledManager = accessibilityEnabledManager,
|
||||
appForegroundManager = appForegroundManager,
|
||||
lifecycleScope = lifecycleScope,
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package com.x8bit.bitwarden.data.autofill.accessibility.manager
|
||||
|
||||
import android.app.Activity
|
||||
|
||||
/**
|
||||
* A helper for dealing with accessibility configuration that must be scoped to a specific
|
||||
* [Activity]. In particular, this should be injected into an [Activity] to ensure that the
|
||||
* [AccessibilityEnabledManager] reports correct values.
|
||||
*/
|
||||
interface AccessibilityActivityManager
|
||||
@@ -0,0 +1,28 @@
|
||||
package com.x8bit.bitwarden.data.autofill.accessibility.manager
|
||||
|
||||
import android.content.Context
|
||||
import androidx.lifecycle.LifecycleCoroutineScope
|
||||
import com.x8bit.bitwarden.data.autofill.accessibility.util.isAccessibilityServiceEnabled
|
||||
import com.x8bit.bitwarden.data.platform.manager.AppForegroundManager
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
|
||||
/**
|
||||
* The default implementation of the [AccessibilityActivityManager].
|
||||
*/
|
||||
class AccessibilityActivityManagerImpl(
|
||||
private val context: Context,
|
||||
private val accessibilityEnabledManager: AccessibilityEnabledManager,
|
||||
appForegroundManager: AppForegroundManager,
|
||||
lifecycleScope: LifecycleCoroutineScope,
|
||||
) : AccessibilityActivityManager {
|
||||
init {
|
||||
appForegroundManager
|
||||
.appForegroundStateFlow
|
||||
.onEach {
|
||||
accessibilityEnabledManager.isAccessibilityEnabled =
|
||||
context.isAccessibilityServiceEnabled
|
||||
}
|
||||
.launchIn(lifecycleScope)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package com.x8bit.bitwarden.data.autofill.accessibility.manager
|
||||
|
||||
import com.x8bit.bitwarden.data.autofill.accessibility.model.AccessibilityAction
|
||||
|
||||
/**
|
||||
* A relay manager used to notify the accessibility service to attempt an autofill.
|
||||
*/
|
||||
interface AccessibilityAutofillManager {
|
||||
/**
|
||||
* Indicates that the Autofill tile has been clicked and we attempt an accessibility-based
|
||||
* autofill.
|
||||
*/
|
||||
var accessibilityAction: AccessibilityAction?
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package com.x8bit.bitwarden.data.autofill.accessibility.manager
|
||||
|
||||
import com.x8bit.bitwarden.data.autofill.accessibility.model.AccessibilityAction
|
||||
|
||||
/**
|
||||
* The default implementation for the [AccessibilityAutofillManager].
|
||||
*/
|
||||
class AccessibilityAutofillManagerImpl : AccessibilityAutofillManager {
|
||||
override var accessibilityAction: AccessibilityAction? = null
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package com.x8bit.bitwarden.data.autofill.accessibility.manager
|
||||
|
||||
import android.app.Activity
|
||||
import com.bitwarden.vault.CipherView
|
||||
|
||||
/**
|
||||
* A manager for completing the accessibility-based autofill process after the user has made a
|
||||
* selection.
|
||||
*/
|
||||
interface AccessibilityCompletionManager {
|
||||
/**
|
||||
* Completes the accessibility-based autofill flow originating with the given [activity] using
|
||||
* the selected [cipherView].
|
||||
*/
|
||||
fun completeAccessibilityAutofill(activity: Activity, cipherView: CipherView)
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
package com.x8bit.bitwarden.data.autofill.accessibility.manager
|
||||
|
||||
import android.app.Activity
|
||||
import com.bitwarden.vault.CipherView
|
||||
import com.x8bit.bitwarden.data.autofill.accessibility.model.AccessibilityAction
|
||||
import com.x8bit.bitwarden.data.autofill.accessibility.util.toUriOrNull
|
||||
import com.x8bit.bitwarden.data.autofill.manager.AutofillTotpManager
|
||||
import com.x8bit.bitwarden.data.autofill.model.AutofillSelectionData
|
||||
import com.x8bit.bitwarden.data.autofill.util.getAutofillSelectionDataOrNull
|
||||
import com.x8bit.bitwarden.data.platform.manager.dispatcher.DispatcherManager
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
/**
|
||||
* Default implementation for the [AccessibilityCompletionManager].
|
||||
*/
|
||||
class AccessibilityCompletionManagerImpl(
|
||||
private val accessibilityAutofillManager: AccessibilityAutofillManager,
|
||||
private val totpManager: AutofillTotpManager,
|
||||
dispatcherManager: DispatcherManager,
|
||||
) : AccessibilityCompletionManager {
|
||||
private val mainScope = CoroutineScope(dispatcherManager.main)
|
||||
|
||||
override fun completeAccessibilityAutofill(activity: Activity, cipherView: CipherView) {
|
||||
val autofillSelectionData = activity
|
||||
.intent
|
||||
?.getAutofillSelectionDataOrNull()
|
||||
?: run {
|
||||
activity.finish()
|
||||
return
|
||||
}
|
||||
if (autofillSelectionData.framework != AutofillSelectionData.Framework.ACCESSIBILITY) {
|
||||
activity.finish()
|
||||
return
|
||||
}
|
||||
val uri = autofillSelectionData
|
||||
.uri
|
||||
?.toUriOrNull()
|
||||
?: run {
|
||||
activity.finish()
|
||||
return
|
||||
}
|
||||
|
||||
accessibilityAutofillManager.accessibilityAction = AccessibilityAction.AttemptFill(
|
||||
cipherView = cipherView,
|
||||
uri = uri,
|
||||
)
|
||||
mainScope.launch {
|
||||
totpManager.tryCopyTotpToClipboard(cipherView = cipherView)
|
||||
activity.finish()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package com.x8bit.bitwarden.data.autofill.accessibility.manager
|
||||
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
|
||||
/**
|
||||
* A container for values specifying whether or not the accessibility service is enabled.
|
||||
*/
|
||||
interface AccessibilityEnabledManager {
|
||||
/**
|
||||
* Whether or not the accessibility service should be considered enabled.
|
||||
*
|
||||
* Note that changing this does not enable or disable autofill; it is only an indicator that
|
||||
* this has occurred elsewhere.
|
||||
*/
|
||||
var isAccessibilityEnabled: Boolean
|
||||
|
||||
/**
|
||||
* Emits updates that track [isAccessibilityEnabled] values.
|
||||
*/
|
||||
val isAccessibilityEnabledStateFlow: StateFlow<Boolean>
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package com.x8bit.bitwarden.data.autofill.accessibility.manager
|
||||
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
|
||||
/**
|
||||
* The default implementation of [AccessibilityEnabledManager].
|
||||
*/
|
||||
class AccessibilityEnabledManagerImpl : AccessibilityEnabledManager {
|
||||
private val mutableIsAccessibilityEnabledStateFlow = MutableStateFlow(value = false)
|
||||
|
||||
override var isAccessibilityEnabled: Boolean
|
||||
get() = mutableIsAccessibilityEnabledStateFlow.value
|
||||
set(value) {
|
||||
mutableIsAccessibilityEnabledStateFlow.value = value
|
||||
}
|
||||
|
||||
override val isAccessibilityEnabledStateFlow: StateFlow<Boolean>
|
||||
get() = mutableIsAccessibilityEnabledStateFlow.asStateFlow()
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
package com.x8bit.bitwarden.data.autofill.accessibility.manager
|
||||
|
||||
import android.net.Uri
|
||||
import android.view.accessibility.AccessibilityNodeInfo
|
||||
|
||||
/**
|
||||
* The default maximum recursive depth that the
|
||||
* [AccessibilityNodeInfoManager.findAccessibilityNodeInfoList] will go.
|
||||
*/
|
||||
const val DEFAULT_MAX_RECURSION_DEPTH: Int = 100
|
||||
|
||||
/**
|
||||
* A manager for finding fields that match particular characteristics.
|
||||
*/
|
||||
interface AccessibilityNodeInfoManager {
|
||||
/**
|
||||
* A helper function for retrieving the appropriate nodes based on the given [predicate].
|
||||
*
|
||||
* This function is recursive but will stop recurring if the depth it reaches is greater than
|
||||
* the [maxRecursionDepth].
|
||||
*/
|
||||
fun findAccessibilityNodeInfoList(
|
||||
rootNode: AccessibilityNodeInfo,
|
||||
maxRecursionDepth: Int = DEFAULT_MAX_RECURSION_DEPTH,
|
||||
predicate: (AccessibilityNodeInfo) -> Boolean,
|
||||
): List<AccessibilityNodeInfo>
|
||||
|
||||
/**
|
||||
* Determines which [AccessibilityNodeInfo] is a username field.
|
||||
*/
|
||||
fun findUsernameAccessibilityNodeInfo(
|
||||
uri: Uri,
|
||||
allNodes: List<AccessibilityNodeInfo>,
|
||||
passwordNodes: List<AccessibilityNodeInfo>,
|
||||
): AccessibilityNodeInfo?
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
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
|
||||
|
||||
private const val MAX_NODE_COUNT: Int = 100
|
||||
|
||||
/**
|
||||
* The default implementation for the [AccessibilityNodeInfoManager].
|
||||
*/
|
||||
class AccessibilityNodeInfoManagerImpl : AccessibilityNodeInfoManager {
|
||||
override fun findAccessibilityNodeInfoList(
|
||||
rootNode: AccessibilityNodeInfo,
|
||||
maxRecursionDepth: Int,
|
||||
predicate: (AccessibilityNodeInfo) -> Boolean,
|
||||
): List<AccessibilityNodeInfo> =
|
||||
findAccessibilityNodeInfoList(
|
||||
rootNode = rootNode,
|
||||
maxRecursionDepth = maxRecursionDepth,
|
||||
currentRecursionDepth = 0,
|
||||
predicate = predicate,
|
||||
)
|
||||
|
||||
override fun findUsernameAccessibilityNodeInfo(
|
||||
uri: Uri,
|
||||
allNodes: List<AccessibilityNodeInfo>,
|
||||
passwordNodes: List<AccessibilityNodeInfo>,
|
||||
): AccessibilityNodeInfo? {
|
||||
val uriPath = uri
|
||||
.path
|
||||
?: return findMissingUsernameNodeInfo(
|
||||
allNodes = allNodes,
|
||||
passwordNodes = passwordNodes,
|
||||
)
|
||||
return uri
|
||||
.authority
|
||||
?.removePrefix(prefix = "www.")
|
||||
?.getKnownUsernameFieldNull()
|
||||
?.let { usernameField ->
|
||||
allNodes.firstOrNull { node ->
|
||||
node.isUsername(
|
||||
uriPath = uriPath,
|
||||
knownUsernameField = usernameField,
|
||||
)
|
||||
}
|
||||
}
|
||||
?: findMissingUsernameNodeInfo(allNodes = allNodes, passwordNodes = passwordNodes)
|
||||
}
|
||||
|
||||
private fun findAccessibilityNodeInfoList(
|
||||
rootNode: AccessibilityNodeInfo,
|
||||
maxRecursionDepth: Int,
|
||||
currentRecursionDepth: Int,
|
||||
predicate: (AccessibilityNodeInfo) -> Boolean,
|
||||
): List<AccessibilityNodeInfo> {
|
||||
if (predicate(rootNode)) return listOf(rootNode)
|
||||
if (currentRecursionDepth >= maxRecursionDepth) return emptyList()
|
||||
val childNodeCount = rootNode.childCount - 1
|
||||
if (childNodeCount > MAX_NODE_COUNT) log(message = "Too many child iterations.")
|
||||
return (0..childNodeCount.coerceAtMost(maximumValue = MAX_NODE_COUNT)).flatMap {
|
||||
val childNode = rootNode.getChild(it) ?: return@flatMap emptyList()
|
||||
if (childNode.hashCode() == this.hashCode()) {
|
||||
log(message = "Child node is the same as parent for some reason.")
|
||||
emptyList()
|
||||
} else {
|
||||
findAccessibilityNodeInfoList(
|
||||
rootNode = childNode,
|
||||
maxRecursionDepth = maxRecursionDepth,
|
||||
currentRecursionDepth = currentRecursionDepth + 1,
|
||||
predicate = predicate,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempts to find a username [AccessibilityNodeInfo] if there isn't one already. This
|
||||
* functions by finding the first known password node and taking the node directly above it.
|
||||
*/
|
||||
private fun findMissingUsernameNodeInfo(
|
||||
allNodes: List<AccessibilityNodeInfo>,
|
||||
passwordNodes: List<AccessibilityNodeInfo>,
|
||||
): AccessibilityNodeInfo? =
|
||||
passwordNodes
|
||||
.firstOrNull()
|
||||
?.let { allNodes.getOrNull(index = allNodes.indexOf(element = it) - 1) }
|
||||
|
||||
private fun log(message: String) {
|
||||
if (!BuildConfig.DEBUG) return
|
||||
Log.i("AccessibilityNodeInfoManager", message)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package com.x8bit.bitwarden.data.autofill.accessibility.manager
|
||||
|
||||
import com.bitwarden.vault.CipherView
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
/**
|
||||
* A manager class used to handle the accessibility autofill selections.
|
||||
*/
|
||||
interface AccessibilitySelectionManager {
|
||||
/**
|
||||
* Emits a [CipherView] as a result of calls to [emitAccessibilitySelection].
|
||||
*/
|
||||
val accessibilitySelectionFlow: Flow<CipherView>
|
||||
|
||||
/**
|
||||
* Triggers an emission via [accessibilitySelectionFlow].
|
||||
*/
|
||||
fun emitAccessibilitySelection(cipherView: CipherView)
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package com.x8bit.bitwarden.data.autofill.accessibility.manager
|
||||
|
||||
import com.bitwarden.vault.CipherView
|
||||
import kotlinx.coroutines.channels.Channel
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.receiveAsFlow
|
||||
|
||||
/**
|
||||
* The default implementation of the [AccessibilitySelectionManager].
|
||||
*/
|
||||
class AccessibilitySelectionManagerImpl : AccessibilitySelectionManager {
|
||||
private val accessibilitySelectionChannel: Channel<CipherView> = Channel(
|
||||
capacity = Int.MAX_VALUE,
|
||||
)
|
||||
|
||||
override val accessibilitySelectionFlow: Flow<CipherView> =
|
||||
accessibilitySelectionChannel.receiveAsFlow()
|
||||
|
||||
override fun emitAccessibilitySelection(cipherView: CipherView) {
|
||||
accessibilitySelectionChannel.trySend(cipherView)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package com.x8bit.bitwarden.data.autofill.accessibility.manager
|
||||
|
||||
/**
|
||||
* A manager for getting the launcher packages from the operating system.
|
||||
*/
|
||||
interface LauncherPackageNameManager {
|
||||
/**
|
||||
* A list of launcher packages from the operating system.
|
||||
*/
|
||||
val launcherPackages: List<String>
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
package com.x8bit.bitwarden.data.autofill.accessibility.manager
|
||||
|
||||
import android.content.Intent
|
||||
import android.content.pm.PackageManager
|
||||
import java.time.Clock
|
||||
|
||||
/**
|
||||
* How frequently the cached launcher list should be refreshed.
|
||||
*/
|
||||
private const val REFRESH_CACHE_MS: Long = 1L * 60L * 60L * 1000L
|
||||
|
||||
/**
|
||||
* The default implementation of the [LauncherPackageNameManager].
|
||||
*/
|
||||
class LauncherPackageNameManagerImpl(
|
||||
private val clockProvider: () -> Clock,
|
||||
private val packageManager: PackageManager,
|
||||
) : LauncherPackageNameManager {
|
||||
private var lastLauncherFetchMs: Long = 0L
|
||||
private var cachedLauncherPackages: List<String>? = null
|
||||
|
||||
override val launcherPackages: List<String>
|
||||
get() {
|
||||
if (cachedLauncherPackages == null ||
|
||||
clockProvider().millis() - lastLauncherFetchMs > REFRESH_CACHE_MS
|
||||
) {
|
||||
updateCachedLauncherPackages()
|
||||
}
|
||||
return cachedLauncherPackages.orEmpty()
|
||||
}
|
||||
|
||||
private fun updateCachedLauncherPackages() {
|
||||
cachedLauncherPackages = packageManager
|
||||
.queryIntentActivities(
|
||||
Intent(Intent.ACTION_MAIN).addCategory(Intent.CATEGORY_HOME),
|
||||
0,
|
||||
)
|
||||
.map { it.activityInfo.packageName }
|
||||
lastLauncherFetchMs = clockProvider().millis()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package com.x8bit.bitwarden.data.autofill.accessibility.model
|
||||
|
||||
import android.net.Uri
|
||||
import com.bitwarden.vault.CipherView
|
||||
|
||||
/**
|
||||
*Represents an action to be taken by the accessibility service.
|
||||
*/
|
||||
sealed class AccessibilityAction {
|
||||
/**
|
||||
* Indicates that the accessibility service should attempt to scan the currently foregrounded
|
||||
* application for a [Uri].
|
||||
*/
|
||||
data object AttemptParseUri : AccessibilityAction()
|
||||
|
||||
/**
|
||||
* Indicates that the accessibility service should attempt to scan the currently foregrounded
|
||||
* application for a fields to fill.
|
||||
*/
|
||||
data class AttemptFill(
|
||||
val cipherView: CipherView,
|
||||
val uri: Uri,
|
||||
) : AccessibilityAction()
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package com.x8bit.bitwarden.data.autofill.accessibility.model
|
||||
|
||||
/**
|
||||
* A model representing a supported browser.
|
||||
*/
|
||||
data class Browser(
|
||||
val packageName: String,
|
||||
val possibleUrlFieldIds: List<String>,
|
||||
val urlExtractor: (String) -> String? = { it },
|
||||
) {
|
||||
constructor(
|
||||
packageName: String,
|
||||
urlFieldId: String,
|
||||
urlExtractor: (String) -> String? = { it },
|
||||
) : this(
|
||||
packageName = packageName,
|
||||
possibleUrlFieldIds = listOf(urlFieldId),
|
||||
urlExtractor = urlExtractor,
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package com.x8bit.bitwarden.data.autofill.accessibility.model
|
||||
|
||||
import android.view.accessibility.AccessibilityNodeInfo
|
||||
|
||||
/**
|
||||
* Represents the fillable fields for accessibility based autofill.
|
||||
*/
|
||||
data class FillableFields(
|
||||
val usernameField: AccessibilityNodeInfo?,
|
||||
val passwordFields: List<AccessibilityNodeInfo>,
|
||||
)
|
||||
@@ -0,0 +1,71 @@
|
||||
package com.x8bit.bitwarden.data.autofill.accessibility.model
|
||||
|
||||
/**
|
||||
* Represents the known username fields for a given [uriAuthority].
|
||||
*/
|
||||
data class KnownUsernameField(
|
||||
val uriAuthority: String,
|
||||
val accessOptions: List<AccessOptions>,
|
||||
) {
|
||||
constructor(
|
||||
uriAuthority: String,
|
||||
accessOption: AccessOptions,
|
||||
) : this(uriAuthority = uriAuthority, accessOptions = listOf(accessOption))
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents the view IDs for a given uri path.
|
||||
*/
|
||||
data class AccessOptions(
|
||||
val matchValue: String,
|
||||
val matchingStrategy: MatchingStrategy = MatchingStrategy.ENDS_WITH_CASE_SENSITIVE,
|
||||
val usernameViewIds: List<String>,
|
||||
) {
|
||||
constructor(
|
||||
matchValue: String,
|
||||
matchingStrategy: MatchingStrategy = MatchingStrategy.ENDS_WITH_CASE_SENSITIVE,
|
||||
usernameViewId: String,
|
||||
) : this(
|
||||
matchValue = matchValue,
|
||||
matchingStrategy = matchingStrategy,
|
||||
usernameViewIds = listOf(usernameViewId),
|
||||
)
|
||||
|
||||
/**
|
||||
* Indicates the matching strategy needed for the particular [AccessOptions].
|
||||
*/
|
||||
enum class MatchingStrategy(
|
||||
val matches: (uriPath: String, matchValue: String) -> Boolean,
|
||||
) {
|
||||
CONTAINS_CASE_INSENSITIVE(
|
||||
matches = { uriPath, matchValue ->
|
||||
uriPath.contains(other = matchValue, ignoreCase = true)
|
||||
},
|
||||
),
|
||||
CONTAINS_CASE_SENSITIVE(
|
||||
matches = { uriPath, matchValue ->
|
||||
uriPath.contains(other = matchValue, ignoreCase = false)
|
||||
},
|
||||
),
|
||||
ENDS_WITH_CASE_INSENSITIVE(
|
||||
matches = { uriPath, matchValue ->
|
||||
uriPath.endsWith(suffix = matchValue, ignoreCase = true)
|
||||
},
|
||||
),
|
||||
ENDS_WITH_CASE_SENSITIVE(
|
||||
matches = { uriPath, matchValue ->
|
||||
uriPath.endsWith(suffix = matchValue, ignoreCase = false)
|
||||
},
|
||||
),
|
||||
STARTS_WITH_CASE_INSENSITIVE(
|
||||
matches = { uriPath, matchValue ->
|
||||
uriPath.startsWith(prefix = matchValue, ignoreCase = true)
|
||||
},
|
||||
),
|
||||
STARTS_WITH_CASE_SENSITIVE(
|
||||
matches = { uriPath, matchValue ->
|
||||
uriPath.startsWith(prefix = matchValue, ignoreCase = false)
|
||||
},
|
||||
),
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package com.x8bit.bitwarden.data.autofill.accessibility.parser
|
||||
|
||||
import android.net.Uri
|
||||
import android.view.accessibility.AccessibilityNodeInfo
|
||||
import com.x8bit.bitwarden.data.autofill.accessibility.model.FillableFields
|
||||
|
||||
/**
|
||||
* A tool for parsing accessibility data from the OS into domain models.
|
||||
*/
|
||||
interface AccessibilityParser {
|
||||
/**
|
||||
* Parses the fillable fields from [rootNode].
|
||||
*/
|
||||
fun parseForFillableFields(rootNode: AccessibilityNodeInfo, uri: Uri): FillableFields
|
||||
|
||||
/**
|
||||
* Parses the [Uri] from [rootNode] and returns a url, package name.
|
||||
*/
|
||||
fun parseForUriOrPackageName(rootNode: AccessibilityNodeInfo): Uri?
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
package com.x8bit.bitwarden.data.autofill.accessibility.parser
|
||||
|
||||
import android.net.Uri
|
||||
import android.view.accessibility.AccessibilityNodeInfo
|
||||
import androidx.core.net.toUri
|
||||
import com.x8bit.bitwarden.data.autofill.accessibility.manager.AccessibilityNodeInfoManager
|
||||
import com.x8bit.bitwarden.data.autofill.accessibility.model.FillableFields
|
||||
import com.x8bit.bitwarden.data.autofill.accessibility.util.getSupportedBrowserOrNull
|
||||
import com.x8bit.bitwarden.data.autofill.accessibility.util.isEditText
|
||||
import com.x8bit.bitwarden.data.autofill.accessibility.util.toUriOrNull
|
||||
import com.x8bit.bitwarden.data.platform.util.hasHttpProtocol
|
||||
|
||||
/**
|
||||
* The default implementation for the [AccessibilityParser].
|
||||
*/
|
||||
class AccessibilityParserImpl(
|
||||
private val accessibilityNodeInfoManager: AccessibilityNodeInfoManager,
|
||||
) : AccessibilityParser {
|
||||
override fun parseForFillableFields(
|
||||
rootNode: AccessibilityNodeInfo,
|
||||
uri: Uri,
|
||||
): FillableFields {
|
||||
val nodes = accessibilityNodeInfoManager
|
||||
.findAccessibilityNodeInfoList(rootNode = rootNode) {
|
||||
it.isEditText || it.isPassword
|
||||
}
|
||||
val passwordNodes = nodes.filter { it.isPassword }
|
||||
return FillableFields(
|
||||
usernameField = accessibilityNodeInfoManager.findUsernameAccessibilityNodeInfo(
|
||||
uri = uri,
|
||||
allNodes = nodes,
|
||||
passwordNodes = passwordNodes,
|
||||
),
|
||||
passwordFields = passwordNodes,
|
||||
)
|
||||
}
|
||||
|
||||
override fun parseForUriOrPackageName(rootNode: AccessibilityNodeInfo): Uri? {
|
||||
val packageName = rootNode.packageName.toString()
|
||||
val browser = packageName
|
||||
.getSupportedBrowserOrNull()
|
||||
?: return "androidapp://$packageName".toUri()
|
||||
return browser
|
||||
.possibleUrlFieldIds
|
||||
.flatMap { viewId ->
|
||||
rootNode
|
||||
.findAccessibilityNodeInfosByViewId("$packageName:id/$viewId")
|
||||
.map { accessibilityNodeInfo ->
|
||||
browser
|
||||
.urlExtractor(accessibilityNodeInfo.text.toString())
|
||||
?.trim()
|
||||
?.let { rawUrl ->
|
||||
if (rawUrl.contains(other = ".") && !rawUrl.hasHttpProtocol()) {
|
||||
"https://$rawUrl"
|
||||
} else {
|
||||
rawUrl
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.firstOrNull()
|
||||
?.toUriOrNull()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package com.x8bit.bitwarden.data.autofill.accessibility.processor
|
||||
|
||||
import android.view.accessibility.AccessibilityNodeInfo
|
||||
|
||||
/**
|
||||
* A class to handle accessibility event processing. This only includes fill requests.
|
||||
*/
|
||||
interface BitwardenAccessibilityProcessor {
|
||||
/**
|
||||
* Processes the [AccessibilityNodeInfo] for autofill options.
|
||||
*/
|
||||
fun processAccessibilityEvent(rootAccessibilityNodeInfo: AccessibilityNodeInfo?)
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
package com.x8bit.bitwarden.data.autofill.accessibility.processor
|
||||
|
||||
import android.content.Context
|
||||
import android.os.PowerManager
|
||||
import android.view.accessibility.AccessibilityNodeInfo
|
||||
import android.widget.Toast
|
||||
import com.x8bit.bitwarden.R
|
||||
import com.x8bit.bitwarden.data.autofill.accessibility.manager.AccessibilityAutofillManager
|
||||
import com.x8bit.bitwarden.data.autofill.accessibility.manager.LauncherPackageNameManager
|
||||
import com.x8bit.bitwarden.data.autofill.accessibility.model.AccessibilityAction
|
||||
import com.x8bit.bitwarden.data.autofill.accessibility.parser.AccessibilityParser
|
||||
import com.x8bit.bitwarden.data.autofill.accessibility.util.fillTextField
|
||||
import com.x8bit.bitwarden.data.autofill.accessibility.util.isSystemPackage
|
||||
import com.x8bit.bitwarden.data.autofill.accessibility.util.shouldSkipPackage
|
||||
import com.x8bit.bitwarden.data.autofill.model.AutofillSelectionData
|
||||
import com.x8bit.bitwarden.data.autofill.util.createAutofillSelectionIntent
|
||||
|
||||
/**
|
||||
* The default implementation of the [BitwardenAccessibilityProcessor].
|
||||
*/
|
||||
class BitwardenAccessibilityProcessorImpl(
|
||||
private val context: Context,
|
||||
private val accessibilityParser: AccessibilityParser,
|
||||
private val accessibilityAutofillManager: AccessibilityAutofillManager,
|
||||
private val launcherPackageNameManager: LauncherPackageNameManager,
|
||||
private val powerManager: PowerManager,
|
||||
) : BitwardenAccessibilityProcessor {
|
||||
override fun processAccessibilityEvent(rootAccessibilityNodeInfo: AccessibilityNodeInfo?) {
|
||||
val rootNode = rootAccessibilityNodeInfo ?: return
|
||||
// Ignore the event when the phone is inactive
|
||||
if (!powerManager.isInteractive) return
|
||||
// We skip if the system package
|
||||
if (rootNode.isSystemPackage) return
|
||||
// We skip any package that is a launcher or unsupported
|
||||
if (rootNode.shouldSkipPackage ||
|
||||
launcherPackageNameManager.launcherPackages.any { it == rootNode.packageName }
|
||||
) {
|
||||
// Clear the action since this event needs to be ignored completely
|
||||
accessibilityAutofillManager.accessibilityAction = null
|
||||
return
|
||||
}
|
||||
|
||||
// Only process the event if the tile was clicked
|
||||
val accessibilityAction = accessibilityAutofillManager.accessibilityAction ?: return
|
||||
accessibilityAutofillManager.accessibilityAction = null
|
||||
|
||||
when (accessibilityAction) {
|
||||
is AccessibilityAction.AttemptFill -> {
|
||||
handleAttemptFill(rootNode = rootNode, attemptFill = accessibilityAction)
|
||||
}
|
||||
|
||||
AccessibilityAction.AttemptParseUri -> handleAttemptParseUri(rootNode = rootNode)
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleAttemptParseUri(rootNode: AccessibilityNodeInfo) {
|
||||
accessibilityParser
|
||||
.parseForUriOrPackageName(rootNode = rootNode)
|
||||
?.let { uri ->
|
||||
context.startActivity(
|
||||
createAutofillSelectionIntent(
|
||||
context = context,
|
||||
framework = AutofillSelectionData.Framework.ACCESSIBILITY,
|
||||
type = AutofillSelectionData.Type.LOGIN,
|
||||
uri = uri.toString(),
|
||||
),
|
||||
)
|
||||
}
|
||||
?: run {
|
||||
Toast
|
||||
.makeText(
|
||||
context,
|
||||
R.string.autofill_tile_uri_not_found,
|
||||
Toast.LENGTH_LONG,
|
||||
)
|
||||
.show()
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleAttemptFill(
|
||||
rootNode: AccessibilityNodeInfo,
|
||||
attemptFill: AccessibilityAction.AttemptFill,
|
||||
) {
|
||||
val loginView = attemptFill.cipherView.login ?: return
|
||||
val fields = accessibilityParser.parseForFillableFields(
|
||||
rootNode = rootNode,
|
||||
uri = attemptFill.uri,
|
||||
)
|
||||
fields.usernameField?.fillTextField(value = loginView.username)
|
||||
fields.passwordFields.forEach { passwordField ->
|
||||
passwordField.fillTextField(value = loginView.password)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
package com.x8bit.bitwarden.data.autofill.accessibility.util
|
||||
|
||||
import android.view.accessibility.AccessibilityNodeInfo
|
||||
import android.widget.EditText
|
||||
import androidx.core.os.bundleOf
|
||||
import com.x8bit.bitwarden.data.autofill.accessibility.model.KnownUsernameField
|
||||
import com.x8bit.bitwarden.data.platform.annotation.OmitFromCoverage
|
||||
|
||||
private const val PACKAGE_NAME_BITWARDEN_PREFIX: String = "com.x8bit.bitwarden"
|
||||
private const val PACKAGE_NAME_SYSTEM_UI: String = "com.android.systemui"
|
||||
private const val PACKAGE_NAME_LAUNCHER_PARTIAL: String = "launcher"
|
||||
private val PACKAGE_NAME_BLOCK_LIST: List<String> = listOf(
|
||||
"com.google.android.googlequicksearchbox",
|
||||
"com.google.android.apps.nexuslauncher",
|
||||
"com.google.android.launcher",
|
||||
"com.computer.desktop.ui.launcher",
|
||||
"com.launcher.notelauncher",
|
||||
"com.anddoes.launcher",
|
||||
"com.actionlauncher.playstore",
|
||||
"ch.deletescape.lawnchair.plah",
|
||||
"com.microsoft.launcher",
|
||||
"com.teslacoilsw.launcher",
|
||||
"com.teslacoilsw.launcher.prime",
|
||||
"is.shortcut",
|
||||
"me.craftsapp.nlauncher",
|
||||
"com.ss.squarehome2",
|
||||
"com.treydev.pns",
|
||||
)
|
||||
|
||||
/**
|
||||
* Returns true if the event is for an unsupported package.
|
||||
*/
|
||||
val AccessibilityNodeInfo.shouldSkipPackage: Boolean
|
||||
get() {
|
||||
val packageName = this.packageName.takeUnless { it.isNullOrBlank() } ?: return true
|
||||
if (packageName.startsWith(prefix = PACKAGE_NAME_BITWARDEN_PREFIX)) return true
|
||||
if (packageName.contains(other = PACKAGE_NAME_LAUNCHER_PARTIAL, ignoreCase = true)) {
|
||||
return true
|
||||
}
|
||||
return PACKAGE_NAME_BLOCK_LIST.contains(packageName)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the event is from the system UI package.
|
||||
*/
|
||||
val AccessibilityNodeInfo.isSystemPackage: Boolean
|
||||
get() = this.packageName == PACKAGE_NAME_SYSTEM_UI
|
||||
|
||||
/**
|
||||
* Fills the [AccessibilityNodeInfo] text field with the [value] provided.
|
||||
*/
|
||||
@OmitFromCoverage
|
||||
fun AccessibilityNodeInfo.fillTextField(value: String?) {
|
||||
performAction(
|
||||
AccessibilityNodeInfo.ACTION_SET_TEXT,
|
||||
bundleOf(AccessibilityNodeInfo.ACTION_ARGUMENT_SET_TEXT_CHARSEQUENCE to value),
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if the [AccessibilityNodeInfo] is an instance of an EditText.
|
||||
*/
|
||||
val AccessibilityNodeInfo.isEditText: Boolean
|
||||
get() = className
|
||||
?.let {
|
||||
try {
|
||||
Class.forName(it.toString())
|
||||
} catch (e: ClassNotFoundException) {
|
||||
null
|
||||
}
|
||||
}
|
||||
?.let { EditText::class.java.isAssignableFrom(it) }
|
||||
?: (className?.contains(other = "EditText") == true)
|
||||
|
||||
/**
|
||||
* Determines if the [AccessibilityNodeInfo] is a username field.
|
||||
*/
|
||||
fun AccessibilityNodeInfo.isUsername(
|
||||
knownUsernameField: KnownUsernameField,
|
||||
uriPath: String,
|
||||
): Boolean {
|
||||
knownUsernameField.accessOptions.map { options ->
|
||||
if (options.matchingStrategy.matches(uriPath, options.matchValue)) {
|
||||
options
|
||||
.usernameViewIds
|
||||
.firstOrNull { viewId -> viewId == this@isUsername.viewIdResourceName }
|
||||
?.let { return true }
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
@@ -0,0 +1,219 @@
|
||||
package com.x8bit.bitwarden.data.autofill.accessibility.util
|
||||
|
||||
import com.x8bit.bitwarden.data.autofill.accessibility.model.Browser
|
||||
|
||||
/**
|
||||
* Determines if the [String] receiver is a package name for a supported browser and returns that
|
||||
* [Browser] if it is a match.
|
||||
*/
|
||||
fun String.getSupportedBrowserOrNull(): Browser? =
|
||||
ACCESSIBILITY_SUPPORTED_BROWSERS.find { it.packageName == this@getSupportedBrowserOrNull }
|
||||
|
||||
/**
|
||||
* A list of supported browsers and the field ID used to find the url bar.
|
||||
*
|
||||
* This list should be kept in order and match the list of compatibility browsers in the
|
||||
* autofill_service_configuration.xml.
|
||||
*/
|
||||
private val ACCESSIBILITY_SUPPORTED_BROWSERS = listOf(
|
||||
Browser(packageName = "alook.browser", urlFieldId = "search_fragment_input_view"),
|
||||
Browser(packageName = "alook.browser.google", urlFieldId = "search_fragment_input_view"),
|
||||
Browser(packageName = "app.vanadium.browser", urlFieldId = "url_bar"),
|
||||
Browser(packageName = "com.amazon.cloud9", urlFieldId = "url"),
|
||||
Browser(packageName = "com.android.browser", urlFieldId = "url"),
|
||||
Browser(packageName = "com.android.chrome", urlFieldId = "url_bar"),
|
||||
// "com.android.htmlviewer": Doesn't have a URL bar
|
||||
Browser(packageName = "com.avast.android.secure.browser", urlFieldId = "editor"),
|
||||
Browser(packageName = "com.avg.android.secure.browser", urlFieldId = "editor"),
|
||||
Browser(packageName = "com.brave.browser", urlFieldId = "url_bar"),
|
||||
Browser(packageName = "com.brave.browser_beta", urlFieldId = "url_bar"),
|
||||
Browser(packageName = "com.brave.browser_default", urlFieldId = "url_bar"),
|
||||
Browser(packageName = "com.brave.browser_dev", urlFieldId = "url_bar"),
|
||||
Browser(packageName = "com.brave.browser_nightly", urlFieldId = "url_bar"),
|
||||
Browser(packageName = "com.chrome.beta", urlFieldId = "url_bar"),
|
||||
Browser(packageName = "com.chrome.canary", urlFieldId = "url_bar"),
|
||||
Browser(packageName = "com.chrome.dev", urlFieldId = "url_bar"),
|
||||
Browser(packageName = "com.cookiegames.smartcookie", urlFieldId = "search"),
|
||||
Browser(
|
||||
packageName = "com.cookiejarapps.android.smartcookieweb",
|
||||
urlFieldId = "mozac_browser_toolbar_url_view",
|
||||
),
|
||||
Browser(packageName = "com.duckduckgo.mobile.android", urlFieldId = "omnibarTextInput"),
|
||||
Browser(packageName = "com.ecosia.android", urlFieldId = "url_bar"),
|
||||
Browser(packageName = "com.google.android.apps.chrome", urlFieldId = "url_bar"),
|
||||
Browser(packageName = "com.google.android.apps.chrome_dev", urlFieldId = "url_bar"),
|
||||
// "com.google.android.captiveportallogin": URL displayed in ActionBar subtitle without viewId
|
||||
Browser(packageName = "com.iode.firefox", urlFieldId = "mozac_browser_toolbar_url_view"),
|
||||
Browser(packageName = "com.jamal2367.styx", urlFieldId = "search"),
|
||||
Browser(packageName = "com.kiwibrowser.browser", urlFieldId = "url_bar"),
|
||||
Browser(packageName = "com.kiwibrowser.browser.dev", urlFieldId = "url_bar"),
|
||||
Browser(packageName = "com.microsoft.emmx", urlFieldId = "url_bar"),
|
||||
Browser(packageName = "com.microsoft.emmx.beta", urlFieldId = "url_bar"),
|
||||
Browser(packageName = "com.microsoft.emmx.canary", urlFieldId = "url_bar"),
|
||||
Browser(packageName = "com.microsoft.emmx.dev", urlFieldId = "url_bar"),
|
||||
Browser(packageName = "com.mmbox.browser", urlFieldId = "search_box"),
|
||||
Browser(packageName = "com.mmbox.xbrowser", urlFieldId = "search_box"),
|
||||
Browser(packageName = "com.mycompany.app.soulbrowser", urlFieldId = "edit_text"),
|
||||
Browser(packageName = "com.naver.whale", urlFieldId = "url_bar"),
|
||||
Browser(packageName = "com.neeva.app", urlFieldId = "full_url_text_view"),
|
||||
Browser(packageName = "com.opera.browser", urlFieldId = "url_field"),
|
||||
Browser(packageName = "com.opera.browser.beta", urlFieldId = "url_field"),
|
||||
Browser(packageName = "com.opera.gx", urlFieldId = "addressbarEdit"),
|
||||
Browser(packageName = "com.opera.mini.native", urlFieldId = "url_field"),
|
||||
Browser(packageName = "com.opera.mini.native.beta", urlFieldId = "url_field"),
|
||||
Browser(packageName = "com.opera.touch", urlFieldId = "addressbarEdit"),
|
||||
Browser(packageName = "com.qflair.browserq", urlFieldId = "url"),
|
||||
Browser(
|
||||
packageName = "com.qwant.liberty",
|
||||
// 2nd = Legacy (before v4)
|
||||
possibleUrlFieldIds = listOf("mozac_browser_toolbar_url_view", "url_bar_title"),
|
||||
),
|
||||
Browser(packageName = "com.rainsee.create", urlFieldId = "search_box"),
|
||||
Browser(packageName = "com.sec.android.app.sbrowser", urlFieldId = "location_bar_edit_text"),
|
||||
Browser(
|
||||
packageName = "com.sec.android.app.sbrowser.beta",
|
||||
urlFieldId = "location_bar_edit_text",
|
||||
),
|
||||
Browser(packageName = "com.stoutner.privacybrowser.free", urlFieldId = "url_edittext"),
|
||||
Browser(packageName = "com.stoutner.privacybrowser.standard", urlFieldId = "url_edittext"),
|
||||
Browser(packageName = "com.vivaldi.browser", urlFieldId = "url_bar"),
|
||||
Browser(packageName = "com.vivaldi.browser.snapshot", urlFieldId = "url_bar"),
|
||||
Browser(packageName = "com.vivaldi.browser.sopranos", urlFieldId = "url_bar"),
|
||||
Browser(
|
||||
packageName = "com.yandex.browser",
|
||||
possibleUrlFieldIds = listOf(
|
||||
"bro_omnibar_address_title_text",
|
||||
"bro_omnibox_collapsed_title",
|
||||
),
|
||||
urlExtractor = {
|
||||
// 0 = Regular Space, 1 = No-break space (00A0)
|
||||
it.split(' ', ' ').firstOrNull()
|
||||
},
|
||||
),
|
||||
Browser(packageName = "com.yjllq.internet", urlFieldId = "search_box"),
|
||||
Browser(packageName = "com.yjllq.kito", urlFieldId = "search_box"),
|
||||
Browser(packageName = "com.yujian.ResideMenuDemo", urlFieldId = "search_box"),
|
||||
Browser(packageName = "com.z28j.feel", urlFieldId = "g2"),
|
||||
Browser(packageName = "idm.internet.download.manager", urlFieldId = "search"),
|
||||
Browser(packageName = "idm.internet.download.manager.adm.lite", urlFieldId = "search"),
|
||||
Browser(packageName = "idm.internet.download.manager.plus", urlFieldId = "search"),
|
||||
Browser(
|
||||
packageName = "io.github.forkmaintainers.iceraven",
|
||||
urlFieldId = "mozac_browser_toolbar_url_view",
|
||||
),
|
||||
Browser(packageName = "mark.via", urlFieldId = "am,an"),
|
||||
Browser(packageName = "mark.via.gp", urlFieldId = "as"),
|
||||
Browser(packageName = "net.dezor.browser", urlFieldId = "url_bar"),
|
||||
Browser(packageName = "net.slions.fulguris.full.download", urlFieldId = "search"),
|
||||
Browser(packageName = "net.slions.fulguris.full.download.debug", urlFieldId = "search"),
|
||||
Browser(packageName = "net.slions.fulguris.full.playstore", urlFieldId = "search"),
|
||||
Browser(packageName = "net.slions.fulguris.full.playstore.debug", urlFieldId = "search"),
|
||||
Browser(
|
||||
packageName = "org.adblockplus.browser",
|
||||
// 2nd = Legacy (before v2)
|
||||
possibleUrlFieldIds = listOf("url_bar", "url_bar_title"),
|
||||
),
|
||||
Browser(
|
||||
packageName = "org.adblockplus.browser.beta",
|
||||
// 2nd = Legacy (before v2)
|
||||
possibleUrlFieldIds = listOf("url_bar", "url_bar_title"),
|
||||
),
|
||||
Browser(packageName = "org.bromite.bromite", urlFieldId = "url_bar"),
|
||||
Browser(packageName = "org.bromite.chromium", urlFieldId = "url_bar"),
|
||||
Browser(packageName = "org.chromium.chrome", urlFieldId = "url_bar"),
|
||||
Browser(packageName = "org.codeaurora.swe.browser", urlFieldId = "url_bar"),
|
||||
Browser(packageName = "org.cromite.cromite", urlFieldId = "url_bar"),
|
||||
Browser(
|
||||
packageName = "org.gnu.icecat",
|
||||
// 2nd = Anticipation
|
||||
possibleUrlFieldIds = listOf("url_bar_title", "mozac_browser_toolbar_url_view"),
|
||||
),
|
||||
Browser(packageName = "org.mozilla.fenix", urlFieldId = "mozac_browser_toolbar_url_view"),
|
||||
// [DEPRECATED ENTRY]
|
||||
Browser(
|
||||
packageName = "org.mozilla.fenix.nightly",
|
||||
urlFieldId = "mozac_browser_toolbar_url_view",
|
||||
),
|
||||
// [DEPRECATED ENTRY]
|
||||
Browser(
|
||||
packageName = "org.mozilla.fennec_aurora",
|
||||
possibleUrlFieldIds = listOf("mozac_browser_toolbar_url_view", "url_bar_title"),
|
||||
),
|
||||
Browser(
|
||||
packageName = "org.mozilla.fennec_fdroid",
|
||||
// 2nd = Legacy
|
||||
possibleUrlFieldIds = listOf("mozac_browser_toolbar_url_view", "url_bar_title"),
|
||||
),
|
||||
Browser(
|
||||
packageName = "org.mozilla.firefox",
|
||||
// 2nd = Legacy
|
||||
possibleUrlFieldIds = listOf("mozac_browser_toolbar_url_view", "url_bar_title"),
|
||||
),
|
||||
Browser(
|
||||
packageName = "org.mozilla.firefox_beta",
|
||||
// 2nd = Legacy
|
||||
possibleUrlFieldIds = listOf("mozac_browser_toolbar_url_view", "url_bar_title"),
|
||||
),
|
||||
Browser(
|
||||
packageName = "org.mozilla.focus",
|
||||
// 2nd = Legacy
|
||||
possibleUrlFieldIds = listOf("mozac_browser_toolbar_url_view", "display_url"),
|
||||
),
|
||||
Browser(
|
||||
packageName = "org.mozilla.focus.beta",
|
||||
// 2nd = Legacy
|
||||
possibleUrlFieldIds = listOf("mozac_browser_toolbar_url_view", "display_url"),
|
||||
),
|
||||
Browser(
|
||||
packageName = "org.mozilla.focus.nightly",
|
||||
// 2nd = Legacy
|
||||
possibleUrlFieldIds = listOf("mozac_browser_toolbar_url_view", "display_url"),
|
||||
),
|
||||
Browser(
|
||||
packageName = "org.mozilla.klar",
|
||||
// 2nd = Legacy
|
||||
possibleUrlFieldIds = listOf("mozac_browser_toolbar_url_view", "display_url"),
|
||||
),
|
||||
Browser(
|
||||
packageName = "org.mozilla.reference.browser",
|
||||
urlFieldId = "mozac_browser_toolbar_url_view",
|
||||
),
|
||||
Browser(packageName = "org.mozilla.rocket", urlFieldId = "display_url"),
|
||||
Browser(
|
||||
packageName = "org.torproject.torbrowser",
|
||||
// 2nd = Legacy (before v10.0.3)
|
||||
possibleUrlFieldIds = listOf("mozac_browser_toolbar_url_view", "url_bar_title"),
|
||||
),
|
||||
Browser(
|
||||
packageName = "org.torproject.torbrowser_alpha",
|
||||
// 2nd = Legacy (before v10.0a8)
|
||||
possibleUrlFieldIds = listOf("mozac_browser_toolbar_url_view", "url_bar_title"),
|
||||
),
|
||||
Browser(packageName = "org.ungoogled.chromium.extensions.stable", urlFieldId = "url_bar"),
|
||||
Browser(packageName = "org.ungoogled.chromium.stable", urlFieldId = "url_bar"),
|
||||
Browser(
|
||||
packageName = "us.spotco.fennec_dos",
|
||||
// 2nd = Legacy
|
||||
possibleUrlFieldIds = listOf("mozac_browser_toolbar_url_view", "url_bar_title"),
|
||||
),
|
||||
|
||||
// [Section B] Entries only present here
|
||||
// TODO: Test the compatibility of these with Autofill Framework
|
||||
Browser(packageName = "acr.browser.barebones", urlFieldId = "search"),
|
||||
Browser(packageName = "acr.browser.lightning", urlFieldId = "search"),
|
||||
Browser(packageName = "com.feedback.browser.wjbrowser", urlFieldId = "addressbar_url"),
|
||||
Browser(packageName = "com.ghostery.android.ghostery", urlFieldId = "search_field"),
|
||||
Browser(packageName = "com.htc.sense.browser", urlFieldId = "title"),
|
||||
Browser(packageName = "com.jerky.browser2", urlFieldId = "enterUrl"),
|
||||
Browser(packageName = "com.ksmobile.cb", urlFieldId = "address_bar_edit_text"),
|
||||
Browser(packageName = "com.lemurbrowser.exts", urlFieldId = "url_bar"),
|
||||
Browser(packageName = "com.linkbubble.playstore", urlFieldId = "url_text"),
|
||||
Browser(packageName = "com.mx.browser", urlFieldId = "address_editor_with_progress"),
|
||||
Browser(packageName = "com.mx.browser.tablet", urlFieldId = "address_editor_with_progress"),
|
||||
Browser(packageName = "com.nubelacorp.javelin", urlFieldId = "enterUrl"),
|
||||
Browser(packageName = "jp.co.fenrir.android.sleipnir", urlFieldId = "url_text"),
|
||||
Browser(packageName = "jp.co.fenrir.android.sleipnir_black", urlFieldId = "url_text"),
|
||||
Browser(packageName = "jp.co.fenrir.android.sleipnir_test", urlFieldId = "url_text"),
|
||||
Browser(packageName = "mobi.mgeek.TunnyBrowser", urlFieldId = "title"),
|
||||
Browser(packageName = "org.iron.srware", urlFieldId = "url_bar"),
|
||||
)
|
||||
@@ -0,0 +1,26 @@
|
||||
package com.x8bit.bitwarden.data.autofill.accessibility.util
|
||||
|
||||
import android.content.Context
|
||||
import android.provider.Settings
|
||||
import com.x8bit.bitwarden.LEGACY_ACCESSIBILITY_SERVICE_NAME
|
||||
import com.x8bit.bitwarden.data.autofill.accessibility.BitwardenAccessibilityService
|
||||
|
||||
/**
|
||||
* Helper method to determine if the [BitwardenAccessibilityService] is enabled.
|
||||
*/
|
||||
val Context.isAccessibilityServiceEnabled: Boolean
|
||||
get() {
|
||||
val appContext = this.applicationContext
|
||||
val accessibilityServiceName = appContext
|
||||
.packageName
|
||||
?.let { "$it/$LEGACY_ACCESSIBILITY_SERVICE_NAME" }
|
||||
?: return false
|
||||
return Settings
|
||||
.Secure
|
||||
.getString(
|
||||
appContext.contentResolver,
|
||||
Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES,
|
||||
)
|
||||
?.contains(accessibilityServiceName)
|
||||
?: false
|
||||
}
|
||||
@@ -0,0 +1,672 @@
|
||||
package com.x8bit.bitwarden.data.autofill.accessibility.util
|
||||
|
||||
import com.x8bit.bitwarden.data.autofill.accessibility.model.AccessOptions
|
||||
import com.x8bit.bitwarden.data.autofill.accessibility.model.KnownUsernameField
|
||||
|
||||
/**
|
||||
* Determines if the [String] receiver is a uri authority for a known username field and returns
|
||||
* that [KnownUsernameField] if it is a match.
|
||||
*/
|
||||
fun String.getKnownUsernameFieldNull(): KnownUsernameField? =
|
||||
LEGACY_KNOWN_USERNAME_FIELDS.find { it.uriAuthority == this@getKnownUsernameFieldNull }
|
||||
|
||||
/**
|
||||
* A list of known username fields and their IDs.
|
||||
*/
|
||||
private val LEGACY_KNOWN_USERNAME_FIELDS: List<KnownUsernameField> = listOf(
|
||||
// SECTION A ——— World-renowned web sites/applications
|
||||
KnownUsernameField(
|
||||
uriAuthority = "amazon.ae",
|
||||
accessOption = AccessOptions(
|
||||
matchValue = "/ap/signin",
|
||||
matchingStrategy = AccessOptions.MatchingStrategy.CONTAINS_CASE_SENSITIVE,
|
||||
usernameViewIds = listOf("ap_email_login", "ap_email"),
|
||||
),
|
||||
),
|
||||
KnownUsernameField(
|
||||
uriAuthority = "amazon.ca",
|
||||
accessOption = AccessOptions(
|
||||
matchValue = "/ap/signin",
|
||||
matchingStrategy = AccessOptions.MatchingStrategy.CONTAINS_CASE_SENSITIVE,
|
||||
usernameViewIds = listOf("ap_email_login", "ap_email"),
|
||||
),
|
||||
),
|
||||
KnownUsernameField(
|
||||
uriAuthority = "amazon.cn",
|
||||
accessOption = AccessOptions(
|
||||
matchValue = "/ap/signin",
|
||||
matchingStrategy = AccessOptions.MatchingStrategy.CONTAINS_CASE_SENSITIVE,
|
||||
usernameViewIds = listOf("ap_email_login", "ap_email"),
|
||||
),
|
||||
),
|
||||
KnownUsernameField(
|
||||
uriAuthority = "amazon.co.jp",
|
||||
accessOption = AccessOptions(
|
||||
matchValue = "/ap/signin",
|
||||
matchingStrategy = AccessOptions.MatchingStrategy.CONTAINS_CASE_SENSITIVE,
|
||||
usernameViewIds = listOf("ap_email_login", "ap_email"),
|
||||
),
|
||||
),
|
||||
KnownUsernameField(
|
||||
uriAuthority = "amazon.co.uk",
|
||||
accessOption = AccessOptions(
|
||||
matchValue = "/ap/signin",
|
||||
matchingStrategy = AccessOptions.MatchingStrategy.CONTAINS_CASE_SENSITIVE,
|
||||
usernameViewIds = listOf("ap_email_login", "ap_email"),
|
||||
),
|
||||
),
|
||||
KnownUsernameField(
|
||||
uriAuthority = "amazon.com",
|
||||
accessOption = AccessOptions(
|
||||
matchValue = "/ap/signin",
|
||||
matchingStrategy = AccessOptions.MatchingStrategy.CONTAINS_CASE_SENSITIVE,
|
||||
usernameViewIds = listOf("ap_email_login", "ap_email"),
|
||||
),
|
||||
),
|
||||
KnownUsernameField(
|
||||
uriAuthority = "amazon.com.au",
|
||||
accessOption = AccessOptions(
|
||||
matchValue = "/ap/signin",
|
||||
matchingStrategy = AccessOptions.MatchingStrategy.CONTAINS_CASE_SENSITIVE,
|
||||
usernameViewIds = listOf("ap_email_login", "ap_email"),
|
||||
),
|
||||
),
|
||||
KnownUsernameField(
|
||||
uriAuthority = "amazon.com.br",
|
||||
accessOption = AccessOptions(
|
||||
matchValue = "/ap/signin",
|
||||
matchingStrategy = AccessOptions.MatchingStrategy.CONTAINS_CASE_SENSITIVE,
|
||||
usernameViewIds = listOf("ap_email_login", "ap_email"),
|
||||
),
|
||||
),
|
||||
KnownUsernameField(
|
||||
uriAuthority = "amazon.com.mx",
|
||||
accessOption = AccessOptions(
|
||||
matchValue = "/ap/signin",
|
||||
matchingStrategy = AccessOptions.MatchingStrategy.CONTAINS_CASE_SENSITIVE,
|
||||
usernameViewIds = listOf("ap_email_login", "ap_email"),
|
||||
),
|
||||
),
|
||||
KnownUsernameField(
|
||||
uriAuthority = "amazon.com.tr",
|
||||
accessOption = AccessOptions(
|
||||
matchValue = "/ap/signin",
|
||||
matchingStrategy = AccessOptions.MatchingStrategy.CONTAINS_CASE_SENSITIVE,
|
||||
usernameViewIds = listOf("ap_email_login", "ap_email"),
|
||||
),
|
||||
),
|
||||
KnownUsernameField(
|
||||
uriAuthority = "amazon.de",
|
||||
accessOption = AccessOptions(
|
||||
matchValue = "/ap/signin",
|
||||
matchingStrategy = AccessOptions.MatchingStrategy.CONTAINS_CASE_SENSITIVE,
|
||||
usernameViewIds = listOf("ap_email_login", "ap_email"),
|
||||
),
|
||||
),
|
||||
KnownUsernameField(
|
||||
uriAuthority = "amazon.es",
|
||||
accessOption = AccessOptions(
|
||||
matchValue = "/ap/signin",
|
||||
matchingStrategy = AccessOptions.MatchingStrategy.CONTAINS_CASE_SENSITIVE,
|
||||
usernameViewIds = listOf("ap_email_login", "ap_email"),
|
||||
),
|
||||
),
|
||||
KnownUsernameField(
|
||||
uriAuthority = "amazon.fr",
|
||||
accessOption = AccessOptions(
|
||||
matchValue = "/ap/signin",
|
||||
matchingStrategy = AccessOptions.MatchingStrategy.CONTAINS_CASE_SENSITIVE,
|
||||
usernameViewIds = listOf("ap_email_login", "ap_email"),
|
||||
),
|
||||
),
|
||||
KnownUsernameField(
|
||||
uriAuthority = "amazon.in",
|
||||
accessOption = AccessOptions(
|
||||
matchValue = "/ap/signin",
|
||||
matchingStrategy = AccessOptions.MatchingStrategy.CONTAINS_CASE_SENSITIVE,
|
||||
usernameViewIds = listOf("ap_email_login", "ap_email"),
|
||||
),
|
||||
),
|
||||
KnownUsernameField(
|
||||
uriAuthority = "amazon.it",
|
||||
accessOption = AccessOptions(
|
||||
matchValue = "/ap/signin",
|
||||
matchingStrategy = AccessOptions.MatchingStrategy.CONTAINS_CASE_SENSITIVE,
|
||||
usernameViewIds = listOf("ap_email_login", "ap_email"),
|
||||
),
|
||||
),
|
||||
KnownUsernameField(
|
||||
uriAuthority = "amazon.nl",
|
||||
accessOption = AccessOptions(
|
||||
matchValue = "/ap/signin",
|
||||
matchingStrategy = AccessOptions.MatchingStrategy.CONTAINS_CASE_SENSITIVE,
|
||||
usernameViewIds = listOf("ap_email_login", "ap_email"),
|
||||
),
|
||||
),
|
||||
KnownUsernameField(
|
||||
uriAuthority = "amazon.pl",
|
||||
accessOption = AccessOptions(
|
||||
matchValue = "/ap/signin",
|
||||
matchingStrategy = AccessOptions.MatchingStrategy.CONTAINS_CASE_SENSITIVE,
|
||||
usernameViewIds = listOf("ap_email_login", "ap_email"),
|
||||
),
|
||||
),
|
||||
KnownUsernameField(
|
||||
uriAuthority = "amazon.sa",
|
||||
accessOption = AccessOptions(
|
||||
matchValue = "/ap/signin",
|
||||
matchingStrategy = AccessOptions.MatchingStrategy.CONTAINS_CASE_SENSITIVE,
|
||||
usernameViewIds = listOf("ap_email_login", "ap_email"),
|
||||
),
|
||||
),
|
||||
KnownUsernameField(
|
||||
uriAuthority = "amazon.se",
|
||||
accessOption = AccessOptions(
|
||||
matchValue = "/ap/signin",
|
||||
matchingStrategy = AccessOptions.MatchingStrategy.CONTAINS_CASE_SENSITIVE,
|
||||
usernameViewIds = listOf("ap_email_login", "ap_email"),
|
||||
),
|
||||
),
|
||||
KnownUsernameField(
|
||||
uriAuthority = "amazon.sg",
|
||||
accessOption = AccessOptions(
|
||||
matchValue = "/ap/signin",
|
||||
matchingStrategy = AccessOptions.MatchingStrategy.CONTAINS_CASE_SENSITIVE,
|
||||
usernameViewIds = listOf("ap_email_login", "ap_email"),
|
||||
),
|
||||
),
|
||||
KnownUsernameField(
|
||||
uriAuthority = "signin.aws.amazon.com",
|
||||
accessOption = AccessOptions(matchValue = "signin", usernameViewId = "resolving_input"),
|
||||
),
|
||||
KnownUsernameField(
|
||||
uriAuthority = "id.atlassian.com",
|
||||
accessOption = AccessOptions(matchValue = "login", usernameViewId = "username"),
|
||||
),
|
||||
KnownUsernameField(
|
||||
uriAuthority = "bitly.com",
|
||||
accessOption = AccessOptions(matchValue = "/sso/url_slug", usernameViewId = "url_slug"),
|
||||
),
|
||||
KnownUsernameField(
|
||||
uriAuthority = "signin.befr.ebay.be",
|
||||
accessOptions = listOf(
|
||||
AccessOptions(
|
||||
matchValue = "eBayISAPI.dll",
|
||||
matchingStrategy = AccessOptions.MatchingStrategy.ENDS_WITH_CASE_INSENSITIVE,
|
||||
usernameViewId = "userid",
|
||||
),
|
||||
AccessOptions(
|
||||
matchValue = "/signin/",
|
||||
matchingStrategy = AccessOptions.MatchingStrategy.CONTAINS_CASE_INSENSITIVE,
|
||||
usernameViewId = "userid",
|
||||
),
|
||||
),
|
||||
),
|
||||
KnownUsernameField(
|
||||
uriAuthority = "signin.benl.ebay.be",
|
||||
accessOptions = listOf(
|
||||
AccessOptions(
|
||||
matchValue = "eBayISAPI.dll",
|
||||
matchingStrategy = AccessOptions.MatchingStrategy.ENDS_WITH_CASE_INSENSITIVE,
|
||||
usernameViewId = "userid",
|
||||
),
|
||||
AccessOptions(
|
||||
matchValue = "/signin/",
|
||||
matchingStrategy = AccessOptions.MatchingStrategy.CONTAINS_CASE_INSENSITIVE,
|
||||
usernameViewId = "userid",
|
||||
),
|
||||
),
|
||||
),
|
||||
KnownUsernameField(
|
||||
uriAuthority = "signin.cafr.ebay.ca",
|
||||
accessOptions = listOf(
|
||||
AccessOptions(
|
||||
matchValue = "eBayISAPI.dll",
|
||||
matchingStrategy = AccessOptions.MatchingStrategy.ENDS_WITH_CASE_INSENSITIVE,
|
||||
usernameViewId = "userid",
|
||||
),
|
||||
AccessOptions(
|
||||
matchValue = "/signin/",
|
||||
matchingStrategy = AccessOptions.MatchingStrategy.CONTAINS_CASE_INSENSITIVE,
|
||||
usernameViewId = "userid",
|
||||
),
|
||||
),
|
||||
),
|
||||
KnownUsernameField(
|
||||
uriAuthority = "signin.ebay.at",
|
||||
accessOptions = listOf(
|
||||
AccessOptions(
|
||||
matchValue = "eBayISAPI.dll",
|
||||
matchingStrategy = AccessOptions.MatchingStrategy.ENDS_WITH_CASE_INSENSITIVE,
|
||||
usernameViewId = "userid",
|
||||
),
|
||||
AccessOptions(
|
||||
matchValue = "/signin/",
|
||||
matchingStrategy = AccessOptions.MatchingStrategy.CONTAINS_CASE_INSENSITIVE,
|
||||
usernameViewId = "userid",
|
||||
),
|
||||
),
|
||||
),
|
||||
KnownUsernameField(
|
||||
uriAuthority = "signin.ebay.be",
|
||||
accessOptions = listOf(
|
||||
AccessOptions(
|
||||
matchValue = "eBayISAPI.dll",
|
||||
matchingStrategy = AccessOptions.MatchingStrategy.ENDS_WITH_CASE_INSENSITIVE,
|
||||
usernameViewId = "userid",
|
||||
),
|
||||
AccessOptions(
|
||||
matchValue = "/signin/",
|
||||
matchingStrategy = AccessOptions.MatchingStrategy.CONTAINS_CASE_INSENSITIVE,
|
||||
usernameViewId = "userid",
|
||||
),
|
||||
),
|
||||
),
|
||||
KnownUsernameField(
|
||||
uriAuthority = "signin.ebay.ca",
|
||||
accessOptions = listOf(
|
||||
AccessOptions(
|
||||
matchValue = "eBayISAPI.dll",
|
||||
matchingStrategy = AccessOptions.MatchingStrategy.ENDS_WITH_CASE_INSENSITIVE,
|
||||
usernameViewId = "userid",
|
||||
),
|
||||
AccessOptions(
|
||||
matchValue = "/signin/",
|
||||
matchingStrategy = AccessOptions.MatchingStrategy.CONTAINS_CASE_INSENSITIVE,
|
||||
usernameViewId = "userid",
|
||||
),
|
||||
),
|
||||
),
|
||||
KnownUsernameField(
|
||||
uriAuthority = "signin.ebay.ch",
|
||||
accessOptions = listOf(
|
||||
AccessOptions(
|
||||
matchValue = "eBayISAPI.dll",
|
||||
matchingStrategy = AccessOptions.MatchingStrategy.ENDS_WITH_CASE_INSENSITIVE,
|
||||
usernameViewId = "userid",
|
||||
),
|
||||
AccessOptions(
|
||||
matchValue = "/signin/",
|
||||
matchingStrategy = AccessOptions.MatchingStrategy.CONTAINS_CASE_INSENSITIVE,
|
||||
usernameViewId = "userid",
|
||||
),
|
||||
),
|
||||
),
|
||||
KnownUsernameField(
|
||||
uriAuthority = "signin.ebay.co.uk",
|
||||
accessOptions = listOf(
|
||||
AccessOptions(
|
||||
matchValue = "eBayISAPI.dll",
|
||||
matchingStrategy = AccessOptions.MatchingStrategy.ENDS_WITH_CASE_INSENSITIVE,
|
||||
usernameViewId = "userid",
|
||||
),
|
||||
AccessOptions(
|
||||
matchValue = "/signin/",
|
||||
matchingStrategy = AccessOptions.MatchingStrategy.CONTAINS_CASE_INSENSITIVE,
|
||||
usernameViewId = "userid",
|
||||
),
|
||||
),
|
||||
),
|
||||
KnownUsernameField(
|
||||
uriAuthority = "signin.ebay.com",
|
||||
accessOptions = listOf(
|
||||
AccessOptions(
|
||||
matchValue = "eBayISAPI.dll",
|
||||
matchingStrategy = AccessOptions.MatchingStrategy.ENDS_WITH_CASE_INSENSITIVE,
|
||||
usernameViewId = "userid",
|
||||
),
|
||||
AccessOptions(
|
||||
matchValue = "/signin/",
|
||||
matchingStrategy = AccessOptions.MatchingStrategy.CONTAINS_CASE_INSENSITIVE,
|
||||
usernameViewId = "userid",
|
||||
),
|
||||
),
|
||||
),
|
||||
KnownUsernameField(
|
||||
uriAuthority = "signin.ebay.com.au",
|
||||
accessOptions = listOf(
|
||||
AccessOptions(
|
||||
matchValue = "eBayISAPI.dll",
|
||||
matchingStrategy = AccessOptions.MatchingStrategy.ENDS_WITH_CASE_INSENSITIVE,
|
||||
usernameViewId = "userid",
|
||||
),
|
||||
AccessOptions(
|
||||
matchValue = "/signin/",
|
||||
matchingStrategy = AccessOptions.MatchingStrategy.CONTAINS_CASE_INSENSITIVE,
|
||||
usernameViewId = "userid",
|
||||
),
|
||||
),
|
||||
),
|
||||
KnownUsernameField(
|
||||
uriAuthority = "signin.ebay.com.hk",
|
||||
accessOptions = listOf(
|
||||
AccessOptions(
|
||||
matchValue = "eBayISAPI.dll",
|
||||
matchingStrategy = AccessOptions.MatchingStrategy.ENDS_WITH_CASE_INSENSITIVE,
|
||||
usernameViewId = "userid",
|
||||
),
|
||||
AccessOptions(
|
||||
matchValue = "/signin/",
|
||||
matchingStrategy = AccessOptions.MatchingStrategy.CONTAINS_CASE_INSENSITIVE,
|
||||
usernameViewId = "userid",
|
||||
),
|
||||
),
|
||||
),
|
||||
KnownUsernameField(
|
||||
uriAuthority = "signin.ebay.com.my",
|
||||
accessOptions = listOf(
|
||||
AccessOptions(
|
||||
matchValue = "eBayISAPI.dll",
|
||||
matchingStrategy = AccessOptions.MatchingStrategy.ENDS_WITH_CASE_INSENSITIVE,
|
||||
usernameViewId = "userid",
|
||||
),
|
||||
AccessOptions(
|
||||
matchValue = "/signin/",
|
||||
matchingStrategy = AccessOptions.MatchingStrategy.CONTAINS_CASE_INSENSITIVE,
|
||||
usernameViewId = "userid",
|
||||
),
|
||||
),
|
||||
),
|
||||
KnownUsernameField(
|
||||
uriAuthority = "signin.ebay.com.sg",
|
||||
accessOptions = listOf(
|
||||
AccessOptions(
|
||||
matchValue = "eBayISAPI.dll",
|
||||
matchingStrategy = AccessOptions.MatchingStrategy.ENDS_WITH_CASE_INSENSITIVE,
|
||||
usernameViewId = "userid",
|
||||
),
|
||||
AccessOptions(
|
||||
matchValue = "/signin/",
|
||||
matchingStrategy = AccessOptions.MatchingStrategy.CONTAINS_CASE_INSENSITIVE,
|
||||
usernameViewId = "userid",
|
||||
),
|
||||
),
|
||||
),
|
||||
KnownUsernameField(
|
||||
uriAuthority = "signin.ebay.de",
|
||||
accessOptions = listOf(
|
||||
AccessOptions(
|
||||
matchValue = "eBayISAPI.dll",
|
||||
matchingStrategy = AccessOptions.MatchingStrategy.ENDS_WITH_CASE_INSENSITIVE,
|
||||
usernameViewId = "userid",
|
||||
),
|
||||
AccessOptions(
|
||||
matchValue = "/signin/",
|
||||
matchingStrategy = AccessOptions.MatchingStrategy.CONTAINS_CASE_INSENSITIVE,
|
||||
usernameViewId = "userid",
|
||||
),
|
||||
),
|
||||
),
|
||||
KnownUsernameField(
|
||||
uriAuthority = "signin.ebay.es",
|
||||
accessOptions = listOf(
|
||||
AccessOptions(
|
||||
matchValue = "eBayISAPI.dll",
|
||||
matchingStrategy = AccessOptions.MatchingStrategy.ENDS_WITH_CASE_INSENSITIVE,
|
||||
usernameViewId = "userid",
|
||||
),
|
||||
AccessOptions(
|
||||
matchValue = "/signin/",
|
||||
matchingStrategy = AccessOptions.MatchingStrategy.CONTAINS_CASE_INSENSITIVE,
|
||||
usernameViewId = "userid",
|
||||
),
|
||||
),
|
||||
),
|
||||
KnownUsernameField(
|
||||
uriAuthority = "signin.ebay.fr",
|
||||
accessOptions = listOf(
|
||||
AccessOptions(
|
||||
matchValue = "eBayISAPI.dll",
|
||||
matchingStrategy = AccessOptions.MatchingStrategy.ENDS_WITH_CASE_INSENSITIVE,
|
||||
usernameViewId = "userid",
|
||||
),
|
||||
AccessOptions(
|
||||
matchValue = "/signin/",
|
||||
matchingStrategy = AccessOptions.MatchingStrategy.CONTAINS_CASE_INSENSITIVE,
|
||||
usernameViewId = "userid",
|
||||
),
|
||||
),
|
||||
),
|
||||
KnownUsernameField(
|
||||
uriAuthority = "signin.ebay.ie",
|
||||
accessOptions = listOf(
|
||||
AccessOptions(
|
||||
matchValue = "eBayISAPI.dll",
|
||||
matchingStrategy = AccessOptions.MatchingStrategy.ENDS_WITH_CASE_INSENSITIVE,
|
||||
usernameViewId = "userid",
|
||||
),
|
||||
AccessOptions(
|
||||
matchValue = "/signin/",
|
||||
matchingStrategy = AccessOptions.MatchingStrategy.CONTAINS_CASE_INSENSITIVE,
|
||||
usernameViewId = "userid",
|
||||
),
|
||||
),
|
||||
),
|
||||
KnownUsernameField(
|
||||
uriAuthority = "signin.ebay.in",
|
||||
accessOptions = listOf(
|
||||
AccessOptions(
|
||||
matchValue = "eBayISAPI.dll",
|
||||
matchingStrategy = AccessOptions.MatchingStrategy.ENDS_WITH_CASE_INSENSITIVE,
|
||||
usernameViewId = "userid",
|
||||
),
|
||||
AccessOptions(
|
||||
matchValue = "/signin/",
|
||||
matchingStrategy = AccessOptions.MatchingStrategy.CONTAINS_CASE_INSENSITIVE,
|
||||
usernameViewId = "userid",
|
||||
),
|
||||
),
|
||||
),
|
||||
KnownUsernameField(
|
||||
uriAuthority = "signin.ebay.it",
|
||||
accessOptions = listOf(
|
||||
AccessOptions(
|
||||
matchValue = "eBayISAPI.dll",
|
||||
matchingStrategy = AccessOptions.MatchingStrategy.ENDS_WITH_CASE_INSENSITIVE,
|
||||
usernameViewId = "userid",
|
||||
),
|
||||
AccessOptions(
|
||||
matchValue = "/signin/",
|
||||
matchingStrategy = AccessOptions.MatchingStrategy.CONTAINS_CASE_INSENSITIVE,
|
||||
usernameViewId = "userid",
|
||||
),
|
||||
),
|
||||
),
|
||||
KnownUsernameField(
|
||||
uriAuthority = "signin.ebay.nl",
|
||||
accessOptions = listOf(
|
||||
AccessOptions(
|
||||
matchValue = "eBayISAPI.dll",
|
||||
matchingStrategy = AccessOptions.MatchingStrategy.ENDS_WITH_CASE_INSENSITIVE,
|
||||
usernameViewId = "userid",
|
||||
),
|
||||
AccessOptions(
|
||||
matchValue = "/signin/",
|
||||
matchingStrategy = AccessOptions.MatchingStrategy.CONTAINS_CASE_INSENSITIVE,
|
||||
usernameViewId = "userid",
|
||||
),
|
||||
),
|
||||
),
|
||||
KnownUsernameField(
|
||||
uriAuthority = "signin.ebay.ph",
|
||||
accessOptions = listOf(
|
||||
AccessOptions(
|
||||
matchValue = "eBayISAPI.dll",
|
||||
matchingStrategy = AccessOptions.MatchingStrategy.ENDS_WITH_CASE_INSENSITIVE,
|
||||
usernameViewId = "userid",
|
||||
),
|
||||
AccessOptions(
|
||||
matchValue = "/signin/",
|
||||
matchingStrategy = AccessOptions.MatchingStrategy.CONTAINS_CASE_INSENSITIVE,
|
||||
usernameViewId = "userid",
|
||||
),
|
||||
),
|
||||
),
|
||||
KnownUsernameField(
|
||||
uriAuthority = "signin.ebay.pl",
|
||||
accessOptions = listOf(
|
||||
AccessOptions(
|
||||
matchValue = "eBayISAPI.dll",
|
||||
matchingStrategy = AccessOptions.MatchingStrategy.ENDS_WITH_CASE_INSENSITIVE,
|
||||
usernameViewId = "userid",
|
||||
),
|
||||
AccessOptions(
|
||||
matchValue = "/signin/",
|
||||
matchingStrategy = AccessOptions.MatchingStrategy.CONTAINS_CASE_INSENSITIVE,
|
||||
usernameViewId = "userid",
|
||||
),
|
||||
),
|
||||
),
|
||||
KnownUsernameField(
|
||||
uriAuthority = "accounts.google.com",
|
||||
accessOptions = listOf(
|
||||
AccessOptions(matchValue = "identifier", usernameViewId = "identifierId"),
|
||||
AccessOptions(matchValue = "ServiceLogin", usernameViewId = "Email"),
|
||||
),
|
||||
),
|
||||
KnownUsernameField(
|
||||
uriAuthority = "paypal.com",
|
||||
accessOptions = listOf(
|
||||
AccessOptions(matchValue = "signin", usernameViewId = "email"),
|
||||
AccessOptions(
|
||||
matchValue = "/connect/",
|
||||
matchingStrategy = AccessOptions.MatchingStrategy.CONTAINS_CASE_SENSITIVE,
|
||||
usernameViewId = "email",
|
||||
),
|
||||
),
|
||||
),
|
||||
KnownUsernameField(
|
||||
uriAuthority = "tumblr.com",
|
||||
accessOption = AccessOptions(
|
||||
matchValue = "login",
|
||||
usernameViewId = "signup_determine_email",
|
||||
),
|
||||
),
|
||||
KnownUsernameField(
|
||||
uriAuthority = "passport.yandex.az",
|
||||
accessOption = AccessOptions(matchValue = "auth", usernameViewId = "passp-field-login"),
|
||||
),
|
||||
KnownUsernameField(
|
||||
uriAuthority = "passport.yandex.by",
|
||||
accessOption = AccessOptions(matchValue = "auth", usernameViewId = "passp-field-login"),
|
||||
),
|
||||
KnownUsernameField(
|
||||
uriAuthority = "passport.yandex.co.il",
|
||||
accessOption = AccessOptions(matchValue = "auth", usernameViewId = "passp-field-login"),
|
||||
),
|
||||
KnownUsernameField(
|
||||
uriAuthority = "passport.yandex.com",
|
||||
accessOption = AccessOptions(matchValue = "auth", usernameViewId = "passp-field-login"),
|
||||
),
|
||||
KnownUsernameField(
|
||||
uriAuthority = "passport.yandex.com.am",
|
||||
accessOption = AccessOptions(matchValue = "auth", usernameViewId = "passp-field-login"),
|
||||
),
|
||||
KnownUsernameField(
|
||||
uriAuthority = "passport.yandex.com.ge",
|
||||
accessOption = AccessOptions(matchValue = "auth", usernameViewId = "passp-field-login"),
|
||||
),
|
||||
KnownUsernameField(
|
||||
uriAuthority = "passport.yandex.com.tr",
|
||||
accessOption = AccessOptions(matchValue = "auth", usernameViewId = "passp-field-login"),
|
||||
),
|
||||
KnownUsernameField(
|
||||
uriAuthority = "passport.yandex.ee",
|
||||
accessOption = AccessOptions(matchValue = "auth", usernameViewId = "passp-field-login"),
|
||||
),
|
||||
KnownUsernameField(
|
||||
uriAuthority = "passport.yandex.fi",
|
||||
accessOption = AccessOptions(matchValue = "auth", usernameViewId = "passp-field-login"),
|
||||
),
|
||||
KnownUsernameField(
|
||||
uriAuthority = "passport.yandex.fr",
|
||||
accessOption = AccessOptions(matchValue = "auth", usernameViewId = "passp-field-login"),
|
||||
),
|
||||
KnownUsernameField(
|
||||
uriAuthority = "passport.yandex.kg",
|
||||
accessOption = AccessOptions(matchValue = "auth", usernameViewId = "passp-field-login"),
|
||||
),
|
||||
KnownUsernameField(
|
||||
uriAuthority = "passport.yandex.kz",
|
||||
accessOption = AccessOptions(matchValue = "auth", usernameViewId = "passp-field-login"),
|
||||
),
|
||||
KnownUsernameField(
|
||||
uriAuthority = "passport.yandex.lt",
|
||||
accessOption = AccessOptions(matchValue = "auth", usernameViewId = "passp-field-login"),
|
||||
),
|
||||
KnownUsernameField(
|
||||
uriAuthority = "passport.yandex.lv",
|
||||
accessOption = AccessOptions(matchValue = "auth", usernameViewId = "passp-field-login"),
|
||||
),
|
||||
KnownUsernameField(
|
||||
uriAuthority = "passport.yandex.md",
|
||||
accessOption = AccessOptions(matchValue = "auth", usernameViewId = "passp-field-login"),
|
||||
),
|
||||
KnownUsernameField(
|
||||
uriAuthority = "passport.yandex.pl",
|
||||
accessOption = AccessOptions(matchValue = "auth", usernameViewId = "passp-field-login"),
|
||||
),
|
||||
KnownUsernameField(
|
||||
uriAuthority = "passport.yandex.ru",
|
||||
accessOption = AccessOptions(matchValue = "auth", usernameViewId = "passp-field-login"),
|
||||
),
|
||||
KnownUsernameField(
|
||||
uriAuthority = "passport.yandex.tj",
|
||||
accessOption = AccessOptions(matchValue = "auth", usernameViewId = "passp-field-login"),
|
||||
),
|
||||
KnownUsernameField(
|
||||
uriAuthority = "passport.yandex.tm",
|
||||
accessOption = AccessOptions(matchValue = "auth", usernameViewId = "passp-field-login"),
|
||||
),
|
||||
KnownUsernameField(
|
||||
uriAuthority = "passport.yandex.ua",
|
||||
accessOption = AccessOptions(matchValue = "auth", usernameViewId = "passp-field-login"),
|
||||
),
|
||||
KnownUsernameField(
|
||||
uriAuthority = "passport.yandex.uz",
|
||||
accessOption = AccessOptions(matchValue = "auth", usernameViewId = "passp-field-login"),
|
||||
),
|
||||
// SECTION B ——— Top 100 worldwide
|
||||
// As of July 2020, all entries that needed to be added from
|
||||
// Top 100 (SimilarWeb, 2019) and Top 50 (Alexa Internet, 2020)
|
||||
// matched section A.
|
||||
// Therefore, no entry currently.
|
||||
|
||||
// SECTION C ——— Top 20 for selected countries
|
||||
// For these selected countries, the Top 20 (SimilarWeb, 2020)
|
||||
// and the Top 20 (Alexa Internet, 2020) are covered.
|
||||
// Mobile and desktop versions supported.
|
||||
// Could not be added, however:
|
||||
// web sites/applications that don't use an "id" attribute for their login field.
|
||||
|
||||
KnownUsernameField(
|
||||
uriAuthority = "cfg.smt.docomo.ne.jp",
|
||||
accessOption = AccessOptions(
|
||||
matchValue = "/auth/",
|
||||
matchingStrategy = AccessOptions.MatchingStrategy.CONTAINS_CASE_SENSITIVE,
|
||||
usernameViewId = "Di_Uid",
|
||||
),
|
||||
),
|
||||
KnownUsernameField(
|
||||
uriAuthority = "id.smt.docomo.ne.jp",
|
||||
accessOption = AccessOptions(
|
||||
matchValue = "/cgi7/",
|
||||
matchingStrategy = AccessOptions.MatchingStrategy.CONTAINS_CASE_SENSITIVE,
|
||||
usernameViewId = "Di_Uid",
|
||||
),
|
||||
),
|
||||
// SECTION D ——— Miscellaneous
|
||||
// No entry, currently.
|
||||
|
||||
// SECTION Z ——— Special forms
|
||||
// Despite "user ID + password" fields both visible, detection rules required.
|
||||
// No entry, currently.
|
||||
|
||||
// Test/example purposes only
|
||||
// GitHub is a VERY special case (signup form, just to test the proper functioning
|
||||
// of special forms).
|
||||
KnownUsernameField(
|
||||
uriAuthority = "github.com",
|
||||
accessOption = AccessOptions(matchValue = "", usernameViewId = "user[login]-footer"),
|
||||
),
|
||||
)
|
||||
@@ -0,0 +1,16 @@
|
||||
package com.x8bit.bitwarden.data.autofill.accessibility.util
|
||||
|
||||
import android.net.Uri
|
||||
import com.x8bit.bitwarden.data.platform.annotation.OmitFromCoverage
|
||||
import java.net.URISyntaxException
|
||||
|
||||
/**
|
||||
* Attempts to parse a [Uri] from a string and returns null if an error occurs.
|
||||
*/
|
||||
@OmitFromCoverage
|
||||
fun String.toUriOrNull(): Uri? =
|
||||
try {
|
||||
Uri.parse(this)
|
||||
} catch (e: URISyntaxException) {
|
||||
null
|
||||
}
|
||||
@@ -13,6 +13,8 @@ import com.x8bit.bitwarden.data.autofill.manager.AutofillCompletionManager
|
||||
import com.x8bit.bitwarden.data.autofill.manager.AutofillCompletionManagerImpl
|
||||
import com.x8bit.bitwarden.data.autofill.manager.AutofillEnabledManager
|
||||
import com.x8bit.bitwarden.data.autofill.manager.AutofillEnabledManagerImpl
|
||||
import com.x8bit.bitwarden.data.autofill.manager.AutofillTotpManager
|
||||
import com.x8bit.bitwarden.data.autofill.manager.AutofillTotpManagerImpl
|
||||
import com.x8bit.bitwarden.data.autofill.parser.AutofillParser
|
||||
import com.x8bit.bitwarden.data.autofill.parser.AutofillParserImpl
|
||||
import com.x8bit.bitwarden.data.autofill.processor.AutofillProcessor
|
||||
@@ -32,6 +34,7 @@ import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import java.time.Clock
|
||||
import javax.inject.Singleton
|
||||
|
||||
/**
|
||||
@@ -56,21 +59,15 @@ object AutofillModule {
|
||||
@Provides
|
||||
fun provideAutofillCompletionManager(
|
||||
autofillParser: AutofillParser,
|
||||
authRepository: AuthRepository,
|
||||
clipboardManager: BitwardenClipboardManager,
|
||||
dispatcherManager: DispatcherManager,
|
||||
settingsRepository: SettingsRepository,
|
||||
vaultRepository: VaultRepository,
|
||||
organizationEventManager: OrganizationEventManager,
|
||||
totpManager: AutofillTotpManager,
|
||||
): AutofillCompletionManager =
|
||||
AutofillCompletionManagerImpl(
|
||||
authRepository = authRepository,
|
||||
autofillParser = autofillParser,
|
||||
clipboardManager = clipboardManager,
|
||||
dispatcherManager = dispatcherManager,
|
||||
settingsRepository = settingsRepository,
|
||||
vaultRepository = vaultRepository,
|
||||
organizationEventManager = organizationEventManager,
|
||||
totpManager = totpManager,
|
||||
)
|
||||
|
||||
@Singleton
|
||||
@@ -82,6 +79,25 @@ object AutofillModule {
|
||||
settingsRepository = settingsRepository,
|
||||
)
|
||||
|
||||
@Singleton
|
||||
@Provides
|
||||
fun providesAutofillTotpManager(
|
||||
@ApplicationContext context: Context,
|
||||
clock: Clock,
|
||||
clipboardManager: BitwardenClipboardManager,
|
||||
authRepository: AuthRepository,
|
||||
settingsRepository: SettingsRepository,
|
||||
vaultRepository: VaultRepository,
|
||||
): AutofillTotpManager =
|
||||
AutofillTotpManagerImpl(
|
||||
context = context,
|
||||
clock = clock,
|
||||
clipboardManager = clipboardManager,
|
||||
authRepository = authRepository,
|
||||
settingsRepository = settingsRepository,
|
||||
vaultRepository = vaultRepository,
|
||||
)
|
||||
|
||||
@Singleton
|
||||
@Provides
|
||||
fun providesAutofillCipherProvider(
|
||||
|
||||
@@ -21,6 +21,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
|
||||
|
||||
/**
|
||||
@@ -41,6 +42,7 @@ object Fido2ProviderModule {
|
||||
fido2CredentialManager: Fido2CredentialManager,
|
||||
dispatcherManager: DispatcherManager,
|
||||
intentManager: IntentManager,
|
||||
clock: Clock,
|
||||
): Fido2ProviderProcessor =
|
||||
Fido2ProviderProcessorImpl(
|
||||
context,
|
||||
@@ -49,6 +51,7 @@ object Fido2ProviderModule {
|
||||
fido2CredentialStore,
|
||||
fido2CredentialManager,
|
||||
intentManager,
|
||||
clock,
|
||||
dispatcherManager,
|
||||
)
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.x8bit.bitwarden.data.autofill.fido2.manager
|
||||
|
||||
import androidx.credentials.provider.CallingAppInfo
|
||||
import com.bitwarden.vault.CipherView
|
||||
import com.x8bit.bitwarden.data.autofill.fido2.model.Fido2CredentialAssertionRequest
|
||||
import com.x8bit.bitwarden.data.autofill.fido2.model.Fido2CredentialAssertionResult
|
||||
@@ -26,10 +27,11 @@ interface Fido2CredentialManager {
|
||||
var authenticationAttempts: Int
|
||||
|
||||
/**
|
||||
* Attempt to validate the RP and origin of the provided [fido2CredentialRequest].
|
||||
* Attempt to validate the RP and origin of the provided [callingAppInfo] and [relyingPartyId].
|
||||
*/
|
||||
suspend fun validateOrigin(
|
||||
fido2CredentialRequest: Fido2CredentialRequest,
|
||||
callingAppInfo: CallingAppInfo,
|
||||
relyingPartyId: String,
|
||||
): Fido2ValidateOriginResult
|
||||
|
||||
/**
|
||||
|
||||
@@ -14,9 +14,7 @@ import com.x8bit.bitwarden.data.autofill.fido2.model.Fido2ValidateOriginResult
|
||||
import com.x8bit.bitwarden.data.autofill.fido2.model.PasskeyAssertionOptions
|
||||
import com.x8bit.bitwarden.data.autofill.fido2.model.PasskeyAttestationOptions
|
||||
import com.x8bit.bitwarden.data.platform.manager.AssetManager
|
||||
import com.x8bit.bitwarden.data.platform.util.asFailure
|
||||
import com.x8bit.bitwarden.data.platform.util.asSuccess
|
||||
import com.x8bit.bitwarden.data.platform.util.flatMap
|
||||
import com.x8bit.bitwarden.data.platform.util.decodeFromStringOrNull
|
||||
import com.x8bit.bitwarden.data.platform.util.getAppOrigin
|
||||
import com.x8bit.bitwarden.data.platform.util.getAppSigningSignatureFingerprint
|
||||
import com.x8bit.bitwarden.data.platform.util.getSignatureFingerprintAsHexString
|
||||
@@ -55,18 +53,18 @@ class Fido2CredentialManagerImpl(
|
||||
selectedCipherView: CipherView,
|
||||
): Fido2RegisterCredentialResult {
|
||||
val clientData = if (fido2CredentialRequest.callingAppInfo.isOriginPopulated()) {
|
||||
fido2CredentialRequest
|
||||
fido2CredentialRequest
|
||||
.callingAppInfo
|
||||
.getAppSigningSignatureFingerprint()
|
||||
?.let { ClientData.DefaultWithCustomHash(hash = it) }
|
||||
?: return Fido2RegisterCredentialResult.Error
|
||||
} else {
|
||||
ClientData.DefaultWithExtraData(
|
||||
androidPackageName = fido2CredentialRequest
|
||||
.callingAppInfo
|
||||
.getAppSigningSignatureFingerprint()
|
||||
?.let { ClientData.DefaultWithCustomHash(hash = it) }
|
||||
?: return Fido2RegisterCredentialResult.Error
|
||||
} else {
|
||||
ClientData.DefaultWithExtraData(
|
||||
androidPackageName = fido2CredentialRequest
|
||||
.callingAppInfo
|
||||
.packageName,
|
||||
)
|
||||
}
|
||||
.packageName,
|
||||
)
|
||||
}
|
||||
val origin = fido2CredentialRequest
|
||||
.origin
|
||||
?: getOriginUrlFromAttestationOptionsOrNull(fido2CredentialRequest.requestJson)
|
||||
@@ -95,13 +93,13 @@ class Fido2CredentialManagerImpl(
|
||||
}
|
||||
|
||||
override suspend fun validateOrigin(
|
||||
fido2CredentialRequest: Fido2CredentialRequest,
|
||||
callingAppInfo: CallingAppInfo,
|
||||
relyingPartyId: String,
|
||||
): Fido2ValidateOriginResult {
|
||||
val callingAppInfo = fido2CredentialRequest.callingAppInfo
|
||||
return if (callingAppInfo.isOriginPopulated()) {
|
||||
validatePrivilegedAppOrigin(callingAppInfo)
|
||||
} else {
|
||||
validateCallingApplicationAssetLinks(fido2CredentialRequest)
|
||||
validateCallingApplicationAssetLinks(callingAppInfo, relyingPartyId)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -136,40 +134,52 @@ class Fido2CredentialManagerImpl(
|
||||
val clientData = request.clientDataHash
|
||||
?.let { ClientData.DefaultWithCustomHash(hash = it) }
|
||||
?: ClientData.DefaultWithExtraData(androidPackageName = callingAppInfo.getAppOrigin())
|
||||
val origin = request.origin
|
||||
val origin = callingAppInfo.origin
|
||||
?: getOriginUrlFromAssertionOptionsOrNull(request.requestJson)
|
||||
?: return Fido2CredentialAssertionResult.Error
|
||||
val relyingPartyId = json
|
||||
.decodeFromStringOrNull<PasskeyAssertionOptions>(request.requestJson)
|
||||
?.relyingPartyId
|
||||
?: return Fido2CredentialAssertionResult.Error
|
||||
|
||||
return vaultSdkSource
|
||||
.authenticateFido2Credential(
|
||||
request = AuthenticateFido2CredentialRequest(
|
||||
userId = userId,
|
||||
origin = origin,
|
||||
requestJson = """{"publicKey": ${request.requestJson}}""",
|
||||
clientData = clientData,
|
||||
selectedCipherView = selectedCipherView,
|
||||
isUserVerificationSupported = true,
|
||||
),
|
||||
fido2CredentialStore = this,
|
||||
)
|
||||
.map { it.toAndroidFido2PublicKeyCredential() }
|
||||
.mapCatching { json.encodeToString(it) }
|
||||
.fold(
|
||||
onSuccess = { Fido2CredentialAssertionResult.Success(it) },
|
||||
onFailure = { Fido2CredentialAssertionResult.Error },
|
||||
)
|
||||
val validateOriginResult = validateOrigin(
|
||||
callingAppInfo = callingAppInfo,
|
||||
relyingPartyId = relyingPartyId,
|
||||
)
|
||||
|
||||
return when (validateOriginResult) {
|
||||
is Fido2ValidateOriginResult.Error -> {
|
||||
Fido2CredentialAssertionResult.Error
|
||||
}
|
||||
|
||||
Fido2ValidateOriginResult.Success -> {
|
||||
vaultSdkSource
|
||||
.authenticateFido2Credential(
|
||||
request = AuthenticateFido2CredentialRequest(
|
||||
userId = userId,
|
||||
origin = origin,
|
||||
requestJson = """{"publicKey": ${request.requestJson}}""",
|
||||
clientData = clientData,
|
||||
selectedCipherView = selectedCipherView,
|
||||
isUserVerificationSupported = true,
|
||||
),
|
||||
fido2CredentialStore = this,
|
||||
)
|
||||
.map { it.toAndroidFido2PublicKeyCredential() }
|
||||
.mapCatching { json.encodeToString(it) }
|
||||
.fold(
|
||||
onSuccess = { Fido2CredentialAssertionResult.Success(it) },
|
||||
onFailure = { Fido2CredentialAssertionResult.Error },
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun validateCallingApplicationAssetLinks(
|
||||
fido2CredentialRequest: Fido2CredentialRequest,
|
||||
callingAppInfo: CallingAppInfo,
|
||||
relyingPartyId: String,
|
||||
): Fido2ValidateOriginResult {
|
||||
val callingAppInfo = fido2CredentialRequest.callingAppInfo
|
||||
return fido2CredentialRequest
|
||||
.requestJson
|
||||
.getRpId(json)
|
||||
.flatMap { rpId ->
|
||||
digitalAssetLinkService.getDigitalAssetLinkForRp(relyingParty = rpId)
|
||||
}
|
||||
return digitalAssetLinkService.getDigitalAssetLinkForRp(relyingParty = relyingPartyId)
|
||||
.onFailure {
|
||||
return Fido2ValidateOriginResult.Error.AssetLinkNotFound
|
||||
}
|
||||
@@ -247,20 +257,6 @@ class Fido2CredentialManagerImpl(
|
||||
}
|
||||
.takeUnless { it.isEmpty() }
|
||||
|
||||
private fun String.getRpId(json: Json): Result<String> {
|
||||
return try {
|
||||
json
|
||||
.decodeFromString<PasskeyAttestationOptions>(this)
|
||||
.relyingParty
|
||||
.id
|
||||
.asSuccess()
|
||||
} catch (e: SerializationException) {
|
||||
e.asFailure()
|
||||
} catch (e: IllegalArgumentException) {
|
||||
e.asFailure()
|
||||
}
|
||||
}
|
||||
|
||||
override fun hasAuthenticationAttemptsRemaining(): Boolean =
|
||||
authenticationAttempts < MAX_AUTHENTICATION_ATTEMPTS
|
||||
|
||||
|
||||
@@ -38,6 +38,7 @@ import com.x8bit.bitwarden.data.vault.repository.model.DecryptFido2CredentialAut
|
||||
import com.x8bit.bitwarden.ui.platform.manager.intent.IntentManager
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
import java.time.Clock
|
||||
import java.util.concurrent.atomic.AtomicInteger
|
||||
|
||||
private const val CREATE_PASSKEY_INTENT = "com.x8bit.bitwarden.fido2.ACTION_CREATE_PASSKEY"
|
||||
@@ -57,6 +58,7 @@ class Fido2ProviderProcessorImpl(
|
||||
private val fido2CredentialStore: Fido2CredentialStore,
|
||||
private val fido2CredentialManager: Fido2CredentialManager,
|
||||
private val intentManager: IntentManager,
|
||||
private val clock: Clock,
|
||||
dispatcherManager: DispatcherManager,
|
||||
) : Fido2ProviderProcessor {
|
||||
|
||||
@@ -111,13 +113,14 @@ class Fido2ProviderProcessorImpl(
|
||||
val userState = authRepository.userStateFlow.value ?: return null
|
||||
|
||||
return BeginCreateCredentialResponse.Builder()
|
||||
.setCreateEntries(userState.accounts.toCreateEntries())
|
||||
.setCreateEntries(userState.accounts.toCreateEntries(userState.activeUserId))
|
||||
.build()
|
||||
}
|
||||
|
||||
private fun List<UserState.Account>.toCreateEntries() = map { it.toCreateEntry() }
|
||||
private fun List<UserState.Account>.toCreateEntries(activeUserId: String) =
|
||||
map { it.toCreateEntry(isActive = activeUserId == it.userId) }
|
||||
|
||||
private fun UserState.Account.toCreateEntry(): CreateEntry {
|
||||
private fun UserState.Account.toCreateEntry(isActive: Boolean): CreateEntry {
|
||||
val accountName = name ?: email
|
||||
return CreateEntry
|
||||
.Builder(
|
||||
@@ -134,6 +137,9 @@ class Fido2ProviderProcessorImpl(
|
||||
accountName,
|
||||
),
|
||||
)
|
||||
// Set the last used time to "now" so the active account is the default option in the
|
||||
// system prompt.
|
||||
.setLastUsedTime(if (isActive) clock.instant() else null)
|
||||
.build()
|
||||
}
|
||||
|
||||
|
||||
@@ -2,11 +2,7 @@ package com.x8bit.bitwarden.data.autofill.manager
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Intent
|
||||
import android.widget.Toast
|
||||
import com.bitwarden.core.DateTime
|
||||
import com.bitwarden.vault.CipherView
|
||||
import com.x8bit.bitwarden.R
|
||||
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
|
||||
import com.x8bit.bitwarden.data.autofill.builder.FilledDataBuilder
|
||||
import com.x8bit.bitwarden.data.autofill.builder.FilledDataBuilderImpl
|
||||
import com.x8bit.bitwarden.data.autofill.model.AutofillRequest
|
||||
@@ -16,13 +12,9 @@ import com.x8bit.bitwarden.data.autofill.util.createAutofillSelectionResultInten
|
||||
import com.x8bit.bitwarden.data.autofill.util.getAutofillAssistStructureOrNull
|
||||
import com.x8bit.bitwarden.data.autofill.util.toAutofillAppInfo
|
||||
import com.x8bit.bitwarden.data.autofill.util.toAutofillCipherProvider
|
||||
import com.x8bit.bitwarden.data.platform.manager.clipboard.BitwardenClipboardManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.dispatcher.DispatcherManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.event.OrganizationEventManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.model.OrganizationEvent
|
||||
import com.x8bit.bitwarden.data.platform.repository.SettingsRepository
|
||||
import com.x8bit.bitwarden.data.vault.repository.VaultRepository
|
||||
import com.x8bit.bitwarden.data.vault.repository.model.GenerateTotpResult
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@@ -31,15 +23,12 @@ import kotlinx.coroutines.launch
|
||||
*/
|
||||
@Suppress("LongParameterList")
|
||||
class AutofillCompletionManagerImpl(
|
||||
private val authRepository: AuthRepository,
|
||||
private val autofillParser: AutofillParser,
|
||||
private val clipboardManager: BitwardenClipboardManager,
|
||||
dispatcherManager: DispatcherManager,
|
||||
private val filledDataBuilderProvider: (CipherView) -> FilledDataBuilder =
|
||||
{ createSingleItemFilledDataBuilder(cipherView = it) },
|
||||
private val settingsRepository: SettingsRepository,
|
||||
private val vaultRepository: VaultRepository,
|
||||
private val organizationEventManager: OrganizationEventManager,
|
||||
private val totpManager: AutofillTotpManager,
|
||||
) : AutofillCompletionManager {
|
||||
private val mainScope = CoroutineScope(dispatcherManager.main)
|
||||
|
||||
@@ -82,10 +71,7 @@ class AutofillCompletionManagerImpl(
|
||||
activity.cancelAndFinish()
|
||||
return@launch
|
||||
}
|
||||
tryCopyTotpToClipboard(
|
||||
activity = activity,
|
||||
cipherView = cipherView,
|
||||
)
|
||||
totpManager.tryCopyTotpToClipboard(cipherView = cipherView)
|
||||
val resultIntent = createAutofillSelectionResultIntent(dataset)
|
||||
activity.setResultAndFinish(resultIntent = resultIntent)
|
||||
cipherView.id?.let {
|
||||
@@ -95,40 +81,6 @@ class AutofillCompletionManagerImpl(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempt to copy the totp code to clipboard. If it succeeds show a toast.
|
||||
*
|
||||
* @param activity An activity for launching a toast.
|
||||
* @param cipherView The [CipherView] for which to generate a TOTP code.
|
||||
*/
|
||||
private suspend fun tryCopyTotpToClipboard(
|
||||
activity: Activity,
|
||||
cipherView: CipherView,
|
||||
) {
|
||||
val isPremium = authRepository.userStateFlow.value?.activeAccount?.isPremium == true
|
||||
val totpAvailableViaPremiumOrOrganization = isPremium || cipherView.organizationUseTotp
|
||||
val totpCode = cipherView.login?.totp
|
||||
val isTotpDisabled = settingsRepository.isAutoCopyTotpDisabled
|
||||
|
||||
if (!isTotpDisabled && totpAvailableViaPremiumOrOrganization && totpCode != null) {
|
||||
val totpResult = vaultRepository.generateTotp(
|
||||
time = DateTime.now(),
|
||||
totpCode = totpCode,
|
||||
)
|
||||
|
||||
if (totpResult is GenerateTotpResult.Success) {
|
||||
clipboardManager.setText(totpResult.code)
|
||||
Toast
|
||||
.makeText(
|
||||
activity.applicationContext,
|
||||
R.string.verification_code_totp,
|
||||
Toast.LENGTH_LONG,
|
||||
)
|
||||
.show()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun createSingleItemFilledDataBuilder(
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
package com.x8bit.bitwarden.data.autofill.manager
|
||||
|
||||
import com.bitwarden.vault.CipherView
|
||||
|
||||
/**
|
||||
* Manages copying the totp code to the clipboard for autofill.
|
||||
*/
|
||||
interface AutofillTotpManager {
|
||||
/**
|
||||
* Attempt to copy the totp code to clipboard. If it succeeds show a toast.
|
||||
*/
|
||||
suspend fun tryCopyTotpToClipboard(cipherView: CipherView)
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user