mirror of
https://github.com/bitwarden/android.git
synced 2026-05-11 22:31:17 -05:00
Compare commits
139 Commits
v2024.7.1
...
pm-6702/ad
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1d05f5f758 | ||
|
|
bd55b9ce72 | ||
|
|
4726cb743a | ||
|
|
244d259804 | ||
|
|
eab94dde79 | ||
|
|
2bb921b592 | ||
|
|
18b58e75f8 | ||
|
|
e2cd3867dd | ||
|
|
524b9e9a08 | ||
|
|
4b35484abb | ||
|
|
d305dc3081 | ||
|
|
dde90a251a | ||
|
|
516cd72f66 | ||
|
|
63884e8518 | ||
|
|
8a4d436f1f | ||
|
|
ab279e2264 | ||
|
|
2876d75a21 | ||
|
|
aaa0ce4ecd | ||
|
|
499bc20850 | ||
|
|
2bed4986a1 | ||
|
|
151b081161 | ||
|
|
e3371b7620 | ||
|
|
551f948644 | ||
|
|
4bd81782c8 | ||
|
|
4dbcec85bb | ||
|
|
5a0b1caecd | ||
|
|
2b13151bd1 | ||
|
|
5e643e11fd | ||
|
|
2789b1cc37 | ||
|
|
b7a47eb91e | ||
|
|
06f6f19255 | ||
|
|
e717183239 | ||
|
|
edb87202d2 | ||
|
|
9b808058f5 | ||
|
|
89589aa907 | ||
|
|
805fea630c | ||
|
|
145f8adf0c | ||
|
|
6bb5ef7417 | ||
|
|
722726882b | ||
|
|
9ed30d7913 | ||
|
|
6c5c0c7c03 | ||
|
|
a57a7e099c | ||
|
|
f17289a104 | ||
|
|
e598fe5714 | ||
|
|
be534f940b | ||
|
|
782b474e54 | ||
|
|
d8471b41ca | ||
|
|
9484eebc70 | ||
|
|
22dae88b42 | ||
|
|
23066769a1 | ||
|
|
59ba585048 | ||
|
|
6c50cbf558 | ||
|
|
0085388446 | ||
|
|
18cd66a34b | ||
|
|
a090000826 | ||
|
|
af82261fba | ||
|
|
b15371bfce | ||
|
|
1e5bee2917 | ||
|
|
3819916241 | ||
|
|
e7c69fc089 | ||
|
|
994a577600 | ||
|
|
02167024b1 | ||
|
|
f110687e76 | ||
|
|
abeb60e237 | ||
|
|
4c8164954d | ||
|
|
7f13822f15 | ||
|
|
31bf696e7e | ||
|
|
f46d12c7b1 | ||
|
|
ad240a9a19 | ||
|
|
93107ec6a3 | ||
|
|
056eb7fdd5 | ||
|
|
bbe50ae0ff | ||
|
|
aae7a6e895 | ||
|
|
32b260ca9f | ||
|
|
0612a5834a | ||
|
|
055fbc1277 | ||
|
|
d0edca67c5 | ||
|
|
d558f94e40 | ||
|
|
1f8d50e788 | ||
|
|
260b3bfb1b | ||
|
|
d5e0ebee12 | ||
|
|
6d22ee9550 | ||
|
|
82096e0625 | ||
|
|
646566edd8 | ||
|
|
b26e1a082e | ||
|
|
deb8f811e5 | ||
|
|
0e90bbb905 | ||
|
|
58a91c15aa | ||
|
|
1daddbc905 | ||
|
|
b6af48fb3b | ||
|
|
3ff70b4598 | ||
|
|
b0079fca5c | ||
|
|
39250e5cb4 | ||
|
|
74132de8ed | ||
|
|
a6bbde2bed | ||
|
|
544eabfaa3 | ||
|
|
680ebc2e47 | ||
|
|
b0f0c0f33b | ||
|
|
c09fe554bc | ||
|
|
5c2ac2e037 | ||
|
|
793971c3a3 | ||
|
|
8ffd14c2fb | ||
|
|
da3d834a91 | ||
|
|
b48837e13c | ||
|
|
b44a320dc8 | ||
|
|
d2432f7cf7 | ||
|
|
7cf7536857 | ||
|
|
779cd1356a | ||
|
|
05dc220303 | ||
|
|
21c1fa7131 | ||
|
|
2475bf5a41 | ||
|
|
62154f5261 | ||
|
|
0e44b21361 | ||
|
|
f3d28551b1 | ||
|
|
1927630acb | ||
|
|
975fa91d36 | ||
|
|
7218ca2477 | ||
|
|
ee87d8ada8 | ||
|
|
8a381d8682 | ||
|
|
1fdfbac7b7 | ||
|
|
7fbc6ea4f3 | ||
|
|
7ddbc99add | ||
|
|
4abf907dc5 | ||
|
|
9ffc0360bd | ||
|
|
c4365c0193 | ||
|
|
1ea1e7918b | ||
|
|
815e779475 | ||
|
|
d9f506dd8f | ||
|
|
4377921d20 | ||
|
|
775a73fe54 | ||
|
|
96324f01d7 | ||
|
|
7d18310f30 | ||
|
|
3d584c84f2 | ||
|
|
f1c486bf9a | ||
|
|
a5224c966c | ||
|
|
9b19c71d95 | ||
|
|
36270ec55a | ||
|
|
94781bc1a9 | ||
|
|
93cde9bfdc |
44
.github/workflows/build.yml
vendored
44
.github/workflows/build.yml
vendored
@@ -28,6 +28,7 @@ on:
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
JAVA_VERSION: 17
|
||||
GITHUB_ACTION_RUN_URL: "https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}"
|
||||
|
||||
jobs:
|
||||
build:
|
||||
@@ -39,7 +40,7 @@ jobs:
|
||||
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
|
||||
|
||||
- name: Validate Gradle wrapper
|
||||
uses: gradle/actions/wrapper-validation@dbbdc275be76ac10734476cc723d82dfe7ec6eda # v3.4.2
|
||||
uses: gradle/actions/wrapper-validation@d9c87d481d55275bb5441eef3fe0e46805f9ef70 # v3.5.0
|
||||
|
||||
- name: Cache Gradle files
|
||||
uses: actions/cache@0c45773b623bea8c8e75f6c82b208c3cf94ea4f9 # v4.0.2
|
||||
@@ -67,7 +68,7 @@ jobs:
|
||||
java-version: ${{ env.JAVA_VERSION }}
|
||||
|
||||
- name: Configure Ruby
|
||||
uses: ruby/setup-ruby@3a77c29278ae80936b4cb030fefc7d21c96c786f # v1.185.0
|
||||
uses: ruby/setup-ruby@a6e6f86333f0a2523ece813039b8b4be04560854 # v1.190.0
|
||||
with:
|
||||
bundler-cache: true
|
||||
|
||||
@@ -98,7 +99,7 @@ jobs:
|
||||
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
|
||||
|
||||
- name: Configure Ruby
|
||||
uses: ruby/setup-ruby@3a77c29278ae80936b4cb030fefc7d21c96c786f # v1.185.0
|
||||
uses: ruby/setup-ruby@a6e6f86333f0a2523ece813039b8b4be04560854 # v1.190.0
|
||||
with:
|
||||
bundler-cache: true
|
||||
|
||||
@@ -149,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@dbbdc275be76ac10734476cc723d82dfe7ec6eda # v3.4.2
|
||||
uses: gradle/actions/wrapper-validation@d9c87d481d55275bb5441eef3fe0e46805f9ef70 # v3.5.0
|
||||
|
||||
- name: Cache Gradle files
|
||||
uses: actions/cache@0c45773b623bea8c8e75f6c82b208c3cf94ea4f9 # v4.0.2
|
||||
@@ -236,7 +237,7 @@ jobs:
|
||||
|
||||
- name: Upload release Play Store .aab artifact
|
||||
if: ${{ (matrix.variant == 'prod') && (matrix.artifact == 'aab') }}
|
||||
uses: actions/upload-artifact@0b2256b8c012f0828dc542b3febcab082c67f72b # v4.3.4
|
||||
uses: actions/upload-artifact@89ef406dd8d7e03cfd12d9e0a4a378f454709029 # v4.3.5
|
||||
with:
|
||||
name: com.x8bit.bitwarden.aab
|
||||
path: app/build/outputs/bundle/standardRelease/com.x8bit.bitwarden-standard-release.aab
|
||||
@@ -244,7 +245,7 @@ jobs:
|
||||
|
||||
- name: Upload beta Play Store .aab artifact
|
||||
if: ${{ (matrix.variant == 'prod') && (matrix.artifact == 'aab') }}
|
||||
uses: actions/upload-artifact@0b2256b8c012f0828dc542b3febcab082c67f72b # v4.3.4
|
||||
uses: actions/upload-artifact@89ef406dd8d7e03cfd12d9e0a4a378f454709029 # v4.3.5
|
||||
with:
|
||||
name: com.x8bit.bitwarden.beta.aab
|
||||
path: app/build/outputs/bundle/standardBeta/com.x8bit.bitwarden-standard-beta.aab
|
||||
@@ -252,7 +253,7 @@ jobs:
|
||||
|
||||
- name: Upload release .apk artifact
|
||||
if: ${{ (matrix.variant == 'prod') && (matrix.artifact == 'apk') }}
|
||||
uses: actions/upload-artifact@0b2256b8c012f0828dc542b3febcab082c67f72b # v4.3.4
|
||||
uses: actions/upload-artifact@89ef406dd8d7e03cfd12d9e0a4a378f454709029 # v4.3.5
|
||||
with:
|
||||
name: com.x8bit.bitwarden.apk
|
||||
path: app/build/outputs/apk/standard/release/com.x8bit.bitwarden-standard-release.apk
|
||||
@@ -260,7 +261,7 @@ jobs:
|
||||
|
||||
- name: Upload beta .apk artifact
|
||||
if: ${{ (matrix.variant == 'prod') && (matrix.artifact == 'apk') }}
|
||||
uses: actions/upload-artifact@0b2256b8c012f0828dc542b3febcab082c67f72b # v4.3.4
|
||||
uses: actions/upload-artifact@89ef406dd8d7e03cfd12d9e0a4a378f454709029 # v4.3.5
|
||||
with:
|
||||
name: com.x8bit.bitwarden.beta.apk
|
||||
path: app/build/outputs/apk/standard/beta/com.x8bit.bitwarden-standard-beta.apk
|
||||
@@ -269,7 +270,7 @@ jobs:
|
||||
# When building variants other than 'prod'
|
||||
- name: Upload other .apk artifact
|
||||
if: ${{ (matrix.variant != 'prod') && (matrix.artifact == 'apk') }}
|
||||
uses: actions/upload-artifact@0b2256b8c012f0828dc542b3febcab082c67f72b # v4.3.4
|
||||
uses: actions/upload-artifact@89ef406dd8d7e03cfd12d9e0a4a378f454709029 # v4.3.5
|
||||
with:
|
||||
name: com.x8bit.bitwarden-${{ matrix.variant }}.apk
|
||||
path: app/build/outputs/apk/standard/debug/com.x8bit.bitwarden-standard-debug.apk
|
||||
@@ -307,7 +308,7 @@ jobs:
|
||||
|
||||
- name: Upload .apk SHA file for release
|
||||
if: ${{ (matrix.variant == 'prod') && (matrix.artifact == 'apk') }}
|
||||
uses: actions/upload-artifact@0b2256b8c012f0828dc542b3febcab082c67f72b # v4.3.4
|
||||
uses: actions/upload-artifact@89ef406dd8d7e03cfd12d9e0a4a378f454709029 # v4.3.5
|
||||
with:
|
||||
name: bw-android-apk-sha256.txt
|
||||
path: ./bw-android-apk-sha256.txt
|
||||
@@ -315,7 +316,7 @@ jobs:
|
||||
|
||||
- name: Upload .apk SHA file for beta
|
||||
if: ${{ (matrix.variant == 'prod') && (matrix.artifact == 'apk') }}
|
||||
uses: actions/upload-artifact@0b2256b8c012f0828dc542b3febcab082c67f72b # v4.3.4
|
||||
uses: actions/upload-artifact@89ef406dd8d7e03cfd12d9e0a4a378f454709029 # v4.3.5
|
||||
with:
|
||||
name: bw-android-beta-apk-sha256.txt
|
||||
path: ./bw-android-beta-apk-sha256.txt
|
||||
@@ -323,7 +324,7 @@ jobs:
|
||||
|
||||
- name: Upload .aab SHA file for release
|
||||
if: ${{ (matrix.variant == 'prod') && (matrix.artifact == 'aab') }}
|
||||
uses: actions/upload-artifact@0b2256b8c012f0828dc542b3febcab082c67f72b # v4.3.4
|
||||
uses: actions/upload-artifact@89ef406dd8d7e03cfd12d9e0a4a378f454709029 # v4.3.5
|
||||
with:
|
||||
name: bw-android-aab-sha256.txt
|
||||
path: ./bw-android-aab-sha256.txt
|
||||
@@ -331,7 +332,7 @@ jobs:
|
||||
|
||||
- name: Upload .aab SHA file for beta
|
||||
if: ${{ (matrix.variant == 'prod') && (matrix.artifact == 'aab') }}
|
||||
uses: actions/upload-artifact@0b2256b8c012f0828dc542b3febcab082c67f72b # v4.3.4
|
||||
uses: actions/upload-artifact@89ef406dd8d7e03cfd12d9e0a4a378f454709029 # v4.3.5
|
||||
with:
|
||||
name: bw-android-beta-aab-sha256.txt
|
||||
path: ./bw-android-beta-aab-sha256.txt
|
||||
@@ -339,7 +340,7 @@ jobs:
|
||||
|
||||
- name: Upload .apk SHA file for other
|
||||
if: ${{ (matrix.variant != 'prod') && (matrix.artifact == 'apk') }}
|
||||
uses: actions/upload-artifact@0b2256b8c012f0828dc542b3febcab082c67f72b # v4.3.4
|
||||
uses: actions/upload-artifact@89ef406dd8d7e03cfd12d9e0a4a378f454709029 # v4.3.5
|
||||
with:
|
||||
name: bw-android-${{ matrix.variant }}-apk-sha256.txt
|
||||
path: ./bw-android-${{ matrix.variant }}-apk-sha256.txt
|
||||
@@ -355,6 +356,7 @@ jobs:
|
||||
APP_PLAY_FIREBASE_CREDS_PATH: ${{ github.workspace }}/secrets/app_play_prod_firebase-creds.json
|
||||
run: |
|
||||
bundle exec fastlane distributeReleasePlayStoreToFirebase \
|
||||
actionUrl:${{ env.GITHUB_ACTION_RUN_URL }} \
|
||||
service_credentials_file:${{ env.APP_PLAY_FIREBASE_CREDS_PATH }}
|
||||
|
||||
- name: Publish beta artifacts to Firebase
|
||||
@@ -363,6 +365,7 @@ jobs:
|
||||
APP_PLAY_FIREBASE_CREDS_PATH: ${{ github.workspace }}/secrets/app_play_prod_firebase-creds.json
|
||||
run: |
|
||||
bundle exec fastlane distributeBetaPlayStoreToFirebase \
|
||||
actionUrl:${{ env.GITHUB_ACTION_RUN_URL }} \
|
||||
service_credentials_file:${{ env.APP_PLAY_FIREBASE_CREDS_PATH }}
|
||||
|
||||
- name: Verify Play Store credentials
|
||||
@@ -384,7 +387,7 @@ jobs:
|
||||
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
|
||||
|
||||
- name: Configure Ruby
|
||||
uses: ruby/setup-ruby@3a77c29278ae80936b4cb030fefc7d21c96c786f # v1.185.0
|
||||
uses: ruby/setup-ruby@a6e6f86333f0a2523ece813039b8b4be04560854 # v1.190.0
|
||||
with:
|
||||
bundler-cache: true
|
||||
|
||||
@@ -421,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@dbbdc275be76ac10734476cc723d82dfe7ec6eda # v3.4.2
|
||||
uses: gradle/actions/wrapper-validation@d9c87d481d55275bb5441eef3fe0e46805f9ef70 # v3.5.0
|
||||
|
||||
- name: Cache Gradle files
|
||||
uses: actions/cache@0c45773b623bea8c8e75f6c82b208c3cf94ea4f9 # v4.0.2
|
||||
@@ -479,7 +482,7 @@ jobs:
|
||||
keyPassword:"${{ env.FDROID_BETA_KEY_PASSWORD }}"
|
||||
|
||||
- name: Upload F-Droid .apk artifact
|
||||
uses: actions/upload-artifact@0b2256b8c012f0828dc542b3febcab082c67f72b # v4.3.4
|
||||
uses: actions/upload-artifact@89ef406dd8d7e03cfd12d9e0a4a378f454709029 # v4.3.5
|
||||
with:
|
||||
name: com.x8bit.bitwarden-fdroid.apk
|
||||
path: app/build/outputs/apk/fdroid/release/com.x8bit.bitwarden-fdroid-release.apk
|
||||
@@ -491,14 +494,14 @@ jobs:
|
||||
> ./bw-fdroid-apk-sha256.txt
|
||||
|
||||
- name: Upload F-Droid SHA file
|
||||
uses: actions/upload-artifact@0b2256b8c012f0828dc542b3febcab082c67f72b # v4.3.4
|
||||
uses: actions/upload-artifact@89ef406dd8d7e03cfd12d9e0a4a378f454709029 # v4.3.5
|
||||
with:
|
||||
name: bw-fdroid-apk-sha256.txt
|
||||
path: ./bw-fdroid-apk-sha256.txt
|
||||
if-no-files-found: error
|
||||
|
||||
- name: Upload F-Droid Beta .apk artifact
|
||||
uses: actions/upload-artifact@0b2256b8c012f0828dc542b3febcab082c67f72b # v4.3.4
|
||||
uses: actions/upload-artifact@89ef406dd8d7e03cfd12d9e0a4a378f454709029 # v4.3.5
|
||||
with:
|
||||
name: com.x8bit.bitwarden-fdroid-beta.apk
|
||||
path: app/build/outputs/apk/fdroid/beta/com.x8bit.bitwarden-fdroid-beta.apk
|
||||
@@ -510,7 +513,7 @@ jobs:
|
||||
> ./bw-fdroid-beta-apk-sha256.txt
|
||||
|
||||
- name: Upload F-Droid Beta SHA file
|
||||
uses: actions/upload-artifact@0b2256b8c012f0828dc542b3febcab082c67f72b # v4.3.4
|
||||
uses: actions/upload-artifact@89ef406dd8d7e03cfd12d9e0a4a378f454709029 # v4.3.5
|
||||
with:
|
||||
name: bw-fdroid-beta-apk-sha256.txt
|
||||
path: ./bw-fdroid-beta-apk-sha256.txt
|
||||
@@ -526,4 +529,5 @@ jobs:
|
||||
APP_FDROID_FIREBASE_CREDS_PATH: ${{ github.workspace }}/secrets/app_fdroid_firebase-creds.json
|
||||
run: |
|
||||
bundle exec fastlane distributeReleaseFDroidToFirebase \
|
||||
actionUrl:${{ env.GITHUB_ACTION_RUN_URL }} \
|
||||
service_credentials_file:${{ env.APP_FDROID_FIREBASE_CREDS_PATH }}
|
||||
|
||||
4
.github/workflows/crowdin-pull.yml
vendored
4
.github/workflows/crowdin-pull.yml
vendored
@@ -10,7 +10,7 @@ on:
|
||||
jobs:
|
||||
crowdin-sync:
|
||||
name: Autosync
|
||||
runs-on: ubuntu-20.04
|
||||
runs-on: ubuntu-22.04
|
||||
env:
|
||||
_CROWDIN_PROJECT_ID: "269690"
|
||||
steps:
|
||||
@@ -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@61ac8b980551f674046220c3e104bddae2916ac5 # v2.0.0
|
||||
uses: crowdin/github-action@6ed209d411599a981ccb978df3be9dc9b8a81699 # v2.1.1
|
||||
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@61ac8b980551f674046220c3e104bddae2916ac5 # v2.0.0
|
||||
uses: crowdin/github-action@6ed209d411599a981ccb978df3be9dc9b8a81699 # v2.1.1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
CROWDIN_API_TOKEN: ${{ steps.retrieve-secrets.outputs.crowdin-api-token }}
|
||||
|
||||
4
.github/workflows/scan.yml
vendored
4
.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@92b6d52097badece63efe997ffe75207010bb80c # 2.0.29
|
||||
uses: checkmarx/ast-github-action@6c56658230f79c227a55120e9b24845d574d5225 # 2.0.31
|
||||
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@b611370bb5703a7efb587f9d136a52ea24c5c38c # v3.25.11
|
||||
uses: github/codeql-action/upload-sarif@afb54ba388a7dca6ecae48f608c4ff05ff4cc77a # v3.25.15
|
||||
with:
|
||||
sarif_file: cx_result.sarif
|
||||
|
||||
|
||||
4
.github/workflows/test.yml
vendored
4
.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@dbbdc275be76ac10734476cc723d82dfe7ec6eda # v3.4.2
|
||||
uses: gradle/actions/wrapper-validation@d9c87d481d55275bb5441eef3fe0e46805f9ef70 # v3.5.0
|
||||
|
||||
- name: Cache Gradle files
|
||||
uses: actions/cache@0c45773b623bea8c8e75f6c82b208c3cf94ea4f9 # v4.0.2
|
||||
@@ -58,7 +58,7 @@ jobs:
|
||||
${{ runner.os }}-build-
|
||||
|
||||
- name: Configure Ruby
|
||||
uses: ruby/setup-ruby@3a77c29278ae80936b4cb030fefc7d21c96c786f # v1.185.0
|
||||
uses: ruby/setup-ruby@a6e6f86333f0a2523ece813039b8b4be04560854 # v1.190.0
|
||||
with:
|
||||
bundler-cache: true
|
||||
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
shroud.reportKover 'App', 'app/build/reports/kover/reportStandardDebug.xml', 80, 80, false
|
||||
28
Gemfile.lock
28
Gemfile.lock
@@ -10,20 +10,20 @@ GEM
|
||||
artifactory (3.0.17)
|
||||
atomos (0.1.3)
|
||||
aws-eventstream (1.3.0)
|
||||
aws-partitions (1.947.0)
|
||||
aws-sdk-core (3.199.0)
|
||||
aws-partitions (1.961.0)
|
||||
aws-sdk-core (3.201.3)
|
||||
aws-eventstream (~> 1, >= 1.3.0)
|
||||
aws-partitions (~> 1, >= 1.651.0)
|
||||
aws-sigv4 (~> 1.8)
|
||||
jmespath (~> 1, >= 1.6.1)
|
||||
aws-sdk-kms (1.87.0)
|
||||
aws-sdk-core (~> 3, >= 3.199.0)
|
||||
aws-sigv4 (~> 1.1)
|
||||
aws-sdk-s3 (1.154.0)
|
||||
aws-sdk-core (~> 3, >= 3.199.0)
|
||||
aws-sdk-kms (1.88.0)
|
||||
aws-sdk-core (~> 3, >= 3.201.0)
|
||||
aws-sigv4 (~> 1.5)
|
||||
aws-sdk-s3 (1.157.0)
|
||||
aws-sdk-core (~> 3, >= 3.201.0)
|
||||
aws-sdk-kms (~> 1)
|
||||
aws-sigv4 (~> 1.8)
|
||||
aws-sigv4 (1.8.0)
|
||||
aws-sigv4 (~> 1.5)
|
||||
aws-sigv4 (1.9.1)
|
||||
aws-eventstream (~> 1, >= 1.0.2)
|
||||
babosa (1.0.4)
|
||||
base64 (0.2.0)
|
||||
@@ -39,7 +39,7 @@ GEM
|
||||
domain_name (0.6.20240107)
|
||||
dotenv (2.8.1)
|
||||
emoji_regex (3.2.3)
|
||||
excon (0.110.0)
|
||||
excon (0.111.0)
|
||||
faraday (1.10.3)
|
||||
faraday-em_http (~> 1.0)
|
||||
faraday-em_synchrony (~> 1.0)
|
||||
@@ -61,7 +61,7 @@ GEM
|
||||
faraday-httpclient (1.0.1)
|
||||
faraday-multipart (1.0.4)
|
||||
multipart-post (~> 2)
|
||||
faraday-net_http (1.0.1)
|
||||
faraday-net_http (1.0.2)
|
||||
faraday-net_http_persistent (1.2.0)
|
||||
faraday-patron (1.0.0)
|
||||
faraday-rack (1.0.0)
|
||||
@@ -69,7 +69,7 @@ GEM
|
||||
faraday_middleware (1.2.0)
|
||||
faraday (~> 1.0)
|
||||
fastimage (2.3.1)
|
||||
fastlane (2.221.1)
|
||||
fastlane (2.222.0)
|
||||
CFPropertyList (>= 2.3, < 4.0.0)
|
||||
addressable (>= 2.8, < 3.0.0)
|
||||
artifactory (~> 3.0)
|
||||
@@ -162,7 +162,7 @@ GEM
|
||||
json (2.7.2)
|
||||
jwt (2.8.2)
|
||||
base64
|
||||
mini_magick (4.13.1)
|
||||
mini_magick (4.13.2)
|
||||
mini_mime (1.1.5)
|
||||
multi_json (1.15.0)
|
||||
multipart-post (2.4.1)
|
||||
@@ -172,7 +172,7 @@ GEM
|
||||
optparse (0.5.0)
|
||||
os (1.1.4)
|
||||
plist (3.7.1)
|
||||
public_suffix (6.0.0)
|
||||
public_suffix (6.0.1)
|
||||
rake (13.2.1)
|
||||
representable (3.2.0)
|
||||
declarative (< 0.1.0)
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
|
||||
## Compatibility
|
||||
|
||||
- **Minimum SDK**: 28
|
||||
- **Minimum SDK**: 29
|
||||
- **Target SDK**: 34
|
||||
- **Device Types Supported**: Phone and Tablet
|
||||
- **Orientations Supported**: Portrait and Landscape
|
||||
|
||||
@@ -65,8 +65,13 @@ android {
|
||||
|
||||
// Beta and Release variants are identical except beta has a different package name
|
||||
create("beta") {
|
||||
initWith(buildTypes.getByName("release"))
|
||||
applicationIdSuffix = ".beta"
|
||||
isDebuggable = false
|
||||
isMinifyEnabled = true
|
||||
proguardFiles(
|
||||
getDefaultProguardFile("proguard-android-optimize.txt"),
|
||||
"proguard-rules.pro"
|
||||
)
|
||||
}
|
||||
release {
|
||||
isDebuggable = false
|
||||
|
||||
@@ -3,15 +3,6 @@
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<application tools:ignore="MissingApplicationIcon">
|
||||
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="com.x8bit.bitwarden.fido2.ACTION_CREATE_PASSKEY" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<!-- Disable Crashlytics for debug builds -->
|
||||
<meta-data
|
||||
android:name="firebase_crashlytics_collection_enabled"
|
||||
|
||||
@@ -55,6 +55,23 @@
|
||||
<data android:mimeType="video/*" />
|
||||
<data android:mimeType="text/*" />
|
||||
</intent-filter>
|
||||
<intent-filter android:autoVerify="true">
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<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" />
|
||||
</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
|
||||
@@ -141,6 +158,27 @@
|
||||
</intent-filter>
|
||||
</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
|
||||
services.
|
||||
-->
|
||||
<!--suppress AndroidDomInspection -->
|
||||
<service
|
||||
android:name="com.x8bit.bitwarden.Autofill.CredentialProviderService"
|
||||
android:enabled="true"
|
||||
android:exported="true"
|
||||
android:label="@string/bitwarden"
|
||||
android:permission="android.permission.BIND_CREDENTIAL_PROVIDER_SERVICE"
|
||||
tools:ignore="MissingClass">
|
||||
<intent-filter>
|
||||
<action android:name="android.service.credentials.CredentialProviderService" />
|
||||
</intent-filter>
|
||||
<meta-data
|
||||
android:name="android.credentials.provider"
|
||||
android:resource="@xml/provider" />
|
||||
</service>
|
||||
|
||||
<!-- This is required to support in-app language picker in Android 12 (API 32) and below -->
|
||||
<service
|
||||
android:name="androidx.appcompat.app.AppLocalesMetadataHolderService"
|
||||
|
||||
@@ -6,9 +6,12 @@ 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.util.getCompleteRegistrationDataIntentOrNull
|
||||
import com.x8bit.bitwarden.data.auth.util.getPasswordlessRequestDataIntentOrNull
|
||||
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
|
||||
import com.x8bit.bitwarden.data.autofill.fido2.util.getFido2GetCredentialsRequestOrNull
|
||||
import com.x8bit.bitwarden.data.autofill.manager.AutofillSelectionManager
|
||||
import com.x8bit.bitwarden.data.autofill.util.getAutofillSaveItemOrNull
|
||||
import com.x8bit.bitwarden.data.autofill.util.getAutofillSelectionDataOrNull
|
||||
@@ -32,6 +35,7 @@ import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.parcelize.Parcelize
|
||||
import java.time.Clock
|
||||
import javax.inject.Inject
|
||||
|
||||
private const val SPECIAL_CIRCUMSTANCE_KEY = "special-circumstance"
|
||||
@@ -51,6 +55,7 @@ class MainViewModel @Inject constructor(
|
||||
private val vaultRepository: VaultRepository,
|
||||
private val authRepository: AuthRepository,
|
||||
private val savedStateHandle: SavedStateHandle,
|
||||
private val clock: Clock,
|
||||
) : BaseViewModel<MainState, MainEvent, MainAction>(
|
||||
initialState = MainState(
|
||||
theme = settingsRepository.appTheme,
|
||||
@@ -110,6 +115,10 @@ class MainViewModel @Inject constructor(
|
||||
.onEach {
|
||||
when (it) {
|
||||
is VaultStateEvent.Locked -> {
|
||||
// Similar to account switching, triggering this action too soon can
|
||||
// interfere with animations or navigation logic, so we will delay slightly.
|
||||
@Suppress("MagicNumber")
|
||||
delay(500)
|
||||
trySendAction(MainAction.Internal.VaultUnlockStateChange)
|
||||
}
|
||||
|
||||
@@ -170,6 +179,7 @@ class MainViewModel @Inject constructor(
|
||||
)
|
||||
}
|
||||
|
||||
@Suppress("LongMethod")
|
||||
private fun handleIntent(
|
||||
intent: Intent,
|
||||
isFirstIntent: Boolean,
|
||||
@@ -181,6 +191,9 @@ class MainViewModel @Inject constructor(
|
||||
val hasGeneratorShortcut = intent.isPasswordGeneratorShortcut
|
||||
val hasVaultShortcut = intent.isMyVaultShortcut
|
||||
val fido2CredentialRequestData = intent.getFido2CredentialRequestOrNull()
|
||||
val completeRegistrationData = intent.getCompleteRegistrationDataIntentOrNull()
|
||||
val fido2CredentialAssertionRequest = intent.getFido2AssertionRequestOrNull()
|
||||
val fido2GetCredentialsRequest = intent.getFido2GetCredentialsRequestOrNull()
|
||||
when {
|
||||
passwordlessRequestData != null -> {
|
||||
specialCircumstanceManager.specialCircumstance =
|
||||
@@ -192,6 +205,17 @@ class MainViewModel @Inject constructor(
|
||||
)
|
||||
}
|
||||
|
||||
completeRegistrationData != null -> {
|
||||
if (authRepository.activeUserId != null) {
|
||||
authRepository.hasPendingAccountAddition = true
|
||||
}
|
||||
specialCircumstanceManager.specialCircumstance =
|
||||
SpecialCircumstance.CompleteRegistration(
|
||||
completeRegistrationData = completeRegistrationData,
|
||||
timestamp = clock.millis(),
|
||||
)
|
||||
}
|
||||
|
||||
autofillSaveItem != null -> {
|
||||
specialCircumstanceManager.specialCircumstance =
|
||||
SpecialCircumstance.AutofillSave(
|
||||
@@ -237,6 +261,20 @@ class MainViewModel @Inject constructor(
|
||||
}
|
||||
}
|
||||
|
||||
fido2CredentialAssertionRequest != null -> {
|
||||
specialCircumstanceManager.specialCircumstance =
|
||||
SpecialCircumstance.Fido2Assertion(
|
||||
fido2AssertionRequest = fido2CredentialAssertionRequest,
|
||||
)
|
||||
}
|
||||
|
||||
fido2GetCredentialsRequest != null -> {
|
||||
specialCircumstanceManager.specialCircumstance =
|
||||
SpecialCircumstance.Fido2GetCredentials(
|
||||
fido2GetCredentialsRequest = fido2GetCredentialsRequest,
|
||||
)
|
||||
}
|
||||
|
||||
hasGeneratorShortcut -> {
|
||||
specialCircumstanceManager.specialCircumstance =
|
||||
SpecialCircumstance.GeneratorShortcut
|
||||
|
||||
@@ -45,12 +45,22 @@ interface AuthDiskSource {
|
||||
*/
|
||||
fun clearData(userId: String)
|
||||
|
||||
/**
|
||||
* Retrieves the state indicating that the user should use a key connector.
|
||||
*/
|
||||
fun getShouldUseKeyConnector(userId: String): 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 chosen to trust this device.
|
||||
*
|
||||
* Note: This indicates intent to trust the device, the device may not be trusted yet.
|
||||
*/
|
||||
fun getShouldTrustDevice(userId: String): Boolean
|
||||
fun getShouldTrustDevice(userId: String): Boolean?
|
||||
|
||||
/**
|
||||
* Stores the boolean indicating that the user has chosen to trust this device for the given
|
||||
@@ -60,25 +70,6 @@ interface AuthDiskSource {
|
||||
*/
|
||||
fun storeShouldTrustDevice(userId: String, shouldTrustDevice: Boolean?)
|
||||
|
||||
/**
|
||||
* Retrieves the "last active time" for the given [userId], in milliseconds.
|
||||
*
|
||||
* This time is intended to be derived from a call to
|
||||
* [SystemClock.elapsedRealtime()](https://developer.android.com/reference/android/os/SystemClock#elapsedRealtime())
|
||||
*/
|
||||
fun getLastActiveTimeMillis(userId: String): Long?
|
||||
|
||||
/**
|
||||
* Stores the [lastActiveTimeMillis] for the given [userId].
|
||||
*
|
||||
* This time is intended to be derived from a call to
|
||||
* [SystemClock.elapsedRealtime()](https://developer.android.com/reference/android/os/SystemClock#elapsedRealtime())
|
||||
*/
|
||||
fun storeLastActiveTimeMillis(
|
||||
userId: String,
|
||||
lastActiveTimeMillis: Long?,
|
||||
)
|
||||
|
||||
/**
|
||||
* Retrieves the number of consecutive invalid lock attempts for the given [userId].
|
||||
*/
|
||||
|
||||
@@ -28,7 +28,6 @@ private const val UNIQUE_APP_ID_KEY = "appId"
|
||||
private const val REMEMBERED_EMAIL_ADDRESS_KEY = "rememberedEmail"
|
||||
private const val REMEMBERED_ORG_IDENTIFIER_KEY = "rememberedOrgIdentifier"
|
||||
private const val STATE_KEY = "state"
|
||||
private const val LAST_ACTIVE_TIME_KEY = "lastActiveTime"
|
||||
private const val INVALID_UNLOCK_ATTEMPTS_KEY = "invalidUnlockAttempts"
|
||||
private const val MASTER_KEY_ENCRYPTION_USER_KEY = "masterKeyEncryptedUserKey"
|
||||
private const val MASTER_KEY_ENCRYPTION_PRIVATE_KEY = "encPrivateKey"
|
||||
@@ -40,6 +39,7 @@ 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 USES_KEY_CONNECTOR = "usesKeyConnector"
|
||||
|
||||
/**
|
||||
* Primary implementation of [AuthDiskSource].
|
||||
@@ -111,7 +111,6 @@ class AuthDiskSourceImpl(
|
||||
.onSubscription { emit(userState) }
|
||||
|
||||
override fun clearData(userId: String) {
|
||||
storeLastActiveTimeMillis(userId = userId, lastActiveTimeMillis = null)
|
||||
storeInvalidUnlockAttempts(userId = userId, invalidUnlockAttempts = null)
|
||||
storeUserKey(userId = userId, userKey = null)
|
||||
storeUserAutoUnlockKey(userId = userId, userAutoUnlockKey = null)
|
||||
@@ -124,33 +123,31 @@ class AuthDiskSourceImpl(
|
||||
storeMasterPasswordHash(userId = userId, passwordHash = null)
|
||||
storePolicies(userId = userId, policies = null)
|
||||
storeAccountTokens(userId = userId, accountTokens = null)
|
||||
storeShouldUseKeyConnector(userId = userId, shouldUseKeyConnector = null)
|
||||
|
||||
// Do not remove the DeviceKey or PendingAuthRequest on logout, these are persisted
|
||||
// indefinitely unless the TDE flow explicitly removes them.
|
||||
}
|
||||
|
||||
override fun getShouldTrustDevice(userId: String): Boolean =
|
||||
requireNotNull(
|
||||
getBoolean(key = SHOULD_TRUST_DEVICE_KEY.appendIdentifier(userId), default = false),
|
||||
override fun getShouldUseKeyConnector(
|
||||
userId: String,
|
||||
): Boolean? = getBoolean(key = USES_KEY_CONNECTOR.appendIdentifier(userId))
|
||||
|
||||
override fun storeShouldUseKeyConnector(userId: String, shouldUseKeyConnector: Boolean?) {
|
||||
putBoolean(
|
||||
key = USES_KEY_CONNECTOR.appendIdentifier(userId),
|
||||
value = shouldUseKeyConnector,
|
||||
)
|
||||
}
|
||||
|
||||
override fun getShouldTrustDevice(
|
||||
userId: String,
|
||||
): Boolean? = getBoolean(key = SHOULD_TRUST_DEVICE_KEY.appendIdentifier(userId))
|
||||
|
||||
override fun storeShouldTrustDevice(userId: String, shouldTrustDevice: Boolean?) {
|
||||
putBoolean(SHOULD_TRUST_DEVICE_KEY.appendIdentifier(userId), shouldTrustDevice)
|
||||
}
|
||||
|
||||
override fun getLastActiveTimeMillis(userId: String): Long? =
|
||||
getLong(key = LAST_ACTIVE_TIME_KEY.appendIdentifier(userId))
|
||||
|
||||
override fun storeLastActiveTimeMillis(
|
||||
userId: String,
|
||||
lastActiveTimeMillis: Long?,
|
||||
) {
|
||||
putLong(
|
||||
key = LAST_ACTIVE_TIME_KEY.appendIdentifier(userId),
|
||||
value = lastActiveTimeMillis,
|
||||
)
|
||||
}
|
||||
|
||||
override fun getInvalidUnlockAttempts(userId: String): Int? =
|
||||
getInt(key = INVALID_UNLOCK_ATTEMPTS_KEY.appendIdentifier(userId))
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ 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
|
||||
@@ -13,6 +14,13 @@ import retrofit2.http.POST
|
||||
* Defines raw calls under the /accounts API with authentication applied.
|
||||
*/
|
||||
interface AuthenticatedAccountsApi {
|
||||
|
||||
/**
|
||||
* Converts the currently active account to a key-connector account.
|
||||
*/
|
||||
@POST("/accounts/convert-to-key-connector")
|
||||
suspend fun convertToKeyConnector(): Result<Unit>
|
||||
|
||||
/**
|
||||
* Creates the keys for the current account.
|
||||
*/
|
||||
@@ -45,6 +53,12 @@ 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.
|
||||
*/
|
||||
|
||||
@@ -5,8 +5,11 @@ import com.x8bit.bitwarden.data.auth.datasource.network.model.PreLoginRequestJso
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.PreLoginResponseJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.PrevalidateSsoResponseJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.RefreshTokenResponseJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.RegisterFinishRequestJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.RegisterRequestJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.RegisterResponseJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.SendVerificationEmailRequestJson
|
||||
import kotlinx.serialization.json.JsonPrimitive
|
||||
import retrofit2.Call
|
||||
import retrofit2.http.Body
|
||||
import retrofit2.http.Field
|
||||
@@ -66,4 +69,14 @@ interface IdentityApi {
|
||||
|
||||
@POST("/accounts/register")
|
||||
suspend fun register(@Body body: RegisterRequestJson): Result<RegisterResponseJson.Success>
|
||||
|
||||
@POST("/accounts/register/finish")
|
||||
suspend fun registerFinish(
|
||||
@Body body: RegisterFinishRequestJson,
|
||||
): Result<RegisterResponseJson.Success>
|
||||
|
||||
@POST("/accounts/register/send-verification-email")
|
||||
suspend fun sendVerificationEmail(
|
||||
@Body body: SendVerificationEmailRequestJson,
|
||||
): Result<JsonPrimitive?>
|
||||
}
|
||||
|
||||
@@ -73,10 +73,8 @@ object AuthNetworkModule {
|
||||
fun providesHaveIBeenPwnedService(
|
||||
retrofits: Retrofits,
|
||||
): HaveIBeenPwnedService = HaveIBeenPwnedServiceImpl(
|
||||
retrofits
|
||||
.staticRetrofitBuilder
|
||||
.baseUrl("https://api.pwnedpasswords.com")
|
||||
.build()
|
||||
api = retrofits
|
||||
.createStaticRetrofit(baseUrl = "https://api.pwnedpasswords.com")
|
||||
.create(),
|
||||
)
|
||||
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
package com.x8bit.bitwarden.data.auth.datasource.network.model
|
||||
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
/**
|
||||
* Represents the request body used to create the key connector keys for an account.
|
||||
*/
|
||||
@Serializable
|
||||
data class KeyConnectorKeyRequestJson(
|
||||
@SerialName("key") val userKey: String,
|
||||
@SerialName("keys") val keys: Keys,
|
||||
@SerialName("kdf") val kdfType: KdfTypeJson,
|
||||
@SerialName("kdfIterations") val kdfIterations: Int?,
|
||||
@SerialName("kdfMemory") val kdfMemory: Int?,
|
||||
@SerialName("kdfParallelism") val kdfParallelism: Int?,
|
||||
@SerialName("orgIdentifier") val organizationIdentifier: String,
|
||||
) {
|
||||
/**
|
||||
* A keys object containing public and private keys.
|
||||
*
|
||||
* @param publicKey the public key (encrypted).
|
||||
* @param encryptedPrivateKey the private key (encrypted).
|
||||
*/
|
||||
@Serializable
|
||||
data class Keys(
|
||||
@SerialName("publicKey")
|
||||
val publicKey: String,
|
||||
|
||||
@SerialName("encryptedPrivateKey")
|
||||
val encryptedPrivateKey: String,
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
package com.x8bit.bitwarden.data.auth.datasource.network.model
|
||||
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.RegisterFinishRequestJson.Keys
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
/**
|
||||
* Request body for register.
|
||||
*
|
||||
* @param email the email to be registered.
|
||||
* @param emailVerificationToken token used to finish the registration process.
|
||||
* @param masterPasswordHash the master password (encrypted).
|
||||
* @param masterPasswordHint the hint for the master password (nullable).
|
||||
* @param captchaResponse the captcha bypass token.
|
||||
* @param userSymmetricKey the user key for the request (encrypted).
|
||||
* @param userAsymmetricKeys a [Keys] object containing public and private keys.
|
||||
* @param kdfType the kdf type represented as an [Int].
|
||||
* @param kdfIterations the number of kdf iterations.
|
||||
*/
|
||||
@Serializable
|
||||
data class RegisterFinishRequestJson(
|
||||
@SerialName("email")
|
||||
val email: String,
|
||||
|
||||
@SerialName("emailVerificationToken")
|
||||
val emailVerificationToken: String,
|
||||
|
||||
@SerialName("masterPasswordHash")
|
||||
val masterPasswordHash: String,
|
||||
|
||||
@SerialName("masterPasswordHint")
|
||||
val masterPasswordHint: String?,
|
||||
|
||||
@SerialName("captchaResponse")
|
||||
val captchaResponse: String?,
|
||||
|
||||
@SerialName("userSymmetricKey")
|
||||
val userSymmetricKey: String,
|
||||
|
||||
@SerialName("userAsymmetricKeys")
|
||||
val userAsymmetricKeys: Keys,
|
||||
|
||||
@SerialName("kdf")
|
||||
val kdfType: KdfTypeJson,
|
||||
|
||||
@SerialName("kdfIterations")
|
||||
val kdfIterations: UInt,
|
||||
) {
|
||||
|
||||
/**
|
||||
* A keys object containing public and private keys.
|
||||
*
|
||||
* @param publicKey the public key (encrypted).
|
||||
* @param encryptedPrivateKey the private key (encrypted).
|
||||
*/
|
||||
@Serializable
|
||||
data class Keys(
|
||||
@SerialName("publicKey")
|
||||
val publicKey: String,
|
||||
|
||||
@SerialName("encryptedPrivateKey")
|
||||
val encryptedPrivateKey: String,
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package com.x8bit.bitwarden.data.auth.datasource.network.model
|
||||
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
/**
|
||||
* Request body for send verification email.
|
||||
*
|
||||
* @param email the email to be registered.
|
||||
* @param name the name to be registered.
|
||||
* @param receiveMarketingEmails the answer to receive marketing emails.
|
||||
*/
|
||||
@Serializable
|
||||
data class SendVerificationEmailRequestJson(
|
||||
@SerialName("email")
|
||||
val email: String,
|
||||
|
||||
@SerialName("name")
|
||||
val name: String?,
|
||||
|
||||
@SerialName("receiveMarketingEmails")
|
||||
val receiveMarketingEmails: Boolean,
|
||||
)
|
||||
@@ -0,0 +1,47 @@
|
||||
package com.x8bit.bitwarden.data.auth.datasource.network.model
|
||||
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
/**
|
||||
* The response body for sending a verification email.
|
||||
*/
|
||||
@Serializable
|
||||
sealed class SendVerificationEmailResponseJson {
|
||||
|
||||
/**
|
||||
* Models a successful json response.
|
||||
*
|
||||
* @param emailVerificationToken the token to verify the email.
|
||||
*/
|
||||
@Serializable
|
||||
data class Success(
|
||||
val emailVerificationToken: String?,
|
||||
) : 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)
|
||||
*/
|
||||
@Serializable
|
||||
data class Invalid(
|
||||
@SerialName("message")
|
||||
val message: String?,
|
||||
|
||||
@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()
|
||||
}
|
||||
@@ -1,6 +1,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.PasswordHintResponseJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.ResendEmailRequestJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.ResetPasswordRequestJson
|
||||
@@ -11,6 +12,11 @@ import com.x8bit.bitwarden.data.auth.datasource.network.model.SetPasswordRequest
|
||||
*/
|
||||
interface AccountsService {
|
||||
|
||||
/**
|
||||
* Converts the currently active account to a key-connector account.
|
||||
*/
|
||||
suspend fun convertToKeyConnector(): Result<Unit>
|
||||
|
||||
/**
|
||||
* Creates a new account's keys.
|
||||
*/
|
||||
@@ -49,6 +55,11 @@ interface AccountsService {
|
||||
*/
|
||||
suspend fun resetPassword(body: ResetPasswordRequestJson): Result<Unit>
|
||||
|
||||
/**
|
||||
* Set the key connector key.
|
||||
*/
|
||||
suspend fun setKeyConnectorKey(body: KeyConnectorKeyRequestJson): Result<Unit>
|
||||
|
||||
/**
|
||||
* Set the password.
|
||||
*/
|
||||
|
||||
@@ -5,6 +5,7 @@ import com.x8bit.bitwarden.data.auth.datasource.network.api.AuthenticatedAccount
|
||||
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.PasswordHintRequestJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.PasswordHintResponseJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.ResendEmailRequestJson
|
||||
@@ -21,6 +22,12 @@ class AccountsServiceImpl(
|
||||
private val json: Json,
|
||||
) : AccountsService {
|
||||
|
||||
/**
|
||||
* Converts the currently active account to a key-connector account.
|
||||
*/
|
||||
override suspend fun convertToKeyConnector(): Result<Unit> =
|
||||
authenticatedAccountsApi.convertToKeyConnector()
|
||||
|
||||
override suspend fun createAccountKeys(
|
||||
publicKey: String,
|
||||
encryptedPrivateKey: String,
|
||||
@@ -93,6 +100,10 @@ class AccountsServiceImpl(
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun setKeyConnectorKey(
|
||||
body: KeyConnectorKeyRequestJson,
|
||||
): Result<Unit> = authenticatedAccountsApi.setKeyConnectorKey(body)
|
||||
|
||||
override suspend fun setPassword(
|
||||
body: SetPasswordRequestJson,
|
||||
): Result<Unit> = authenticatedAccountsApi.setPassword(body)
|
||||
|
||||
@@ -5,8 +5,10 @@ import com.x8bit.bitwarden.data.auth.datasource.network.model.IdentityTokenAuthM
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.PreLoginResponseJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.PrevalidateSsoResponseJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.RefreshTokenResponseJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.RegisterFinishRequestJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.RegisterRequestJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.RegisterResponseJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.SendVerificationEmailRequestJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.TwoFactorDataModel
|
||||
|
||||
/**
|
||||
@@ -58,4 +60,16 @@ interface IdentityService {
|
||||
* @param refreshToken The refresh token needed to obtain a new token.
|
||||
*/
|
||||
fun refreshTokenSynchronously(refreshToken: String): Result<RefreshTokenResponseJson>
|
||||
|
||||
/**
|
||||
* Send a verification email.
|
||||
*/
|
||||
suspend fun sendVerificationEmail(
|
||||
body: SendVerificationEmailRequestJson,
|
||||
): Result<String?>
|
||||
|
||||
/**
|
||||
* Register a new account to Bitwarden using email verification flow.
|
||||
*/
|
||||
suspend fun registerFinish(body: RegisterFinishRequestJson): Result<RegisterResponseJson>
|
||||
}
|
||||
|
||||
@@ -7,8 +7,10 @@ import com.x8bit.bitwarden.data.auth.datasource.network.model.PreLoginRequestJso
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.PreLoginResponseJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.PrevalidateSsoResponseJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.RefreshTokenResponseJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.RegisterFinishRequestJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.RegisterRequestJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.RegisterResponseJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.SendVerificationEmailRequestJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.TwoFactorDataModel
|
||||
import com.x8bit.bitwarden.data.platform.datasource.network.model.toBitwardenError
|
||||
import com.x8bit.bitwarden.data.platform.datasource.network.util.base64UrlEncode
|
||||
@@ -32,16 +34,21 @@ class IdentityServiceImpl(
|
||||
.register(body)
|
||||
.recoverCatching { throwable ->
|
||||
val bitwardenError = throwable.toBitwardenError()
|
||||
bitwardenError.parseErrorBodyOrNull<RegisterResponseJson.CaptchaRequired>(
|
||||
code = 400,
|
||||
json = json,
|
||||
) ?: bitwardenError.parseErrorBodyOrNull<RegisterResponseJson.Invalid>(
|
||||
codes = listOf(400, 429),
|
||||
json = json,
|
||||
) ?: bitwardenError.parseErrorBodyOrNull<RegisterResponseJson.Error>(
|
||||
code = 429,
|
||||
json = json,
|
||||
) ?: throw throwable
|
||||
bitwardenError
|
||||
.parseErrorBodyOrNull<RegisterResponseJson.CaptchaRequired>(
|
||||
code = 400,
|
||||
json = json,
|
||||
)
|
||||
?: bitwardenError
|
||||
.parseErrorBodyOrNull<RegisterResponseJson.Invalid>(
|
||||
codes = listOf(400, 429),
|
||||
json = json,
|
||||
)
|
||||
?: bitwardenError.parseErrorBodyOrNull<RegisterResponseJson.Error>(
|
||||
code = 429,
|
||||
json = json,
|
||||
)
|
||||
?: throw throwable
|
||||
}
|
||||
|
||||
@Suppress("MagicNumber")
|
||||
@@ -101,4 +108,32 @@ class IdentityServiceImpl(
|
||||
refreshToken = refreshToken,
|
||||
)
|
||||
.executeForResult()
|
||||
|
||||
@Suppress("MagicNumber")
|
||||
override suspend fun registerFinish(
|
||||
body: RegisterFinishRequestJson,
|
||||
): Result<RegisterResponseJson> =
|
||||
api
|
||||
.registerFinish(body)
|
||||
.recoverCatching { throwable ->
|
||||
val bitwardenError = throwable.toBitwardenError()
|
||||
bitwardenError
|
||||
.parseErrorBodyOrNull<RegisterResponseJson.Invalid>(
|
||||
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
|
||||
.sendVerificationEmail(body = body)
|
||||
.map { it?.content }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,11 +7,11 @@ import com.bitwarden.core.RegisterKeyResponse
|
||||
import com.bitwarden.core.RegisterTdeKeyResponse
|
||||
import com.bitwarden.crypto.HashPurpose
|
||||
import com.bitwarden.crypto.Kdf
|
||||
import com.bitwarden.sdk.Client
|
||||
import com.bitwarden.sdk.ClientAuth
|
||||
import com.x8bit.bitwarden.data.auth.datasource.sdk.model.PasswordStrength
|
||||
import com.x8bit.bitwarden.data.auth.datasource.sdk.util.toPasswordStrengthOrNull
|
||||
import com.x8bit.bitwarden.data.auth.datasource.sdk.util.toUByte
|
||||
import com.x8bit.bitwarden.data.platform.datasource.sdk.BaseSdkSource
|
||||
import com.x8bit.bitwarden.data.platform.manager.SdkClientManager
|
||||
|
||||
/**
|
||||
@@ -19,12 +19,13 @@ import com.x8bit.bitwarden.data.platform.manager.SdkClientManager
|
||||
* [ClientAuth].
|
||||
*/
|
||||
class AuthSdkSourceImpl(
|
||||
private val sdkClientManager: SdkClientManager,
|
||||
) : AuthSdkSource {
|
||||
sdkClientManager: SdkClientManager,
|
||||
) : BaseSdkSource(sdkClientManager = sdkClientManager),
|
||||
AuthSdkSource {
|
||||
|
||||
override suspend fun getNewAuthRequest(
|
||||
email: String,
|
||||
): Result<AuthRequestResponse> = runCatching {
|
||||
): Result<AuthRequestResponse> = runCatchingWithLogs {
|
||||
getClient()
|
||||
.auth()
|
||||
.newAuthRequest(
|
||||
@@ -35,7 +36,7 @@ class AuthSdkSourceImpl(
|
||||
override suspend fun getUserFingerprint(
|
||||
email: String,
|
||||
publicKey: String,
|
||||
): Result<String> = runCatching {
|
||||
): Result<String> = runCatchingWithLogs {
|
||||
getClient()
|
||||
.platform()
|
||||
.fingerprint(
|
||||
@@ -51,7 +52,7 @@ class AuthSdkSourceImpl(
|
||||
password: String,
|
||||
kdf: Kdf,
|
||||
purpose: HashPurpose,
|
||||
): Result<String> = runCatching {
|
||||
): Result<String> = runCatchingWithLogs {
|
||||
getClient()
|
||||
.auth()
|
||||
.hashPassword(
|
||||
@@ -66,7 +67,7 @@ class AuthSdkSourceImpl(
|
||||
email: String,
|
||||
password: String,
|
||||
kdf: Kdf,
|
||||
): Result<RegisterKeyResponse> = runCatching {
|
||||
): Result<RegisterKeyResponse> = runCatchingWithLogs {
|
||||
getClient()
|
||||
.auth()
|
||||
.makeRegisterKeys(
|
||||
@@ -81,7 +82,7 @@ class AuthSdkSourceImpl(
|
||||
email: String,
|
||||
orgPublicKey: String,
|
||||
rememberDevice: Boolean,
|
||||
): Result<RegisterTdeKeyResponse> = runCatching {
|
||||
): Result<RegisterTdeKeyResponse> = runCatchingWithLogs {
|
||||
getClient(userId = userId)
|
||||
.auth()
|
||||
.makeRegisterTdeKeys(
|
||||
@@ -95,7 +96,7 @@ class AuthSdkSourceImpl(
|
||||
email: String,
|
||||
password: String,
|
||||
additionalInputs: List<String>,
|
||||
): Result<PasswordStrength> = runCatching {
|
||||
): Result<PasswordStrength> = runCatchingWithLogs {
|
||||
@Suppress("UnsafeCallOnNullableType")
|
||||
getClient()
|
||||
.auth()
|
||||
@@ -111,7 +112,7 @@ class AuthSdkSourceImpl(
|
||||
password: String,
|
||||
passwordStrength: PasswordStrength,
|
||||
policy: MasterPasswordPolicyOptions,
|
||||
): Result<Boolean> = runCatching {
|
||||
): Result<Boolean> = runCatchingWithLogs {
|
||||
getClient()
|
||||
.auth()
|
||||
.satisfiesPolicy(
|
||||
@@ -120,8 +121,4 @@ class AuthSdkSourceImpl(
|
||||
policy = policy,
|
||||
)
|
||||
}
|
||||
|
||||
private suspend fun getClient(
|
||||
userId: String? = null,
|
||||
): Client = sdkClientManager.getOrCreateClient(userId = userId)
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ class TrustedDeviceManagerImpl(
|
||||
private val devicesService: DevicesService,
|
||||
) : TrustedDeviceManager {
|
||||
override suspend fun trustThisDeviceIfNecessary(userId: String): Result<Boolean> =
|
||||
if (!authDiskSource.getShouldTrustDevice(userId = userId)) {
|
||||
if (authDiskSource.getShouldTrustDevice(userId = userId) != true) {
|
||||
false.asSuccess()
|
||||
} else {
|
||||
vaultSdkSource
|
||||
|
||||
@@ -1,9 +1,18 @@
|
||||
package com.x8bit.bitwarden.data.auth.manager
|
||||
|
||||
import com.x8bit.bitwarden.data.auth.manager.model.LogoutEvent
|
||||
import kotlinx.coroutines.flow.SharedFlow
|
||||
|
||||
/**
|
||||
* Manages the logging out of users and clearing of their data.
|
||||
*/
|
||||
interface UserLogoutManager {
|
||||
|
||||
/**
|
||||
* Observable flow of [LogoutEvent]s
|
||||
*/
|
||||
val logoutEventFlow: SharedFlow<LogoutEvent>
|
||||
|
||||
/**
|
||||
* Completely logs out the given [userId], removing all data.
|
||||
* If [isExpired] is true, a toast will be displayed
|
||||
|
||||
@@ -5,14 +5,19 @@ import android.widget.Toast
|
||||
import androidx.annotation.StringRes
|
||||
import com.x8bit.bitwarden.R
|
||||
import com.x8bit.bitwarden.data.auth.datasource.disk.AuthDiskSource
|
||||
import com.x8bit.bitwarden.data.auth.manager.model.LogoutEvent
|
||||
import com.x8bit.bitwarden.data.platform.datasource.disk.PushDiskSource
|
||||
import com.x8bit.bitwarden.data.platform.datasource.disk.SettingsDiskSource
|
||||
import com.x8bit.bitwarden.data.platform.manager.dispatcher.DispatcherManager
|
||||
import com.x8bit.bitwarden.data.platform.repository.util.bufferedMutableSharedFlow
|
||||
import com.x8bit.bitwarden.data.tools.generator.datasource.disk.GeneratorDiskSource
|
||||
import com.x8bit.bitwarden.data.tools.generator.datasource.disk.PasswordHistoryDiskSource
|
||||
import com.x8bit.bitwarden.data.vault.datasource.disk.VaultDiskSource
|
||||
import com.x8bit.bitwarden.data.vault.datasource.sdk.VaultSdkSource
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.flow.SharedFlow
|
||||
import kotlinx.coroutines.flow.asSharedFlow
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
/**
|
||||
@@ -33,43 +38,30 @@ class UserLogoutManagerImpl(
|
||||
private val scope = CoroutineScope(dispatcherManager.unconfined)
|
||||
private val mainScope = CoroutineScope(dispatcherManager.main)
|
||||
|
||||
private val mutableLogoutEventFlow: MutableSharedFlow<LogoutEvent> =
|
||||
bufferedMutableSharedFlow()
|
||||
override val logoutEventFlow: SharedFlow<LogoutEvent> = mutableLogoutEventFlow.asSharedFlow()
|
||||
|
||||
override fun logout(userId: String, isExpired: Boolean) {
|
||||
val currentUserState = authDiskSource.userState ?: return
|
||||
authDiskSource.userState ?: return
|
||||
|
||||
if (isExpired) {
|
||||
showToast(message = R.string.login_expired)
|
||||
}
|
||||
|
||||
// Remove the active user from the accounts map
|
||||
val updatedAccounts = currentUserState
|
||||
.accounts
|
||||
.filterKeys { it != userId }
|
||||
val ableToSwitchToNewAccount = switchUserIfAvailable(
|
||||
currentUserId = userId,
|
||||
isExpired = isExpired,
|
||||
removeCurrentUserFromAccounts = true,
|
||||
)
|
||||
|
||||
// Check if there is a new active user
|
||||
if (updatedAccounts.isNotEmpty()) {
|
||||
if (userId == currentUserState.activeUserId && !isExpired) {
|
||||
showToast(message = R.string.account_switched_automatically)
|
||||
}
|
||||
|
||||
// If we logged out a non-active user, we want to leave the active user unchanged.
|
||||
// If we logged out the active user, we want to set the active user to the first one
|
||||
// in the list.
|
||||
val updatedActiveUserId = currentUserState
|
||||
.activeUserId
|
||||
.takeUnless { it == userId }
|
||||
?: updatedAccounts.entries.first().key
|
||||
|
||||
// Update the user information and emit an updated token
|
||||
authDiskSource.userState = currentUserState.copy(
|
||||
activeUserId = updatedActiveUserId,
|
||||
accounts = updatedAccounts,
|
||||
)
|
||||
} else {
|
||||
if (!ableToSwitchToNewAccount) {
|
||||
// Update the user information and log out
|
||||
authDiskSource.userState = null
|
||||
}
|
||||
|
||||
clearData(userId = userId)
|
||||
mutableLogoutEventFlow.tryEmit(LogoutEvent(loggedOutUserId = userId))
|
||||
}
|
||||
|
||||
override fun softLogout(userId: String) {
|
||||
@@ -82,7 +74,10 @@ class UserLogoutManagerImpl(
|
||||
val vaultTimeoutInMinutes = settingsDiskSource.getVaultTimeoutInMinutes(userId = userId)
|
||||
val vaultTimeoutAction = settingsDiskSource.getVaultTimeoutAction(userId = userId)
|
||||
|
||||
switchUserIfAvailable(currentUserId = userId, removeCurrentUserFromAccounts = false)
|
||||
|
||||
clearData(userId = userId)
|
||||
mutableLogoutEventFlow.tryEmit(LogoutEvent(loggedOutUserId = userId))
|
||||
|
||||
// Restore data that is still required
|
||||
settingsDiskSource.apply {
|
||||
@@ -112,4 +107,46 @@ class UserLogoutManagerImpl(
|
||||
private fun showToast(@StringRes message: Int) {
|
||||
mainScope.launch { Toast.makeText(context, message, Toast.LENGTH_SHORT).show() }
|
||||
}
|
||||
|
||||
private fun switchUserIfAvailable(
|
||||
currentUserId: String,
|
||||
removeCurrentUserFromAccounts: Boolean,
|
||||
isExpired: Boolean = false,
|
||||
): Boolean {
|
||||
val currentUserState = authDiskSource.userState ?: return false
|
||||
|
||||
val currentAccountsMap = currentUserState.accounts
|
||||
|
||||
// Remove the active user from the accounts map
|
||||
val updatedAccounts = currentAccountsMap
|
||||
.filterKeys { it != currentUserId }
|
||||
|
||||
// Check if there is a new active user
|
||||
return if (updatedAccounts.isNotEmpty()) {
|
||||
if (currentUserId == currentUserState.activeUserId && !isExpired) {
|
||||
showToast(message = R.string.account_switched_automatically)
|
||||
}
|
||||
|
||||
// If we logged out a non-active user, we want to leave the active user unchanged.
|
||||
// If we logged out the active user, we want to set the active user to the first one
|
||||
// in the list.
|
||||
val updatedActiveUserId = currentUserState
|
||||
.activeUserId
|
||||
.takeUnless { it == currentUserId }
|
||||
?: updatedAccounts.entries.first().key
|
||||
|
||||
// Update the user information and emit an updated token
|
||||
authDiskSource.userState = currentUserState.copy(
|
||||
activeUserId = updatedActiveUserId,
|
||||
accounts = if (removeCurrentUserFromAccounts) {
|
||||
updatedAccounts
|
||||
} else {
|
||||
currentAccountsMap
|
||||
},
|
||||
)
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
package com.x8bit.bitwarden.data.auth.manager.model
|
||||
|
||||
/**
|
||||
* Result class to share the [loggedOutUserId] of a user
|
||||
* that was successfully logged out.
|
||||
*/
|
||||
data class LogoutEvent(
|
||||
val loggedOutUserId: String,
|
||||
)
|
||||
@@ -19,10 +19,12 @@ import com.x8bit.bitwarden.data.auth.repository.model.RegisterResult
|
||||
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
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.SendVerificationEmailResult
|
||||
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.UserState
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.ValidatePasswordResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.ValidatePinResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.VerifyOtpResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.util.CaptchaCallbackTokenResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.util.DuoCallbackTokenResult
|
||||
@@ -129,6 +131,12 @@ interface AuthRepository : AuthenticatorProvider, AuthRequestManager {
|
||||
*/
|
||||
val organizations: List<SyncResponseJson.Profile.Organization>
|
||||
|
||||
/**
|
||||
* Whether or not the welcome carousel should be displayed, based on the feature flag and
|
||||
* whether the user has ever logged in or created an account before.
|
||||
*/
|
||||
val showWelcomeCarousel: Boolean
|
||||
|
||||
/**
|
||||
* Clears the pending deletion state that occurs when the an account is successfully deleted.
|
||||
*/
|
||||
@@ -238,11 +246,6 @@ interface AuthRepository : AuthenticatorProvider, AuthRequestManager {
|
||||
*/
|
||||
fun switchAccount(userId: String): SwitchAccountResult
|
||||
|
||||
/**
|
||||
* Updates the "last active time" for the current user.
|
||||
*/
|
||||
fun updateLastActiveTime()
|
||||
|
||||
/**
|
||||
* Attempt to register a new account with the given parameters.
|
||||
*/
|
||||
@@ -251,6 +254,7 @@ interface AuthRepository : AuthenticatorProvider, AuthRequestManager {
|
||||
email: String,
|
||||
masterPassword: String,
|
||||
masterPasswordHint: String?,
|
||||
emailVerificationToken: String? = null,
|
||||
captchaToken: String?,
|
||||
shouldCheckDataBreaches: Boolean,
|
||||
isMasterPasswordStrong: Boolean,
|
||||
@@ -342,9 +346,23 @@ interface AuthRepository : AuthenticatorProvider, AuthRequestManager {
|
||||
*/
|
||||
suspend fun validatePassword(password: String): ValidatePasswordResult
|
||||
|
||||
/**
|
||||
* Validates the PIN for the current logged in user.
|
||||
*/
|
||||
suspend fun validatePin(pin: String): ValidatePinResult
|
||||
|
||||
/**
|
||||
* Validates the given [password] against the master password
|
||||
* policies for the current user.
|
||||
*/
|
||||
suspend fun validatePasswordAgainstPolicies(password: String): Boolean
|
||||
|
||||
/**
|
||||
* Send a verification email.
|
||||
*/
|
||||
suspend fun sendVerificationEmail(
|
||||
email: String,
|
||||
name: String,
|
||||
receiveMarketingEmails: Boolean,
|
||||
): SendVerificationEmailResult
|
||||
}
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
package com.x8bit.bitwarden.data.auth.repository
|
||||
|
||||
import android.os.SystemClock
|
||||
import com.bitwarden.core.AuthRequestMethod
|
||||
import com.bitwarden.core.InitUserCryptoMethod
|
||||
import com.bitwarden.core.InitUserCryptoRequest
|
||||
import com.bitwarden.crypto.HashPurpose
|
||||
import com.bitwarden.crypto.Kdf
|
||||
import com.x8bit.bitwarden.data.auth.datasource.disk.AuthDiskSource
|
||||
import com.x8bit.bitwarden.data.auth.datasource.disk.model.AccountJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.disk.model.AccountTokensJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.disk.model.ForcePasswordResetReason
|
||||
import com.x8bit.bitwarden.data.auth.datasource.disk.model.UserStateJson
|
||||
@@ -15,10 +16,12 @@ import com.x8bit.bitwarden.data.auth.datasource.network.model.GetTokenResponseJs
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.IdentityTokenAuthModel
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.PasswordHintResponseJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.RefreshTokenResponseJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.RegisterFinishRequestJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.RegisterRequestJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.RegisterResponseJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.ResendEmailRequestJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.ResetPasswordRequestJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.SendVerificationEmailRequestJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.SetPasswordRequestJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.TrustedDeviceUserDecryptionOptionsJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.TwoFactorAuthMethod
|
||||
@@ -49,14 +52,17 @@ import com.x8bit.bitwarden.data.auth.repository.model.RegisterResult
|
||||
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
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.SendVerificationEmailResult
|
||||
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.UserOrganizations
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.UserState
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.ValidatePasswordResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.ValidatePinResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.VaultUnlockType
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.VerifyOtpResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.toLoginErrorResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.util.CaptchaCallbackTokenResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.util.DuoCallbackTokenResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.util.SsoCallbackResult
|
||||
@@ -74,9 +80,11 @@ import com.x8bit.bitwarden.data.auth.repository.util.userSwitchingChangesFlow
|
||||
import com.x8bit.bitwarden.data.auth.util.KdfParamsConstants.DEFAULT_PBKDF2_ITERATIONS
|
||||
import com.x8bit.bitwarden.data.auth.util.YubiKeyResult
|
||||
import com.x8bit.bitwarden.data.auth.util.toSdkParams
|
||||
import com.x8bit.bitwarden.data.platform.manager.FeatureFlagManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.PolicyManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.PushManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.dispatcher.DispatcherManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.model.FlagKey
|
||||
import com.x8bit.bitwarden.data.platform.manager.util.getActivePolicies
|
||||
import com.x8bit.bitwarden.data.platform.repository.EnvironmentRepository
|
||||
import com.x8bit.bitwarden.data.platform.repository.SettingsRepository
|
||||
@@ -87,8 +95,10 @@ import com.x8bit.bitwarden.data.platform.util.flatMap
|
||||
import com.x8bit.bitwarden.data.vault.datasource.network.model.PolicyTypeJson
|
||||
import com.x8bit.bitwarden.data.vault.datasource.network.model.SyncResponseJson
|
||||
import com.x8bit.bitwarden.data.vault.datasource.sdk.VaultSdkSource
|
||||
import com.x8bit.bitwarden.data.vault.datasource.sdk.model.InitializeCryptoResult
|
||||
import com.x8bit.bitwarden.data.vault.repository.VaultRepository
|
||||
import com.x8bit.bitwarden.data.vault.repository.model.VaultUnlockData
|
||||
import com.x8bit.bitwarden.data.vault.repository.model.VaultUnlockError
|
||||
import com.x8bit.bitwarden.data.vault.repository.model.VaultUnlockResult
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
@@ -135,9 +145,9 @@ class AuthRepositoryImpl(
|
||||
private val trustedDeviceManager: TrustedDeviceManager,
|
||||
private val userLogoutManager: UserLogoutManager,
|
||||
private val policyManager: PolicyManager,
|
||||
private val featureFlagManager: FeatureFlagManager,
|
||||
pushManager: PushManager,
|
||||
dispatcherManager: DispatcherManager,
|
||||
private val elapsedRealtimeMillisProvider: () -> Long = { SystemClock.elapsedRealtime() },
|
||||
) : AuthRepository,
|
||||
AuthRequestManager by authRequestManager {
|
||||
/**
|
||||
@@ -314,6 +324,10 @@ class AuthRepositoryImpl(
|
||||
override val organizations: List<SyncResponseJson.Profile.Organization>
|
||||
get() = activeUserId?.let { authDiskSource.getOrganizations(it) }.orEmpty()
|
||||
|
||||
override val showWelcomeCarousel: Boolean
|
||||
get() = !settingsRepository.hasUserLoggedInOrCreatedAccount &&
|
||||
featureFlagManager.getFeatureFlag(FlagKey.OnboardingCarousel)
|
||||
|
||||
init {
|
||||
pushManager
|
||||
.syncOrgKeysFlow
|
||||
@@ -456,7 +470,8 @@ class AuthRepositoryImpl(
|
||||
userId = userId,
|
||||
email = account.profile.email,
|
||||
orgPublicKey = organizationKeys.publicKey,
|
||||
rememberDevice = authDiskSource.getShouldTrustDevice(userId = userId),
|
||||
rememberDevice = authDiskSource
|
||||
.getShouldTrustDevice(userId = userId) == true,
|
||||
)
|
||||
}
|
||||
.flatMap { keys ->
|
||||
@@ -508,18 +523,21 @@ class AuthRepositoryImpl(
|
||||
val userId = profile.userId
|
||||
val privateKey = authDiskSource.getPrivateKey(userId = userId)
|
||||
?: return LoginResult.Error(errorMessage = null)
|
||||
vaultRepository.unlockVault(
|
||||
userId = userId,
|
||||
email = profile.email,
|
||||
kdf = profile.toSdkParams(),
|
||||
privateKey = privateKey,
|
||||
initUserCryptoMethod = InitUserCryptoMethod.AuthRequest(
|
||||
requestPrivateKey = requestPrivateKey,
|
||||
method = AuthRequestMethod.UserKey(protectedUserKey = asymmetricalKey),
|
||||
),
|
||||
// We should already have the org keys from the login sync.
|
||||
organizationKeys = authDiskSource.getOrganizationKeys(userId = userId),
|
||||
)
|
||||
|
||||
checkForVaultUnlockError(
|
||||
onVaultUnlockError = { error ->
|
||||
return error.toLoginErrorResult()
|
||||
},
|
||||
) {
|
||||
unlockVault(
|
||||
accountProfile = profile,
|
||||
privateKey = privateKey,
|
||||
initUserCryptoMethod = InitUserCryptoMethod.AuthRequest(
|
||||
requestPrivateKey = requestPrivateKey,
|
||||
method = AuthRequestMethod.UserKey(protectedUserKey = asymmetricalKey),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
authDiskSource.storeUserKey(userId = userId, userKey = asymmetricalKey)
|
||||
vaultRepository.syncIfNecessary()
|
||||
@@ -704,19 +722,12 @@ class AuthRepositoryImpl(
|
||||
return SwitchAccountResult.AccountSwitched
|
||||
}
|
||||
|
||||
override fun updateLastActiveTime() {
|
||||
val userId = activeUserId ?: return
|
||||
authDiskSource.storeLastActiveTimeMillis(
|
||||
userId = userId,
|
||||
lastActiveTimeMillis = elapsedRealtimeMillisProvider(),
|
||||
)
|
||||
}
|
||||
|
||||
@Suppress("LongMethod")
|
||||
override suspend fun register(
|
||||
email: String,
|
||||
masterPassword: String,
|
||||
masterPasswordHint: String?,
|
||||
emailVerificationToken: String?,
|
||||
captchaToken: String?,
|
||||
shouldCheckDataBreaches: Boolean,
|
||||
isMasterPasswordStrong: Boolean,
|
||||
@@ -745,21 +756,40 @@ class AuthRepositoryImpl(
|
||||
kdf = kdf,
|
||||
)
|
||||
.flatMap { registerKeyResponse ->
|
||||
identityService.register(
|
||||
body = RegisterRequestJson(
|
||||
email = email,
|
||||
masterPasswordHash = registerKeyResponse.masterPasswordHash,
|
||||
masterPasswordHint = masterPasswordHint,
|
||||
captchaResponse = captchaToken,
|
||||
key = registerKeyResponse.encryptedUserKey,
|
||||
keys = RegisterRequestJson.Keys(
|
||||
publicKey = registerKeyResponse.keys.public,
|
||||
encryptedPrivateKey = registerKeyResponse.keys.private,
|
||||
if (emailVerificationToken == null) {
|
||||
identityService.register(
|
||||
body = RegisterRequestJson(
|
||||
email = email,
|
||||
masterPasswordHash = registerKeyResponse.masterPasswordHash,
|
||||
masterPasswordHint = masterPasswordHint,
|
||||
captchaResponse = captchaToken,
|
||||
key = registerKeyResponse.encryptedUserKey,
|
||||
keys = RegisterRequestJson.Keys(
|
||||
publicKey = registerKeyResponse.keys.public,
|
||||
encryptedPrivateKey = registerKeyResponse.keys.private,
|
||||
),
|
||||
kdfType = kdf.toKdfTypeJson(),
|
||||
kdfIterations = kdf.iterations,
|
||||
),
|
||||
kdfType = kdf.toKdfTypeJson(),
|
||||
kdfIterations = kdf.iterations,
|
||||
),
|
||||
)
|
||||
)
|
||||
} else {
|
||||
identityService.registerFinish(
|
||||
body = RegisterFinishRequestJson(
|
||||
email = email,
|
||||
masterPasswordHash = registerKeyResponse.masterPasswordHash,
|
||||
masterPasswordHint = masterPasswordHint,
|
||||
emailVerificationToken = emailVerificationToken,
|
||||
captchaResponse = captchaToken,
|
||||
userSymmetricKey = registerKeyResponse.encryptedUserKey,
|
||||
userAsymmetricKeys = RegisterFinishRequestJson.Keys(
|
||||
publicKey = registerKeyResponse.keys.public,
|
||||
encryptedPrivateKey = registerKeyResponse.keys.private,
|
||||
),
|
||||
kdfType = kdf.toKdfTypeJson(),
|
||||
kdfIterations = kdf.iterations,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
.fold(
|
||||
onSuccess = {
|
||||
@@ -954,9 +984,9 @@ class AuthRepositoryImpl(
|
||||
)
|
||||
}
|
||||
|
||||
VaultUnlockResult.AuthenticationError,
|
||||
VaultUnlockResult.GenericError,
|
||||
is VaultUnlockResult.AuthenticationError,
|
||||
VaultUnlockResult.InvalidStateError,
|
||||
VaultUnlockResult.GenericError,
|
||||
-> {
|
||||
IllegalStateException("Failed to unlock vault").asFailure()
|
||||
}
|
||||
@@ -1103,11 +1133,78 @@ class AuthRepositoryImpl(
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun validatePin(pin: String): ValidatePinResult {
|
||||
val activeAccount = authDiskSource
|
||||
.userState
|
||||
?.activeAccount
|
||||
?.profile
|
||||
?: return ValidatePinResult.Error
|
||||
val privateKey = authDiskSource
|
||||
.getPrivateKey(userId = activeAccount.userId)
|
||||
?: return ValidatePinResult.Error
|
||||
val pinProtectedUserKey = authDiskSource
|
||||
.getPinProtectedUserKey(userId = activeAccount.userId)
|
||||
?: return ValidatePinResult.Error
|
||||
|
||||
// HACK: As the SDK doesn't provide a way to directly validate the pin yet, we instead
|
||||
// try to initialize the user crypto, and if it succeeds then the PIN is correct, otherwise
|
||||
// the PIN is incorrect.
|
||||
return vaultSdkSource
|
||||
.initializeCrypto(
|
||||
userId = activeAccount.userId,
|
||||
request = InitUserCryptoRequest(
|
||||
kdfParams = activeAccount.toSdkParams(),
|
||||
email = activeAccount.email,
|
||||
privateKey = privateKey,
|
||||
method = InitUserCryptoMethod.Pin(
|
||||
pin = pin,
|
||||
pinProtectedUserKey = pinProtectedUserKey,
|
||||
),
|
||||
),
|
||||
)
|
||||
.fold(
|
||||
onSuccess = {
|
||||
when (it) {
|
||||
InitializeCryptoResult.Success -> {
|
||||
ValidatePinResult.Success(isValid = true)
|
||||
}
|
||||
|
||||
is InitializeCryptoResult.AuthenticationError -> {
|
||||
ValidatePinResult.Success(isValid = false)
|
||||
}
|
||||
}
|
||||
},
|
||||
onFailure = { ValidatePinResult.Error },
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun validatePasswordAgainstPolicies(
|
||||
password: String,
|
||||
): Boolean = passwordPolicies
|
||||
.all { validatePasswordAgainstPolicy(password, it) }
|
||||
|
||||
override suspend fun sendVerificationEmail(
|
||||
email: String,
|
||||
name: String,
|
||||
receiveMarketingEmails: Boolean,
|
||||
): SendVerificationEmailResult =
|
||||
identityService
|
||||
.sendVerificationEmail(
|
||||
SendVerificationEmailRequestJson(
|
||||
email = email,
|
||||
name = name,
|
||||
receiveMarketingEmails = receiveMarketingEmails,
|
||||
),
|
||||
)
|
||||
.fold(
|
||||
onSuccess = {
|
||||
SendVerificationEmailResult.Success(it)
|
||||
},
|
||||
onFailure = {
|
||||
SendVerificationEmailResult.Error(null)
|
||||
},
|
||||
)
|
||||
|
||||
@Suppress("CyclomaticComplexMethod")
|
||||
private suspend fun validatePasswordAgainstPolicy(
|
||||
password: String,
|
||||
@@ -1317,6 +1414,52 @@ class AuthRepositoryImpl(
|
||||
environmentUrlData = environmentRepository.environment.environmentUrlData,
|
||||
)
|
||||
val userId = userStateJson.activeUserId
|
||||
|
||||
checkForVaultUnlockError(
|
||||
onVaultUnlockError = { vaultUnlockError ->
|
||||
return@userStateTransaction vaultUnlockError.toLoginErrorResult()
|
||||
},
|
||||
) {
|
||||
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,
|
||||
deviceData = deviceData,
|
||||
)
|
||||
} else {
|
||||
password?.let {
|
||||
unlockVaultWithPasswordOnLoginSuccess(
|
||||
loginResponse = loginResponse,
|
||||
userStateJson = userStateJson,
|
||||
password = it,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
password?.let {
|
||||
// Save the master password hash.
|
||||
authSdkSource
|
||||
.hashPassword(
|
||||
email = email,
|
||||
password = it,
|
||||
kdf = userStateJson.activeAccount.profile.toSdkParams(),
|
||||
purpose = HashPurpose.LOCAL_AUTHORIZATION,
|
||||
)
|
||||
.onSuccess { passwordHash ->
|
||||
authDiskSource.storeMasterPasswordHash(
|
||||
userId = userId,
|
||||
passwordHash = passwordHash,
|
||||
)
|
||||
}
|
||||
|
||||
// Cache the password to verify against any password policies after the sync completes.
|
||||
passwordsToCheckMap.put(userId, it)
|
||||
}
|
||||
|
||||
authDiskSource.storeAccountTokens(
|
||||
userId = userId,
|
||||
accountTokens = AccountTokensJson(
|
||||
@@ -1345,161 +1488,17 @@ class AuthRepositoryImpl(
|
||||
organizationIdentifier = orgIdentifier
|
||||
}
|
||||
|
||||
// Handle the Trusted Device Encryption flow
|
||||
loginResponse.userDecryptionOptions?.trustedDeviceUserDecryptionOptions?.let { options ->
|
||||
loginResponse.privateKey?.let { privateKey ->
|
||||
handleLoginCommonSuccessTrustedDeviceUserDecryptionOptions(
|
||||
trustedDeviceDecryptionOptions = options,
|
||||
userStateJson = userStateJson,
|
||||
privateKey = privateKey,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Remove any cached data after successfully logging in.
|
||||
identityTokenAuthModel = null
|
||||
twoFactorResponse = null
|
||||
resendEmailRequestJson = null
|
||||
twoFactorDeviceData = null
|
||||
|
||||
// Attempt to unlock the vault with password if possible.
|
||||
password?.let {
|
||||
if (loginResponse.privateKey != null && loginResponse.key != null) {
|
||||
vaultRepository.unlockVault(
|
||||
userId = userId,
|
||||
email = userStateJson.activeAccount.profile.email,
|
||||
kdf = userStateJson.activeAccount.profile.toSdkParams(),
|
||||
userKey = loginResponse.key,
|
||||
privateKey = loginResponse.privateKey,
|
||||
masterPassword = it,
|
||||
// We can separately unlock vault for organization data after
|
||||
// receiving the sync response if this data is currently absent.
|
||||
organizationKeys = null,
|
||||
)
|
||||
}
|
||||
|
||||
// Save the master password hash.
|
||||
authSdkSource
|
||||
.hashPassword(
|
||||
email = email,
|
||||
password = it,
|
||||
kdf = userStateJson.activeAccount.profile.toSdkParams(),
|
||||
purpose = HashPurpose.LOCAL_AUTHORIZATION,
|
||||
)
|
||||
.onSuccess { passwordHash ->
|
||||
authDiskSource.storeMasterPasswordHash(
|
||||
userId = userId,
|
||||
passwordHash = passwordHash,
|
||||
)
|
||||
}
|
||||
|
||||
// Cache the password to verify against any password policies after the sync completes.
|
||||
passwordsToCheckMap.put(userId, it)
|
||||
}
|
||||
|
||||
// Attempt to unlock the vault with auth request if possible.
|
||||
// These values will only be null during the Just-in-Time provisioning flow.
|
||||
if (loginResponse.privateKey != null && loginResponse.key != null) {
|
||||
deviceData?.let { model ->
|
||||
vaultRepository.unlockVault(
|
||||
userId = userId,
|
||||
email = userStateJson.activeAccount.profile.email,
|
||||
kdf = userStateJson.activeAccount.profile.toSdkParams(),
|
||||
privateKey = loginResponse.privateKey,
|
||||
initUserCryptoMethod = InitUserCryptoMethod.AuthRequest(
|
||||
requestPrivateKey = model.privateKey,
|
||||
method = model
|
||||
.masterPasswordHash
|
||||
?.let {
|
||||
AuthRequestMethod.MasterKey(
|
||||
protectedMasterKey = model.asymmetricalKey,
|
||||
authRequestKey = loginResponse.key,
|
||||
)
|
||||
}
|
||||
?: AuthRequestMethod.UserKey(protectedUserKey = model.asymmetricalKey),
|
||||
),
|
||||
// We can separately unlock vault for organization data after
|
||||
// receiving the sync response if this data is currently absent.
|
||||
organizationKeys = null,
|
||||
)
|
||||
// We are purposely not storing the master password hash here since it is not
|
||||
// formatted in in a manner that we can use. We will store it properly the next
|
||||
// time the user enters their master password and it is validated.
|
||||
}
|
||||
}
|
||||
|
||||
settingsRepository.setDefaultsIfNecessary(userId = userId)
|
||||
vaultRepository.syncIfNecessary()
|
||||
hasPendingAccountAddition = false
|
||||
LoginResult.Success
|
||||
}
|
||||
|
||||
/**
|
||||
* A helper method to handle the [TrustedDeviceUserDecryptionOptionsJson] specific to TDE.
|
||||
*/
|
||||
private suspend fun handleLoginCommonSuccessTrustedDeviceUserDecryptionOptions(
|
||||
trustedDeviceDecryptionOptions: TrustedDeviceUserDecryptionOptionsJson,
|
||||
userStateJson: UserStateJson,
|
||||
privateKey: String,
|
||||
) {
|
||||
val userId = userStateJson.activeUserId
|
||||
val deviceKey = authDiskSource.getDeviceKey(userId = userId)
|
||||
if (deviceKey == null) {
|
||||
// A null device key means this device is not trusted.
|
||||
val pendingRequest = authDiskSource.getPendingAuthRequest(userId = userId) ?: return
|
||||
authRequestManager
|
||||
.getAuthRequestIfApproved(pendingRequest.requestId)
|
||||
.getOrNull()
|
||||
?.let { request ->
|
||||
// For approved requests the key will always be present.
|
||||
val userKey = requireNotNull(request.key)
|
||||
vaultRepository.unlockVault(
|
||||
userId = userId,
|
||||
email = userStateJson.activeAccount.profile.email,
|
||||
kdf = userStateJson.activeAccount.profile.toSdkParams(),
|
||||
privateKey = privateKey,
|
||||
initUserCryptoMethod = InitUserCryptoMethod.AuthRequest(
|
||||
requestPrivateKey = pendingRequest.requestPrivateKey,
|
||||
method = AuthRequestMethod.UserKey(protectedUserKey = userKey),
|
||||
),
|
||||
// We can separately unlock vault for organization data after
|
||||
// receiving the sync response if this data is currently absent.
|
||||
organizationKeys = null,
|
||||
)
|
||||
authDiskSource.storeUserKey(userId = userId, userKey = userKey)
|
||||
}
|
||||
authDiskSource.storePendingAuthRequest(
|
||||
userId = userId,
|
||||
pendingAuthRequest = null,
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
val encryptedPrivateKey = trustedDeviceDecryptionOptions.encryptedPrivateKey
|
||||
val encryptedUserKey = trustedDeviceDecryptionOptions.encryptedUserKey
|
||||
if (encryptedPrivateKey == null || encryptedUserKey == null) {
|
||||
// If we have a device key but server is missing private key and user key, we
|
||||
// need to clear the device key and let the user go through the TDE flow again.
|
||||
authDiskSource.storeDeviceKey(userId = userId, deviceKey = null)
|
||||
return
|
||||
}
|
||||
vaultRepository.unlockVault(
|
||||
userId = userId,
|
||||
email = userStateJson.activeAccount.profile.email,
|
||||
kdf = userStateJson.activeAccount.profile.toSdkParams(),
|
||||
privateKey = privateKey,
|
||||
initUserCryptoMethod = InitUserCryptoMethod.DeviceKey(
|
||||
deviceKey = deviceKey,
|
||||
protectedDevicePrivateKey = encryptedPrivateKey,
|
||||
deviceProtectedUserKey = encryptedUserKey,
|
||||
),
|
||||
// We can separately unlock vault for organization data after
|
||||
// receiving the sync response if this data is currently absent.
|
||||
organizationKeys = null,
|
||||
)
|
||||
authDiskSource.storeUserKey(userId = userId, userKey = encryptedUserKey)
|
||||
}
|
||||
|
||||
/**
|
||||
* A helper method that processes the [GetTokenResponseJson.TwoFactorRequired] when logging in.
|
||||
*/
|
||||
@@ -1525,6 +1524,182 @@ class AuthRepositoryImpl(
|
||||
return LoginResult.TwoFactorRequired
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempt to unlock the current user's vault with password data.
|
||||
*/
|
||||
private suspend fun unlockVaultWithPasswordOnLoginSuccess(
|
||||
loginResponse: GetTokenResponseJson.Success,
|
||||
userStateJson: UserStateJson,
|
||||
password: String?,
|
||||
): VaultUnlockResult? {
|
||||
// Attempt to unlock the vault with password if possible.
|
||||
val masterPassword = password ?: return null
|
||||
val privateKey = loginResponse.privateKey ?: return null
|
||||
val key = loginResponse.key ?: return null
|
||||
return unlockVault(
|
||||
accountProfile = userStateJson.activeAccount.profile,
|
||||
privateKey = privateKey,
|
||||
initUserCryptoMethod = InitUserCryptoMethod.Password(
|
||||
password = masterPassword,
|
||||
userKey = key,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempt to unlock the current user's vault with trusted device specific data.
|
||||
*/
|
||||
private suspend fun unlockVaultWithTdeOnLoginSuccess(
|
||||
loginResponse: GetTokenResponseJson.Success,
|
||||
userStateJson: UserStateJson,
|
||||
deviceData: DeviceDataModel?,
|
||||
): VaultUnlockResult? {
|
||||
// Attempt to unlock the vault with auth request if possible.
|
||||
// These values will only be null during the Just-in-Time provisioning flow.
|
||||
if (loginResponse.privateKey != null && loginResponse.key != null) {
|
||||
deviceData?.let { model ->
|
||||
return unlockVault(
|
||||
accountProfile = userStateJson.activeAccount.profile,
|
||||
privateKey = loginResponse.privateKey,
|
||||
initUserCryptoMethod = InitUserCryptoMethod.AuthRequest(
|
||||
requestPrivateKey = model.privateKey,
|
||||
method = model
|
||||
.masterPasswordHash
|
||||
?.let {
|
||||
AuthRequestMethod.MasterKey(
|
||||
protectedMasterKey = model.asymmetricalKey,
|
||||
authRequestKey = loginResponse.key,
|
||||
)
|
||||
}
|
||||
?: AuthRequestMethod.UserKey(protectedUserKey = model.asymmetricalKey),
|
||||
),
|
||||
)
|
||||
// We are purposely not storing the master password hash here since it is not
|
||||
// formatted in in a manner that we can use. We will store it properly the next
|
||||
// time the user enters their master password and it is validated.
|
||||
}
|
||||
}
|
||||
|
||||
// Handle the Trusted Device Encryption flow
|
||||
return loginResponse
|
||||
.userDecryptionOptions
|
||||
?.trustedDeviceUserDecryptionOptions
|
||||
?.let { options ->
|
||||
loginResponse.privateKey?.let { privateKey ->
|
||||
unlockVaultWithTrustedDeviceUserDecryptionOptionsAndStoreKeys(
|
||||
options = options,
|
||||
userStateJson = userStateJson,
|
||||
privateKey = privateKey,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A helper method to handle the [TrustedDeviceUserDecryptionOptionsJson] specific to TDE
|
||||
* and store the necessary keys when appropriate.
|
||||
*/
|
||||
private suspend fun unlockVaultWithTrustedDeviceUserDecryptionOptionsAndStoreKeys(
|
||||
options: TrustedDeviceUserDecryptionOptionsJson,
|
||||
userStateJson: UserStateJson,
|
||||
privateKey: String,
|
||||
): VaultUnlockResult? {
|
||||
var vaultUnlockResult: VaultUnlockResult? = null
|
||||
val userId = userStateJson.activeUserId
|
||||
val deviceKey = authDiskSource.getDeviceKey(userId = userId)
|
||||
if (deviceKey == null) {
|
||||
// A null device key means this device is not trusted.
|
||||
val pendingRequest = authDiskSource
|
||||
.getPendingAuthRequest(userId = userId)
|
||||
?: return null
|
||||
authRequestManager
|
||||
.getAuthRequestIfApproved(pendingRequest.requestId)
|
||||
.getOrNull()
|
||||
?.let { request ->
|
||||
// For approved requests the key will always be present.
|
||||
val userKey = requireNotNull(request.key)
|
||||
vaultUnlockResult = unlockVault(
|
||||
accountProfile = userStateJson.activeAccount.profile,
|
||||
privateKey = privateKey,
|
||||
initUserCryptoMethod = InitUserCryptoMethod.AuthRequest(
|
||||
requestPrivateKey = pendingRequest.requestPrivateKey,
|
||||
method = AuthRequestMethod.UserKey(protectedUserKey = userKey),
|
||||
),
|
||||
)
|
||||
authDiskSource.storeUserKey(userId = userId, userKey = userKey)
|
||||
}
|
||||
authDiskSource.storePendingAuthRequest(
|
||||
userId = userId,
|
||||
pendingAuthRequest = null,
|
||||
)
|
||||
return vaultUnlockResult
|
||||
}
|
||||
|
||||
val encryptedPrivateKey = options.encryptedPrivateKey
|
||||
val encryptedUserKey = options.encryptedUserKey
|
||||
|
||||
if (encryptedPrivateKey == null || encryptedUserKey == null) {
|
||||
// If we have a device key but server is missing private key and user key, we
|
||||
// need to clear the device key and let the user go through the TDE flow again.
|
||||
authDiskSource.storeDeviceKey(userId = userId, deviceKey = null)
|
||||
return null
|
||||
}
|
||||
|
||||
vaultUnlockResult = unlockVault(
|
||||
accountProfile = userStateJson.activeAccount.profile,
|
||||
privateKey = privateKey,
|
||||
initUserCryptoMethod = InitUserCryptoMethod.DeviceKey(
|
||||
deviceKey = deviceKey,
|
||||
protectedDevicePrivateKey = encryptedPrivateKey,
|
||||
deviceProtectedUserKey = encryptedUserKey,
|
||||
),
|
||||
)
|
||||
|
||||
if (vaultUnlockResult is VaultUnlockResult.Success) {
|
||||
authDiskSource.storeUserKey(userId = userId, userKey = encryptedUserKey)
|
||||
}
|
||||
return vaultUnlockResult
|
||||
}
|
||||
|
||||
/**
|
||||
* A helper function to unlock the vault for the user associated with the [accountProfile].
|
||||
*/
|
||||
private suspend fun unlockVault(
|
||||
accountProfile: AccountJson.Profile,
|
||||
privateKey: String,
|
||||
initUserCryptoMethod: InitUserCryptoMethod,
|
||||
): VaultUnlockResult {
|
||||
val userId = accountProfile.userId
|
||||
return vaultRepository.unlockVault(
|
||||
userId = userId,
|
||||
email = accountProfile.email,
|
||||
kdf = accountProfile.toSdkParams(),
|
||||
privateKey = privateKey,
|
||||
initUserCryptoMethod = initUserCryptoMethod,
|
||||
// The value for the organization keys here will typically be null. We can separately
|
||||
// unlock the vault for organization data after receiving the sync response if this
|
||||
// data is currently absent. These keys may be present during certain multi-phase login
|
||||
// processes or if we needed to delete the user's token due to an encrypted data
|
||||
// corruption issue and they are forced to log back in.
|
||||
organizationKeys = authDiskSource.getOrganizationKeys(userId = userId),
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* A helper function to check for a vault unlock related error when logging in.
|
||||
*
|
||||
* @param onVaultUnlockError a lambda function to be invoked in the event a [VaultUnlockError]
|
||||
* is produced via the passed in [block]
|
||||
* @param block a lambda representing logic which produces either a [VaultUnlockResult] which
|
||||
* is castable to [VaultUnlockError] or `null`
|
||||
*/
|
||||
private inline fun checkForVaultUnlockError(
|
||||
onVaultUnlockError: (VaultUnlockError) -> Unit,
|
||||
block: () -> VaultUnlockResult?,
|
||||
) {
|
||||
(block() as? VaultUnlockError)?.also(onVaultUnlockError)
|
||||
}
|
||||
|
||||
//endregion LoginCommon
|
||||
|
||||
/**
|
||||
|
||||
@@ -12,6 +12,7 @@ import com.x8bit.bitwarden.data.auth.manager.TrustedDeviceManager
|
||||
import com.x8bit.bitwarden.data.auth.manager.UserLogoutManager
|
||||
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
|
||||
import com.x8bit.bitwarden.data.auth.repository.AuthRepositoryImpl
|
||||
import com.x8bit.bitwarden.data.platform.manager.FeatureFlagManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.PolicyManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.PushManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.dispatcher.DispatcherManager
|
||||
@@ -52,6 +53,7 @@ object AuthRepositoryModule {
|
||||
userLogoutManager: UserLogoutManager,
|
||||
pushManager: PushManager,
|
||||
policyManager: PolicyManager,
|
||||
featureFlagManager: FeatureFlagManager,
|
||||
): AuthRepository = AuthRepositoryImpl(
|
||||
accountsService = accountsService,
|
||||
devicesService = devicesService,
|
||||
@@ -70,5 +72,6 @@ object AuthRepositoryModule {
|
||||
userLogoutManager = userLogoutManager,
|
||||
pushManager = pushManager,
|
||||
policyManager = policyManager,
|
||||
featureFlagManager = featureFlagManager,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
package com.x8bit.bitwarden.data.auth.repository.model
|
||||
|
||||
import com.x8bit.bitwarden.data.vault.repository.model.VaultUnlockError
|
||||
import com.x8bit.bitwarden.data.vault.repository.model.VaultUnlockResult
|
||||
|
||||
/**
|
||||
* Helper function to map a [VaultUnlockError] to a [LoginResult.Error] with
|
||||
* the necessary `message` if applicable.
|
||||
*/
|
||||
fun VaultUnlockError.toLoginErrorResult(): LoginResult.Error = when (this) {
|
||||
is VaultUnlockResult.AuthenticationError -> LoginResult.Error(this.message)
|
||||
VaultUnlockResult.GenericError,
|
||||
VaultUnlockResult.InvalidStateError,
|
||||
-> LoginResult.Error(errorMessage = null)
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package com.x8bit.bitwarden.data.auth.repository.model
|
||||
|
||||
/**
|
||||
* Models result of sending a verification email.
|
||||
*/
|
||||
sealed class SendVerificationEmailResult {
|
||||
/**
|
||||
* Email sent succeeded.
|
||||
*
|
||||
* @param emailVerificationToken the token to verify the email.
|
||||
*/
|
||||
data class Success(
|
||||
val emailVerificationToken: String?,
|
||||
) : SendVerificationEmailResult()
|
||||
|
||||
/**
|
||||
* There was an error sending the email.
|
||||
*
|
||||
* @param errorMessage a message describing the error.
|
||||
*/
|
||||
data class Error(val errorMessage: String?) : SendVerificationEmailResult()
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package com.x8bit.bitwarden.data.auth.repository.model
|
||||
|
||||
/**
|
||||
* Models result of determining if a PIN is valid.
|
||||
*/
|
||||
sealed class ValidatePinResult {
|
||||
/**
|
||||
* The validity of the PIN was checked successfully and [isValid].
|
||||
*/
|
||||
data class Success(
|
||||
val isValid: Boolean,
|
||||
) : ValidatePinResult()
|
||||
|
||||
/**
|
||||
* There was an error determining if the validity of the PIN.
|
||||
*/
|
||||
data object Error : ValidatePinResult()
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package com.x8bit.bitwarden.data.auth.util
|
||||
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import com.x8bit.bitwarden.data.platform.manager.model.CompleteRegistrationData
|
||||
|
||||
/**
|
||||
* Checks if the given [Intent] contains data to complete registration.
|
||||
* The [CompleteRegistrationData] will be returned when present.
|
||||
*/
|
||||
fun Intent.getCompleteRegistrationDataIntentOrNull(): CompleteRegistrationData? {
|
||||
val sanitizedUriString = data.toString().replace("/#/", "/")
|
||||
val uri = runCatching { Uri.parse(sanitizedUriString) }.getOrNull() ?: return null
|
||||
uri.host ?: return null
|
||||
if (uri.path != "/finish-signup") return null
|
||||
val email = uri.getQueryParameter("email") ?: return null
|
||||
val verificationToken = uri.getQueryParameter("token") ?: return null
|
||||
val fromEmail = uri.getBooleanQueryParameter("fromEmail", true)
|
||||
return CompleteRegistrationData(
|
||||
email = email,
|
||||
verificationToken = verificationToken,
|
||||
fromEmail = fromEmail,
|
||||
)
|
||||
}
|
||||
@@ -2,7 +2,6 @@ package com.x8bit.bitwarden.data.autofill.builder
|
||||
|
||||
import android.service.autofill.FillRequest
|
||||
import android.service.autofill.SaveInfo
|
||||
import com.x8bit.bitwarden.data.autofill.model.AutofillAppInfo
|
||||
import com.x8bit.bitwarden.data.autofill.model.AutofillPartition
|
||||
|
||||
/**
|
||||
@@ -12,13 +11,11 @@ interface SaveInfoBuilder {
|
||||
/**
|
||||
* Build a save info out the provided data. If that isn't possible, return null.
|
||||
*
|
||||
* @param autofillAppInfo App data that is required for building the [SaveInfo].
|
||||
* @param autofillPartition The portion of the processed [FillRequest] that will be filled.
|
||||
* @param fillRequest The [FillRequest] that initiated the autofill flow.
|
||||
* @param packageName The package name that was extracted from the [FillRequest].
|
||||
*/
|
||||
fun build(
|
||||
autofillAppInfo: AutofillAppInfo,
|
||||
autofillPartition: AutofillPartition,
|
||||
fillRequest: FillRequest,
|
||||
packageName: String?,
|
||||
|
||||
@@ -1,10 +1,7 @@
|
||||
package com.x8bit.bitwarden.data.autofill.builder
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.os.Build
|
||||
import android.service.autofill.FillRequest
|
||||
import android.service.autofill.SaveInfo
|
||||
import com.x8bit.bitwarden.data.autofill.model.AutofillAppInfo
|
||||
import com.x8bit.bitwarden.data.autofill.model.AutofillPartition
|
||||
import com.x8bit.bitwarden.data.platform.repository.SettingsRepository
|
||||
|
||||
@@ -16,9 +13,7 @@ class SaveInfoBuilderImpl(
|
||||
val settingsRepository: SettingsRepository,
|
||||
) : SaveInfoBuilder {
|
||||
|
||||
@SuppressLint("InlinedApi")
|
||||
override fun build(
|
||||
autofillAppInfo: AutofillAppInfo,
|
||||
autofillPartition: AutofillPartition,
|
||||
fillRequest: FillRequest,
|
||||
packageName: String?,
|
||||
@@ -29,12 +24,8 @@ class SaveInfoBuilderImpl(
|
||||
|
||||
// Docs state that password fields cannot be reliably saved
|
||||
// in Compat mode since they show as masked values.
|
||||
val isInCompatMode = if (autofillAppInfo.sdkInt >= Build.VERSION_CODES.Q) {
|
||||
// Attempt to automatically establish compat request mode on Android 10+
|
||||
(fillRequest.flags or FillRequest.FLAG_COMPATIBILITY_MODE_REQUEST) == fillRequest.flags
|
||||
} else {
|
||||
COMPAT_BROWSERS.contains(packageName)
|
||||
}
|
||||
val isInCompatMode = (fillRequest.flags or
|
||||
FillRequest.FLAG_COMPATIBILITY_MODE_REQUEST) == fillRequest.flags
|
||||
|
||||
// If login and compat mode, the password might be obfuscated,
|
||||
// in which case we should skip the save request.
|
||||
@@ -58,103 +49,3 @@ class SaveInfoBuilderImpl(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* These browsers function using the compatibility shim for the Autofill Framework.
|
||||
*
|
||||
* Ensure that these entries are sorted alphabetically and keep this list synchronized with the
|
||||
* values in /xml/autofill_service_configuration.xml and
|
||||
* /xml-v30/autofill_service_configuration.xml.
|
||||
*/
|
||||
private val COMPAT_BROWSERS: List<String> = listOf(
|
||||
"alook.browser",
|
||||
"alook.browser.google",
|
||||
"app.vanadium.browser",
|
||||
"com.amazon.cloud9",
|
||||
"com.android.browser",
|
||||
"com.android.chrome",
|
||||
"com.android.htmlviewer",
|
||||
"com.avast.android.secure.browser",
|
||||
"com.avg.android.secure.browser",
|
||||
"com.brave.browser",
|
||||
"com.brave.browser_beta",
|
||||
"com.brave.browser_default",
|
||||
"com.brave.browser_dev",
|
||||
"com.brave.browser_nightly",
|
||||
"com.chrome.beta",
|
||||
"com.chrome.canary",
|
||||
"com.chrome.dev",
|
||||
"com.cookiegames.smartcookie",
|
||||
"com.cookiejarapps.android.smartcookieweb",
|
||||
"com.ecosia.android",
|
||||
"com.google.android.apps.chrome",
|
||||
"com.google.android.apps.chrome_dev",
|
||||
"com.google.android.captiveportallogin",
|
||||
"com.iode.firefox",
|
||||
"com.jamal2367.styx",
|
||||
"com.kiwibrowser.browser",
|
||||
"com.kiwibrowser.browser.dev",
|
||||
"com.lemurbrowser.exts",
|
||||
"com.microsoft.emmx",
|
||||
"com.microsoft.emmx.beta",
|
||||
"com.microsoft.emmx.canary",
|
||||
"com.microsoft.emmx.dev",
|
||||
"com.mmbox.browser",
|
||||
"com.mmbox.xbrowser",
|
||||
"com.mycompany.app.soulbrowser",
|
||||
"com.naver.whale",
|
||||
"com.neeva.app",
|
||||
"com.opera.browser",
|
||||
"com.opera.browser.beta",
|
||||
"com.opera.gx",
|
||||
"com.opera.mini.native",
|
||||
"com.opera.mini.native.beta",
|
||||
"com.opera.touch",
|
||||
"com.qflair.browserq",
|
||||
"com.qwant.liberty",
|
||||
"com.rainsee.create",
|
||||
"com.sec.android.app.sbrowser",
|
||||
"com.sec.android.app.sbrowser.beta",
|
||||
"com.stoutner.privacybrowser.free",
|
||||
"com.stoutner.privacybrowser.standard",
|
||||
"com.vivaldi.browser",
|
||||
"com.vivaldi.browser.snapshot",
|
||||
"com.vivaldi.browser.sopranos",
|
||||
"com.yandex.browser",
|
||||
"com.yjllq.internet",
|
||||
"com.yjllq.kito",
|
||||
"com.yujian.ResideMenuDemo",
|
||||
"com.z28j.feel",
|
||||
"idm.internet.download.manager",
|
||||
"idm.internet.download.manager.adm.lite",
|
||||
"idm.internet.download.manager.plus",
|
||||
"io.github.forkmaintainers.iceraven",
|
||||
"mark.via",
|
||||
"mark.via.gp",
|
||||
"net.dezor.browser",
|
||||
"net.slions.fulguris.full.download",
|
||||
"net.slions.fulguris.full.download.debug",
|
||||
"net.slions.fulguris.full.playstore",
|
||||
"net.slions.fulguris.full.playstore.debug",
|
||||
"org.adblockplus.browser",
|
||||
"org.adblockplus.browser.beta",
|
||||
"org.bromite.bromite",
|
||||
"org.bromite.chromium",
|
||||
"org.chromium.chrome",
|
||||
"org.codeaurora.swe.browser",
|
||||
"org.cromite.cromite",
|
||||
"org.gnu.icecat",
|
||||
"org.mozilla.fenix",
|
||||
"org.mozilla.fenix.nightly",
|
||||
"org.mozilla.fennec_aurora",
|
||||
"org.mozilla.fennec_fdroid",
|
||||
"org.mozilla.firefox",
|
||||
"org.mozilla.firefox_beta",
|
||||
"org.mozilla.reference.browser",
|
||||
"org.mozilla.rocket",
|
||||
"org.torproject.torbrowser",
|
||||
"org.torproject.torbrowser_alpha",
|
||||
"org.ungoogled.chromium.extensions.stable",
|
||||
"org.ungoogled.chromium.stable",
|
||||
"us.spotco.fennec_dos",
|
||||
)
|
||||
|
||||
@@ -19,6 +19,7 @@ import com.x8bit.bitwarden.data.autofill.processor.AutofillProcessor
|
||||
import com.x8bit.bitwarden.data.autofill.processor.AutofillProcessorImpl
|
||||
import com.x8bit.bitwarden.data.autofill.provider.AutofillCipherProvider
|
||||
import com.x8bit.bitwarden.data.autofill.provider.AutofillCipherProviderImpl
|
||||
import com.x8bit.bitwarden.data.platform.manager.CrashLogsManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.PolicyManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.ciphermatching.CipherMatchingManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.clipboard.BitwardenClipboardManager
|
||||
@@ -72,6 +73,7 @@ object AutofillModule {
|
||||
organizationEventManager = organizationEventManager,
|
||||
)
|
||||
|
||||
@Singleton
|
||||
@Provides
|
||||
fun providesAutofillParser(
|
||||
settingsRepository: SettingsRepository,
|
||||
@@ -80,6 +82,7 @@ object AutofillModule {
|
||||
settingsRepository = settingsRepository,
|
||||
)
|
||||
|
||||
@Singleton
|
||||
@Provides
|
||||
fun providesAutofillCipherProvider(
|
||||
authRepository: AuthRepository,
|
||||
@@ -92,6 +95,7 @@ object AutofillModule {
|
||||
vaultRepository = vaultRepository,
|
||||
)
|
||||
|
||||
@Singleton
|
||||
@Provides
|
||||
fun providesAutofillProcessor(
|
||||
dispatcherManager: DispatcherManager,
|
||||
@@ -101,6 +105,7 @@ object AutofillModule {
|
||||
policyManager: PolicyManager,
|
||||
saveInfoBuilder: SaveInfoBuilder,
|
||||
settingsRepository: SettingsRepository,
|
||||
crashLogsManager: CrashLogsManager,
|
||||
): AutofillProcessor =
|
||||
AutofillProcessorImpl(
|
||||
dispatcherManager = dispatcherManager,
|
||||
@@ -110,8 +115,10 @@ object AutofillModule {
|
||||
policyManager = policyManager,
|
||||
saveInfoBuilder = saveInfoBuilder,
|
||||
settingsRepository = settingsRepository,
|
||||
crashLogsManager = crashLogsManager,
|
||||
)
|
||||
|
||||
@Singleton
|
||||
@Provides
|
||||
fun providesFillDataBuilder(
|
||||
autofillCipherProvider: AutofillCipherProvider,
|
||||
@@ -119,6 +126,7 @@ object AutofillModule {
|
||||
autofillCipherProvider = autofillCipherProvider,
|
||||
)
|
||||
|
||||
@Singleton
|
||||
@Provides
|
||||
fun providesFillResponseBuilder(): FillResponseBuilder = FillResponseBuilderImpl()
|
||||
|
||||
|
||||
@@ -24,10 +24,7 @@ object Fido2NetworkModule {
|
||||
): DigitalAssetLinkService =
|
||||
DigitalAssetLinkServiceImpl(
|
||||
digitalAssetLinkApi = retrofits
|
||||
.staticRetrofitBuilder
|
||||
// This URL will be overridden dynamically.
|
||||
.baseUrl("https://www.bitwarden.com")
|
||||
.build()
|
||||
.createStaticRetrofit()
|
||||
.create(),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ import com.x8bit.bitwarden.data.autofill.fido2.processor.Fido2ProviderProcessorI
|
||||
import com.x8bit.bitwarden.data.platform.manager.AssetManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.dispatcher.DispatcherManager
|
||||
import com.x8bit.bitwarden.data.vault.datasource.sdk.VaultSdkSource
|
||||
import com.x8bit.bitwarden.data.vault.repository.VaultRepository
|
||||
import com.x8bit.bitwarden.ui.platform.manager.intent.IntentManager
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
@@ -35,12 +36,18 @@ object Fido2ProviderModule {
|
||||
fun provideCredentialProviderProcessor(
|
||||
@ApplicationContext context: Context,
|
||||
authRepository: AuthRepository,
|
||||
vaultRepository: VaultRepository,
|
||||
fido2CredentialStore: Fido2CredentialStore,
|
||||
fido2CredentialManager: Fido2CredentialManager,
|
||||
dispatcherManager: DispatcherManager,
|
||||
intentManager: IntentManager,
|
||||
): Fido2ProviderProcessor =
|
||||
Fido2ProviderProcessorImpl(
|
||||
context,
|
||||
authRepository,
|
||||
vaultRepository,
|
||||
fido2CredentialStore,
|
||||
fido2CredentialManager,
|
||||
intentManager,
|
||||
dispatcherManager,
|
||||
)
|
||||
|
||||
@@ -1,22 +1,30 @@
|
||||
package com.x8bit.bitwarden.data.autofill.fido2.manager
|
||||
|
||||
import com.bitwarden.vault.CipherView
|
||||
import com.x8bit.bitwarden.data.autofill.fido2.datasource.network.model.PublicKeyCredentialCreationOptions
|
||||
import com.x8bit.bitwarden.data.autofill.fido2.model.Fido2CredentialAssertionRequest
|
||||
import com.x8bit.bitwarden.data.autofill.fido2.model.Fido2CredentialAssertionResult
|
||||
import com.x8bit.bitwarden.data.autofill.fido2.model.Fido2CredentialRequest
|
||||
import com.x8bit.bitwarden.data.autofill.fido2.model.Fido2RegisterCredentialResult
|
||||
import com.x8bit.bitwarden.data.autofill.fido2.model.Fido2ValidateOriginResult
|
||||
import com.x8bit.bitwarden.data.autofill.fido2.model.PasskeyAssertionOptions
|
||||
import com.x8bit.bitwarden.data.autofill.fido2.model.PasskeyAttestationOptions
|
||||
|
||||
/**
|
||||
* Responsible for managing FIDO 2 credential registration and authentication.
|
||||
*/
|
||||
interface Fido2CredentialManager {
|
||||
|
||||
/**
|
||||
* Returns true when the user has performed an explicit verification action. E.g., biometric
|
||||
* verification, device credential verification, or vault unlock.
|
||||
*/
|
||||
var isUserVerified: Boolean
|
||||
|
||||
/**
|
||||
* The number of times the user has attempted to authenticate with their password or PIN
|
||||
* for the FIDO 2 user verification flow.
|
||||
*/
|
||||
var authenticationAttempts: Int
|
||||
|
||||
/**
|
||||
* Attempt to validate the RP and origin of the provided [fido2CredentialRequest].
|
||||
*/
|
||||
@@ -25,11 +33,18 @@ interface Fido2CredentialManager {
|
||||
): Fido2ValidateOriginResult
|
||||
|
||||
/**
|
||||
* Attempt to extract FIDO 2 passkey creation options from the system [requestJson], or null.
|
||||
* Attempt to extract FIDO 2 passkey attestation options from the system [requestJson], or null.
|
||||
*/
|
||||
fun getPasskeyCreateOptionsOrNull(
|
||||
fun getPasskeyAttestationOptionsOrNull(
|
||||
requestJson: String,
|
||||
): PublicKeyCredentialCreationOptions?
|
||||
): PasskeyAttestationOptions?
|
||||
|
||||
/**
|
||||
* Attempt to extract FIDO 2 passkey assertion options from the system [requestJson], or null.
|
||||
*/
|
||||
fun getPasskeyAssertionOptionsOrNull(
|
||||
requestJson: String,
|
||||
): PasskeyAssertionOptions?
|
||||
|
||||
/**
|
||||
* Register a new FIDO 2 credential to a users vault.
|
||||
@@ -39,4 +54,18 @@ interface Fido2CredentialManager {
|
||||
fido2CredentialRequest: Fido2CredentialRequest,
|
||||
selectedCipherView: CipherView,
|
||||
): Fido2RegisterCredentialResult
|
||||
|
||||
/**
|
||||
* Authenticate a FIDO credential against a cipher in the users vault.
|
||||
*/
|
||||
suspend fun authenticateFido2Credential(
|
||||
userId: String,
|
||||
request: Fido2CredentialAssertionRequest,
|
||||
selectedCipherView: CipherView,
|
||||
): Fido2CredentialAssertionResult
|
||||
|
||||
/**
|
||||
* Whether or not the user has authentication attempts remaining.
|
||||
*/
|
||||
fun hasAuthenticationAttemptsRemaining(): Boolean
|
||||
}
|
||||
|
||||
@@ -5,11 +5,14 @@ import com.bitwarden.fido.ClientData
|
||||
import com.bitwarden.sdk.Fido2CredentialStore
|
||||
import com.bitwarden.vault.CipherView
|
||||
import com.x8bit.bitwarden.data.autofill.fido2.datasource.network.model.DigitalAssetLinkResponseJson
|
||||
import com.x8bit.bitwarden.data.autofill.fido2.datasource.network.model.PublicKeyCredentialCreationOptions
|
||||
import com.x8bit.bitwarden.data.autofill.fido2.datasource.network.service.DigitalAssetLinkService
|
||||
import com.x8bit.bitwarden.data.autofill.fido2.model.Fido2CredentialAssertionRequest
|
||||
import com.x8bit.bitwarden.data.autofill.fido2.model.Fido2CredentialAssertionResult
|
||||
import com.x8bit.bitwarden.data.autofill.fido2.model.Fido2CredentialRequest
|
||||
import com.x8bit.bitwarden.data.autofill.fido2.model.Fido2RegisterCredentialResult
|
||||
import com.x8bit.bitwarden.data.autofill.fido2.model.Fido2ValidateOriginResult
|
||||
import com.x8bit.bitwarden.data.autofill.fido2.model.PasskeyAssertionOptions
|
||||
import com.x8bit.bitwarden.data.autofill.fido2.model.PasskeyAttestationOptions
|
||||
import com.x8bit.bitwarden.data.platform.manager.AssetManager
|
||||
import com.x8bit.bitwarden.data.platform.util.asFailure
|
||||
import com.x8bit.bitwarden.data.platform.util.asSuccess
|
||||
@@ -19,8 +22,10 @@ import com.x8bit.bitwarden.data.platform.util.getAppSigningSignatureFingerprint
|
||||
import com.x8bit.bitwarden.data.platform.util.getSignatureFingerprintAsHexString
|
||||
import com.x8bit.bitwarden.data.platform.util.validatePrivilegedApp
|
||||
import com.x8bit.bitwarden.data.vault.datasource.sdk.VaultSdkSource
|
||||
import com.x8bit.bitwarden.data.vault.datasource.sdk.model.AuthenticateFido2CredentialRequest
|
||||
import com.x8bit.bitwarden.data.vault.datasource.sdk.model.RegisterFido2CredentialRequest
|
||||
import com.x8bit.bitwarden.data.vault.datasource.sdk.util.toAndroidAttestationResponse
|
||||
import com.x8bit.bitwarden.data.vault.datasource.sdk.util.toAndroidFido2PublicKeyCredential
|
||||
import kotlinx.serialization.SerializationException
|
||||
import kotlinx.serialization.encodeToString
|
||||
import kotlinx.serialization.json.Json
|
||||
@@ -30,6 +35,7 @@ private const val ALLOW_LIST_FILE_NAME = "fido2_privileged_allow_list.json"
|
||||
/**
|
||||
* Primary implementation of [Fido2CredentialManager].
|
||||
*/
|
||||
@Suppress("TooManyFunctions")
|
||||
class Fido2CredentialManagerImpl(
|
||||
private val assetManager: AssetManager,
|
||||
private val digitalAssetLinkService: DigitalAssetLinkService,
|
||||
@@ -41,24 +47,30 @@ class Fido2CredentialManagerImpl(
|
||||
|
||||
override var isUserVerified: Boolean = false
|
||||
|
||||
override var authenticationAttempts: Int = 0
|
||||
|
||||
override suspend fun registerFido2Credential(
|
||||
userId: String,
|
||||
fido2CredentialRequest: Fido2CredentialRequest,
|
||||
selectedCipherView: CipherView,
|
||||
): Fido2RegisterCredentialResult {
|
||||
val clientData = if (fido2CredentialRequest.callingAppInfo.isOriginPopulated()) {
|
||||
fido2CredentialRequest.callingAppInfo.getAppSigningSignatureFingerprint()
|
||||
?.let { ClientData.DefaultWithCustomHash(hash = it) }
|
||||
?: return Fido2RegisterCredentialResult.Error
|
||||
} else {
|
||||
ClientData.DefaultWithExtraData(
|
||||
androidPackageName = fido2CredentialRequest
|
||||
fido2CredentialRequest
|
||||
.callingAppInfo
|
||||
.getAppOrigin(),
|
||||
)
|
||||
}
|
||||
val origin = fido2CredentialRequest.origin
|
||||
?: fido2CredentialRequest.callingAppInfo.getAppOrigin()
|
||||
.getAppSigningSignatureFingerprint()
|
||||
?.let { ClientData.DefaultWithCustomHash(hash = it) }
|
||||
?: return Fido2RegisterCredentialResult.Error
|
||||
} else {
|
||||
ClientData.DefaultWithExtraData(
|
||||
androidPackageName = fido2CredentialRequest
|
||||
.callingAppInfo
|
||||
.packageName,
|
||||
)
|
||||
}
|
||||
val origin = fido2CredentialRequest
|
||||
.origin
|
||||
?: getOriginUrlFromAttestationOptionsOrNull(fido2CredentialRequest.requestJson)
|
||||
?: return Fido2RegisterCredentialResult.Error
|
||||
|
||||
return vaultSdkSource
|
||||
.registerFido2Credential(
|
||||
@@ -93,17 +105,61 @@ class Fido2CredentialManagerImpl(
|
||||
}
|
||||
}
|
||||
|
||||
override fun getPasskeyCreateOptionsOrNull(
|
||||
override fun getPasskeyAttestationOptionsOrNull(
|
||||
requestJson: String,
|
||||
): PublicKeyCredentialCreationOptions? =
|
||||
): PasskeyAttestationOptions? =
|
||||
try {
|
||||
json.decodeFromString<PublicKeyCredentialCreationOptions>(requestJson)
|
||||
json.decodeFromString<PasskeyAttestationOptions>(requestJson)
|
||||
} catch (e: SerializationException) {
|
||||
null
|
||||
} catch (e: IllegalArgumentException) {
|
||||
null
|
||||
}
|
||||
|
||||
override fun getPasskeyAssertionOptionsOrNull(
|
||||
requestJson: String,
|
||||
): PasskeyAssertionOptions? =
|
||||
try {
|
||||
json.decodeFromString<PasskeyAssertionOptions>(requestJson)
|
||||
} catch (e: SerializationException) {
|
||||
null
|
||||
} catch (e: IllegalArgumentException) {
|
||||
null
|
||||
}
|
||||
|
||||
override suspend fun authenticateFido2Credential(
|
||||
userId: String,
|
||||
request: Fido2CredentialAssertionRequest,
|
||||
selectedCipherView: CipherView,
|
||||
): Fido2CredentialAssertionResult {
|
||||
val callingAppInfo = request.callingAppInfo
|
||||
val clientData = request.clientDataHash
|
||||
?.let { ClientData.DefaultWithCustomHash(hash = it) }
|
||||
?: ClientData.DefaultWithExtraData(androidPackageName = callingAppInfo.getAppOrigin())
|
||||
val origin = request.origin
|
||||
?: getOriginUrlFromAssertionOptionsOrNull(request.requestJson)
|
||||
?: 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 },
|
||||
)
|
||||
}
|
||||
|
||||
private suspend fun validateCallingApplicationAssetLinks(
|
||||
fido2CredentialRequest: Fido2CredentialRequest,
|
||||
): Fido2ValidateOriginResult {
|
||||
@@ -194,7 +250,7 @@ class Fido2CredentialManagerImpl(
|
||||
private fun String.getRpId(json: Json): Result<String> {
|
||||
return try {
|
||||
json
|
||||
.decodeFromString<PublicKeyCredentialCreationOptions>(this)
|
||||
.decodeFromString<PasskeyAttestationOptions>(this)
|
||||
.relyingParty
|
||||
.id
|
||||
.asSuccess()
|
||||
@@ -204,4 +260,21 @@ class Fido2CredentialManagerImpl(
|
||||
e.asFailure()
|
||||
}
|
||||
}
|
||||
|
||||
override fun hasAuthenticationAttemptsRemaining(): Boolean =
|
||||
authenticationAttempts < MAX_AUTHENTICATION_ATTEMPTS
|
||||
|
||||
private fun getOriginUrlFromAssertionOptionsOrNull(requestJson: String) =
|
||||
getPasskeyAssertionOptionsOrNull(requestJson)
|
||||
?.relyingPartyId
|
||||
?.let { "$HTTPS$it" }
|
||||
|
||||
private fun getOriginUrlFromAttestationOptionsOrNull(requestJson: String) =
|
||||
getPasskeyAttestationOptionsOrNull(requestJson)
|
||||
?.relyingParty
|
||||
?.id
|
||||
?.let { "$HTTPS$it" }
|
||||
}
|
||||
|
||||
private const val MAX_AUTHENTICATION_ATTEMPTS = 5
|
||||
private const val HTTPS = "https://"
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
package com.x8bit.bitwarden.data.autofill.fido2.model
|
||||
|
||||
import android.content.pm.SigningInfo
|
||||
import android.os.Parcelable
|
||||
import androidx.credentials.provider.CallingAppInfo
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
/**
|
||||
* Models a FIDO 2 credential authentication request parsed from the launching intent.
|
||||
*/
|
||||
@Parcelize
|
||||
data class Fido2CredentialAssertionRequest(
|
||||
val cipherId: String?,
|
||||
val credentialId: String?,
|
||||
val requestJson: String,
|
||||
val clientDataHash: ByteArray?,
|
||||
val packageName: String,
|
||||
val signingInfo: SigningInfo,
|
||||
val origin: String?,
|
||||
) : Parcelable {
|
||||
val callingAppInfo: CallingAppInfo
|
||||
get() = CallingAppInfo(packageName, signingInfo, origin)
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package com.x8bit.bitwarden.data.autofill.fido2.model
|
||||
|
||||
/**
|
||||
* Represents possible outcomes of a FIDO 2 credential assertion request.
|
||||
*/
|
||||
sealed class Fido2CredentialAssertionResult {
|
||||
|
||||
/**
|
||||
* Indicates the assertion request completed and [responseJson] was successfully generated.
|
||||
*/
|
||||
data class Success(val responseJson: String) : Fido2CredentialAssertionResult()
|
||||
|
||||
/**
|
||||
* Indicates there was an error and the assertion was not successful.
|
||||
*/
|
||||
data object Error : Fido2CredentialAssertionResult()
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package com.x8bit.bitwarden.data.autofill.fido2.model
|
||||
|
||||
import android.content.pm.SigningInfo
|
||||
import android.os.Bundle
|
||||
import android.os.Parcelable
|
||||
import androidx.credentials.provider.BeginGetPublicKeyCredentialOption
|
||||
import androidx.credentials.provider.CallingAppInfo
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
/**
|
||||
* Models a FIDO 2 request to retrieve FIDO credentials parsed from the launching intent.
|
||||
*/
|
||||
@Parcelize
|
||||
data class Fido2GetCredentialsRequest(
|
||||
val candidateQueryData: Bundle,
|
||||
val id: String,
|
||||
val requestJson: String,
|
||||
val clientDataHash: ByteArray? = null,
|
||||
val packageName: String,
|
||||
val signingInfo: SigningInfo,
|
||||
val origin: String?,
|
||||
) : Parcelable {
|
||||
val callingAppInfo: CallingAppInfo
|
||||
get() = CallingAppInfo(packageName, signingInfo, origin)
|
||||
|
||||
val option: BeginGetPublicKeyCredentialOption
|
||||
get() = BeginGetPublicKeyCredentialOption(
|
||||
candidateQueryData,
|
||||
id,
|
||||
requestJson,
|
||||
clientDataHash,
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package com.x8bit.bitwarden.data.autofill.fido2.model
|
||||
|
||||
import androidx.credentials.provider.BeginGetPublicKeyCredentialOption
|
||||
import com.bitwarden.fido.Fido2CredentialAutofillView
|
||||
|
||||
/**
|
||||
* Represents the result of a FIDO 2 Get Credentials request.
|
||||
*/
|
||||
sealed class Fido2GetCredentialsResult {
|
||||
/**
|
||||
* Indicates credentials were successfully queried.
|
||||
*
|
||||
* @param options Original request options provided by the relying party.
|
||||
* @param credentials Collection of [Fido2CredentialAutofillView]s matching the original request
|
||||
* parameters. This may be an empty list if no matching values were found.
|
||||
*/
|
||||
data class Success(
|
||||
val options: BeginGetPublicKeyCredentialOption,
|
||||
val credentials: List<Fido2CredentialAutofillView>,
|
||||
) : Fido2GetCredentialsResult()
|
||||
|
||||
/**
|
||||
* Indicates an error was encountered when querying for matching credentials.
|
||||
*/
|
||||
data object Error : Fido2GetCredentialsResult()
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
package com.x8bit.bitwarden.data.autofill.fido2.model
|
||||
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
/**
|
||||
* Models a FIDO 2 public key credential.
|
||||
*/
|
||||
@Serializable
|
||||
data class Fido2PublicKeyCredential(
|
||||
@SerialName("id")
|
||||
val id: String,
|
||||
@SerialName("rawId")
|
||||
val rawId: String,
|
||||
@SerialName("type")
|
||||
val type: String,
|
||||
@SerialName("authenticatorAttachment")
|
||||
val authenticatorAttachment: String?,
|
||||
@SerialName("response")
|
||||
val response: Fido2AssertionResponse,
|
||||
@SerialName("clientExtensionResults")
|
||||
val clientExtensionResults: ClientExtensionResults,
|
||||
) {
|
||||
|
||||
/**
|
||||
* Models a FIDO 2 public key assertion response.
|
||||
*/
|
||||
@Serializable
|
||||
data class Fido2AssertionResponse(
|
||||
@SerialName("clientDataJSON")
|
||||
val clientDataJson: String?,
|
||||
@SerialName("authenticatorData")
|
||||
val authenticatorData: String,
|
||||
@SerialName("signature")
|
||||
val signature: String,
|
||||
@SerialName("userHandle")
|
||||
val userHandle: String?,
|
||||
)
|
||||
|
||||
/**
|
||||
* Models FIDO 2 credential properties provided by a client.
|
||||
*/
|
||||
@Serializable
|
||||
data class ClientExtensionResults(
|
||||
@SerialName("credProps")
|
||||
val credentialProperties: CredentialProperties?,
|
||||
) {
|
||||
/**
|
||||
* Models the FIDO 2 credential properties provided by a client.
|
||||
*/
|
||||
@Serializable
|
||||
data class CredentialProperties(
|
||||
@SerialName("rk")
|
||||
val residentKey: Boolean?,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package com.x8bit.bitwarden.data.autofill.fido2.model
|
||||
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
/**
|
||||
* Models the request options for a passkey request, based off the spec found at:
|
||||
* https://www.w3.org/TR/webauthn-2/#dictionary-assertion-options
|
||||
*/
|
||||
@Serializable
|
||||
data class PasskeyAssertionOptions(
|
||||
@SerialName("challenge")
|
||||
val challenge: String,
|
||||
@SerialName("allowCredentials")
|
||||
val allowCredentials: List<PublicKeyCredentialDescriptor>?,
|
||||
@SerialName("rpId")
|
||||
val relyingPartyId: String?,
|
||||
@SerialName("userVerification")
|
||||
val userVerification: UserVerificationRequirement = UserVerificationRequirement.PREFERRED,
|
||||
)
|
||||
@@ -1,13 +1,13 @@
|
||||
package com.x8bit.bitwarden.data.autofill.fido2.datasource.network.model
|
||||
package com.x8bit.bitwarden.data.autofill.fido2.model
|
||||
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
/**
|
||||
* Models a FIDO 2 credential creation request options received from a Relying Party (RP).
|
||||
* Models FIDO 2 credential creation request options received from a Relying Party (RP).
|
||||
*/
|
||||
@Serializable
|
||||
data class PublicKeyCredentialCreationOptions(
|
||||
data class PasskeyAttestationOptions(
|
||||
@SerialName("authenticatorSelection")
|
||||
val authenticatorSelection: AuthenticatorSelectionCriteria,
|
||||
@SerialName("challenge")
|
||||
@@ -32,7 +32,7 @@ data class PublicKeyCredentialCreationOptions(
|
||||
@SerialName("residentKey")
|
||||
val residentKeyRequirement: ResidentKeyRequirement? = null,
|
||||
@SerialName("userVerification")
|
||||
val userVerification: UserVerificationRequirement? = null,
|
||||
val userVerification: UserVerificationRequirement = UserVerificationRequirement.PREFERRED,
|
||||
) {
|
||||
/**
|
||||
* Enum class representing the types of attachments associated with selection criteria.
|
||||
@@ -63,46 +63,8 @@ data class PublicKeyCredentialCreationOptions(
|
||||
@SerialName("required")
|
||||
REQUIRED,
|
||||
}
|
||||
|
||||
/**
|
||||
* Enum class indicating the type of user verification requested by the relying party.
|
||||
*/
|
||||
@Serializable
|
||||
enum class UserVerificationRequirement {
|
||||
/**
|
||||
* User verification should not be performed.
|
||||
*/
|
||||
@SerialName("discouraged")
|
||||
DISCOURAGED,
|
||||
|
||||
/**
|
||||
* User verification is preferred, if supported by the device or application.
|
||||
*/
|
||||
@SerialName("preferred")
|
||||
PREFERRED,
|
||||
|
||||
/**
|
||||
* User verification is required. If is cannot be performed the registration process
|
||||
* should be terminated.
|
||||
*/
|
||||
@SerialName("required")
|
||||
REQUIRED,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents details about a credential provided in the creation options.
|
||||
*/
|
||||
@Serializable
|
||||
data class PublicKeyCredentialDescriptor(
|
||||
@SerialName("type")
|
||||
val type: String,
|
||||
@SerialName("id")
|
||||
val id: String,
|
||||
@SerialName("transports")
|
||||
val transports: List<String>,
|
||||
)
|
||||
|
||||
/**
|
||||
* Represents parameters for a credential in the creation options.
|
||||
*/
|
||||
@@ -0,0 +1,17 @@
|
||||
package com.x8bit.bitwarden.data.autofill.fido2.model
|
||||
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
/**
|
||||
* Represents details about a credential provided in the creation options.
|
||||
*/
|
||||
@Serializable
|
||||
data class PublicKeyCredentialDescriptor(
|
||||
@SerialName("type")
|
||||
val type: String,
|
||||
@SerialName("id")
|
||||
val id: String,
|
||||
@SerialName("transports")
|
||||
val transports: List<String>,
|
||||
)
|
||||
@@ -0,0 +1,29 @@
|
||||
package com.x8bit.bitwarden.data.autofill.fido2.model
|
||||
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
/**
|
||||
* Enum class indicating the type of user verification requested by the relying party.
|
||||
*/
|
||||
@Serializable
|
||||
enum class UserVerificationRequirement {
|
||||
/**
|
||||
* User verification should not be performed.
|
||||
*/
|
||||
@SerialName("discouraged")
|
||||
DISCOURAGED,
|
||||
|
||||
/**
|
||||
* User verification is preferred, if supported by the device or application.
|
||||
*/
|
||||
@SerialName("preferred")
|
||||
PREFERRED,
|
||||
|
||||
/**
|
||||
* User verification is required. If is cannot be performed the registration process
|
||||
* should be terminated.
|
||||
*/
|
||||
@SerialName("required")
|
||||
REQUIRED,
|
||||
}
|
||||
@@ -10,35 +10,52 @@ import androidx.credentials.exceptions.ClearCredentialUnsupportedException
|
||||
import androidx.credentials.exceptions.CreateCredentialCancellationException
|
||||
import androidx.credentials.exceptions.CreateCredentialException
|
||||
import androidx.credentials.exceptions.CreateCredentialUnknownException
|
||||
import androidx.credentials.exceptions.GetCredentialCancellationException
|
||||
import androidx.credentials.exceptions.GetCredentialException
|
||||
import androidx.credentials.exceptions.GetCredentialUnknownException
|
||||
import androidx.credentials.exceptions.GetCredentialUnsupportedException
|
||||
import androidx.credentials.provider.AuthenticationAction
|
||||
import androidx.credentials.provider.BeginCreateCredentialRequest
|
||||
import androidx.credentials.provider.BeginCreateCredentialResponse
|
||||
import androidx.credentials.provider.BeginCreatePublicKeyCredentialRequest
|
||||
import androidx.credentials.provider.BeginGetCredentialRequest
|
||||
import androidx.credentials.provider.BeginGetCredentialResponse
|
||||
import androidx.credentials.provider.BeginGetPublicKeyCredentialOption
|
||||
import androidx.credentials.provider.CreateEntry
|
||||
import androidx.credentials.provider.CredentialEntry
|
||||
import androidx.credentials.provider.ProviderClearCredentialStateRequest
|
||||
import androidx.credentials.provider.PublicKeyCredentialEntry
|
||||
import com.bitwarden.fido.Fido2CredentialAutofillView
|
||||
import com.bitwarden.sdk.Fido2CredentialStore
|
||||
import com.x8bit.bitwarden.R
|
||||
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.UserState
|
||||
import com.x8bit.bitwarden.data.autofill.fido2.manager.Fido2CredentialManager
|
||||
import com.x8bit.bitwarden.data.autofill.util.isActiveWithFido2Credentials
|
||||
import com.x8bit.bitwarden.data.platform.manager.dispatcher.DispatcherManager
|
||||
import com.x8bit.bitwarden.data.vault.repository.VaultRepository
|
||||
import com.x8bit.bitwarden.data.vault.repository.model.DecryptFido2CredentialAutofillViewResult
|
||||
import com.x8bit.bitwarden.ui.platform.manager.intent.IntentManager
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.cancel
|
||||
import kotlinx.coroutines.launch
|
||||
import java.util.concurrent.atomic.AtomicInteger
|
||||
|
||||
private const val CREATE_PASSKEY_INTENT = "com.x8bit.bitwarden.fido2.ACTION_CREATE_PASSKEY"
|
||||
const val GET_PASSKEY_INTENT = "com.x8bit.bitwarden.fido2.ACTION_GET_PASSKEY"
|
||||
const val UNLOCK_ACCOUNT_INTENT = "com.x8bit.bitwarden.fido2.ACTION_UNLOCK_ACCOUNT"
|
||||
|
||||
/**
|
||||
* The default implementation of [Fido2ProviderProcessor]. Its purpose is to handle FIDO2 related
|
||||
* processing.
|
||||
*/
|
||||
@Suppress("LongParameterList")
|
||||
@RequiresApi(Build.VERSION_CODES.S)
|
||||
class Fido2ProviderProcessorImpl(
|
||||
private val context: Context,
|
||||
private val authRepository: AuthRepository,
|
||||
private val vaultRepository: VaultRepository,
|
||||
private val fido2CredentialStore: Fido2CredentialStore,
|
||||
private val fido2CredentialManager: Fido2CredentialManager,
|
||||
private val intentManager: IntentManager,
|
||||
dispatcherManager: DispatcherManager,
|
||||
) : Fido2ProviderProcessor {
|
||||
@@ -51,22 +68,23 @@ class Fido2ProviderProcessorImpl(
|
||||
cancellationSignal: CancellationSignal,
|
||||
callback: OutcomeReceiver<BeginCreateCredentialResponse, CreateCredentialException>,
|
||||
) {
|
||||
cancellationSignal.setOnCancelListener {
|
||||
callback.onError(CreateCredentialCancellationException())
|
||||
scope.cancel()
|
||||
}
|
||||
|
||||
val userId = authRepository.activeUserId
|
||||
if (userId == null) {
|
||||
callback.onError(CreateCredentialUnknownException("Active user is required."))
|
||||
return
|
||||
}
|
||||
|
||||
scope.launch {
|
||||
val createCredentialJob = scope.launch {
|
||||
processCreateCredentialRequest(request = request)
|
||||
?.let { callback.onResult(it) }
|
||||
?: callback.onError(CreateCredentialUnknownException())
|
||||
}
|
||||
cancellationSignal.setOnCancelListener {
|
||||
if (createCredentialJob.isActive) {
|
||||
createCredentialJob.cancel()
|
||||
}
|
||||
callback.onError(CreateCredentialCancellationException())
|
||||
}
|
||||
}
|
||||
|
||||
private fun processCreateCredentialRequest(
|
||||
@@ -124,10 +142,120 @@ class Fido2ProviderProcessorImpl(
|
||||
cancellationSignal: CancellationSignal,
|
||||
callback: OutcomeReceiver<BeginGetCredentialResponse, GetCredentialException>,
|
||||
) {
|
||||
// no-op: RFU
|
||||
callback.onError(GetCredentialUnsupportedException())
|
||||
// If the user is not logged in, return an error.
|
||||
val userState = authRepository.userStateFlow.value
|
||||
if (userState == null) {
|
||||
callback.onError(GetCredentialUnknownException("Active user is required."))
|
||||
return
|
||||
}
|
||||
|
||||
// Return an unlock action if the current account is locked.
|
||||
if (!userState.activeAccount.isVaultUnlocked) {
|
||||
val authenticationAction = AuthenticationAction(
|
||||
title = context.getString(R.string.unlock),
|
||||
pendingIntent = intentManager.createFido2UnlockPendingIntent(
|
||||
action = UNLOCK_ACCOUNT_INTENT,
|
||||
requestCode = requestCode.getAndIncrement(),
|
||||
),
|
||||
)
|
||||
|
||||
callback.onResult(
|
||||
BeginGetCredentialResponse(
|
||||
authenticationActions = listOf(authenticationAction),
|
||||
),
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
// Otherwise, find all matching credentials from the current vault.
|
||||
val getCredentialJob = scope.launch {
|
||||
try {
|
||||
val credentialEntries = getMatchingFido2CredentialEntries(
|
||||
userId = userState.activeUserId,
|
||||
request = request,
|
||||
)
|
||||
|
||||
callback.onResult(
|
||||
BeginGetCredentialResponse(
|
||||
credentialEntries = credentialEntries,
|
||||
),
|
||||
)
|
||||
} catch (e: GetCredentialException) {
|
||||
callback.onError(e)
|
||||
}
|
||||
}
|
||||
cancellationSignal.setOnCancelListener {
|
||||
callback.onError(GetCredentialCancellationException())
|
||||
getCredentialJob.cancel()
|
||||
}
|
||||
}
|
||||
|
||||
@Throws(GetCredentialUnsupportedException::class)
|
||||
private suspend fun getMatchingFido2CredentialEntries(
|
||||
userId: String,
|
||||
request: BeginGetCredentialRequest,
|
||||
): List<CredentialEntry> =
|
||||
request
|
||||
.beginGetCredentialOptions
|
||||
.flatMap { option ->
|
||||
if (option is BeginGetPublicKeyCredentialOption) {
|
||||
val relyingPartyId = fido2CredentialManager
|
||||
.getPasskeyAssertionOptionsOrNull(requestJson = option.requestJson)
|
||||
?.relyingPartyId
|
||||
?: throw GetCredentialUnknownException("Invalid data.")
|
||||
buildCredentialEntries(relyingPartyId, option)
|
||||
} else {
|
||||
throw GetCredentialUnsupportedException("Unsupported option.")
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun buildCredentialEntries(
|
||||
relyingPartyId: String,
|
||||
option: BeginGetPublicKeyCredentialOption,
|
||||
): List<CredentialEntry> {
|
||||
val cipherViews = vaultRepository
|
||||
.ciphersStateFlow
|
||||
.value
|
||||
.data
|
||||
?.filter { it.isActiveWithFido2Credentials }
|
||||
?: emptyList()
|
||||
val result = vaultRepository
|
||||
.getDecryptedFido2CredentialAutofillViews(cipherViews)
|
||||
return when (result) {
|
||||
DecryptFido2CredentialAutofillViewResult.Error -> {
|
||||
throw GetCredentialUnknownException("Error decrypting credentials.")
|
||||
}
|
||||
|
||||
is DecryptFido2CredentialAutofillViewResult.Success -> {
|
||||
result
|
||||
.fido2CredentialAutofillViews
|
||||
.filter { it.rpId == relyingPartyId }
|
||||
.toCredentialEntries(option)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun List<Fido2CredentialAutofillView>.toCredentialEntries(
|
||||
option: BeginGetPublicKeyCredentialOption,
|
||||
): List<CredentialEntry> =
|
||||
this
|
||||
.map {
|
||||
PublicKeyCredentialEntry
|
||||
.Builder(
|
||||
context = context,
|
||||
username = it.userNameForUi ?: context.getString(R.string.no_username),
|
||||
pendingIntent = intentManager
|
||||
.createFido2GetCredentialPendingIntent(
|
||||
action = GET_PASSKEY_INTENT,
|
||||
credentialId = it.credentialId.toString(),
|
||||
cipherId = it.cipherId,
|
||||
requestCode = requestCode.getAndIncrement(),
|
||||
),
|
||||
beginGetPublicKeyCredentialOption = option,
|
||||
)
|
||||
.build()
|
||||
}
|
||||
|
||||
override fun processClearCredentialStateRequest(
|
||||
request: ProviderClearCredentialStateRequest,
|
||||
cancellationSignal: CancellationSignal,
|
||||
|
||||
@@ -3,26 +3,32 @@ package com.x8bit.bitwarden.data.autofill.fido2.util
|
||||
import android.content.Intent
|
||||
import android.os.Build
|
||||
import androidx.credentials.CreatePublicKeyCredentialRequest
|
||||
import androidx.credentials.GetPublicKeyCredentialOption
|
||||
import androidx.credentials.provider.BeginGetPublicKeyCredentialOption
|
||||
import androidx.credentials.provider.PendingIntentHandler
|
||||
import com.x8bit.bitwarden.data.autofill.fido2.model.Fido2CredentialAssertionRequest
|
||||
import com.x8bit.bitwarden.data.autofill.fido2.model.Fido2CredentialRequest
|
||||
import com.x8bit.bitwarden.data.platform.annotation.OmitFromCoverage
|
||||
import com.x8bit.bitwarden.data.autofill.fido2.model.Fido2GetCredentialsRequest
|
||||
import com.x8bit.bitwarden.data.platform.util.isBuildVersionBelow
|
||||
import com.x8bit.bitwarden.ui.platform.manager.intent.EXTRA_KEY_CIPHER_ID
|
||||
import com.x8bit.bitwarden.ui.platform.manager.intent.EXTRA_KEY_CREDENTIAL_ID
|
||||
import com.x8bit.bitwarden.ui.platform.manager.intent.EXTRA_KEY_USER_ID
|
||||
|
||||
/**
|
||||
* Checks if this [Intent] contains a [Fido2CredentialRequest] related to an ongoing FIDO 2
|
||||
* credential creation process.
|
||||
*/
|
||||
@OmitFromCoverage
|
||||
fun Intent.getFido2CredentialRequestOrNull(): Fido2CredentialRequest? {
|
||||
if (isBuildVersionBelow(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)) return null
|
||||
|
||||
val systemRequest = PendingIntentHandler.retrieveProviderCreateCredentialRequest(this)
|
||||
val systemRequest = PendingIntentHandler
|
||||
.retrieveProviderCreateCredentialRequest(this)
|
||||
?: return null
|
||||
|
||||
val createPublicKeyRequest =
|
||||
systemRequest.callingRequest as? CreatePublicKeyCredentialRequest
|
||||
?: return null
|
||||
val createPublicKeyRequest = systemRequest
|
||||
.callingRequest
|
||||
as? CreatePublicKeyCredentialRequest
|
||||
?: return null
|
||||
|
||||
val userId = getStringExtra(EXTRA_KEY_USER_ID)
|
||||
?: return null
|
||||
@@ -35,3 +41,67 @@ fun Intent.getFido2CredentialRequestOrNull(): Fido2CredentialRequest? {
|
||||
origin = systemRequest.callingAppInfo.origin,
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if this [Intent] contains a [Fido2CredentialAssertionRequest] related to an ongoing FIDO 2
|
||||
* credential authentication process.
|
||||
*/
|
||||
fun Intent.getFido2AssertionRequestOrNull(): Fido2CredentialAssertionRequest? {
|
||||
if (isBuildVersionBelow(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)) return null
|
||||
|
||||
val systemRequest = PendingIntentHandler
|
||||
.retrieveProviderGetCredentialRequest(this)
|
||||
?: return null
|
||||
|
||||
val option: GetPublicKeyCredentialOption = systemRequest
|
||||
.credentialOptions
|
||||
.firstNotNullOfOrNull { it as? GetPublicKeyCredentialOption }
|
||||
?: return null
|
||||
|
||||
val credentialId = getStringExtra(EXTRA_KEY_CREDENTIAL_ID)
|
||||
?: return null
|
||||
|
||||
val cipherId = getStringExtra(EXTRA_KEY_CIPHER_ID)
|
||||
?: return null
|
||||
|
||||
return Fido2CredentialAssertionRequest(
|
||||
cipherId = cipherId,
|
||||
credentialId = credentialId,
|
||||
requestJson = option.requestJson,
|
||||
clientDataHash = option.clientDataHash,
|
||||
packageName = systemRequest.callingAppInfo.packageName,
|
||||
signingInfo = systemRequest.callingAppInfo.signingInfo,
|
||||
origin = systemRequest.callingAppInfo.origin,
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if this [Intent] contains a [Fido2GetCredentialsRequest] related to an ongoing FIDO 2
|
||||
* credential lookup process.
|
||||
*/
|
||||
fun Intent.getFido2GetCredentialsRequestOrNull(): Fido2GetCredentialsRequest? {
|
||||
if (isBuildVersionBelow(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)) return null
|
||||
|
||||
val systemRequest = PendingIntentHandler
|
||||
.retrieveBeginGetCredentialRequest(this)
|
||||
?: return null
|
||||
|
||||
val option: BeginGetPublicKeyCredentialOption = systemRequest
|
||||
.beginGetCredentialOptions
|
||||
.firstNotNullOfOrNull { it as? BeginGetPublicKeyCredentialOption }
|
||||
?: return null
|
||||
|
||||
val callingAppInfo = systemRequest
|
||||
.callingAppInfo
|
||||
?: return null
|
||||
|
||||
return Fido2GetCredentialsRequest(
|
||||
candidateQueryData = option.candidateQueryData,
|
||||
id = option.id,
|
||||
requestJson = option.requestJson,
|
||||
clientDataHash = option.clientDataHash,
|
||||
packageName = callingAppInfo.packageName,
|
||||
signingInfo = callingAppInfo.signingInfo,
|
||||
origin = callingAppInfo.origin,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ sealed class AutofillView {
|
||||
* @param autofillType The autofill field type. (ex: View.AUTOFILL_TYPE_TEXT)
|
||||
* @param isFocused Whether the view is currently focused.
|
||||
* @param textValue A text value that represents the input present in the field.
|
||||
* @param hasPasswordTerms Indicates that the field includes password terms.
|
||||
*/
|
||||
data class Data(
|
||||
val autofillId: AutofillId,
|
||||
@@ -22,6 +23,7 @@ sealed class AutofillView {
|
||||
val autofillType: Int,
|
||||
val isFocused: Boolean,
|
||||
val textValue: String?,
|
||||
val hasPasswordTerms: Boolean,
|
||||
)
|
||||
|
||||
/**
|
||||
|
||||
@@ -64,6 +64,7 @@ class AutofillParserImpl(
|
||||
/**
|
||||
* Parse the [AssistStructure] into an [AutofillRequest].
|
||||
*/
|
||||
@Suppress("LongMethod")
|
||||
private fun parseInternal(
|
||||
assistStructure: AssistStructure,
|
||||
autofillAppInfo: AutofillAppInfo,
|
||||
@@ -71,13 +72,24 @@ class AutofillParserImpl(
|
||||
): AutofillRequest {
|
||||
// Parse the `assistStructure` into internal models.
|
||||
val traversalDataList = assistStructure.traverse()
|
||||
// Flatten the autofill views for processing.
|
||||
val autofillViews = traversalDataList
|
||||
.map { it.autofillViews }
|
||||
// Take only the autofill views from the node that currently has focus.
|
||||
// Then remove all the fields that cannot be filled with data.
|
||||
// We fallback to taking all the fillable views if nothing has focus.
|
||||
val autofillViewsList = traversalDataList.map { it.autofillViews }
|
||||
val autofillViews = autofillViewsList
|
||||
.filter { views -> views.any { it.data.isFocused } }
|
||||
.flatten()
|
||||
.filter { it !is AutofillView.Unused }
|
||||
.takeUnless { it.isEmpty() }
|
||||
?: autofillViewsList
|
||||
.flatten()
|
||||
.filter { it !is AutofillView.Unused }
|
||||
|
||||
// Find the focused view.
|
||||
val focusedView = autofillViews.firstOrNull { it.data.isFocused }
|
||||
// Find the focused view, or fallback to the first fillable item on the screen (so
|
||||
// we at least have something to hook into)
|
||||
val focusedView = autofillViews
|
||||
.firstOrNull { it.data.isFocused }
|
||||
?: autofillViews.firstOrNull()
|
||||
|
||||
val packageName = traversalDataList.buildPackageNameOrNull(
|
||||
assistStructure = assistStructure,
|
||||
@@ -108,6 +120,7 @@ class AutofillParserImpl(
|
||||
|
||||
is AutofillView.Unused -> {
|
||||
// The view is unfillable since the field is not meant to be used for autofill.
|
||||
// This will never happen since we filter out all unused views above.
|
||||
return AutofillRequest.Unfillable
|
||||
}
|
||||
}
|
||||
@@ -148,9 +161,29 @@ private fun AssistStructure.traverse(): List<ViewNodeTraversalData> =
|
||||
windowNode
|
||||
.rootViewNode
|
||||
?.traverse()
|
||||
?.updateForMissingPasswordFields()
|
||||
?.updateForMissingUsernameFields()
|
||||
}
|
||||
|
||||
/**
|
||||
* This helper function updates the [ViewNodeTraversalData] if necessary for missing password
|
||||
* fields that were marked invalid because they contained a specific `hint` or `idEntry`. If the
|
||||
* current `ViewNodeTraversalData` contains at least one password fields, we do not add any fields.
|
||||
*/
|
||||
private fun ViewNodeTraversalData.updateForMissingPasswordFields(): ViewNodeTraversalData =
|
||||
if (this.autofillViews.none { it is AutofillView.Login.Password }) {
|
||||
this.copyAndMapAutofillViews { _, autofillView ->
|
||||
if (autofillView is AutofillView.Unused && autofillView.data.hasPasswordTerms) {
|
||||
AutofillView.Login.Password(data = autofillView.data)
|
||||
} else {
|
||||
autofillView
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// We already have password fields available, so no need to add more.
|
||||
this
|
||||
}
|
||||
|
||||
/**
|
||||
* This helper function updates the [ViewNodeTraversalData] if necessary for missing username
|
||||
* fields that could have been missed. If the current `ViewNodeTraversalData` contains password
|
||||
@@ -164,26 +197,13 @@ private fun ViewNodeTraversalData.updateForMissingUsernameFields(): ViewNodeTrav
|
||||
return if (passwordPositions.any() &&
|
||||
this.autofillViews.none { it is AutofillView.Login.Username }
|
||||
) {
|
||||
val updatedAutofillViews = autofillViews.mapIndexed { index, autofillView ->
|
||||
this.copyAndMapAutofillViews { index, autofillView ->
|
||||
if (autofillView is AutofillView.Unused && passwordPositions.contains(index + 1)) {
|
||||
AutofillView.Login.Username(data = autofillView.data)
|
||||
} else {
|
||||
autofillView
|
||||
}
|
||||
}
|
||||
val previousUnusedIds = autofillViews
|
||||
.filterIsInstance<AutofillView.Unused>()
|
||||
.map { it.data.autofillId }
|
||||
.toSet()
|
||||
val currentUnusedIds = updatedAutofillViews
|
||||
.filterIsInstance<AutofillView.Unused>()
|
||||
.map { it.data.autofillId }
|
||||
.toSet()
|
||||
val unignoredAutofillIds = previousUnusedIds - currentUnusedIds
|
||||
this.copy(
|
||||
autofillViews = updatedAutofillViews,
|
||||
ignoreAutofillIds = this.ignoreAutofillIds - unignoredAutofillIds,
|
||||
)
|
||||
} else {
|
||||
// We already have username fields available or there are no password fields, so no need
|
||||
// to search for them.
|
||||
@@ -191,6 +211,29 @@ private fun ViewNodeTraversalData.updateForMissingUsernameFields(): ViewNodeTrav
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This helper function loops through all the [ViewNodeTraversalData.autofillViews] and returns the
|
||||
* fully updated `ViewNodeTraversalData`.
|
||||
*/
|
||||
private fun ViewNodeTraversalData.copyAndMapAutofillViews(
|
||||
mapper: (index: Int, autofillView: AutofillView) -> AutofillView,
|
||||
): ViewNodeTraversalData {
|
||||
val updatedAutofillViews = autofillViews.mapIndexed(mapper)
|
||||
val previousUnusedIds = autofillViews
|
||||
.filterIsInstance<AutofillView.Unused>()
|
||||
.map { it.data.autofillId }
|
||||
.toSet()
|
||||
val currentUnusedIds = updatedAutofillViews
|
||||
.filterIsInstance<AutofillView.Unused>()
|
||||
.map { it.data.autofillId }
|
||||
.toSet()
|
||||
val unignoredAutofillIds = previousUnusedIds - currentUnusedIds
|
||||
return this.copy(
|
||||
autofillViews = updatedAutofillViews,
|
||||
ignoreAutofillIds = this.ignoreAutofillIds - unignoredAutofillIds,
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Recursively traverse this [AssistStructure.ViewNode] and all of its descendants. Convert the
|
||||
* data into [ViewNodeTraversalData].
|
||||
|
||||
@@ -13,6 +13,7 @@ import com.x8bit.bitwarden.data.autofill.model.AutofillRequest
|
||||
import com.x8bit.bitwarden.data.autofill.parser.AutofillParser
|
||||
import com.x8bit.bitwarden.data.autofill.util.createAutofillSavedItemIntentSender
|
||||
import com.x8bit.bitwarden.data.autofill.util.toAutofillSaveItem
|
||||
import com.x8bit.bitwarden.data.platform.manager.CrashLogsManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.PolicyManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.dispatcher.DispatcherManager
|
||||
import com.x8bit.bitwarden.data.platform.repository.SettingsRepository
|
||||
@@ -34,6 +35,7 @@ class AutofillProcessorImpl(
|
||||
private val parser: AutofillParser,
|
||||
private val saveInfoBuilder: SaveInfoBuilder,
|
||||
private val settingsRepository: SettingsRepository,
|
||||
private val crashLogsManager: CrashLogsManager,
|
||||
) : AutofillProcessor {
|
||||
|
||||
/**
|
||||
@@ -55,11 +57,14 @@ class AutofillProcessorImpl(
|
||||
// Set the listener so that any long running work is cancelled when it is no longer needed.
|
||||
cancellationSignal.setOnCancelListener { job.cancel() }
|
||||
// Process the OS data and handle invoking the callback with the result.
|
||||
process(
|
||||
autofillAppInfo = autofillAppInfo,
|
||||
fillCallback = fillCallback,
|
||||
fillRequest = request,
|
||||
)
|
||||
job.cancel()
|
||||
job = scope.launch {
|
||||
process(
|
||||
autofillAppInfo = autofillAppInfo,
|
||||
fillCallback = fillCallback,
|
||||
fillRequest = request,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun processSaveRequest(
|
||||
@@ -106,7 +111,7 @@ class AutofillProcessorImpl(
|
||||
/**
|
||||
* Process the [fillRequest] and invoke the [FillCallback] with the response.
|
||||
*/
|
||||
private fun process(
|
||||
private suspend fun process(
|
||||
autofillAppInfo: AutofillAppInfo,
|
||||
fillCallback: FillCallback,
|
||||
fillRequest: FillRequest,
|
||||
@@ -118,27 +123,30 @@ class AutofillProcessorImpl(
|
||||
)
|
||||
when (autofillRequest) {
|
||||
is AutofillRequest.Fillable -> {
|
||||
job.cancel()
|
||||
job = scope.launch {
|
||||
// Fulfill the [autofillRequest].
|
||||
val filledData = filledDataBuilder.build(
|
||||
autofillRequest = autofillRequest,
|
||||
)
|
||||
val saveInfo = saveInfoBuilder.build(
|
||||
autofillAppInfo = autofillAppInfo,
|
||||
autofillPartition = autofillRequest.partition,
|
||||
fillRequest = fillRequest,
|
||||
packageName = autofillRequest.packageName,
|
||||
)
|
||||
// Fulfill the [autofillRequest].
|
||||
val filledData = filledDataBuilder.build(
|
||||
autofillRequest = autofillRequest,
|
||||
)
|
||||
val saveInfo = saveInfoBuilder.build(
|
||||
autofillPartition = autofillRequest.partition,
|
||||
fillRequest = fillRequest,
|
||||
packageName = autofillRequest.packageName,
|
||||
)
|
||||
|
||||
// Load the filledData and saveInfo into a FillResponse.
|
||||
val response = fillResponseBuilder.build(
|
||||
autofillAppInfo = autofillAppInfo,
|
||||
filledData = filledData,
|
||||
saveInfo = saveInfo,
|
||||
)
|
||||
// Load the filledData and saveInfo into a FillResponse.
|
||||
val response = fillResponseBuilder.build(
|
||||
autofillAppInfo = autofillAppInfo,
|
||||
filledData = filledData,
|
||||
saveInfo = saveInfo,
|
||||
)
|
||||
|
||||
@Suppress("TooGenericExceptionCaught")
|
||||
try {
|
||||
fillCallback.onSuccess(response)
|
||||
} catch (e: RuntimeException) {
|
||||
// This is to catch any TransactionTooLargeExceptions that could occur here.
|
||||
// These exceptions get wrapped as a RuntimeException.
|
||||
crashLogsManager.trackNonFatalException(e)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -6,11 +6,22 @@ import com.bitwarden.vault.CipherView
|
||||
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
|
||||
import com.x8bit.bitwarden.data.autofill.model.AutofillCipher
|
||||
import com.x8bit.bitwarden.data.platform.manager.ciphermatching.CipherMatchingManager
|
||||
import com.x8bit.bitwarden.data.platform.util.firstWithTimeoutOrNull
|
||||
import com.x8bit.bitwarden.data.platform.util.subtitle
|
||||
import com.x8bit.bitwarden.data.vault.repository.VaultRepository
|
||||
import com.x8bit.bitwarden.data.vault.repository.model.VaultUnlockData
|
||||
import com.x8bit.bitwarden.data.vault.repository.util.statusFor
|
||||
import kotlinx.coroutines.flow.first
|
||||
|
||||
/**
|
||||
* The duration, in milliseconds, we should wait while waiting for the vault status to not be
|
||||
* 'UNLOCKING' before proceeding.
|
||||
*/
|
||||
private const val VAULT_LOCKED_TIMEOUT_MS: Long = 500L
|
||||
|
||||
/**
|
||||
* The duration, in milliseconds, we should wait while retrieving ciphers before proceeding.
|
||||
*/
|
||||
private const val GET_CIPHERS_TIMEOUT_MS: Long = 2_000L
|
||||
|
||||
/**
|
||||
* The default [AutofillCipherProvider] implementation. This service is used for getting current
|
||||
@@ -28,9 +39,11 @@ class AutofillCipherProviderImpl(
|
||||
|
||||
// Wait for any unlocking actions to finish. This can be relevant on startup for Never lock
|
||||
// accounts.
|
||||
vaultRepository.vaultUnlockDataStateFlow.first {
|
||||
it.statusFor(userId) != VaultUnlockData.Status.UNLOCKING
|
||||
}
|
||||
vaultRepository
|
||||
.vaultUnlockDataStateFlow
|
||||
.firstWithTimeoutOrNull(timeMillis = VAULT_LOCKED_TIMEOUT_MS) {
|
||||
it.statusFor(userId = userId) != VaultUnlockData.Status.UNLOCKING
|
||||
}
|
||||
|
||||
return !vaultRepository.isVaultUnlocked(userId = userId)
|
||||
}
|
||||
@@ -105,6 +118,6 @@ class AutofillCipherProviderImpl(
|
||||
vaultRepository
|
||||
.ciphersStateFlow
|
||||
.takeUnless { isVaultLocked() }
|
||||
?.first { it.data != null }
|
||||
?.firstWithTimeoutOrNull(timeMillis = GET_CIPHERS_TIMEOUT_MS) { it.data != null }
|
||||
?.data
|
||||
}
|
||||
|
||||
@@ -49,4 +49,4 @@ fun CipherView.toAutofillCipherProvider(): AutofillCipherProvider =
|
||||
* Returns true when the cipher is not deleted and contains at least one FIDO 2 credential.
|
||||
*/
|
||||
val CipherView.isActiveWithFido2Credentials: Boolean
|
||||
get() = deletedDate == null && login?.fido2Credentials.isNullOrEmpty().not()
|
||||
get() = deletedDate == null && !(login?.fido2Credentials.isNullOrEmpty())
|
||||
|
||||
@@ -94,6 +94,7 @@ fun AssistStructure.ViewNode.toAutofillView(): AutofillView? =
|
||||
autofillType = this.autofillType,
|
||||
isFocused = this.isFocused,
|
||||
textValue = this.autofillValue?.extractTextValue(),
|
||||
hasPasswordTerms = this.hasPasswordTerms(),
|
||||
)
|
||||
buildAutofillView(
|
||||
autofillOptions = autofillOptions,
|
||||
@@ -171,8 +172,6 @@ fun AssistStructure.ViewNode.isPasswordField(
|
||||
): Boolean {
|
||||
if (supportedHint == View.AUTOFILL_HINT_PASSWORD) return true
|
||||
|
||||
if (this.hint?.containsAnyTerms(SUPPORTED_RAW_PASSWORD_HINTS) == true) return true
|
||||
|
||||
val isInvalidField = this.idEntry?.containsAnyTerms(IGNORED_RAW_HINTS) == true ||
|
||||
this.hint?.containsAnyTerms(IGNORED_RAW_HINTS) == true
|
||||
val isUsernameField = this.isUsernameField(supportedHint)
|
||||
@@ -183,6 +182,13 @@ fun AssistStructure.ViewNode.isPasswordField(
|
||||
.isPasswordField()
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether this [AssistStructure.ViewNode] includes any password specific terms.
|
||||
*/
|
||||
fun AssistStructure.ViewNode.hasPasswordTerms(): Boolean =
|
||||
this.idEntry?.containsAnyTerms(SUPPORTED_RAW_PASSWORD_HINTS) == true ||
|
||||
this.hint?.containsAnyTerms(SUPPORTED_RAW_PASSWORD_HINTS) == true
|
||||
|
||||
/**
|
||||
* Check whether this [AssistStructure.ViewNode] represents a username field.
|
||||
*/
|
||||
|
||||
@@ -9,6 +9,8 @@ import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import java.security.GeneralSecurityException
|
||||
import java.security.KeyStore
|
||||
import javax.inject.Singleton
|
||||
|
||||
/**
|
||||
@@ -35,14 +37,57 @@ object PreferenceModule {
|
||||
fun provideEncryptedSharedPreferences(
|
||||
application: Application,
|
||||
): SharedPreferences =
|
||||
EncryptedSharedPreferences
|
||||
.create(
|
||||
application,
|
||||
"${application.packageName}_encrypted_preferences",
|
||||
MasterKey.Builder(application)
|
||||
.setKeyScheme(MasterKey.KeyScheme.AES256_GCM)
|
||||
.build(),
|
||||
EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
|
||||
EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM,
|
||||
)
|
||||
@Suppress("TooGenericExceptionCaught")
|
||||
try {
|
||||
getEncryptedSharedPreferences(application = application)
|
||||
} catch (e: GeneralSecurityException) {
|
||||
// Handle when a bad master key or key-set has been attempted
|
||||
destroyEncryptedSharedPreferencesAndRebuild(application = application)
|
||||
} catch (e: RuntimeException) {
|
||||
// Handle KeystoreExceptions that get wrapped up in a RuntimeException
|
||||
destroyEncryptedSharedPreferencesAndRebuild(application = application)
|
||||
}
|
||||
|
||||
/**
|
||||
* Completely destroys the keystore master key and encrypted shared preferences file. This will
|
||||
* cause all users to be logged out since the access and refresh tokens will be removed.
|
||||
*
|
||||
* This is not desirable and should only be called if we have completely failed to access our
|
||||
* encrypted shared preferences instance.
|
||||
*/
|
||||
private fun destroyEncryptedSharedPreferencesAndRebuild(
|
||||
application: Application,
|
||||
): SharedPreferences {
|
||||
// Delete the master key
|
||||
KeyStore.getInstance(KeyStore.getDefaultType()).run {
|
||||
load(null)
|
||||
deleteEntry(MasterKey.DEFAULT_MASTER_KEY_ALIAS)
|
||||
}
|
||||
// Deletes the encrypted shared preferences file
|
||||
application.deleteSharedPreferences(application.encryptedSharedPreferencesName)
|
||||
// Attempts to create the encrypted shared preferences instance
|
||||
return getEncryptedSharedPreferences(application = application)
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method to get the app's encrypted shared preferences instance.
|
||||
*/
|
||||
private fun getEncryptedSharedPreferences(
|
||||
application: Application,
|
||||
): SharedPreferences =
|
||||
EncryptedSharedPreferences.create(
|
||||
application,
|
||||
application.encryptedSharedPreferencesName,
|
||||
MasterKey.Builder(application)
|
||||
.setKeyScheme(MasterKey.KeyScheme.AES256_GCM)
|
||||
.build(),
|
||||
EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
|
||||
EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM,
|
||||
)
|
||||
|
||||
/**
|
||||
* Helper method to get the app's encrypted shared preferences name.
|
||||
*/
|
||||
private val Application.encryptedSharedPreferencesName: String
|
||||
get() = "${packageName}_encrypted_preferences"
|
||||
}
|
||||
|
||||
@@ -11,18 +11,15 @@ abstract class BaseDiskSource(
|
||||
private val sharedPreferences: SharedPreferences,
|
||||
) {
|
||||
/**
|
||||
* Gets the [Boolean] for the given [key] from [SharedPreferences], or return the [default]
|
||||
* value if that key is not present.
|
||||
* Gets the [Boolean] for the given [key] from [SharedPreferences], or returns `null` if that
|
||||
* key is not present.
|
||||
*/
|
||||
protected fun getBoolean(
|
||||
key: String,
|
||||
default: Boolean? = null,
|
||||
): Boolean? =
|
||||
protected fun getBoolean(key: String): Boolean? =
|
||||
if (sharedPreferences.contains(key.withBase())) {
|
||||
sharedPreferences.getBoolean(key.withBase(), false)
|
||||
} else {
|
||||
// Make sure we can return a null value as a default if necessary
|
||||
default
|
||||
null
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -42,18 +39,15 @@ abstract class BaseDiskSource(
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the [Int] for the given [key] from [SharedPreferences], or return the [default] value
|
||||
* if that key is not present.
|
||||
* Gets the [Int] for the given [key] from [SharedPreferences], or returns `null` if that key
|
||||
* is not present.
|
||||
*/
|
||||
protected fun getInt(
|
||||
key: String,
|
||||
default: Int? = null,
|
||||
): Int? =
|
||||
protected fun getInt(key: String): Int? =
|
||||
if (sharedPreferences.contains(key.withBase())) {
|
||||
sharedPreferences.getInt(key.withBase(), 0)
|
||||
} else {
|
||||
// Make sure we can return a null value as a default if necessary
|
||||
default
|
||||
null
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -73,18 +67,15 @@ abstract class BaseDiskSource(
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the [Long] for the given [key] from [SharedPreferences], or return the [default] value
|
||||
* if that key is not present.
|
||||
* Gets the [Long] for the given [key] from [SharedPreferences], or returns `null` if that key
|
||||
* is not present.
|
||||
*/
|
||||
protected fun getLong(
|
||||
key: String,
|
||||
default: Long? = null,
|
||||
): Long? =
|
||||
protected fun getLong(key: String): Long? =
|
||||
if (sharedPreferences.contains(key.withBase())) {
|
||||
sharedPreferences.getLong(key.withBase(), 0)
|
||||
} else {
|
||||
// Make sure we can return a null value as a default if necessary
|
||||
default
|
||||
null
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -105,8 +96,7 @@ abstract class BaseDiskSource(
|
||||
|
||||
protected fun getString(
|
||||
key: String,
|
||||
default: String? = null,
|
||||
): String? = sharedPreferences.getString(key.withBase(), default)
|
||||
): String? = sharedPreferences.getString(key.withBase(), null)
|
||||
|
||||
protected fun putString(
|
||||
key: String,
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
package com.x8bit.bitwarden.data.platform.datasource.disk
|
||||
|
||||
import com.x8bit.bitwarden.data.platform.datasource.disk.model.ServerConfig
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
/**
|
||||
* Primary access point for server configuration-related disk information.
|
||||
*/
|
||||
interface ConfigDiskSource {
|
||||
|
||||
/**
|
||||
* The currently persisted [ServerConfig] (or `null` if not set).
|
||||
*/
|
||||
var serverConfig: ServerConfig?
|
||||
|
||||
/**
|
||||
* Emits updates that track [ServerConfig]. This will replay the last known value,
|
||||
* if any.
|
||||
*/
|
||||
val serverConfigFlow: Flow<ServerConfig?>
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
package com.x8bit.bitwarden.data.platform.datasource.disk
|
||||
|
||||
import android.content.SharedPreferences
|
||||
import com.x8bit.bitwarden.data.platform.datasource.disk.model.ServerConfig
|
||||
import com.x8bit.bitwarden.data.platform.repository.util.bufferedMutableSharedFlow
|
||||
import com.x8bit.bitwarden.data.platform.util.decodeFromStringOrNull
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.onSubscription
|
||||
import kotlinx.serialization.encodeToString
|
||||
import kotlinx.serialization.json.Json
|
||||
|
||||
private const val SERVER_CONFIGURATIONS = "serverConfigurations"
|
||||
|
||||
/**
|
||||
* Primary implementation of [ConfigDiskSource].
|
||||
*/
|
||||
class ConfigDiskSourceImpl(
|
||||
sharedPreferences: SharedPreferences,
|
||||
private val json: Json,
|
||||
) : BaseDiskSource(sharedPreferences = sharedPreferences),
|
||||
ConfigDiskSource {
|
||||
|
||||
override var serverConfig: ServerConfig?
|
||||
get() = getString(key = SERVER_CONFIGURATIONS)?.let { json.decodeFromStringOrNull(it) }
|
||||
set(value) {
|
||||
putString(
|
||||
key = SERVER_CONFIGURATIONS,
|
||||
value = value?.let { json.encodeToString(it) },
|
||||
)
|
||||
mutableServerConfigFlow.tryEmit(value)
|
||||
}
|
||||
|
||||
override val serverConfigFlow: Flow<ServerConfig?>
|
||||
get() = mutableServerConfigFlow.onSubscription { emit(serverConfig) }
|
||||
|
||||
private val mutableServerConfigFlow = bufferedMutableSharedFlow<ServerConfig?>(replay = 1)
|
||||
}
|
||||
@@ -17,4 +17,14 @@ interface EnvironmentDiskSource {
|
||||
* if any.
|
||||
*/
|
||||
val preAuthEnvironmentUrlDataFlow: Flow<EnvironmentUrlDataJson?>
|
||||
|
||||
/**
|
||||
* Gets the pre authentication urls for the given [userEmail].
|
||||
*/
|
||||
fun getPreAuthEnvironmentUrlDataForEmail(userEmail: String): EnvironmentUrlDataJson?
|
||||
|
||||
/**
|
||||
* Stores the [urls] for the given [userEmail].
|
||||
*/
|
||||
fun storePreAuthEnvironmentUrlDataForEmail(userEmail: String, urls: EnvironmentUrlDataJson)
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import kotlinx.serialization.encodeToString
|
||||
import kotlinx.serialization.json.Json
|
||||
|
||||
private const val PRE_AUTH_URLS_KEY = "preAuthEnvironmentUrls"
|
||||
private const val EMAIL_VERIFICATION_URLS = "emailVerificationUrls"
|
||||
|
||||
/**
|
||||
* Primary implementation of [EnvironmentDiskSource].
|
||||
@@ -24,7 +25,7 @@ class EnvironmentDiskSourceImpl(
|
||||
set(value) {
|
||||
putString(
|
||||
key = PRE_AUTH_URLS_KEY,
|
||||
value = value?.let { json.encodeToString(value) },
|
||||
value = value?.let { json.encodeToString(it) },
|
||||
)
|
||||
mutableEnvironmentUrlDataFlow.tryEmit(value)
|
||||
}
|
||||
@@ -35,4 +36,22 @@ class EnvironmentDiskSourceImpl(
|
||||
|
||||
private val mutableEnvironmentUrlDataFlow =
|
||||
bufferedMutableSharedFlow<EnvironmentUrlDataJson?>(replay = 1)
|
||||
|
||||
override fun getPreAuthEnvironmentUrlDataForEmail(
|
||||
userEmail: String,
|
||||
): EnvironmentUrlDataJson? =
|
||||
getString(key = EMAIL_VERIFICATION_URLS.appendIdentifier(userEmail))
|
||||
?.let {
|
||||
json.decodeFromStringOrNull(it)
|
||||
}
|
||||
|
||||
override fun storePreAuthEnvironmentUrlDataForEmail(
|
||||
userEmail: String,
|
||||
urls: EnvironmentUrlDataJson,
|
||||
) {
|
||||
putString(
|
||||
key = EMAIL_VERIFICATION_URLS.appendIdentifier(userEmail),
|
||||
value = json.encodeToString(urls),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,7 +35,7 @@ class PushDiskSourceImpl(
|
||||
}
|
||||
|
||||
override fun getLastPushTokenRegistrationDate(userId: String): ZonedDateTime? {
|
||||
return getLong(LAST_REGISTRATION_DATE_KEY.appendIdentifier(userId), null)
|
||||
return getLong(LAST_REGISTRATION_DATE_KEY.appendIdentifier(userId))
|
||||
?.let { getZoneDateTimeFromBinaryLong(it) }
|
||||
}
|
||||
|
||||
|
||||
@@ -6,6 +6,8 @@ import android.content.SharedPreferences
|
||||
import androidx.room.Room
|
||||
import com.x8bit.bitwarden.data.platform.datasource.di.EncryptedPreferences
|
||||
import com.x8bit.bitwarden.data.platform.datasource.di.UnencryptedPreferences
|
||||
import com.x8bit.bitwarden.data.platform.datasource.disk.ConfigDiskSource
|
||||
import com.x8bit.bitwarden.data.platform.datasource.disk.ConfigDiskSourceImpl
|
||||
import com.x8bit.bitwarden.data.platform.datasource.disk.EnvironmentDiskSource
|
||||
import com.x8bit.bitwarden.data.platform.datasource.disk.EnvironmentDiskSourceImpl
|
||||
import com.x8bit.bitwarden.data.platform.datasource.disk.EventDiskSource
|
||||
@@ -51,6 +53,17 @@ object PlatformDiskModule {
|
||||
json = json,
|
||||
)
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideConfigDiskSource(
|
||||
@UnencryptedPreferences sharedPreferences: SharedPreferences,
|
||||
json: Json,
|
||||
): ConfigDiskSource =
|
||||
ConfigDiskSourceImpl(
|
||||
sharedPreferences = sharedPreferences,
|
||||
json = json,
|
||||
)
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideEventDatabase(app: Application): PlatformDatabase =
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
package com.x8bit.bitwarden.data.platform.datasource.disk.model
|
||||
|
||||
import com.x8bit.bitwarden.data.platform.datasource.network.model.ConfigResponseJson
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
/**
|
||||
* A higher-level wrapper around [ConfigResponseJson] that provides a timestamp
|
||||
* to check if a sync is necessary
|
||||
*
|
||||
* @property lastSync The [Long] of the last sync.
|
||||
* @property serverData The raw [ConfigResponseJson] that contains specific data of the
|
||||
* server configuration
|
||||
*/
|
||||
@Serializable
|
||||
data class ServerConfig(
|
||||
val lastSync: Long,
|
||||
val serverData: ConfigResponseJson,
|
||||
)
|
||||
@@ -9,9 +9,13 @@ import retrofit2.Call
|
||||
import retrofit2.Callback
|
||||
import retrofit2.HttpException
|
||||
import retrofit2.Response
|
||||
import retrofit2.Response.success
|
||||
import java.lang.reflect.Type
|
||||
|
||||
/**
|
||||
* The integer code value for a "No Content" response.
|
||||
*/
|
||||
private const val NO_CONTENT_RESPONSE_CODE: Int = 204
|
||||
|
||||
/**
|
||||
* A [Call] for wrapping a network request into a [Result].
|
||||
*/
|
||||
@@ -23,58 +27,28 @@ class ResultCall<T>(
|
||||
|
||||
override fun clone(): Call<Result<T>> = ResultCall(backingCall, successType)
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
private fun createResult(body: T?): Result<T> {
|
||||
return when {
|
||||
body != null -> body.asSuccess()
|
||||
successType == Unit::class.java -> (Unit as T).asSuccess()
|
||||
else -> IllegalStateException("Unexpected null body!").asFailure()
|
||||
}
|
||||
}
|
||||
|
||||
override fun enqueue(callback: Callback<Result<T>>): Unit = backingCall.enqueue(
|
||||
object : Callback<T> {
|
||||
override fun onResponse(call: Call<T>, response: Response<T>) {
|
||||
val body = response.body()
|
||||
val result: Result<T> = if (!response.isSuccessful) {
|
||||
HttpException(response).asFailure()
|
||||
} else {
|
||||
createResult(body)
|
||||
}
|
||||
callback.onResponse(this@ResultCall, success(result))
|
||||
callback.onResponse(this@ResultCall, Response.success(response.toResult()))
|
||||
}
|
||||
|
||||
override fun onFailure(call: Call<T>, t: Throwable) {
|
||||
val result: Result<T> = t.asFailure()
|
||||
callback.onResponse(this@ResultCall, success(result))
|
||||
callback.onResponse(this@ResultCall, Response.success(t.asFailure()))
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
/**
|
||||
* Synchronously send the request and return its response as a [Result].
|
||||
*/
|
||||
fun executeForResult(): Result<T> = requireNotNull(execute().body())
|
||||
|
||||
@Suppress("TooGenericExceptionCaught")
|
||||
override fun execute(): Response<Result<T>> {
|
||||
val response = try {
|
||||
backingCall.execute()
|
||||
override fun execute(): Response<Result<T>> =
|
||||
try {
|
||||
Response.success(backingCall.execute().toResult())
|
||||
} catch (ioException: IOException) {
|
||||
return success(ioException.asFailure())
|
||||
Response.success(ioException.asFailure())
|
||||
} catch (runtimeException: RuntimeException) {
|
||||
return success(runtimeException.asFailure())
|
||||
Response.success(runtimeException.asFailure())
|
||||
}
|
||||
|
||||
return success(
|
||||
if (!response.isSuccessful) {
|
||||
HttpException(response).asFailure()
|
||||
} else {
|
||||
createResult(response.body())
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
override fun isCanceled(): Boolean = backingCall.isCanceled
|
||||
|
||||
override fun isExecuted(): Boolean = backingCall.isExecuted
|
||||
@@ -82,4 +56,27 @@ class ResultCall<T>(
|
||||
override fun request(): Request = backingCall.request()
|
||||
|
||||
override fun timeout(): Timeout = backingCall.timeout()
|
||||
|
||||
/**
|
||||
* Synchronously send the request and return its response as a [Result].
|
||||
*/
|
||||
fun executeForResult(): Result<T> = requireNotNull(execute().body())
|
||||
|
||||
private fun Response<T>.toResult(): Result<T> =
|
||||
if (!this.isSuccessful) {
|
||||
HttpException(this).asFailure()
|
||||
} else {
|
||||
val body = this.body()
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
when {
|
||||
// We got a nonnull T as the body, just return it.
|
||||
body != null -> body.asSuccess()
|
||||
// We expected the body to be null since the successType is Unit, just return Unit.
|
||||
successType == Unit::class.java -> (Unit as T).asSuccess()
|
||||
// We allow null for 204's, just return null.
|
||||
this.code() == NO_CONTENT_RESPONSE_CODE -> (null as T).asSuccess()
|
||||
// All other null bodies result in an error.
|
||||
else -> IllegalStateException("Unexpected null body!").asFailure()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package com.x8bit.bitwarden.data.platform.datasource.network.model
|
||||
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.json.JsonPrimitive
|
||||
|
||||
/**
|
||||
* Represents the response model for configuration data fetched from the server.
|
||||
@@ -31,7 +32,7 @@ data class ConfigResponseJson(
|
||||
val environment: EnvironmentJson?,
|
||||
|
||||
@SerialName("featureStates")
|
||||
val featureStates: Map<String, Boolean>?,
|
||||
val featureStates: Map<String, JsonPrimitive>?,
|
||||
) {
|
||||
/**
|
||||
* Represents a server in the configuration response.
|
||||
|
||||
@@ -2,6 +2,7 @@ package com.x8bit.bitwarden.data.platform.datasource.network.retrofit
|
||||
|
||||
import com.x8bit.bitwarden.data.platform.datasource.network.interceptor.BaseUrlInterceptors
|
||||
import retrofit2.Retrofit
|
||||
import retrofit2.http.Url
|
||||
|
||||
/**
|
||||
* A collection of various [Retrofit] instances that serve different purposes.
|
||||
@@ -36,11 +37,14 @@ interface Retrofits {
|
||||
val unauthenticatedIdentityRetrofit: Retrofit
|
||||
|
||||
/**
|
||||
* Allows access to static API calls (ex: external APIs) that do not therefore require
|
||||
* authentication with Bitwarden's servers.
|
||||
* Allows access to static API calls (ex: external APIs).
|
||||
*
|
||||
* No base URL is supplied as part of the builder and no longer is added to make this URL
|
||||
* dynamically updatable.
|
||||
* @param isAuthenticated Indicates if the [Retrofit] instance should use authentication.
|
||||
* @param baseUrl The static base url associated with this retrofit instance. This can be
|
||||
* overridden with the [Url] annotation.
|
||||
*/
|
||||
val staticRetrofitBuilder: Retrofit.Builder
|
||||
fun createStaticRetrofit(
|
||||
isAuthenticated: Boolean = false,
|
||||
baseUrl: String = "https://api.bitwarden.com",
|
||||
): Retrofit
|
||||
}
|
||||
|
||||
@@ -60,19 +60,22 @@ class RetrofitsImpl(
|
||||
|
||||
//endregion Unauthenticated Retrofits
|
||||
|
||||
//region Other Retrofits
|
||||
//region Static Retrofit
|
||||
|
||||
override val staticRetrofitBuilder: Retrofit.Builder
|
||||
get() =
|
||||
baseRetrofitBuilder
|
||||
.client(
|
||||
baseOkHttpClient
|
||||
.newBuilder()
|
||||
.addInterceptor(loggingInterceptor)
|
||||
.build(),
|
||||
)
|
||||
override fun createStaticRetrofit(isAuthenticated: Boolean, baseUrl: String): Retrofit {
|
||||
val baseClient = if (isAuthenticated) authenticatedOkHttpClient else baseOkHttpClient
|
||||
return baseRetrofitBuilder
|
||||
.baseUrl(baseUrl)
|
||||
.client(
|
||||
baseClient
|
||||
.newBuilder()
|
||||
.addInterceptor(loggingInterceptor)
|
||||
.build(),
|
||||
)
|
||||
.build()
|
||||
}
|
||||
|
||||
//endregion Other Retrofits
|
||||
//endregion Static Retrofit
|
||||
|
||||
//region Helper properties and functions
|
||||
private val loggingInterceptor: HttpLoggingInterceptor by lazy {
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
package com.x8bit.bitwarden.data.platform.datasource.sdk
|
||||
|
||||
import android.util.Log
|
||||
import com.bitwarden.sdk.Client
|
||||
import com.x8bit.bitwarden.BuildConfig
|
||||
import com.x8bit.bitwarden.data.platform.manager.SdkClientManager
|
||||
|
||||
/**
|
||||
* Base class for simplifying sdk interactions.
|
||||
*/
|
||||
@Suppress("UnnecessaryAbstractClass")
|
||||
abstract class BaseSdkSource(
|
||||
protected val sdkClientManager: SdkClientManager,
|
||||
) {
|
||||
/**
|
||||
* Helper function to retrieve the [Client] associated with the given [userId].
|
||||
*/
|
||||
protected suspend fun getClient(
|
||||
userId: String? = null,
|
||||
): Client = sdkClientManager.getOrCreateClient(userId = userId)
|
||||
|
||||
/**
|
||||
* Invokes the [block] with `this` value as its receiver and returns its result if it was
|
||||
* successful and catches any exception that was thrown from the `block` and wrapping it as a
|
||||
* failure.
|
||||
*/
|
||||
protected inline fun <T, R> T.runCatchingWithLogs(
|
||||
block: T.() -> R,
|
||||
): Result<R> = runCatching(block = block)
|
||||
.onFailure {
|
||||
if (BuildConfig.DEBUG) {
|
||||
Log.w(this@BaseSdkSource::class.java.simpleName, it)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
package com.x8bit.bitwarden.data.platform.manager
|
||||
|
||||
import com.x8bit.bitwarden.data.platform.manager.model.FlagKey
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
/**
|
||||
* Manages the available feature flags for the Bitwarden application.
|
||||
*/
|
||||
interface FeatureFlagManager {
|
||||
/**
|
||||
* Returns a map of constant feature flags that are only used locally.
|
||||
*/
|
||||
val sdkFeatureFlags: Map<String, Boolean>
|
||||
|
||||
/**
|
||||
* Returns a flow emitting the value of flag [key] which is of generic type [T].
|
||||
* If the value of the flag cannot be retrieved, the default value of [key] will be returned
|
||||
*/
|
||||
fun <T : Any> getFeatureFlagFlow(key: FlagKey<T>): Flow<T>
|
||||
|
||||
/**
|
||||
* Get value for feature flag with [key] and returns it as generic type [T].
|
||||
* If no value is found the given [key] its default value will be returned.
|
||||
* Cached flags can be invalidated with [forceRefresh]
|
||||
*/
|
||||
suspend fun <T : Any> getFeatureFlag(
|
||||
key: FlagKey<T>,
|
||||
forceRefresh: Boolean,
|
||||
): T
|
||||
|
||||
/**
|
||||
* Gets the value for feature flag with [key] and returns it as generic type [T].
|
||||
* If no value is found the given [key] its [FlagKey.defaultValue] will be returned.
|
||||
*/
|
||||
fun <T : Any> getFeatureFlag(
|
||||
key: FlagKey<T>,
|
||||
): T
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
package com.x8bit.bitwarden.data.platform.manager
|
||||
|
||||
import com.x8bit.bitwarden.data.platform.datasource.disk.model.ServerConfig
|
||||
import com.x8bit.bitwarden.data.platform.manager.model.FlagKey
|
||||
import com.x8bit.bitwarden.data.platform.repository.ServerConfigRepository
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.map
|
||||
|
||||
private const val CIPHER_KEY_ENCRYPTION_KEY = "enableCipherKeyEncryption"
|
||||
|
||||
/**
|
||||
* Primary implementation of [FeatureFlagManager].
|
||||
*/
|
||||
class FeatureFlagManagerImpl(
|
||||
private val serverConfigRepository: ServerConfigRepository,
|
||||
) : FeatureFlagManager {
|
||||
|
||||
override val sdkFeatureFlags: Map<String, Boolean>
|
||||
get() = mapOf(CIPHER_KEY_ENCRYPTION_KEY to true)
|
||||
|
||||
override fun <T : Any> getFeatureFlagFlow(key: FlagKey<T>): Flow<T> =
|
||||
serverConfigRepository
|
||||
.serverConfigStateFlow
|
||||
.map { serverConfig ->
|
||||
serverConfig.getFlagValueOrDefault(key = key)
|
||||
}
|
||||
|
||||
override suspend fun <T : Any> getFeatureFlag(
|
||||
key: FlagKey<T>,
|
||||
forceRefresh: Boolean,
|
||||
): T =
|
||||
serverConfigRepository
|
||||
.getServerConfig(forceRefresh = forceRefresh)
|
||||
.getFlagValueOrDefault(key = key)
|
||||
|
||||
override fun <T : Any> getFeatureFlag(key: FlagKey<T>): T =
|
||||
serverConfigRepository
|
||||
.serverConfigStateFlow
|
||||
.value
|
||||
.getFlagValueOrDefault(key = key)
|
||||
}
|
||||
|
||||
private fun <T : Any> ServerConfig?.getFlagValueOrDefault(key: FlagKey<T>): T {
|
||||
val defaultValue = key.defaultValue
|
||||
if (!key.isRemotelyConfigured) return key.defaultValue
|
||||
return this
|
||||
?.serverData
|
||||
?.featureStates
|
||||
?.get(key.keyName)
|
||||
?.let {
|
||||
try {
|
||||
// Suppressed since we are checking the type before doing the cast
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
when (defaultValue::class) {
|
||||
Boolean::class -> it.content.toBoolean() as T
|
||||
String::class -> it.content as T
|
||||
Int::class -> it.content.toInt() as T
|
||||
else -> defaultValue
|
||||
}
|
||||
} catch (ex: ClassCastException) {
|
||||
defaultValue
|
||||
} catch (ex: NumberFormatException) {
|
||||
defaultValue
|
||||
}
|
||||
}
|
||||
?: defaultValue
|
||||
}
|
||||
@@ -7,17 +7,23 @@ import com.x8bit.bitwarden.data.platform.datasource.network.interceptor.AuthToke
|
||||
import com.x8bit.bitwarden.data.platform.datasource.network.interceptor.BaseUrlInterceptors
|
||||
import com.x8bit.bitwarden.data.platform.manager.dispatcher.DispatcherManager
|
||||
import com.x8bit.bitwarden.data.platform.repository.EnvironmentRepository
|
||||
import com.x8bit.bitwarden.data.platform.repository.ServerConfigRepository
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.flow.debounce
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
|
||||
private const val ENVIRONMENT_DEBOUNCE_TIMEOUT_MS: Long = 500L
|
||||
|
||||
/**
|
||||
* Primary implementation of [NetworkConfigManager].
|
||||
*/
|
||||
@Suppress("LongParameterList")
|
||||
class NetworkConfigManagerImpl(
|
||||
authRepository: AuthRepository,
|
||||
private val authTokenInterceptor: AuthTokenInterceptor,
|
||||
environmentRepository: EnvironmentRepository,
|
||||
serverConfigRepository: ServerConfigRepository,
|
||||
private val baseUrlInterceptors: BaseUrlInterceptors,
|
||||
refreshAuthenticator: RefreshAuthenticator,
|
||||
dispatcherManager: DispatcherManager,
|
||||
@@ -37,11 +43,18 @@ class NetworkConfigManagerImpl(
|
||||
}
|
||||
.launchIn(collectionScope)
|
||||
|
||||
@Suppress("OPT_IN_USAGE")
|
||||
environmentRepository
|
||||
.environmentStateFlow
|
||||
.onEach { environment ->
|
||||
baseUrlInterceptors.environment = environment
|
||||
}
|
||||
.debounce(timeoutMillis = ENVIRONMENT_DEBOUNCE_TIMEOUT_MS)
|
||||
.onEach { _ ->
|
||||
// This updates the stored service configuration by performing a network request.
|
||||
// We debounce it to avoid rapid repeated requests.
|
||||
serverConfigRepository.getServerConfig(forceRefresh = true)
|
||||
}
|
||||
.launchIn(collectionScope)
|
||||
|
||||
refreshAuthenticator.authenticatorProvider = authRepository
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
package com.x8bit.bitwarden.data.platform.manager
|
||||
|
||||
/**
|
||||
* A manager for caching resources that are large and could be performance impacting to load
|
||||
* multiple times.
|
||||
*/
|
||||
interface ResourceCacheManager {
|
||||
/**
|
||||
* Retrieves the exception suffix list used for matching a cipher against a domain.
|
||||
*/
|
||||
val domainExceptionSuffixes: List<String>
|
||||
|
||||
/**
|
||||
* Retrieves the normal suffix list used for matching a cipher against a domain.
|
||||
*/
|
||||
val domainNormalSuffixes: List<String>
|
||||
|
||||
/**
|
||||
* Retrieves the wild card suffix list used for matching a cipher against a domain.
|
||||
*/
|
||||
val domainWildCardSuffixes: List<String>
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
package com.x8bit.bitwarden.data.platform.manager
|
||||
|
||||
import android.content.Context
|
||||
import com.x8bit.bitwarden.R
|
||||
|
||||
/**
|
||||
* Primary implementation of [ResourceCacheManager].
|
||||
*/
|
||||
class ResourceCacheManagerImpl(
|
||||
private val context: Context,
|
||||
) : ResourceCacheManager {
|
||||
override val domainExceptionSuffixes: List<String> by lazy {
|
||||
context
|
||||
.resources
|
||||
.getStringArray(R.array.exception_suffixes)
|
||||
.toList()
|
||||
}
|
||||
|
||||
override val domainNormalSuffixes: List<String> by lazy {
|
||||
context
|
||||
.resources
|
||||
.getStringArray(R.array.normal_suffixes)
|
||||
.toList()
|
||||
}
|
||||
|
||||
override val domainWildCardSuffixes: List<String> by lazy {
|
||||
context
|
||||
.resources
|
||||
.getStringArray(R.array.wild_card_suffixes)
|
||||
.toList()
|
||||
}
|
||||
}
|
||||
@@ -1,16 +1,15 @@
|
||||
package com.x8bit.bitwarden.data.platform.manager
|
||||
|
||||
import com.bitwarden.sdk.Client
|
||||
import com.x8bit.bitwarden.data.vault.datasource.sdk.BitwardenFeatureFlagManager
|
||||
|
||||
/**
|
||||
* Primary implementation of [SdkClientManager].
|
||||
*/
|
||||
class SdkClientManagerImpl(
|
||||
private val featureFlagManager: BitwardenFeatureFlagManager,
|
||||
private val featureFlagManager: FeatureFlagManager,
|
||||
private val clientProvider: suspend () -> Client = {
|
||||
Client(settings = null).apply {
|
||||
platform().loadFlags(featureFlagManager.featureFlags)
|
||||
platform().loadFlags(featureFlagManager.sdkFeatureFlags)
|
||||
}
|
||||
},
|
||||
) : SdkClientManager {
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
package com.x8bit.bitwarden.data.platform.manager.ciphermatching
|
||||
|
||||
import android.content.Context
|
||||
import com.bitwarden.vault.CipherView
|
||||
import com.bitwarden.vault.LoginUriView
|
||||
import com.bitwarden.vault.UriMatchType
|
||||
import com.x8bit.bitwarden.data.platform.manager.ResourceCacheManager
|
||||
import com.x8bit.bitwarden.data.platform.repository.SettingsRepository
|
||||
import com.x8bit.bitwarden.data.platform.util.firstWithTimeoutOrNull
|
||||
import com.x8bit.bitwarden.data.platform.util.getDomainOrNull
|
||||
import com.x8bit.bitwarden.data.platform.util.getHostWithPortOrNull
|
||||
import com.x8bit.bitwarden.data.platform.util.getWebHostFromAndroidUriOrNull
|
||||
@@ -13,7 +14,6 @@ import com.x8bit.bitwarden.data.platform.util.regexOrNull
|
||||
import com.x8bit.bitwarden.data.vault.repository.VaultRepository
|
||||
import com.x8bit.bitwarden.data.vault.repository.model.DomainsData
|
||||
import com.x8bit.bitwarden.ui.platform.feature.settings.autofill.util.toSdkUriMatchType
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.mapNotNull
|
||||
import kotlin.text.RegexOption
|
||||
import kotlin.text.isNullOrBlank
|
||||
@@ -21,12 +21,17 @@ import kotlin.text.lowercase
|
||||
import kotlin.text.matches
|
||||
import kotlin.text.startsWith
|
||||
|
||||
/**
|
||||
* The duration, in milliseconds, we should wait while retrieving domain data before we proceed.
|
||||
*/
|
||||
private const val GET_DOMAINS_TIMEOUT_MS: Long = 1_000L
|
||||
|
||||
/**
|
||||
* The default [CipherMatchingManager] implementation. This class is responsible for matching
|
||||
* ciphers based on special criteria.
|
||||
*/
|
||||
class CipherMatchingManagerImpl(
|
||||
private val context: Context,
|
||||
private val resourceCacheManager: ResourceCacheManager,
|
||||
private val settingsRepository: SettingsRepository,
|
||||
private val vaultRepository: VaultRepository,
|
||||
) : CipherMatchingManager {
|
||||
@@ -37,12 +42,13 @@ class CipherMatchingManagerImpl(
|
||||
val equivalentDomainsData = vaultRepository
|
||||
.domainsStateFlow
|
||||
.mapNotNull { it.data }
|
||||
.first()
|
||||
.firstWithTimeoutOrNull(timeMillis = GET_DOMAINS_TIMEOUT_MS)
|
||||
?: return emptyList()
|
||||
|
||||
val isAndroidApp = matchUri.isAndroidApp()
|
||||
val defaultUriMatchType = settingsRepository.defaultUriMatchType.toSdkUriMatchType()
|
||||
val domain = matchUri
|
||||
.getDomainOrNull(context = context)
|
||||
.getDomainOrNull(resourceCacheManager = resourceCacheManager)
|
||||
?.lowercase()
|
||||
|
||||
// Retrieve domains that are considered equivalent to the specified matchUri for cipher
|
||||
@@ -61,8 +67,8 @@ class CipherMatchingManagerImpl(
|
||||
ciphers
|
||||
.forEach { cipherView ->
|
||||
val matchResult = checkForCipherMatch(
|
||||
resourceCacheManager = resourceCacheManager,
|
||||
cipherView = cipherView,
|
||||
context = context,
|
||||
defaultUriMatchType = defaultUriMatchType,
|
||||
isAndroidApp = isAndroidApp,
|
||||
matchUri = matchUri,
|
||||
@@ -136,7 +142,7 @@ private fun getMatchingDomains(
|
||||
* provide details on the match quality.
|
||||
*
|
||||
* @param cipherView The cipher to be judged for a match.
|
||||
* @param context A context for getting string resources.
|
||||
* @param resourceCacheManager The [ResourceCacheManager] for fetching cached resources.
|
||||
* @param defaultUriMatchType The global default [UriMatchType].
|
||||
* @param isAndroidApp Whether or not the [matchUri] belongs to an Android app.
|
||||
* @param matchingDomains The set of domains that match the domain of [matchUri].
|
||||
@@ -144,8 +150,8 @@ private fun getMatchingDomains(
|
||||
*/
|
||||
@Suppress("LongParameterList")
|
||||
private fun checkForCipherMatch(
|
||||
resourceCacheManager: ResourceCacheManager,
|
||||
cipherView: CipherView,
|
||||
context: Context,
|
||||
defaultUriMatchType: UriMatchType,
|
||||
isAndroidApp: Boolean,
|
||||
matchingDomains: MatchingDomains,
|
||||
@@ -156,7 +162,7 @@ private fun checkForCipherMatch(
|
||||
?.uris
|
||||
?.map { loginUriView ->
|
||||
loginUriView.checkForMatch(
|
||||
context = context,
|
||||
resourceCacheManager = resourceCacheManager,
|
||||
defaultUriMatchType = defaultUriMatchType,
|
||||
isAndroidApp = isAndroidApp,
|
||||
matchingDomains = matchingDomains,
|
||||
@@ -174,14 +180,14 @@ private fun checkForCipherMatch(
|
||||
/**
|
||||
* Check to see how well this [LoginUriView] matches [matchUri].
|
||||
*
|
||||
* @param context A context for getting app information.
|
||||
* @param resourceCacheManager The [ResourceCacheManager] for fetching cached resources.
|
||||
* @param defaultUriMatchType The global default [UriMatchType].
|
||||
* @param isAndroidApp Whether or not the [matchUri] belongs to an Android app.
|
||||
* @param matchingDomains The set of domains that match the domain of [matchUri].
|
||||
* @param matchUri The uri that this [LoginUriView] is being matched to.
|
||||
*/
|
||||
private fun LoginUriView.checkForMatch(
|
||||
context: Context,
|
||||
resourceCacheManager: ResourceCacheManager,
|
||||
defaultUriMatchType: UriMatchType,
|
||||
isAndroidApp: Boolean,
|
||||
matchingDomains: MatchingDomains,
|
||||
@@ -194,7 +200,7 @@ private fun LoginUriView.checkForMatch(
|
||||
when (matchType) {
|
||||
UriMatchType.DOMAIN -> {
|
||||
checkUriForDomainMatch(
|
||||
context = context,
|
||||
resourceCacheManager = resourceCacheManager,
|
||||
isAndroidApp = isAndroidApp,
|
||||
matchingDomains = matchingDomains,
|
||||
uri = loginViewUri,
|
||||
@@ -228,8 +234,8 @@ private fun LoginUriView.checkForMatch(
|
||||
* Check to see if [uri] matches [matchingDomains] in some way.
|
||||
*/
|
||||
private fun checkUriForDomainMatch(
|
||||
resourceCacheManager: ResourceCacheManager,
|
||||
isAndroidApp: Boolean,
|
||||
context: Context,
|
||||
matchingDomains: MatchingDomains,
|
||||
uri: String,
|
||||
): MatchResult = when {
|
||||
@@ -237,7 +243,7 @@ private fun checkUriForDomainMatch(
|
||||
isAndroidApp && matchingDomains.fuzzyMatches.contains(uri) -> MatchResult.FUZZY
|
||||
else -> {
|
||||
val domain = uri
|
||||
.getDomainOrNull(context = context)
|
||||
.getDomainOrNull(resourceCacheManager = resourceCacheManager)
|
||||
?.lowercase()
|
||||
|
||||
// We only care about fuzzy matches if we are isAndroidApp is true because the fuzzu
|
||||
|
||||
@@ -22,6 +22,8 @@ import com.x8bit.bitwarden.data.platform.manager.BiometricsEncryptionManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.BiometricsEncryptionManagerImpl
|
||||
import com.x8bit.bitwarden.data.platform.manager.CrashLogsManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.CrashLogsManagerImpl
|
||||
import com.x8bit.bitwarden.data.platform.manager.FeatureFlagManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.FeatureFlagManagerImpl
|
||||
import com.x8bit.bitwarden.data.platform.manager.NetworkConfigManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.NetworkConfigManagerImpl
|
||||
import com.x8bit.bitwarden.data.platform.manager.NetworkConnectionManager
|
||||
@@ -30,6 +32,8 @@ import com.x8bit.bitwarden.data.platform.manager.PolicyManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.PolicyManagerImpl
|
||||
import com.x8bit.bitwarden.data.platform.manager.PushManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.PushManagerImpl
|
||||
import com.x8bit.bitwarden.data.platform.manager.ResourceCacheManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.ResourceCacheManagerImpl
|
||||
import com.x8bit.bitwarden.data.platform.manager.SdkClientManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.SdkClientManagerImpl
|
||||
import com.x8bit.bitwarden.data.platform.manager.ciphermatching.CipherMatchingManager
|
||||
@@ -45,8 +49,8 @@ import com.x8bit.bitwarden.data.platform.manager.garbage.GarbageCollectionManage
|
||||
import com.x8bit.bitwarden.data.platform.manager.restriction.RestrictionManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.restriction.RestrictionManagerImpl
|
||||
import com.x8bit.bitwarden.data.platform.repository.EnvironmentRepository
|
||||
import com.x8bit.bitwarden.data.platform.repository.ServerConfigRepository
|
||||
import com.x8bit.bitwarden.data.platform.repository.SettingsRepository
|
||||
import com.x8bit.bitwarden.data.vault.datasource.sdk.BitwardenFeatureFlagManager
|
||||
import com.x8bit.bitwarden.data.vault.repository.VaultRepository
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
@@ -90,12 +94,12 @@ object PlatformManagerModule {
|
||||
@Provides
|
||||
@Singleton
|
||||
fun providesCipherMatchingManager(
|
||||
@ApplicationContext context: Context,
|
||||
resourceCacheManager: ResourceCacheManager,
|
||||
settingsRepository: SettingsRepository,
|
||||
vaultRepository: VaultRepository,
|
||||
): CipherMatchingManager =
|
||||
CipherMatchingManagerImpl(
|
||||
context = context,
|
||||
resourceCacheManager = resourceCacheManager,
|
||||
settingsRepository = settingsRepository,
|
||||
vaultRepository = vaultRepository,
|
||||
)
|
||||
@@ -134,10 +138,19 @@ object PlatformManagerModule {
|
||||
dispatcherManager = dispatcherManager,
|
||||
)
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun providesFeatureFlagManager(
|
||||
serverConfigRepository: ServerConfigRepository,
|
||||
): FeatureFlagManager =
|
||||
FeatureFlagManagerImpl(
|
||||
serverConfigRepository = serverConfigRepository,
|
||||
)
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideSdkClientManager(
|
||||
featureFlagManager: BitwardenFeatureFlagManager,
|
||||
featureFlagManager: FeatureFlagManager,
|
||||
): SdkClientManager = SdkClientManagerImpl(
|
||||
featureFlagManager = featureFlagManager,
|
||||
)
|
||||
@@ -148,6 +161,7 @@ object PlatformManagerModule {
|
||||
authRepository: AuthRepository,
|
||||
authTokenInterceptor: AuthTokenInterceptor,
|
||||
environmentRepository: EnvironmentRepository,
|
||||
serverConfigRepository: ServerConfigRepository,
|
||||
baseUrlInterceptors: BaseUrlInterceptors,
|
||||
refreshAuthenticator: RefreshAuthenticator,
|
||||
dispatcherManager: DispatcherManager,
|
||||
@@ -156,6 +170,7 @@ object PlatformManagerModule {
|
||||
authRepository = authRepository,
|
||||
authTokenInterceptor = authTokenInterceptor,
|
||||
environmentRepository = environmentRepository,
|
||||
serverConfigRepository = serverConfigRepository,
|
||||
baseUrlInterceptors = baseUrlInterceptors,
|
||||
refreshAuthenticator = refreshAuthenticator,
|
||||
dispatcherManager = dispatcherManager,
|
||||
@@ -229,4 +244,10 @@ object PlatformManagerModule {
|
||||
environmentRepository = environmentRepository,
|
||||
restrictionsManager = requireNotNull(context.getSystemService()),
|
||||
)
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideResourceCacheManager(
|
||||
@ApplicationContext context: Context,
|
||||
): ResourceCacheManager = ResourceCacheManagerImpl(context = context)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
package com.x8bit.bitwarden.data.platform.manager.model
|
||||
|
||||
import android.os.Parcelable
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
/**
|
||||
* Required data to complete ongoing registration process.
|
||||
*
|
||||
* @property email The email of the user creating the account.
|
||||
* @property verificationToken The token required to finish the registration process.
|
||||
* @property fromEmail indicates that this information came from an email AppLink.
|
||||
*/
|
||||
@Parcelize
|
||||
data class CompleteRegistrationData(
|
||||
val email: String,
|
||||
val verificationToken: String,
|
||||
val fromEmail: Boolean,
|
||||
) : Parcelable
|
||||
@@ -0,0 +1,76 @@
|
||||
package com.x8bit.bitwarden.data.platform.manager.model
|
||||
|
||||
/**
|
||||
* Class to hold feature flag keys.
|
||||
*/
|
||||
sealed class FlagKey<out T : Any> {
|
||||
/**
|
||||
* The string value of the given key. This must match the network value.
|
||||
*/
|
||||
abstract val keyName: String
|
||||
|
||||
/**
|
||||
* The value to be used if the flags value cannot be determined or is not remotely configured.
|
||||
*/
|
||||
abstract val defaultValue: T
|
||||
|
||||
/**
|
||||
* Indicates if the flag should respect the network value or not.
|
||||
*/
|
||||
abstract val isRemotelyConfigured: Boolean
|
||||
|
||||
/**
|
||||
* Data object holding the key for Email Verification feature.
|
||||
*/
|
||||
data object EmailVerification : FlagKey<Boolean>() {
|
||||
override val keyName: String = "email-verification"
|
||||
override val defaultValue: Boolean = false
|
||||
override val isRemotelyConfigured: Boolean = false
|
||||
}
|
||||
|
||||
/**
|
||||
* Data object holding the feature flag key for the Onboarding Carousel feature.
|
||||
*/
|
||||
data object OnboardingCarousel : FlagKey<Boolean>() {
|
||||
override val keyName: String = "native-carousel-flow"
|
||||
override val defaultValue: Boolean = false
|
||||
override val isRemotelyConfigured: Boolean = false
|
||||
}
|
||||
|
||||
/**
|
||||
* Data object holding the feature flag key for the new onboarding feature.
|
||||
*/
|
||||
data object OnboardingFlow : FlagKey<Boolean>() {
|
||||
override val keyName: String = "native-create-account-flow"
|
||||
override val defaultValue: Boolean = false
|
||||
override val isRemotelyConfigured: Boolean = false
|
||||
}
|
||||
|
||||
/**
|
||||
* Data object holding the key for a [Boolean] flag to be used in tests.
|
||||
*/
|
||||
data object DummyBoolean : FlagKey<Boolean>() {
|
||||
override val keyName: String = "dummy-boolean"
|
||||
override val defaultValue: Boolean = false
|
||||
override val isRemotelyConfigured: Boolean = true
|
||||
}
|
||||
|
||||
/**
|
||||
* Data object holding the key for an [Int] flag to be used in tests.
|
||||
*/
|
||||
data class DummyInt(
|
||||
override val isRemotelyConfigured: Boolean = true,
|
||||
) : FlagKey<Int>() {
|
||||
override val keyName: String = "dummy-int"
|
||||
override val defaultValue: Int = Int.MIN_VALUE
|
||||
}
|
||||
|
||||
/**
|
||||
* Data object holding the key for a [String] flag to be used in tests.
|
||||
*/
|
||||
data object DummyString : FlagKey<String>() {
|
||||
override val keyName: String = "dummy-string"
|
||||
override val defaultValue: String = "defaultValue"
|
||||
override val isRemotelyConfigured: Boolean = true
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,9 @@
|
||||
package com.x8bit.bitwarden.data.platform.manager.model
|
||||
|
||||
import android.os.Parcelable
|
||||
import com.x8bit.bitwarden.data.autofill.fido2.model.Fido2CredentialAssertionRequest
|
||||
import com.x8bit.bitwarden.data.autofill.fido2.model.Fido2CredentialRequest
|
||||
import com.x8bit.bitwarden.data.autofill.fido2.model.Fido2GetCredentialsRequest
|
||||
import com.x8bit.bitwarden.data.autofill.model.AutofillSaveItem
|
||||
import com.x8bit.bitwarden.data.autofill.model.AutofillSelectionData
|
||||
import com.x8bit.bitwarden.ui.platform.manager.intent.IntentManager
|
||||
@@ -48,6 +50,15 @@ sealed class SpecialCircumstance : Parcelable {
|
||||
val shouldFinishWhenComplete: Boolean,
|
||||
) : SpecialCircumstance()
|
||||
|
||||
/**
|
||||
* The app was launched via AppLink in order to allow the user complete an ongoing registration.
|
||||
*/
|
||||
@Parcelize
|
||||
data class CompleteRegistration(
|
||||
val completeRegistrationData: CompleteRegistrationData,
|
||||
val timestamp: Long,
|
||||
) : SpecialCircumstance()
|
||||
|
||||
/**
|
||||
* The app was launched via the credential manager framework in order to allow the user to
|
||||
* manually save a passkey to their vault.
|
||||
@@ -57,6 +68,24 @@ sealed class SpecialCircumstance : Parcelable {
|
||||
val fido2CredentialRequest: Fido2CredentialRequest,
|
||||
) : SpecialCircumstance()
|
||||
|
||||
/**
|
||||
* The app was launched via the credential manager framework in order to authenticate a FIDO 2
|
||||
* credential saved to the user's vault.
|
||||
*/
|
||||
@Parcelize
|
||||
data class Fido2Assertion(
|
||||
val fido2AssertionRequest: Fido2CredentialAssertionRequest,
|
||||
) : SpecialCircumstance()
|
||||
|
||||
/**
|
||||
* The app was launched via the credential manager framework request to retrieve passkeys
|
||||
* associated with the requesting entity.
|
||||
*/
|
||||
@Parcelize
|
||||
data class Fido2GetCredentials(
|
||||
val fido2GetCredentialsRequest: Fido2GetCredentialsRequest,
|
||||
) : SpecialCircumstance()
|
||||
|
||||
/**
|
||||
* The app was launched via deeplink to the generator.
|
||||
*/
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package com.x8bit.bitwarden.data.platform.manager.util
|
||||
|
||||
import com.x8bit.bitwarden.data.autofill.fido2.model.Fido2CredentialAssertionRequest
|
||||
import com.x8bit.bitwarden.data.autofill.fido2.model.Fido2CredentialRequest
|
||||
import com.x8bit.bitwarden.data.autofill.fido2.model.Fido2GetCredentialsRequest
|
||||
import com.x8bit.bitwarden.data.autofill.model.AutofillSaveItem
|
||||
import com.x8bit.bitwarden.data.autofill.model.AutofillSelectionData
|
||||
import com.x8bit.bitwarden.data.platform.manager.model.SpecialCircumstance
|
||||
@@ -17,6 +19,9 @@ fun SpecialCircumstance.toAutofillSaveItemOrNull(): AutofillSaveItem? =
|
||||
SpecialCircumstance.GeneratorShortcut -> null
|
||||
SpecialCircumstance.VaultShortcut -> null
|
||||
is SpecialCircumstance.Fido2Save -> null
|
||||
is SpecialCircumstance.CompleteRegistration -> null
|
||||
is SpecialCircumstance.Fido2Assertion -> null
|
||||
is SpecialCircumstance.Fido2GetCredentials -> null
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -31,6 +36,9 @@ fun SpecialCircumstance.toAutofillSelectionDataOrNull(): AutofillSelectionData?
|
||||
SpecialCircumstance.GeneratorShortcut -> null
|
||||
SpecialCircumstance.VaultShortcut -> null
|
||||
is SpecialCircumstance.Fido2Save -> null
|
||||
is SpecialCircumstance.CompleteRegistration -> null
|
||||
is SpecialCircumstance.Fido2Assertion -> null
|
||||
is SpecialCircumstance.Fido2GetCredentials -> null
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -41,3 +49,21 @@ fun SpecialCircumstance.toFido2RequestOrNull(): Fido2CredentialRequest? =
|
||||
is SpecialCircumstance.Fido2Save -> this.fido2CredentialRequest
|
||||
else -> null
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns [Fido2CredentialAssertionRequest] when contained in the given [SpecialCircumstance].
|
||||
*/
|
||||
fun SpecialCircumstance.toFido2AssertionRequestOrNull(): Fido2CredentialAssertionRequest? =
|
||||
when (this) {
|
||||
is SpecialCircumstance.Fido2Assertion -> this.fido2AssertionRequest
|
||||
else -> null
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns [Fido2CredentialAssertionRequest] when contained in the given [SpecialCircumstance].
|
||||
*/
|
||||
fun SpecialCircumstance.toFido2GetCredentialsRequestOrNull(): Fido2GetCredentialsRequest? =
|
||||
when (this) {
|
||||
is SpecialCircumstance.Fido2GetCredentials -> this.fido2GetCredentialsRequest
|
||||
else -> null
|
||||
}
|
||||
|
||||
@@ -16,4 +16,15 @@ interface EnvironmentRepository {
|
||||
* Emits updates that track [environment].
|
||||
*/
|
||||
val environmentStateFlow: StateFlow<Environment>
|
||||
|
||||
/**
|
||||
* Stores the current environment for the given [userEmail].
|
||||
*/
|
||||
fun saveCurrentEnvironmentForEmail(userEmail: String)
|
||||
|
||||
/**
|
||||
* Loads the environment for the given [userEmail].
|
||||
* returns boolean indicates if the load was successful
|
||||
*/
|
||||
fun loadEnvironmentForEmail(userEmail: String): Boolean
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import com.x8bit.bitwarden.data.auth.datasource.disk.AuthDiskSource
|
||||
import com.x8bit.bitwarden.data.platform.datasource.disk.EnvironmentDiskSource
|
||||
import com.x8bit.bitwarden.data.platform.manager.dispatcher.DispatcherManager
|
||||
import com.x8bit.bitwarden.data.platform.repository.model.Environment
|
||||
import com.x8bit.bitwarden.data.platform.repository.util.toEnvironmentUrls
|
||||
import com.x8bit.bitwarden.data.platform.repository.util.toEnvironmentUrlsOrDefault
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
@@ -55,4 +56,19 @@ class EnvironmentRepositoryImpl(
|
||||
}
|
||||
.launchIn(scope)
|
||||
}
|
||||
|
||||
override fun loadEnvironmentForEmail(userEmail: String): Boolean {
|
||||
val urls = environmentDiskSource
|
||||
.getPreAuthEnvironmentUrlDataForEmail(userEmail)
|
||||
?: return false
|
||||
environment = urls.toEnvironmentUrls()
|
||||
return true
|
||||
}
|
||||
|
||||
override fun saveCurrentEnvironmentForEmail(userEmail: String) =
|
||||
environmentDiskSource
|
||||
.storePreAuthEnvironmentUrlDataForEmail(
|
||||
userEmail = userEmail,
|
||||
urls = environment.environmentUrlData,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
package com.x8bit.bitwarden.data.platform.repository
|
||||
|
||||
import com.x8bit.bitwarden.data.platform.datasource.disk.model.ServerConfig
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
|
||||
/**
|
||||
* Provides an API for observing the server config state.
|
||||
*/
|
||||
interface ServerConfigRepository {
|
||||
|
||||
/**
|
||||
* Emits updates that track [ServerConfig].
|
||||
*/
|
||||
val serverConfigStateFlow: StateFlow<ServerConfig?>
|
||||
|
||||
/**
|
||||
* Gets the state [ServerConfig]. If needed or forced by [forceRefresh],
|
||||
* updates the values using server side data.
|
||||
*/
|
||||
suspend fun getServerConfig(forceRefresh: Boolean): ServerConfig?
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
package com.x8bit.bitwarden.data.platform.repository
|
||||
|
||||
import com.x8bit.bitwarden.data.platform.datasource.disk.ConfigDiskSource
|
||||
import com.x8bit.bitwarden.data.platform.datasource.disk.model.ServerConfig
|
||||
import com.x8bit.bitwarden.data.platform.datasource.network.service.ConfigService
|
||||
import com.x8bit.bitwarden.data.platform.manager.dispatcher.DispatcherManager
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import java.time.Clock
|
||||
import java.time.Instant
|
||||
|
||||
/**
|
||||
* Primary implementation of [ServerConfigRepositoryImpl].
|
||||
*/
|
||||
class ServerConfigRepositoryImpl(
|
||||
private val configDiskSource: ConfigDiskSource,
|
||||
private val configService: ConfigService,
|
||||
private val clock: Clock,
|
||||
dispatcherManager: DispatcherManager,
|
||||
) : ServerConfigRepository {
|
||||
|
||||
private val unconfinedScope = CoroutineScope(dispatcherManager.unconfined)
|
||||
|
||||
override val serverConfigStateFlow: StateFlow<ServerConfig?>
|
||||
get() = configDiskSource
|
||||
.serverConfigFlow
|
||||
.stateIn(
|
||||
scope = unconfinedScope,
|
||||
started = SharingStarted.Eagerly,
|
||||
initialValue = configDiskSource.serverConfig,
|
||||
)
|
||||
|
||||
override suspend fun getServerConfig(forceRefresh: Boolean): ServerConfig? {
|
||||
val localConfig = configDiskSource.serverConfig
|
||||
val needsRefresh = localConfig == null ||
|
||||
Instant
|
||||
.ofEpochMilli(localConfig.lastSync)
|
||||
.isAfter(
|
||||
clock.instant().plusSeconds(MINIMUM_CONFIG_SYNC_INTERVAL_SEC),
|
||||
)
|
||||
|
||||
if (needsRefresh || forceRefresh) {
|
||||
configService
|
||||
.getConfig()
|
||||
.onSuccess { configResponse ->
|
||||
val serverConfig = ServerConfig(
|
||||
lastSync = clock.instant().toEpochMilli(),
|
||||
serverData = configResponse,
|
||||
)
|
||||
configDiskSource.serverConfig = serverConfig
|
||||
return serverConfig
|
||||
}
|
||||
}
|
||||
|
||||
// If we are unable to retrieve a configuration from the server,
|
||||
// fall back to the local configuration.
|
||||
return localConfig
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val MINIMUM_CONFIG_SYNC_INTERVAL_SEC: Long = 60 * 60
|
||||
}
|
||||
}
|
||||
@@ -3,13 +3,17 @@ package com.x8bit.bitwarden.data.platform.repository.di
|
||||
import android.view.autofill.AutofillManager
|
||||
import com.x8bit.bitwarden.data.auth.datasource.disk.AuthDiskSource
|
||||
import com.x8bit.bitwarden.data.autofill.manager.AutofillEnabledManager
|
||||
import com.x8bit.bitwarden.data.platform.datasource.disk.ConfigDiskSource
|
||||
import com.x8bit.bitwarden.data.platform.datasource.disk.EnvironmentDiskSource
|
||||
import com.x8bit.bitwarden.data.platform.datasource.disk.SettingsDiskSource
|
||||
import com.x8bit.bitwarden.data.platform.datasource.network.service.ConfigService
|
||||
import com.x8bit.bitwarden.data.platform.manager.BiometricsEncryptionManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.PolicyManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.dispatcher.DispatcherManager
|
||||
import com.x8bit.bitwarden.data.platform.repository.EnvironmentRepository
|
||||
import com.x8bit.bitwarden.data.platform.repository.EnvironmentRepositoryImpl
|
||||
import com.x8bit.bitwarden.data.platform.repository.ServerConfigRepository
|
||||
import com.x8bit.bitwarden.data.platform.repository.ServerConfigRepositoryImpl
|
||||
import com.x8bit.bitwarden.data.platform.repository.SettingsRepository
|
||||
import com.x8bit.bitwarden.data.platform.repository.SettingsRepositoryImpl
|
||||
import com.x8bit.bitwarden.data.vault.datasource.sdk.VaultSdkSource
|
||||
@@ -17,6 +21,7 @@ import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import java.time.Clock
|
||||
import javax.inject.Singleton
|
||||
|
||||
/**
|
||||
@@ -26,6 +31,21 @@ import javax.inject.Singleton
|
||||
@InstallIn(SingletonComponent::class)
|
||||
object PlatformRepositoryModule {
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideServerConfigRepository(
|
||||
configDiskSource: ConfigDiskSource,
|
||||
configService: ConfigService,
|
||||
clock: Clock,
|
||||
dispatcherManager: DispatcherManager,
|
||||
): ServerConfigRepository =
|
||||
ServerConfigRepositoryImpl(
|
||||
configDiskSource = configDiskSource,
|
||||
configService = configService,
|
||||
clock = clock,
|
||||
dispatcherManager = dispatcherManager,
|
||||
)
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideEnvironmentRepository(
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
package com.x8bit.bitwarden.data.platform.util
|
||||
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.withTimeoutOrNull
|
||||
|
||||
/**
|
||||
* Returns the first element emitted by the [Flow] or `null` if the operation exceeds the given
|
||||
* timeout of [timeMillis].
|
||||
*/
|
||||
suspend fun <T> Flow<T>.firstWithTimeoutOrNull(
|
||||
timeMillis: Long,
|
||||
): T? = withTimeoutOrNull(timeMillis = timeMillis) { first() }
|
||||
|
||||
/**
|
||||
* Returns the first element emitted by the [Flow] matching the given [predicate] or `null` if the
|
||||
* operation exceeds the given timeout of [timeMillis].
|
||||
*/
|
||||
suspend fun <T> Flow<T>.firstWithTimeoutOrNull(
|
||||
timeMillis: Long,
|
||||
predicate: suspend (T) -> Boolean,
|
||||
): T? = withTimeoutOrNull(timeMillis = timeMillis) { first(predicate) }
|
||||
@@ -3,37 +3,24 @@
|
||||
package com.x8bit.bitwarden.data.platform.util
|
||||
|
||||
import android.content.Intent
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.os.Parcelable
|
||||
import androidx.core.content.IntentCompat
|
||||
import androidx.core.os.BundleCompat
|
||||
import com.x8bit.bitwarden.data.platform.annotation.OmitFromCoverage
|
||||
|
||||
/**
|
||||
* A means of retrieving a [Parcelable] from an [Intent] using the given [name] in a manner that
|
||||
* is safe across SDK versions.
|
||||
*/
|
||||
inline fun <reified T> Intent.getSafeParcelableExtra(name: String): T? =
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
getParcelableExtra(
|
||||
name,
|
||||
T::class.java,
|
||||
)
|
||||
} else {
|
||||
@Suppress("DEPRECATION")
|
||||
getParcelableExtra(name)
|
||||
}
|
||||
inline fun <reified T> Intent.getSafeParcelableExtra(
|
||||
name: String,
|
||||
): T? = IntentCompat.getParcelableExtra(this, name, T::class.java)
|
||||
|
||||
/**
|
||||
* A means of retrieving a [Parcelable] from a [Bundle] using the given [name] in a manner that
|
||||
* is safe across SDK versions.
|
||||
*/
|
||||
inline fun <reified T> Bundle.getSafeParcelableExtra(name: String): T? =
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
getParcelable(
|
||||
name,
|
||||
T::class.java,
|
||||
)
|
||||
} else {
|
||||
@Suppress("DEPRECATION")
|
||||
getParcelable(name)
|
||||
}
|
||||
inline fun <reified T> Bundle.getSafeParcelableExtra(
|
||||
name: String,
|
||||
): T? = BundleCompat.getParcelable(this, name, T::class.java)
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
package com.x8bit.bitwarden.data.platform.util
|
||||
|
||||
import java.util.Locale
|
||||
|
||||
/**
|
||||
* String [Comparator] where the characters are compared giving precedence to
|
||||
* special characters.
|
||||
*/
|
||||
object SpecialCharWithPrecedenceComparator : Comparator<String> {
|
||||
override fun compare(str1: String, str2: String): Int {
|
||||
val minLength = minOf(str1.length, str2.length)
|
||||
for (i in 0 until minLength) {
|
||||
val char1 = str1[i]
|
||||
val char2 = str2[i]
|
||||
val compareResult = compareCharsSpecialCharsWithPrecedence(char1, char2)
|
||||
if (compareResult != 0) {
|
||||
return compareResult
|
||||
}
|
||||
}
|
||||
// If all compared chars are the same give precedence to the shorter String.
|
||||
return str1.length - str2.length
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Compare two characters, where a special character is considered with higher precedence over
|
||||
* letters and numbers. If both characters are a letter and they are equal ignoring the case,
|
||||
* give priority to the lowercase instance. If they are both a digit or a non-equal letter
|
||||
* use the default [String.compareTo] converting the chars to the [Locale] specific uppercase
|
||||
* String.
|
||||
*/
|
||||
private fun compareCharsSpecialCharsWithPrecedence(c1: Char, c2: Char): Int {
|
||||
return when {
|
||||
c1.isLetterOrDigit() && !c2.isLetterOrDigit() -> 1
|
||||
!c1.isLetterOrDigit() && c2.isLetterOrDigit() -> -1
|
||||
c1.isLetter() && c2.isLetter() && c1.equals(other = c2, ignoreCase = true) -> {
|
||||
compareLettersLowerCaseFirst(c1 = c1, c2 = c2)
|
||||
}
|
||||
|
||||
else -> {
|
||||
val upperCaseStr1 = c1.toString().uppercase(Locale.getDefault())
|
||||
val upperCaseStr2 = c2.toString().uppercase(Locale.getDefault())
|
||||
upperCaseStr1.compareTo(upperCaseStr2)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Compare two equal letters ignoring case (i.e. 'A' == 'a'), give precedence to the
|
||||
* the character which is lowercase. If both [c1] and [c2] are equal and the
|
||||
* same case return 0 to indicate they are the same.
|
||||
*/
|
||||
private fun compareLettersLowerCaseFirst(c1: Char, c2: Char): Int {
|
||||
require(
|
||||
value = c1.isLetter() &&
|
||||
c2.isLetter() &&
|
||||
c1.equals(other = c2, ignoreCase = true),
|
||||
) {
|
||||
"Both character must be the same letter, case does not matter."
|
||||
}
|
||||
|
||||
return when {
|
||||
!c1.isLowerCase() && c2.isLowerCase() -> 1
|
||||
c1.isLowerCase() && !c2.isLowerCase() -> -1
|
||||
else -> 0
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
package com.x8bit.bitwarden.data.platform.util
|
||||
|
||||
import android.content.Context
|
||||
import com.x8bit.bitwarden.data.platform.annotation.OmitFromCoverage
|
||||
import com.x8bit.bitwarden.data.platform.manager.ResourceCacheManager
|
||||
import java.net.URI
|
||||
import java.net.URISyntaxException
|
||||
|
||||
@@ -47,28 +47,26 @@ fun String.getWebHostFromAndroidUriOrNull(): String? =
|
||||
/**
|
||||
* Extract the domain name from this [String] if possible, otherwise return null.
|
||||
*/
|
||||
fun String.getDomainOrNull(context: Context): String? =
|
||||
fun String.getDomainOrNull(resourceCacheManager: ResourceCacheManager): String? =
|
||||
this
|
||||
.toUriOrNull()
|
||||
?.parseDomainOrNull(context = context)
|
||||
?.parseDomainOrNull(resourceCacheManager = resourceCacheManager)
|
||||
|
||||
/**
|
||||
* Extract the host with port from this [String] if possible, otherwise return null.
|
||||
* Extract the host with optional port from this [String] if possible, otherwise return null.
|
||||
*/
|
||||
@OmitFromCoverage
|
||||
fun String.getHostWithPortOrNull(): String? =
|
||||
this
|
||||
.toUriOrNull()
|
||||
?.let { uri ->
|
||||
val host = uri.host
|
||||
val port = uri.port
|
||||
|
||||
if (host != null && port != -1) {
|
||||
"$host:$port"
|
||||
} else {
|
||||
null
|
||||
}
|
||||
fun String.getHostWithPortOrNull(): String? {
|
||||
val uri = this.toUriOrNull() ?: return null
|
||||
return uri.host?.let { host ->
|
||||
val port = uri.port
|
||||
if (port != -1) {
|
||||
"$host:$port"
|
||||
} else {
|
||||
host
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Find the indices of the last occurrences of [substring] within this [String]. Return null if no
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package com.x8bit.bitwarden.data.platform.util
|
||||
|
||||
import android.content.Context
|
||||
import com.x8bit.bitwarden.R
|
||||
import com.x8bit.bitwarden.data.platform.manager.ResourceCacheManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.model.DomainName
|
||||
import java.net.URI
|
||||
|
||||
@@ -17,7 +16,7 @@ private const val IP_REGEX: String =
|
||||
/**
|
||||
* Parses the base domain from the URL. Returns null if unavailable.
|
||||
*/
|
||||
fun URI.parseDomainOrNull(context: Context): String? {
|
||||
fun URI.parseDomainOrNull(resourceCacheManager: ResourceCacheManager): String? {
|
||||
val host = this.host ?: return null
|
||||
val isIpAddress = host.matches(IP_REGEX.toRegex())
|
||||
|
||||
@@ -25,7 +24,7 @@ fun URI.parseDomainOrNull(context: Context): String? {
|
||||
host
|
||||
} else {
|
||||
parseDomainNameOrNullInternal(
|
||||
context = context,
|
||||
resourceCacheManager = resourceCacheManager,
|
||||
host = host,
|
||||
)
|
||||
?.domain
|
||||
@@ -35,13 +34,13 @@ fun URI.parseDomainOrNull(context: Context): String? {
|
||||
/**
|
||||
* Parses a URL to get the breakdown of a URL's domain. Returns null if invalid.
|
||||
*/
|
||||
fun URI.parseDomainNameOrNull(context: Context): DomainName? =
|
||||
fun URI.parseDomainNameOrNull(resourceCacheManager: ResourceCacheManager): DomainName? =
|
||||
this
|
||||
// URI is a platform type and host can be null.
|
||||
.host
|
||||
?.let { nonNullHost ->
|
||||
parseDomainNameOrNullInternal(
|
||||
context = context,
|
||||
resourceCacheManager = resourceCacheManager,
|
||||
host = nonNullHost,
|
||||
)
|
||||
}
|
||||
@@ -53,21 +52,12 @@ fun URI.parseDomainNameOrNull(context: Context): DomainName? =
|
||||
*/
|
||||
@Suppress("LongMethod")
|
||||
private fun parseDomainNameOrNullInternal(
|
||||
context: Context,
|
||||
resourceCacheManager: ResourceCacheManager,
|
||||
host: String,
|
||||
): DomainName? {
|
||||
val exceptionSuffixes = context
|
||||
.resources
|
||||
.getStringArray(R.array.exception_suffixes)
|
||||
.toList()
|
||||
val normalSuffixes = context
|
||||
.resources
|
||||
.getStringArray(R.array.normal_suffixes)
|
||||
.toList()
|
||||
val wildCardSuffixes = context
|
||||
.resources
|
||||
.getStringArray(R.array.wild_card_suffixes)
|
||||
.toList()
|
||||
val exceptionSuffixes = resourceCacheManager.domainExceptionSuffixes
|
||||
val normalSuffixes = resourceCacheManager.domainNormalSuffixes
|
||||
val wildCardSuffixes = resourceCacheManager.domainWildCardSuffixes
|
||||
|
||||
// Split the host into parts separated by a period. Start with the last part and incrementally
|
||||
// add back the earlier parts to build a list of any matching domains in the data set.
|
||||
|
||||
@@ -3,8 +3,8 @@ package com.x8bit.bitwarden.data.tools.generator.datasource.sdk
|
||||
import com.bitwarden.generators.PassphraseGeneratorRequest
|
||||
import com.bitwarden.generators.PasswordGeneratorRequest
|
||||
import com.bitwarden.generators.UsernameGeneratorRequest
|
||||
import com.bitwarden.sdk.Client
|
||||
import com.bitwarden.sdk.ClientGenerators
|
||||
import com.x8bit.bitwarden.data.platform.datasource.sdk.BaseSdkSource
|
||||
import com.x8bit.bitwarden.data.platform.manager.SdkClientManager
|
||||
|
||||
/**
|
||||
@@ -14,46 +14,43 @@ import com.x8bit.bitwarden.data.platform.manager.SdkClientManager
|
||||
* [ClientGenerators] provided by the Bitwarden SDK.
|
||||
*/
|
||||
class GeneratorSdkSourceImpl(
|
||||
private val sdkClientManager: SdkClientManager,
|
||||
) : GeneratorSdkSource {
|
||||
sdkClientManager: SdkClientManager,
|
||||
) : BaseSdkSource(sdkClientManager = sdkClientManager),
|
||||
GeneratorSdkSource {
|
||||
|
||||
override suspend fun generatePassword(
|
||||
request: PasswordGeneratorRequest,
|
||||
): Result<String> = runCatching {
|
||||
): Result<String> = runCatchingWithLogs {
|
||||
getClient().generators().password(request)
|
||||
}
|
||||
|
||||
override suspend fun generatePassphrase(
|
||||
request: PassphraseGeneratorRequest,
|
||||
): Result<String> = runCatching {
|
||||
): Result<String> = runCatchingWithLogs {
|
||||
getClient().generators().passphrase(request)
|
||||
}
|
||||
|
||||
override suspend fun generatePlusAddressedEmail(
|
||||
request: UsernameGeneratorRequest.Subaddress,
|
||||
): Result<String> = runCatching {
|
||||
): Result<String> = runCatchingWithLogs {
|
||||
getClient().generators().username(request)
|
||||
}
|
||||
|
||||
override suspend fun generateCatchAllEmail(
|
||||
request: UsernameGeneratorRequest.Catchall,
|
||||
): Result<String> = runCatching {
|
||||
): Result<String> = runCatchingWithLogs {
|
||||
getClient().generators().username(request)
|
||||
}
|
||||
|
||||
override suspend fun generateRandomWord(
|
||||
request: UsernameGeneratorRequest.Word,
|
||||
): Result<String> = runCatching {
|
||||
): Result<String> = runCatchingWithLogs {
|
||||
getClient().generators().username(request)
|
||||
}
|
||||
|
||||
override suspend fun generateForwardedServiceEmail(
|
||||
request: UsernameGeneratorRequest.Forwarded,
|
||||
): Result<String> = runCatching {
|
||||
): Result<String> = runCatchingWithLogs {
|
||||
getClient().generators().username(request)
|
||||
}
|
||||
|
||||
private suspend fun getClient(
|
||||
userId: String? = null,
|
||||
): Client = sdkClientManager.getOrCreateClient(userId = userId)
|
||||
}
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
package com.x8bit.bitwarden.data.tools.generator.repository.model
|
||||
|
||||
import androidx.annotation.Keep
|
||||
import com.x8bit.bitwarden.data.platform.datasource.network.serializer.BaseEnumeratedIntSerializer
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
/**
|
||||
* A data class representing the configuration options for both password and passphrase generation.
|
||||
*
|
||||
* @property type The type of passcode to be generated, as defined in PasscodeType.
|
||||
* @property length The total length of the generated password.
|
||||
* @property allowAmbiguousChar Indicates whether ambiguous characters are allowed in the password.
|
||||
* @property hasNumbers Indicates whether the password should contain numbers.
|
||||
@@ -23,6 +26,8 @@ import kotlinx.serialization.Serializable
|
||||
*/
|
||||
@Serializable
|
||||
data class PasscodeGenerationOptions(
|
||||
@SerialName("type")
|
||||
val type: PasscodeType,
|
||||
|
||||
// Password-specific options
|
||||
|
||||
@@ -69,4 +74,22 @@ data class PasscodeGenerationOptions(
|
||||
|
||||
@SerialName("includeNumber")
|
||||
val allowIncludeNumber: Boolean,
|
||||
)
|
||||
) {
|
||||
/**
|
||||
* Represents different Passcode types.
|
||||
*/
|
||||
@Serializable(with = PasscodeTypeSerializer::class)
|
||||
enum class PasscodeType {
|
||||
@SerialName("0")
|
||||
PASSWORD,
|
||||
|
||||
@SerialName("1")
|
||||
PASSPHRASE,
|
||||
}
|
||||
}
|
||||
|
||||
@Keep
|
||||
private class PasscodeTypeSerializer :
|
||||
BaseEnumeratedIntSerializer<PasscodeGenerationOptions.PasscodeType>(
|
||||
PasscodeGenerationOptions.PasscodeType.entries.toTypedArray(),
|
||||
)
|
||||
|
||||
@@ -66,10 +66,16 @@ class VaultDiskSourceImpl(
|
||||
ciphersDao
|
||||
.getAllCiphers(userId = userId)
|
||||
.map { entities ->
|
||||
entities.map { entity ->
|
||||
withContext(dispatcherManager.default) {
|
||||
json.decodeFromString<SyncResponseJson.Cipher>(entity.cipherJson)
|
||||
}
|
||||
withContext(context = dispatcherManager.default) {
|
||||
entities
|
||||
.map { entity ->
|
||||
async {
|
||||
json.decodeFromString<SyncResponseJson.Cipher>(
|
||||
string = entity.cipherJson,
|
||||
)
|
||||
}
|
||||
}
|
||||
.awaitAll()
|
||||
}
|
||||
},
|
||||
)
|
||||
@@ -180,10 +186,14 @@ class VaultDiskSourceImpl(
|
||||
sendsDao
|
||||
.getAllSends(userId = userId)
|
||||
.map { entities ->
|
||||
entities.map { entity ->
|
||||
withContext(dispatcherManager.default) {
|
||||
json.decodeFromString<SyncResponseJson.Send>(entity.sendJson)
|
||||
}
|
||||
withContext(context = dispatcherManager.default) {
|
||||
entities
|
||||
.map { entity ->
|
||||
async {
|
||||
json.decodeFromString<SyncResponseJson.Send>(entity.sendJson)
|
||||
}
|
||||
}
|
||||
.awaitAll()
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user