mirror of
https://github.com/bitwarden/android.git
synced 2026-05-10 05:49:44 -05:00
Compare commits
266 Commits
update-tem
...
v2024.8.1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
17c579bfc2 | ||
|
|
3c39d8beac | ||
|
|
2a057bb1fb | ||
|
|
f778d7ecd1 | ||
|
|
4c983525d3 | ||
|
|
e32a9f303d | ||
|
|
522e3bb939 | ||
|
|
0676cf8826 | ||
|
|
5173dfd424 | ||
|
|
5bc31448b4 | ||
|
|
e9a7136a9a | ||
|
|
c36d0851ca | ||
|
|
ace5f19375 | ||
|
|
88b40cfd10 | ||
|
|
38e693f92c | ||
|
|
9dbb40f33b | ||
|
|
76a3265bbb | ||
|
|
666c165b6f | ||
|
|
9db09c18cc | ||
|
|
b7330392cc | ||
|
|
162da64567 | ||
|
|
09f497ca9b | ||
|
|
87d7143cc8 | ||
|
|
23bcfad717 | ||
|
|
f1f16cfee5 | ||
|
|
82d3b44712 | ||
|
|
b56a21b6e5 | ||
|
|
eb2ba8e598 | ||
|
|
91f039ecb6 | ||
|
|
0d6aeee870 | ||
|
|
a0a5070ac7 | ||
|
|
e7bd966e94 | ||
|
|
075956ce17 | ||
|
|
13b256d4e9 | ||
|
|
5761e9510a | ||
|
|
3b3b9ef33b | ||
|
|
17fd3ec0f0 | ||
|
|
43a6495b98 | ||
|
|
86dabea39f | ||
|
|
8d08b5f7c5 | ||
|
|
13c29c8296 | ||
|
|
eac5516a94 | ||
|
|
88b674f54c | ||
|
|
bcc24a2e25 | ||
|
|
e14f399e2d | ||
|
|
ad2c575b39 | ||
|
|
57c2e7ee4e | ||
|
|
55b57a605e | ||
|
|
397c78b4af | ||
|
|
9e372c29d1 | ||
|
|
82fd7f01f8 | ||
|
|
a15b84a5bf | ||
|
|
5f46423638 | ||
|
|
8aebd36465 | ||
|
|
b4f864d89c | ||
|
|
8c8db78da6 | ||
|
|
b18d9f53c6 | ||
|
|
7134d89352 | ||
|
|
5a7dc198dd | ||
|
|
7dbfcfdea2 | ||
|
|
b56ccd1bab | ||
|
|
f05828c87d | ||
|
|
48817f0fe4 | ||
|
|
3bed2581af | ||
|
|
acb125b2b9 | ||
|
|
72e5aedccd | ||
|
|
9148a750a5 | ||
|
|
d4600c5c83 | ||
|
|
8094b3fd22 | ||
|
|
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 | ||
|
|
291af8d017 | ||
|
|
b0ff0b9185 | ||
|
|
7653d71b3d | ||
|
|
5ea2f1c736 | ||
|
|
721c69619e | ||
|
|
58937d4e20 | ||
|
|
ed53abb29f | ||
|
|
53c5d11076 | ||
|
|
4d65230476 | ||
|
|
20d37e2f90 | ||
|
|
101b807b5c | ||
|
|
3931af096d | ||
|
|
c6d05b4631 | ||
|
|
27747b6cb9 | ||
|
|
dbf1d423e8 | ||
|
|
eb771e9dfa | ||
|
|
a84694b100 | ||
|
|
9b240ddf5f | ||
|
|
c409132825 | ||
|
|
f6f28f6a58 | ||
|
|
b2300328e1 | ||
|
|
9205dbef59 | ||
|
|
0651494393 | ||
|
|
4f3d42264d | ||
|
|
0cdae9bccf | ||
|
|
5677373421 | ||
|
|
0dc606b470 | ||
|
|
65237c18d7 | ||
|
|
adf7916a4c | ||
|
|
01b786fcf9 | ||
|
|
99c50a3fc8 | ||
|
|
e9057cb866 | ||
|
|
4b0c6ad911 | ||
|
|
263b401dc6 | ||
|
|
cae000ef5f | ||
|
|
5856403c66 | ||
|
|
02b5542388 | ||
|
|
870de652be | ||
|
|
f13679cd2c | ||
|
|
9e0e07967f | ||
|
|
d182b4edf1 | ||
|
|
a2572d996b | ||
|
|
c3d2389829 | ||
|
|
074979095b | ||
|
|
ba95a53ebf | ||
|
|
bb6a7af423 | ||
|
|
b181d0d026 | ||
|
|
f5039d72b9 | ||
|
|
44e2596a30 | ||
|
|
32e3f1e9ba | ||
|
|
d7032b8475 | ||
|
|
7d03b62dd7 | ||
|
|
fd7ceef2ee | ||
|
|
581539b0d0 | ||
|
|
fae92be38a | ||
|
|
9f354bf248 | ||
|
|
fc84a89dd2 | ||
|
|
1d6707ef82 | ||
|
|
82e2c82666 |
14
.github/ISSUE_TEMPLATE/bug.yml
vendored
14
.github/ISSUE_TEMPLATE/bug.yml
vendored
@@ -1,4 +1,4 @@
|
||||
name: Android Bug Report
|
||||
name: Android Beta Bug Report
|
||||
description: File a bug report
|
||||
labels: [ bug ]
|
||||
body:
|
||||
@@ -7,7 +7,19 @@ body:
|
||||
value: |
|
||||
Thanks for taking the time to fill out this bug report!
|
||||
|
||||
> [!WARNING]
|
||||
> This is the new native Bitwarden Beta app repository. For the publicly available apps in App Store / Play Store, submit your report in [bitwarden/mobile](https://github.com/bitwarden/mobile)
|
||||
|
||||
|
||||
Please do not submit feature requests. The [Community Forums](https://community.bitwarden.com) has a section for submitting, voting for, and discussing product feature requests.
|
||||
- type: checkboxes
|
||||
id: beta
|
||||
attributes:
|
||||
label: Bitwarden Beta
|
||||
options:
|
||||
- label: "I'm using the new native Bitwarden Beta app and I'm aware that legacy .NET app bugs should be reported in [bitwarden/mobile](https://github.com/bitwarden/mobile)"
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: reproduce
|
||||
attributes:
|
||||
|
||||
3
.github/ISSUE_TEMPLATE/config.yml
vendored
3
.github/ISSUE_TEMPLATE/config.yml
vendored
@@ -1,5 +1,8 @@
|
||||
blank_issues_enabled: false
|
||||
contact_links:
|
||||
- name: Legacy Android Bug Reports
|
||||
url: https://github.com/bitwarden/mobile/issues
|
||||
about: Bugs found in the publicly available .NET MAUI app should be reported in [bitwarden/mobile](https://github.com/bitwarden/mobile)
|
||||
- name: Feature Requests
|
||||
url: https://community.bitwarden.com/c/feature-requests/
|
||||
about: Request new features using the Community Forums. Please search existing feature requests before making a new one.
|
||||
|
||||
124
.github/workflows/build.yml
vendored
124
.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@af1da67850ed9a4cedd57bfd976089dd991e2582 # v4.0.0
|
||||
|
||||
- name: Cache Gradle files
|
||||
uses: actions/cache@0c45773b623bea8c8e75f6c82b208c3cf94ea4f9 # v4.0.2
|
||||
@@ -61,13 +62,13 @@ jobs:
|
||||
${{ runner.os }}-build-
|
||||
|
||||
- name: Configure JDK
|
||||
uses: actions/setup-java@99b8673ff64fbf99d8d325f52d9a5bdedb8483e9 # v4.2.1
|
||||
uses: actions/setup-java@6a0805fcefea3d4657a47ac4c165951e33482018 # v4.2.2
|
||||
with:
|
||||
distribution: "temurin"
|
||||
java-version: ${{ env.JAVA_VERSION }}
|
||||
|
||||
- name: Configure Ruby
|
||||
uses: ruby/setup-ruby@3783f195e29b74ae398d7caca108814bbafde90e # v1.180.1
|
||||
uses: ruby/setup-ruby@a6e6f86333f0a2523ece813039b8b4be04560854 # v1.190.0
|
||||
with:
|
||||
bundler-cache: true
|
||||
|
||||
@@ -91,14 +92,14 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
variant: ["prod", "qa"]
|
||||
variant: ["prod", "dev"]
|
||||
artifact: ["apk", "aab"]
|
||||
steps:
|
||||
- name: Check out repo
|
||||
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
|
||||
|
||||
- name: Configure Ruby
|
||||
uses: ruby/setup-ruby@3783f195e29b74ae398d7caca108814bbafde90e # v1.180.1
|
||||
uses: ruby/setup-ruby@a6e6f86333f0a2523ece813039b8b4be04560854 # v1.190.0
|
||||
with:
|
||||
bundler-cache: true
|
||||
|
||||
@@ -138,7 +139,7 @@ jobs:
|
||||
--name google-services.json --file ${{ github.workspace }}/app/src/standardBeta/google-services.json --output none
|
||||
|
||||
- name: Download Firebase credentials
|
||||
if: ${{ matrix.variant == 'prod' && inputs.distribute-to-firebase }}
|
||||
if: ${{ matrix.variant == 'prod' && (inputs.distribute-to-firebase || github.event_name == 'push') }}
|
||||
env:
|
||||
ACCOUNT_NAME: bitwardenci
|
||||
CONTAINER_NAME: mobile
|
||||
@@ -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@af1da67850ed9a4cedd57bfd976089dd991e2582 # v4.0.0
|
||||
|
||||
- name: Cache Gradle files
|
||||
uses: actions/cache@0c45773b623bea8c8e75f6c82b208c3cf94ea4f9 # v4.0.2
|
||||
@@ -171,7 +172,7 @@ jobs:
|
||||
${{ runner.os }}-build-
|
||||
|
||||
- name: Configure JDK
|
||||
uses: actions/setup-java@99b8673ff64fbf99d8d325f52d9a5bdedb8483e9 # v4.2.1
|
||||
uses: actions/setup-java@6a0805fcefea3d4657a47ac4c165951e33482018 # v4.2.2
|
||||
with:
|
||||
distribution: "temurin"
|
||||
java-version: ${{ env.JAVA_VERSION }}
|
||||
@@ -229,14 +230,14 @@ jobs:
|
||||
keyAlias:bitwarden-beta \
|
||||
keyPassword:${{ env.PLAY_BETA_KEY_PASSWORD }}
|
||||
|
||||
- name: Generate QA Play Store APKs
|
||||
- name: Generate debug Play Store APKs
|
||||
if: ${{ (matrix.variant != 'prod') && (matrix.artifact == 'apk') }}
|
||||
run: |
|
||||
bundle exec fastlane assembleDebugApks
|
||||
|
||||
- name: Upload release Play Store .aab artifact
|
||||
if: ${{ (matrix.variant == 'prod') && (matrix.artifact == 'aab') }}
|
||||
uses: actions/upload-artifact@65462800fd760344b1a7b4382951275a0abb4808 # v4.3.3
|
||||
uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6
|
||||
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@65462800fd760344b1a7b4382951275a0abb4808 # v4.3.3
|
||||
uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6
|
||||
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@65462800fd760344b1a7b4382951275a0abb4808 # v4.3.3
|
||||
uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6
|
||||
with:
|
||||
name: com.x8bit.bitwarden.apk
|
||||
path: app/build/outputs/apk/standard/release/com.x8bit.bitwarden-standard-release.apk
|
||||
@@ -260,18 +261,18 @@ jobs:
|
||||
|
||||
- name: Upload beta .apk artifact
|
||||
if: ${{ (matrix.variant == 'prod') && (matrix.artifact == 'apk') }}
|
||||
uses: actions/upload-artifact@65462800fd760344b1a7b4382951275a0abb4808 # v4.3.3
|
||||
uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6
|
||||
with:
|
||||
name: com.x8bit.bitwarden.beta.apk
|
||||
path: app/build/outputs/apk/standard/beta/com.x8bit.bitwarden-standard-beta.apk
|
||||
if-no-files-found: error
|
||||
|
||||
# When building variants other than 'prod'
|
||||
- name: Upload other .apk artifact
|
||||
- name: Upload debug .apk artifact
|
||||
if: ${{ (matrix.variant != 'prod') && (matrix.artifact == 'apk') }}
|
||||
uses: actions/upload-artifact@65462800fd760344b1a7b4382951275a0abb4808 # v4.3.3
|
||||
uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6
|
||||
with:
|
||||
name: com.x8bit.bitwarden-${{ matrix.variant }}.apk
|
||||
name: com.x8bit.bitwarden.${{ matrix.variant }}.apk
|
||||
path: app/build/outputs/apk/standard/debug/com.x8bit.bitwarden-standard-debug.apk
|
||||
if-no-files-found: error
|
||||
|
||||
@@ -279,90 +280,92 @@ jobs:
|
||||
if: ${{ (matrix.variant == 'prod') && (matrix.artifact == 'apk') }}
|
||||
run: |
|
||||
sha256sum "app/build/outputs/apk/standard/release/com.x8bit.bitwarden-standard-release.apk" \
|
||||
> ./bw-android-apk-sha256.txt
|
||||
> ./com.x8bit.bitwarden.apk-sha256.txt
|
||||
|
||||
- name: Create checksum for beta .apk artifact
|
||||
if: ${{ (matrix.variant == 'prod') && (matrix.artifact == 'apk') }}
|
||||
run: |
|
||||
sha256sum "app/build/outputs/apk/standard/beta/com.x8bit.bitwarden-standard-beta.apk" \
|
||||
> ./bw-android-beta-apk-sha256.txt
|
||||
> ./com.x8bit.bitwarden.beta.apk-sha256.txt
|
||||
|
||||
- name: Create checksum for release .aab artifact
|
||||
if: ${{ (matrix.variant == 'prod') && (matrix.artifact == 'aab') }}
|
||||
run: |
|
||||
sha256sum "app/build/outputs/bundle/standardRelease/com.x8bit.bitwarden-standard-release.aab" \
|
||||
> ./bw-android-aab-sha256.txt
|
||||
> ./com.x8bit.bitwarden.aab-sha256.txt
|
||||
|
||||
- name: Create checksum for beta .aab artifact
|
||||
if: ${{ (matrix.variant == 'prod') && (matrix.artifact == 'aab') }}
|
||||
run: |
|
||||
sha256sum "app/build/outputs/bundle/standardBeta/com.x8bit.bitwarden-standard-beta.aab" \
|
||||
> ./bw-android-beta-aab-sha256.txt
|
||||
> ./com.x8bit.bitwarden.beta.aab-sha256.txt
|
||||
|
||||
- name: Create checksum for other .apk artifact
|
||||
- name: Create checksum for Debug .apk artifact
|
||||
if: ${{ (matrix.variant != 'prod') && (matrix.artifact == 'apk') }}
|
||||
run: |
|
||||
sha256sum "app/build/outputs/apk/standard/debug/com.x8bit.bitwarden-standard-debug.apk" \
|
||||
> ./bw-android-${{ matrix.variant }}-apk-sha256.txt
|
||||
> ./com.x8bit.bitwarden.${{ matrix.variant }}.apk-sha256.txt
|
||||
|
||||
- name: Upload .apk SHA file for release
|
||||
if: ${{ (matrix.variant == 'prod') && (matrix.artifact == 'apk') }}
|
||||
uses: actions/upload-artifact@65462800fd760344b1a7b4382951275a0abb4808 # v4.3.3
|
||||
uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6
|
||||
with:
|
||||
name: bw-android-apk-sha256.txt
|
||||
path: ./bw-android-apk-sha256.txt
|
||||
name: com.x8bit.bitwarden.apk-sha256.txt
|
||||
path: ./com.x8bit.bitwarden.apk-sha256.txt
|
||||
if-no-files-found: error
|
||||
|
||||
- name: Upload .apk SHA file for beta
|
||||
if: ${{ (matrix.variant == 'prod') && (matrix.artifact == 'apk') }}
|
||||
uses: actions/upload-artifact@65462800fd760344b1a7b4382951275a0abb4808 # v4.3.3
|
||||
uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6
|
||||
with:
|
||||
name: bw-android-beta-apk-sha256.txt
|
||||
path: ./bw-android-beta-apk-sha256.txt
|
||||
name: com.x8bit.bitwarden.beta.apk-sha256.txt
|
||||
path: ./com.x8bit.bitwarden.beta.apk-sha256.txt
|
||||
if-no-files-found: error
|
||||
|
||||
- name: Upload .aab SHA file for release
|
||||
if: ${{ (matrix.variant == 'prod') && (matrix.artifact == 'aab') }}
|
||||
uses: actions/upload-artifact@65462800fd760344b1a7b4382951275a0abb4808 # v4.3.3
|
||||
uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6
|
||||
with:
|
||||
name: bw-android-aab-sha256.txt
|
||||
path: ./bw-android-aab-sha256.txt
|
||||
name: com.x8bit.bitwarden.aab-sha256.txt
|
||||
path: ./com.x8bit.bitwarden.aab-sha256.txt
|
||||
if-no-files-found: error
|
||||
|
||||
- name: Upload .aab SHA file for beta
|
||||
if: ${{ (matrix.variant == 'prod') && (matrix.artifact == 'aab') }}
|
||||
uses: actions/upload-artifact@65462800fd760344b1a7b4382951275a0abb4808 # v4.3.3
|
||||
uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6
|
||||
with:
|
||||
name: bw-android-beta-aab-sha256.txt
|
||||
path: ./bw-android-beta-aab-sha256.txt
|
||||
name: com.x8bit.bitwarden.beta.aab-sha256.txt
|
||||
path: ./com.x8bit.bitwarden.beta.aab-sha256.txt
|
||||
if-no-files-found: error
|
||||
|
||||
- name: Upload .apk SHA file for other
|
||||
- name: Upload .apk SHA file for debug
|
||||
if: ${{ (matrix.variant != 'prod') && (matrix.artifact == 'apk') }}
|
||||
uses: actions/upload-artifact@65462800fd760344b1a7b4382951275a0abb4808 # v4.3.3
|
||||
uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6
|
||||
with:
|
||||
name: bw-android-${{ matrix.variant }}-apk-sha256.txt
|
||||
path: ./bw-android-${{ matrix.variant }}-apk-sha256.txt
|
||||
name: com.x8bit.bitwarden.${{ matrix.variant }}.apk-sha256.txt
|
||||
path: ./com.x8bit.bitwarden.${{ matrix.variant }}.apk-sha256.txt
|
||||
if-no-files-found: error
|
||||
|
||||
- name: Install Firebase app distribution plugin
|
||||
if: ${{ matrix.variant == 'prod' && github.ref_name == 'main' && inputs.distribute-to-firebase }}
|
||||
if: ${{ matrix.variant == 'prod' && github.ref_name == 'main' && (inputs.distribute-to-firebase || github.event_name == 'push') }}
|
||||
run: bundle exec fastlane add_plugin firebase_app_distribution
|
||||
|
||||
- name: Publish release artifacts to Firebase
|
||||
if: ${{ matrix.variant == 'prod' && matrix.artifact == 'apk' && github.ref_name == 'main' && inputs.distribute-to-firebase }}
|
||||
if: ${{ matrix.variant == 'prod' && matrix.artifact == 'apk' && github.ref_name == 'main' && (inputs.distribute-to-firebase || github.event_name == 'push') }}
|
||||
env:
|
||||
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
|
||||
if: ${{ (matrix.variant == 'prod' && matrix.artifact == 'apk') && github.ref_name == 'main' && inputs.distribute-to-firebase }}
|
||||
if: ${{ (matrix.variant == 'prod' && matrix.artifact == 'apk') && github.ref_name == 'main' && (inputs.distribute-to-firebase || github.event_name == 'push') }}
|
||||
env:
|
||||
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@3783f195e29b74ae398d7caca108814bbafde90e # v1.180.1
|
||||
uses: ruby/setup-ruby@a6e6f86333f0a2523ece813039b8b4be04560854 # v1.190.0
|
||||
with:
|
||||
bundler-cache: true
|
||||
|
||||
@@ -410,7 +413,7 @@ jobs:
|
||||
--name app_beta_fdroid-keystore.jks --file ${{ github.workspace }}/keystores/app_beta_fdroid-keystore.jks --output none
|
||||
|
||||
- name: Download Firebase credentials
|
||||
if: ${{ inputs.distribute-to-firebase }}
|
||||
if: ${{ inputs.distribute-to-firebase || github.event_name == 'push' }}
|
||||
env:
|
||||
ACCOUNT_NAME: bitwardenci
|
||||
CONTAINER_NAME: mobile
|
||||
@@ -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@af1da67850ed9a4cedd57bfd976089dd991e2582 # v4.0.0
|
||||
|
||||
- name: Cache Gradle files
|
||||
uses: actions/cache@0c45773b623bea8c8e75f6c82b208c3cf94ea4f9 # v4.0.2
|
||||
@@ -443,7 +446,7 @@ jobs:
|
||||
${{ runner.os }}-build-
|
||||
|
||||
- name: Configure JDK
|
||||
uses: actions/setup-java@99b8673ff64fbf99d8d325f52d9a5bdedb8483e9 # v4.2.1
|
||||
uses: actions/setup-java@6a0805fcefea3d4657a47ac4c165951e33482018 # v4.2.2
|
||||
with:
|
||||
distribution: "temurin"
|
||||
java-version: ${{ env.JAVA_VERSION }}
|
||||
@@ -466,7 +469,6 @@ jobs:
|
||||
keyAlias:bitwarden \
|
||||
keyPassword:"${{ env.FDROID_STORE_PASSWORD }}"
|
||||
|
||||
# Generate the F-Droid APK for publishing
|
||||
- name: Generate F-Droid Beta Artifacts
|
||||
env:
|
||||
FDROID_BETA_KEYSTORE_PASSWORD: ${{ secrets.FDROID_BETA_KEYSTORE_PASSWORD }}
|
||||
@@ -479,7 +481,7 @@ jobs:
|
||||
keyPassword:"${{ env.FDROID_BETA_KEY_PASSWORD }}"
|
||||
|
||||
- name: Upload F-Droid .apk artifact
|
||||
uses: actions/upload-artifact@65462800fd760344b1a7b4382951275a0abb4808 # v4.3.3
|
||||
uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6
|
||||
with:
|
||||
name: com.x8bit.bitwarden-fdroid.apk
|
||||
path: app/build/outputs/apk/fdroid/release/com.x8bit.bitwarden-fdroid-release.apk
|
||||
@@ -488,13 +490,32 @@ jobs:
|
||||
- name: Create checksum for F-Droid artifact
|
||||
run: |
|
||||
sha256sum "app/build/outputs/apk/fdroid/release/com.x8bit.bitwarden-fdroid-release.apk" \
|
||||
> ./bw-fdroid-apk-sha256.txt
|
||||
> ./com.x8bit.bitwarden-fdroid.apk-sha256.txt
|
||||
|
||||
- name: Upload F-Droid SHA file
|
||||
uses: actions/upload-artifact@65462800fd760344b1a7b4382951275a0abb4808 # v4.3.3
|
||||
uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6
|
||||
with:
|
||||
name: bw-fdroid-apk-sha256.txt
|
||||
path: ./bw-fdroid-apk-sha256.txt
|
||||
name: com.x8bit.bitwarden-fdroid.apk-sha256.txt
|
||||
path: ./com.x8bit.bitwarden-fdroid.apk-sha256.txt
|
||||
if-no-files-found: error
|
||||
|
||||
- name: Upload F-Droid Beta .apk artifact
|
||||
uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6
|
||||
with:
|
||||
name: com.x8bit.bitwarden.beta-fdroid.apk
|
||||
path: app/build/outputs/apk/fdroid/beta/com.x8bit.bitwarden-fdroid-beta.apk
|
||||
if-no-files-found: error
|
||||
|
||||
- name: Create checksum for F-Droid Beta artifact
|
||||
run: |
|
||||
sha256sum "app/build/outputs/apk/fdroid/beta/com.x8bit.bitwarden-fdroid-beta.apk" \
|
||||
> ./com.x8bit.bitwarden.beta-fdroid.apk-sha256.txt
|
||||
|
||||
- name: Upload F-Droid Beta SHA file
|
||||
uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6
|
||||
with:
|
||||
name: com.x8bit.bitwarden.beta-fdroid.apk-sha256.txt
|
||||
path: ./com.x8bit.bitwarden.beta-fdroid.apk-sha256.txt
|
||||
if-no-files-found: error
|
||||
|
||||
- name: Install Firebase app distribution plugin
|
||||
@@ -507,4 +528,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@30849777a3cba6ee9a09e24e195272b8287a0a5b # v1.20.4
|
||||
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@30849777a3cba6ee9a09e24e195272b8287a0a5b # v1.20.4
|
||||
uses: crowdin/github-action@6ed209d411599a981ccb978df3be9dc9b8a81699 # v2.1.1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
CROWDIN_API_TOKEN: ${{ steps.retrieve-secrets.outputs.crowdin-api-token }}
|
||||
|
||||
6
.github/workflows/scan.yml
vendored
6
.github/workflows/scan.yml
vendored
@@ -31,7 +31,7 @@ jobs:
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
|
||||
- name: Scan with Checkmarx
|
||||
uses: checkmarx/ast-github-action@92b6d52097badece63efe997ffe75207010bb80c # 2.0.29
|
||||
uses: checkmarx/ast-github-action@1fe318de2993222574e6249750ba9000a4e2a6cd # 2.0.33
|
||||
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@23acc5c183826b7a8a97bce3cecc52db901f8251 # v3.25.10
|
||||
uses: github/codeql-action/upload-sarif@883d8588e56d1753a8a58c1c86e88976f0c23449 # v3.26.3
|
||||
with:
|
||||
sarif_file: cx_result.sarif
|
||||
|
||||
@@ -66,7 +66,7 @@ jobs:
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
|
||||
- name: Scan with SonarCloud
|
||||
uses: sonarsource/sonarcloud-github-action@e44258b109568baa0df60ed515909fc6c72cba92 # v2.3.0
|
||||
uses: sonarsource/sonarcloud-github-action@eb211723266fe8e83102bac7361f0a05c3ac1d1b # v3.0.0
|
||||
env:
|
||||
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
6
.github/workflows/test.yml
vendored
6
.github/workflows/test.yml
vendored
@@ -36,7 +36,7 @@ jobs:
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
|
||||
- name: Validate Gradle wrapper
|
||||
uses: gradle/actions/wrapper-validation@dbbdc275be76ac10734476cc723d82dfe7ec6eda # v3.4.2
|
||||
uses: gradle/actions/wrapper-validation@af1da67850ed9a4cedd57bfd976089dd991e2582 # v4.0.0
|
||||
|
||||
- name: Cache Gradle files
|
||||
uses: actions/cache@0c45773b623bea8c8e75f6c82b208c3cf94ea4f9 # v4.0.2
|
||||
@@ -58,12 +58,12 @@ jobs:
|
||||
${{ runner.os }}-build-
|
||||
|
||||
- name: Configure Ruby
|
||||
uses: ruby/setup-ruby@3783f195e29b74ae398d7caca108814bbafde90e # v1.180.1
|
||||
uses: ruby/setup-ruby@a6e6f86333f0a2523ece813039b8b4be04560854 # v1.190.0
|
||||
with:
|
||||
bundler-cache: true
|
||||
|
||||
- name: Configure JDK
|
||||
uses: actions/setup-java@99b8673ff64fbf99d8d325f52d9a5bdedb8483e9 # v4.2.1
|
||||
uses: actions/setup-java@6a0805fcefea3d4657a47ac4c165951e33482018 # v4.2.2
|
||||
with:
|
||||
distribution: "temurin"
|
||||
java-version: ${{ env.JAVA_VERSION }}
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
shroud.reportKover 'App', 'app/build/reports/kover/reportStandardDebug.xml', 80, 80, false
|
||||
40
Gemfile.lock
40
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.966.0)
|
||||
aws-sdk-core (3.201.5)
|
||||
aws-eventstream (~> 1, >= 1.3.0)
|
||||
aws-partitions (~> 1, >= 1.651.0)
|
||||
aws-sigv4 (~> 1.8)
|
||||
aws-sigv4 (~> 1.9)
|
||||
jmespath (~> 1, >= 1.6.1)
|
||||
aws-sdk-kms (1.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.158.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)
|
||||
@@ -134,7 +134,7 @@ GEM
|
||||
google-apis-core (>= 0.11.0, < 2.a)
|
||||
google-apis-storage_v1 (0.31.0)
|
||||
google-apis-core (>= 0.11.0, < 2.a)
|
||||
google-cloud-core (1.7.0)
|
||||
google-cloud-core (1.7.1)
|
||||
google-cloud-env (>= 1.0, < 3.a)
|
||||
google-cloud-errors (~> 1.0)
|
||||
google-cloud-env (1.6.0)
|
||||
@@ -155,14 +155,14 @@ GEM
|
||||
os (>= 0.9, < 2.0)
|
||||
signet (>= 0.16, < 2.a)
|
||||
highline (2.0.3)
|
||||
http-cookie (1.0.6)
|
||||
http-cookie (1.0.7)
|
||||
domain_name (~> 0.5)
|
||||
httpclient (2.8.3)
|
||||
jmespath (1.6.2)
|
||||
json (2.7.2)
|
||||
jwt (2.8.2)
|
||||
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,14 +172,14 @@ 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)
|
||||
trailblazer-option (>= 0.1.1, < 0.2.0)
|
||||
uber (< 0.2.0)
|
||||
retriable (3.1.2)
|
||||
rexml (3.2.9)
|
||||
rexml (3.3.5)
|
||||
strscan
|
||||
rouge (2.0.7)
|
||||
ruby2_keywords (0.0.5)
|
||||
@@ -207,13 +207,13 @@ GEM
|
||||
uber (0.1.0)
|
||||
unicode-display_width (2.5.0)
|
||||
word_wrap (1.0.0)
|
||||
xcodeproj (1.24.0)
|
||||
xcodeproj (1.25.0)
|
||||
CFPropertyList (>= 2.3.3, < 4.0)
|
||||
atomos (~> 0.1.3)
|
||||
claide (>= 1.0.2, < 2.0)
|
||||
colored2 (~> 3.1)
|
||||
nanaimo (~> 0.3.0)
|
||||
rexml (~> 3.2.4)
|
||||
rexml (>= 3.3.2, < 4.0)
|
||||
xcpretty (0.3.0)
|
||||
rouge (~> 2.0.7)
|
||||
xcpretty-travis-formatter (1.0.1)
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
# Bitwarden Android
|
||||
# Bitwarden Android (BETA)
|
||||
|
||||
> [!TIP]
|
||||
> This repo has the new native Android app, currently in [Beta](https://community.bitwarden.com/t/about-the-beta-program/39185). Looking for the legacy .NET MAUI apps? Head on over to [bitwarden/mobile](https://github.com/bitwarden/mobile)
|
||||
|
||||
## Contents
|
||||
|
||||
@@ -8,7 +11,7 @@
|
||||
|
||||
## Compatibility
|
||||
|
||||
- **Minimum SDK**: 28
|
||||
- **Minimum SDK**: 29
|
||||
- **Target SDK**: 34
|
||||
- **Device Types Supported**: Phone and Tablet
|
||||
- **Orientations Supported**: Portrait and Landscape
|
||||
|
||||
@@ -61,12 +61,21 @@ android {
|
||||
signingConfig = signingConfigs.getByName("debug")
|
||||
isDebuggable = true
|
||||
isMinifyEnabled = false
|
||||
|
||||
buildConfigField(type = "boolean", name = "HAS_DEBUG_MENU", value = "true")
|
||||
}
|
||||
|
||||
// Beta and Release variants are identical except beta has a different package name
|
||||
create("beta") {
|
||||
initWith(buildTypes.getByName("release"))
|
||||
applicationIdSuffix = ".beta"
|
||||
isDebuggable = false
|
||||
isMinifyEnabled = true
|
||||
proguardFiles(
|
||||
getDefaultProguardFile("proguard-android-optimize.txt"),
|
||||
"proguard-rules.pro"
|
||||
)
|
||||
|
||||
buildConfigField(type = "boolean", name = "HAS_DEBUG_MENU", value = "false")
|
||||
}
|
||||
release {
|
||||
isDebuggable = false
|
||||
@@ -75,6 +84,8 @@ android {
|
||||
getDefaultProguardFile("proguard-android-optimize.txt"),
|
||||
"proguard-rules.pro"
|
||||
)
|
||||
|
||||
buildConfigField(type = "boolean", name = "HAS_DEBUG_MENU", value = "false")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -253,7 +264,7 @@ kover {
|
||||
tasks {
|
||||
getByName("check") {
|
||||
// Add detekt with type resolution to check
|
||||
dependsOn("detektMain")
|
||||
dependsOn("detekt")
|
||||
}
|
||||
|
||||
withType<io.gitlab.arturbosch.detekt.Detekt>().configureEach {
|
||||
@@ -289,6 +300,7 @@ sonar {
|
||||
property("sonar.sources", "app/src/")
|
||||
property("sonar.tests", "app/src/")
|
||||
property("sonar.test.inclusions", "app/src/test/")
|
||||
property("sonar.exclusions", "app/src/test/")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -11,4 +11,6 @@ class CrashLogsManagerImpl(
|
||||
legacyAppCenterMigrator: LegacyAppCenterMigrator,
|
||||
) : CrashLogsManager {
|
||||
override var isEnabled: Boolean = true
|
||||
|
||||
override fun trackNonFatalException(e: Exception) = Unit
|
||||
}
|
||||
|
||||
@@ -55,6 +55,26 @@
|
||||
<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="vault.bitwarden.com" />
|
||||
<data android:host="vault.bitwarden.eu" />
|
||||
<data android:host="*.bitwarden.pw" />
|
||||
<data android:pathPattern="/redirect-connector.*" />
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="com.x8bit.bitwarden.fido2.ACTION_CREATE_PASSKEY" />
|
||||
<action android:name="com.x8bit.bitwarden.fido2.ACTION_GET_PASSKEY" />
|
||||
<action android:name="com.x8bit.bitwarden.fido2.ACTION_UNLOCK_ACCOUNT" />
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<activity
|
||||
@@ -141,6 +161,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"
|
||||
@@ -151,6 +192,42 @@
|
||||
android:value="true" />
|
||||
</service>
|
||||
|
||||
<!--
|
||||
The GeneratorTileService name below refers to the legacy Xamarin app's service name.
|
||||
This must always match in order for the app to properly query if it is providing generator
|
||||
tile services.
|
||||
-->
|
||||
<!--suppress AndroidDomInspection -->
|
||||
<service
|
||||
android:name="com.x8bit.bitwarden.GeneratorTileService"
|
||||
android:exported="true"
|
||||
android:icon="@drawable/ic_generator"
|
||||
android:label="@string/password_generator"
|
||||
android:permission="android.permission.BIND_QUICK_SETTINGS_TILE"
|
||||
tools:ignore="MissingClass">
|
||||
<intent-filter>
|
||||
<action android:name="android.service.quicksettings.action.QS_TILE" />
|
||||
</intent-filter>
|
||||
</service>
|
||||
|
||||
<!--
|
||||
The MyVaultTileService name below refers to the legacy Xamarin app's service name.
|
||||
This must always match in order for the app to properly query if it is providing vault
|
||||
tile services.
|
||||
-->
|
||||
<!--suppress AndroidDomInspection -->
|
||||
<service
|
||||
android:name="com.x8bit.bitwarden.MyVaultTileService"
|
||||
android:exported="true"
|
||||
android:icon="@drawable/ic_notification"
|
||||
android:label="@string/my_vault"
|
||||
android:permission="android.permission.BIND_QUICK_SETTINGS_TILE"
|
||||
tools:ignore="MissingClass">
|
||||
<intent-filter>
|
||||
<action android:name="android.service.quicksettings.action.QS_TILE" />
|
||||
</intent-filter>
|
||||
</service>
|
||||
|
||||
<meta-data
|
||||
android:name="android.content.APP_RESTRICTIONS"
|
||||
android:resource="@xml/app_restrictions" />
|
||||
|
||||
@@ -7,10 +7,14 @@ import androidx.core.app.AppComponentFactory
|
||||
import com.x8bit.bitwarden.data.autofill.BitwardenAutofillService
|
||||
import com.x8bit.bitwarden.data.autofill.fido2.BitwardenFido2ProviderService
|
||||
import com.x8bit.bitwarden.data.platform.annotation.OmitFromCoverage
|
||||
import com.x8bit.bitwarden.data.tiles.BitwardenGeneratorTileService
|
||||
import com.x8bit.bitwarden.data.tiles.BitwardenVaultTileService
|
||||
|
||||
private const val LEGACY_AUTOFILL_SERVICE_NAME = "com.x8bit.bitwarden.Autofill.AutofillService"
|
||||
private const val LEGACY_CREDENTIAL_SERVICE_NAME =
|
||||
"com.x8bit.bitwarden.Autofill.CredentialProviderService"
|
||||
private const val LEGACY_VAULT_TILE_SERVICE_NAME = "com.x8bit.bitwarden.MyVaultTileService"
|
||||
private const val LEGACY_GENERATOR_TILE_SERVICE_NAME = "com.x8bit.bitwarden.GeneratorTileService"
|
||||
|
||||
/**
|
||||
* A factory class that allows us to intercept when a manifest element is being instantiated
|
||||
@@ -20,10 +24,11 @@ private const val LEGACY_CREDENTIAL_SERVICE_NAME =
|
||||
@OmitFromCoverage
|
||||
class BitwardenAppComponentFactory : AppComponentFactory() {
|
||||
/**
|
||||
* Used to intercept when the [BitwardenAutofillService] or [BitwardenFido2ProviderService] is
|
||||
* being instantiated and modify which service is created. This is required because the
|
||||
* [className] used in the manifest must match the legacy Xamarin app service name but the
|
||||
* service name in this app is different.
|
||||
* Used to intercept when the [BitwardenAutofillService], [BitwardenFido2ProviderService],
|
||||
* [BitwardenVaultTileService], or [BitwardenGeneratorTileService] is being instantiated and
|
||||
* modify which service is created. This is required because the [className] used in the
|
||||
* manifest must match the legacy Xamarin app service name but the service name in this app is
|
||||
* different.
|
||||
*/
|
||||
override fun instantiateServiceCompat(
|
||||
cl: ClassLoader,
|
||||
@@ -48,6 +53,18 @@ class BitwardenAppComponentFactory : AppComponentFactory() {
|
||||
}
|
||||
}
|
||||
|
||||
LEGACY_VAULT_TILE_SERVICE_NAME -> {
|
||||
super.instantiateServiceCompat(cl, BitwardenVaultTileService::class.java.name, intent)
|
||||
}
|
||||
|
||||
LEGACY_GENERATOR_TILE_SERVICE_NAME -> {
|
||||
super.instantiateServiceCompat(
|
||||
cl,
|
||||
BitwardenGeneratorTileService::class.java.name,
|
||||
intent,
|
||||
)
|
||||
}
|
||||
|
||||
else -> super.instantiateServiceCompat(cl, className, intent)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,8 @@ package com.x8bit.bitwarden
|
||||
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.view.KeyEvent
|
||||
import android.view.MotionEvent
|
||||
import android.view.WindowManager
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.activity.viewModels
|
||||
@@ -11,17 +13,18 @@ import androidx.compose.runtime.getValue
|
||||
import androidx.core.os.LocaleListCompat
|
||||
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.navigation.compose.rememberNavController
|
||||
import com.x8bit.bitwarden.data.autofill.manager.AutofillActivityManager
|
||||
import com.x8bit.bitwarden.data.autofill.manager.AutofillCompletionManager
|
||||
import com.x8bit.bitwarden.data.platform.annotation.OmitFromCoverage
|
||||
import com.x8bit.bitwarden.data.platform.repository.SettingsRepository
|
||||
import com.x8bit.bitwarden.ui.platform.base.util.EventsEffect
|
||||
import com.x8bit.bitwarden.ui.platform.composition.LocalManagerProvider
|
||||
import com.x8bit.bitwarden.ui.platform.feature.debugmenu.manager.DebugMenuLaunchManager
|
||||
import com.x8bit.bitwarden.ui.platform.feature.debugmenu.navigateToDebugMenuScreen
|
||||
import com.x8bit.bitwarden.ui.platform.feature.rootnav.RootNavScreen
|
||||
import com.x8bit.bitwarden.ui.platform.theme.BitwardenTheme
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import javax.inject.Inject
|
||||
|
||||
/**
|
||||
@@ -42,13 +45,14 @@ class MainActivity : AppCompatActivity() {
|
||||
@Inject
|
||||
lateinit var settingsRepository: SettingsRepository
|
||||
|
||||
@Inject
|
||||
lateinit var debugLaunchManager: DebugMenuLaunchManager
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
var shouldShowSplashScreen = true
|
||||
installSplashScreen().setKeepOnScreenCondition { shouldShowSplashScreen }
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
observeViewModelEvents()
|
||||
|
||||
if (savedInstanceState == null) {
|
||||
mainViewModel.trySendAction(
|
||||
MainAction.ReceiveFirstIntent(
|
||||
@@ -66,11 +70,20 @@ class MainActivity : AppCompatActivity() {
|
||||
}
|
||||
setContent {
|
||||
val state by mainViewModel.stateFlow.collectAsStateWithLifecycle()
|
||||
val navController = rememberNavController()
|
||||
EventsEffect(viewModel = mainViewModel) { event ->
|
||||
when (event) {
|
||||
is MainEvent.CompleteAutofill -> handleCompleteAutofill(event)
|
||||
MainEvent.Recreate -> handleRecreate()
|
||||
MainEvent.NavigateToDebugMenu -> navController.navigateToDebugMenuScreen()
|
||||
}
|
||||
}
|
||||
updateScreenCapture(isScreenCaptureAllowed = state.isScreenCaptureAllowed)
|
||||
LocalManagerProvider {
|
||||
BitwardenTheme(theme = state.theme) {
|
||||
RootNavScreen(
|
||||
onSplashScreenRemoved = { shouldShowSplashScreen = false },
|
||||
navController = navController,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -93,16 +106,18 @@ class MainActivity : AppCompatActivity() {
|
||||
currentFocus?.clearFocus()
|
||||
}
|
||||
|
||||
private fun observeViewModelEvents() {
|
||||
mainViewModel
|
||||
.eventFlow
|
||||
.onEach { event ->
|
||||
when (event) {
|
||||
is MainEvent.CompleteAutofill -> handleCompleteAutofill(event)
|
||||
MainEvent.Recreate -> handleRecreate()
|
||||
}
|
||||
}
|
||||
.launchIn(lifecycleScope)
|
||||
override fun dispatchTouchEvent(event: MotionEvent): Boolean = debugLaunchManager
|
||||
.actionOnInputEvent(event = event, action = ::sendOpenDebugMenuEvent)
|
||||
.takeIf { it }
|
||||
?: super.dispatchTouchEvent(event)
|
||||
|
||||
override fun dispatchKeyEvent(event: KeyEvent): Boolean = debugLaunchManager
|
||||
.actionOnInputEvent(event = event, action = ::sendOpenDebugMenuEvent)
|
||||
.takeIf { it }
|
||||
?: super.dispatchKeyEvent(event)
|
||||
|
||||
private fun sendOpenDebugMenuEvent() {
|
||||
mainViewModel.trySendAction(MainAction.OpenDebugMenu)
|
||||
}
|
||||
|
||||
private fun handleCompleteAutofill(event: MainEvent.CompleteAutofill) {
|
||||
|
||||
@@ -6,8 +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
|
||||
@@ -31,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"
|
||||
@@ -44,11 +49,13 @@ class MainViewModel @Inject constructor(
|
||||
autofillSelectionManager: AutofillSelectionManager,
|
||||
private val specialCircumstanceManager: SpecialCircumstanceManager,
|
||||
private val garbageCollectionManager: GarbageCollectionManager,
|
||||
private val fido2CredentialManager: Fido2CredentialManager,
|
||||
private val intentManager: IntentManager,
|
||||
settingsRepository: SettingsRepository,
|
||||
vaultRepository: VaultRepository,
|
||||
private val vaultRepository: VaultRepository,
|
||||
private val authRepository: AuthRepository,
|
||||
private val savedStateHandle: SavedStateHandle,
|
||||
private val clock: Clock,
|
||||
) : BaseViewModel<MainState, MainEvent, MainAction>(
|
||||
initialState = MainState(
|
||||
theme = settingsRepository.appTheme,
|
||||
@@ -108,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)
|
||||
}
|
||||
|
||||
@@ -129,9 +140,14 @@ class MainViewModel @Inject constructor(
|
||||
is MainAction.Internal.VaultUnlockStateChange -> handleVaultUnlockStateChange()
|
||||
is MainAction.ReceiveFirstIntent -> handleFirstIntentReceived(action)
|
||||
is MainAction.ReceiveNewIntent -> handleNewIntentReceived(action)
|
||||
MainAction.OpenDebugMenu -> handleOpenDebugMenu()
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleOpenDebugMenu() {
|
||||
sendEvent(MainEvent.NavigateToDebugMenu)
|
||||
}
|
||||
|
||||
private fun handleAutofillSelectionReceive(
|
||||
action: MainAction.Internal.AutofillSelectionReceive,
|
||||
) {
|
||||
@@ -168,6 +184,7 @@ class MainViewModel @Inject constructor(
|
||||
)
|
||||
}
|
||||
|
||||
@Suppress("LongMethod")
|
||||
private fun handleIntent(
|
||||
intent: Intent,
|
||||
isFirstIntent: Boolean,
|
||||
@@ -179,6 +196,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 =
|
||||
@@ -190,6 +210,17 @@ class MainViewModel @Inject constructor(
|
||||
)
|
||||
}
|
||||
|
||||
completeRegistrationData != null -> {
|
||||
if (authRepository.activeUserId != null) {
|
||||
authRepository.hasPendingAccountAddition = true
|
||||
}
|
||||
specialCircumstanceManager.specialCircumstance =
|
||||
SpecialCircumstance.PreLogin.CompleteRegistration(
|
||||
completeRegistrationData = completeRegistrationData,
|
||||
timestamp = clock.millis(),
|
||||
)
|
||||
}
|
||||
|
||||
autofillSaveItem != null -> {
|
||||
specialCircumstanceManager.specialCircumstance =
|
||||
SpecialCircumstance.AutofillSave(
|
||||
@@ -218,6 +249,10 @@ class MainViewModel @Inject constructor(
|
||||
}
|
||||
|
||||
fido2CredentialRequestData != null -> {
|
||||
// Set the user's verification status when a new FIDO 2 request is received to force
|
||||
// explicit verification if the user's vault is unlocked when the request is
|
||||
// received.
|
||||
fido2CredentialManager.isUserVerified = false
|
||||
specialCircumstanceManager.specialCircumstance =
|
||||
SpecialCircumstance.Fido2Save(
|
||||
fido2CredentialRequest = fido2CredentialRequestData,
|
||||
@@ -231,6 +266,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
|
||||
@@ -271,6 +320,11 @@ sealed class MainAction {
|
||||
*/
|
||||
data class ReceiveNewIntent(val intent: Intent) : MainAction()
|
||||
|
||||
/**
|
||||
* Receive event to open the debug menu.
|
||||
*/
|
||||
data object OpenDebugMenu : MainAction()
|
||||
|
||||
/**
|
||||
* Actions for internal use by the ViewModel.
|
||||
*/
|
||||
@@ -322,4 +376,9 @@ sealed class MainEvent {
|
||||
* Event indicating that the UI should recreate itself.
|
||||
*/
|
||||
data object Recreate : MainEvent()
|
||||
|
||||
/**
|
||||
* Navigate to the debug menu.
|
||||
*/
|
||||
data object NavigateToDebugMenu : MainEvent()
|
||||
}
|
||||
|
||||
@@ -45,12 +45,37 @@ interface AuthDiskSource {
|
||||
*/
|
||||
fun clearData(userId: String)
|
||||
|
||||
/**
|
||||
* Retrieves the state indicating that the user should use a key connector.
|
||||
*/
|
||||
fun getShouldUseKeyConnector(userId: String): Boolean?
|
||||
|
||||
/**
|
||||
* Retrieves the state indicating that the user should use a key connector as a flow.
|
||||
*/
|
||||
fun getShouldUseKeyConnectorFlow(userId: String): Flow<Boolean?>
|
||||
|
||||
/**
|
||||
* Stores the boolean indicating that the user should use a key connector.
|
||||
*/
|
||||
fun storeShouldUseKeyConnector(userId: String, shouldUseKeyConnector: Boolean?)
|
||||
|
||||
/**
|
||||
* Retrieves the state indicating that the user has completed login with TDE.
|
||||
*/
|
||||
fun getIsTdeLoginComplete(userId: String): Boolean?
|
||||
|
||||
/**
|
||||
* Stores the boolean indicating that the user has completed login with TDE.
|
||||
*/
|
||||
fun storeIsTdeLoginComplete(userId: String, isTdeLoginComplete: Boolean?)
|
||||
|
||||
/**
|
||||
* Retrieves the state indicating that the user has chosen to trust this device.
|
||||
*
|
||||
* 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 +85,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,8 @@ private const val TWO_FACTOR_TOKEN_KEY = "twoFactorToken"
|
||||
private const val MASTER_PASSWORD_HASH_KEY = "keyHash"
|
||||
private const val POLICIES_KEY = "policies"
|
||||
private const val SHOULD_TRUST_DEVICE_KEY = "shouldTrustDevice"
|
||||
private const val TDE_LOGIN_COMPLETE = "tdeLoginComplete"
|
||||
private const val USES_KEY_CONNECTOR = "usesKeyConnector"
|
||||
|
||||
/**
|
||||
* Primary implementation of [AuthDiskSource].
|
||||
@@ -57,6 +58,8 @@ class AuthDiskSourceImpl(
|
||||
AuthDiskSource {
|
||||
|
||||
private val inMemoryPinProtectedUserKeys = mutableMapOf<String, String?>()
|
||||
private val mutableShouldUseKeyConnectorFlowMap =
|
||||
mutableMapOf<String, MutableSharedFlow<Boolean?>>()
|
||||
private val mutableOrganizationsFlowMap =
|
||||
mutableMapOf<String, MutableSharedFlow<List<SyncResponseJson.Profile.Organization>?>>()
|
||||
private val mutablePoliciesFlowMap =
|
||||
@@ -111,7 +114,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 +126,45 @@ class AuthDiskSourceImpl(
|
||||
storeMasterPasswordHash(userId = userId, passwordHash = null)
|
||||
storePolicies(userId = userId, policies = null)
|
||||
storeAccountTokens(userId = userId, accountTokens = null)
|
||||
storeShouldUseKeyConnector(userId = userId, shouldUseKeyConnector = null)
|
||||
storeIsTdeLoginComplete(userId = userId, isTdeLoginComplete = 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 getShouldUseKeyConnectorFlow(userId: String): Flow<Boolean?> =
|
||||
getMutableShouldUseKeyConnectorFlowMap(userId = userId)
|
||||
.onSubscription { emit(getShouldUseKeyConnector(userId = userId)) }
|
||||
|
||||
override fun getShouldUseKeyConnector(
|
||||
userId: String,
|
||||
): Boolean? = getBoolean(key = USES_KEY_CONNECTOR.appendIdentifier(userId))
|
||||
|
||||
override fun storeShouldUseKeyConnector(userId: String, shouldUseKeyConnector: Boolean?) {
|
||||
putBoolean(
|
||||
key = USES_KEY_CONNECTOR.appendIdentifier(userId),
|
||||
value = shouldUseKeyConnector,
|
||||
)
|
||||
getMutableShouldUseKeyConnectorFlowMap(userId = userId).tryEmit(shouldUseKeyConnector)
|
||||
}
|
||||
|
||||
override fun getIsTdeLoginComplete(
|
||||
userId: String,
|
||||
): Boolean? = getBoolean(key = TDE_LOGIN_COMPLETE.appendIdentifier(userId))
|
||||
|
||||
override fun storeIsTdeLoginComplete(userId: String, isTdeLoginComplete: Boolean?) {
|
||||
putBoolean(TDE_LOGIN_COMPLETE.appendIdentifier(userId), isTdeLoginComplete)
|
||||
}
|
||||
|
||||
override fun getShouldTrustDevice(
|
||||
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))
|
||||
|
||||
@@ -384,6 +398,13 @@ class AuthDiskSourceImpl(
|
||||
putString(key = UNIQUE_APP_ID_KEY, value = it)
|
||||
}
|
||||
|
||||
private fun getMutableShouldUseKeyConnectorFlowMap(
|
||||
userId: String,
|
||||
): MutableSharedFlow<Boolean?> =
|
||||
mutableShouldUseKeyConnectorFlowMap.getOrPut(userId) {
|
||||
bufferedMutableSharedFlow(replay = 1)
|
||||
}
|
||||
|
||||
private fun getMutableOrganizationsFlow(
|
||||
userId: String,
|
||||
): MutableSharedFlow<List<SyncResponseJson.Profile.Organization>?> =
|
||||
|
||||
@@ -13,6 +13,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.
|
||||
*/
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
package com.x8bit.bitwarden.data.auth.datasource.network.api
|
||||
|
||||
import androidx.annotation.Keep
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.KeyConnectorMasterKeyRequestJson
|
||||
import retrofit2.http.Body
|
||||
import retrofit2.http.POST
|
||||
import retrofit2.http.Url
|
||||
|
||||
/**
|
||||
* Defines raw calls specific for key connectors that use custom urls.
|
||||
*/
|
||||
@Keep
|
||||
interface AuthenticatedKeyConnectorApi {
|
||||
@POST
|
||||
suspend fun storeMasterKeyToKeyConnector(
|
||||
@Url url: String,
|
||||
@Body body: KeyConnectorMasterKeyRequestJson,
|
||||
): Result<Unit>
|
||||
}
|
||||
@@ -1,14 +1,17 @@
|
||||
package com.x8bit.bitwarden.data.auth.datasource.network.api
|
||||
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.KeyConnectorKeyRequestJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.PasswordHintRequestJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.ResendEmailRequestJson
|
||||
import com.x8bit.bitwarden.data.platform.datasource.network.util.HEADER_KEY_AUTHORIZATION
|
||||
import retrofit2.http.Body
|
||||
import retrofit2.http.Header
|
||||
import retrofit2.http.POST
|
||||
|
||||
/**
|
||||
* Defines raw calls under the /accounts API.
|
||||
*/
|
||||
interface AccountsApi {
|
||||
interface UnauthenticatedAccountsApi {
|
||||
@POST("/accounts/password-hint")
|
||||
suspend fun passwordHintRequest(
|
||||
@Body body: PasswordHintRequestJson,
|
||||
@@ -18,4 +21,10 @@ interface AccountsApi {
|
||||
suspend fun resendVerificationCodeEmail(
|
||||
@Body body: ResendEmailRequestJson,
|
||||
): Result<Unit>
|
||||
|
||||
@POST("/accounts/set-key-connector-key")
|
||||
suspend fun setKeyConnectorKey(
|
||||
@Body body: KeyConnectorKeyRequestJson,
|
||||
@Header(HEADER_KEY_AUTHORIZATION) bearerToken: String,
|
||||
): Result<Unit>
|
||||
}
|
||||
@@ -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
|
||||
@@ -19,7 +22,7 @@ import retrofit2.http.Query
|
||||
/**
|
||||
* Defines raw calls under the /identity API.
|
||||
*/
|
||||
interface IdentityApi {
|
||||
interface UnauthenticatedIdentityApi {
|
||||
|
||||
@POST("/connect/token")
|
||||
@Suppress("LongParameterList")
|
||||
@@ -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?>
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package com.x8bit.bitwarden.data.auth.datasource.network.api
|
||||
|
||||
import androidx.annotation.Keep
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.KeyConnectorMasterKeyRequestJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.KeyConnectorMasterKeyResponseJson
|
||||
import com.x8bit.bitwarden.data.platform.datasource.network.util.HEADER_KEY_AUTHORIZATION
|
||||
import retrofit2.http.Body
|
||||
import retrofit2.http.GET
|
||||
import retrofit2.http.Header
|
||||
import retrofit2.http.POST
|
||||
import retrofit2.http.Url
|
||||
|
||||
/**
|
||||
* Defines raw calls specific for key connectors that use custom urls.
|
||||
*/
|
||||
@Keep
|
||||
interface UnauthenticatedKeyConnectorApi {
|
||||
@POST
|
||||
suspend fun storeMasterKeyToKeyConnector(
|
||||
@Url url: String,
|
||||
@Header(HEADER_KEY_AUTHORIZATION) bearerToken: String,
|
||||
@Body body: KeyConnectorMasterKeyRequestJson,
|
||||
): Result<Unit>
|
||||
|
||||
@GET
|
||||
suspend fun getMasterKeyFromKeyConnector(
|
||||
@Url url: String,
|
||||
@Header(HEADER_KEY_AUTHORIZATION) bearerToken: String,
|
||||
): Result<KeyConnectorMasterKeyResponseJson>
|
||||
}
|
||||
@@ -8,7 +8,7 @@ import retrofit2.http.POST
|
||||
/**
|
||||
* Defines raw calls under the /organizations API.
|
||||
*/
|
||||
interface OrganizationApi {
|
||||
interface UnauthenticatedOrganizationApi {
|
||||
/**
|
||||
* Checks for the claimed domain organization of an email for SSO purposes.
|
||||
*/
|
||||
@@ -36,8 +36,12 @@ object AuthNetworkModule {
|
||||
retrofits: Retrofits,
|
||||
json: Json,
|
||||
): AccountsService = AccountsServiceImpl(
|
||||
accountsApi = retrofits.unauthenticatedApiRetrofit.create(),
|
||||
unauthenticatedAccountsApi = retrofits.unauthenticatedApiRetrofit.create(),
|
||||
authenticatedAccountsApi = retrofits.authenticatedApiRetrofit.create(),
|
||||
unauthenticatedKeyConnectorApi = retrofits.createStaticRetrofit().create(),
|
||||
authenticatedKeyConnectorApi = retrofits
|
||||
.createStaticRetrofit(isAuthenticated = true)
|
||||
.create(),
|
||||
json = json,
|
||||
)
|
||||
|
||||
@@ -64,7 +68,7 @@ object AuthNetworkModule {
|
||||
retrofits: Retrofits,
|
||||
json: Json,
|
||||
): IdentityService = IdentityServiceImpl(
|
||||
api = retrofits.unauthenticatedIdentityRetrofit.create(),
|
||||
unauthenticatedIdentityApi = retrofits.unauthenticatedIdentityRetrofit.create(),
|
||||
json = json,
|
||||
)
|
||||
|
||||
@@ -73,10 +77,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(),
|
||||
)
|
||||
|
||||
@@ -95,6 +97,6 @@ object AuthNetworkModule {
|
||||
retrofits: Retrofits,
|
||||
): OrganizationService = OrganizationServiceImpl(
|
||||
authenticatedOrganizationApi = retrofits.authenticatedApiRetrofit.create(),
|
||||
organizationApi = retrofits.unauthenticatedApiRetrofit.create(),
|
||||
unauthenticatedOrganizationApi = retrofits.unauthenticatedApiRetrofit.create(),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -28,6 +28,7 @@ sealed class GetTokenResponseJson {
|
||||
* this token will be cached and used for future auth requests.
|
||||
* @property masterPasswordPolicyOptions The options available for a user's master password.
|
||||
* @property userDecryptionOptions The options available to a user for decryption.
|
||||
* @property keyConnectorUrl URL to the user's key connector.
|
||||
*/
|
||||
@Serializable
|
||||
data class Success(
|
||||
@@ -75,6 +76,9 @@ sealed class GetTokenResponseJson {
|
||||
|
||||
@SerialName("UserDecryptionOptions")
|
||||
val userDecryptionOptions: UserDecryptionOptionsJson?,
|
||||
|
||||
@SerialName("KeyConnectorUrl")
|
||||
val keyConnectorUrl: String?,
|
||||
) : GetTokenResponseJson()
|
||||
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,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,12 @@
|
||||
package com.x8bit.bitwarden.data.auth.datasource.network.model
|
||||
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
/**
|
||||
* Represents the request body used to store the master key in the cloud.
|
||||
*/
|
||||
@Serializable
|
||||
data class KeyConnectorMasterKeyRequestJson(
|
||||
@SerialName("Key") val masterKey: String,
|
||||
)
|
||||
@@ -0,0 +1,12 @@
|
||||
package com.x8bit.bitwarden.data.auth.datasource.network.model
|
||||
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
/**
|
||||
* Represents the response body used to retrieve the master key from the cloud.
|
||||
*/
|
||||
@Serializable
|
||||
data class KeyConnectorMasterKeyResponseJson(
|
||||
@SerialName("key") val masterKey: String,
|
||||
)
|
||||
@@ -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,
|
||||
)
|
||||
}
|
||||
@@ -46,7 +46,6 @@ sealed class RegisterResponseJson {
|
||||
/**
|
||||
* Represents the json body of an invalid register request.
|
||||
*
|
||||
* @param message
|
||||
* @param validationErrors a map where each value is a list of error messages for each key.
|
||||
* The values in the array should be used for display to the user, since the keys tend to come
|
||||
* back as nonsense. (eg: empty string key)
|
||||
@@ -54,18 +53,17 @@ sealed class RegisterResponseJson {
|
||||
@Serializable
|
||||
data class Invalid(
|
||||
@SerialName("message")
|
||||
val message: String?,
|
||||
private val invalidMessage: String? = null,
|
||||
|
||||
@SerialName("Message")
|
||||
private val errorMessage: String? = null,
|
||||
|
||||
@SerialName("validationErrors")
|
||||
val validationErrors: Map<String, List<String>>?,
|
||||
) : RegisterResponseJson()
|
||||
|
||||
/**
|
||||
* A different register error with a message.
|
||||
*/
|
||||
@Serializable
|
||||
data class Error(
|
||||
@SerialName("Message")
|
||||
val message: String?,
|
||||
) : RegisterResponseJson()
|
||||
) : RegisterResponseJson() {
|
||||
/**
|
||||
* A generic error message.
|
||||
*/
|
||||
val message: String? get() = invalidMessage ?: errorMessage
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,45 @@
|
||||
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 validationErrors a map where each value is a list of error messages for each key.
|
||||
* The values in the array should be used for display to the user, since the keys tend to come
|
||||
* back as nonsense. (eg: empty string key)
|
||||
*/
|
||||
@Serializable
|
||||
data class Invalid(
|
||||
@SerialName("message")
|
||||
private val invalidMessage: String? = null,
|
||||
|
||||
@SerialName("Message")
|
||||
private val errorMessage: String? = null,
|
||||
|
||||
@SerialName("validationErrors")
|
||||
val validationErrors: Map<String, List<String>>?,
|
||||
) : SendVerificationEmailResponseJson() {
|
||||
/**
|
||||
* A generic error message.
|
||||
*/
|
||||
val message: String? get() = invalidMessage ?: errorMessage
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,8 @@
|
||||
package com.x8bit.bitwarden.data.auth.datasource.network.service
|
||||
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.DeleteAccountResponseJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.KeyConnectorKeyRequestJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.KeyConnectorMasterKeyResponseJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.PasswordHintResponseJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.ResendEmailRequestJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.ResetPasswordRequestJson
|
||||
@@ -9,8 +11,14 @@ import com.x8bit.bitwarden.data.auth.datasource.network.model.SetPasswordRequest
|
||||
/**
|
||||
* Provides an API for querying accounts endpoints.
|
||||
*/
|
||||
@Suppress("TooManyFunctions")
|
||||
interface AccountsService {
|
||||
|
||||
/**
|
||||
* Converts the currently active account to a key-connector account.
|
||||
*/
|
||||
suspend fun convertToKeyConnector(): Result<Unit>
|
||||
|
||||
/**
|
||||
* Creates a new account's keys.
|
||||
*/
|
||||
@@ -49,8 +57,50 @@ interface AccountsService {
|
||||
*/
|
||||
suspend fun resetPassword(body: ResetPasswordRequestJson): Result<Unit>
|
||||
|
||||
/**
|
||||
* Set the key connector key.
|
||||
*
|
||||
* This API requires the [accessToken] to be passed in manually because it occurs during the
|
||||
* login process.
|
||||
*/
|
||||
suspend fun setKeyConnectorKey(
|
||||
accessToken: String,
|
||||
body: KeyConnectorKeyRequestJson,
|
||||
): Result<Unit>
|
||||
|
||||
/**
|
||||
* Set the password.
|
||||
*/
|
||||
suspend fun setPassword(body: SetPasswordRequestJson): Result<Unit>
|
||||
|
||||
/**
|
||||
* Retrieves the master key from the key connector.
|
||||
*
|
||||
* This API requires the [accessToken] to be passed in manually because it occurs during the
|
||||
* login process.
|
||||
*/
|
||||
suspend fun getMasterKeyFromKeyConnector(
|
||||
url: String,
|
||||
accessToken: String,
|
||||
): Result<KeyConnectorMasterKeyResponseJson>
|
||||
|
||||
/**
|
||||
* Stores the master key to the key connector.
|
||||
*/
|
||||
suspend fun storeMasterKeyToKeyConnector(
|
||||
url: String,
|
||||
masterKey: String,
|
||||
): Result<Unit>
|
||||
|
||||
/**
|
||||
* Stores the master key to the key connector.
|
||||
*
|
||||
* This API requires the [accessToken] to be passed in manually because it occurs during the
|
||||
* login process.
|
||||
*/
|
||||
suspend fun storeMasterKeyToKeyConnector(
|
||||
url: String,
|
||||
accessToken: String,
|
||||
masterKey: String,
|
||||
): Result<Unit>
|
||||
}
|
||||
|
||||
@@ -1,10 +1,15 @@
|
||||
package com.x8bit.bitwarden.data.auth.datasource.network.service
|
||||
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.api.AccountsApi
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.api.AuthenticatedAccountsApi
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.api.AuthenticatedKeyConnectorApi
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.api.UnauthenticatedAccountsApi
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.api.UnauthenticatedKeyConnectorApi
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.CreateAccountKeysRequest
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.DeleteAccountRequestJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.DeleteAccountResponseJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.KeyConnectorKeyRequestJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.KeyConnectorMasterKeyRequestJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.KeyConnectorMasterKeyResponseJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.PasswordHintRequestJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.PasswordHintResponseJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.ResendEmailRequestJson
|
||||
@@ -12,15 +17,28 @@ import com.x8bit.bitwarden.data.auth.datasource.network.model.ResetPasswordReque
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.SetPasswordRequestJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.VerifyOtpRequestJson
|
||||
import com.x8bit.bitwarden.data.platform.datasource.network.model.toBitwardenError
|
||||
import com.x8bit.bitwarden.data.platform.datasource.network.util.HEADER_VALUE_BEARER_PREFIX
|
||||
import com.x8bit.bitwarden.data.platform.datasource.network.util.parseErrorBodyOrNull
|
||||
import kotlinx.serialization.json.Json
|
||||
|
||||
/**
|
||||
* The default implementation of the [AccountsService].
|
||||
*/
|
||||
@Suppress("TooManyFunctions")
|
||||
class AccountsServiceImpl(
|
||||
private val accountsApi: AccountsApi,
|
||||
private val unauthenticatedAccountsApi: UnauthenticatedAccountsApi,
|
||||
private val authenticatedAccountsApi: AuthenticatedAccountsApi,
|
||||
private val unauthenticatedKeyConnectorApi: UnauthenticatedKeyConnectorApi,
|
||||
private val authenticatedKeyConnectorApi: AuthenticatedKeyConnectorApi,
|
||||
private val json: Json,
|
||||
) : AccountsService {
|
||||
|
||||
/**
|
||||
* 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,
|
||||
@@ -69,7 +87,7 @@ class AccountsServiceImpl(
|
||||
override suspend fun requestPasswordHint(
|
||||
email: String,
|
||||
): Result<PasswordHintResponseJson> =
|
||||
accountsApi
|
||||
unauthenticatedAccountsApi
|
||||
.passwordHintRequest(PasswordHintRequestJson(email))
|
||||
.map { PasswordHintResponseJson.Success }
|
||||
.recoverCatching { throwable ->
|
||||
@@ -83,7 +101,7 @@ class AccountsServiceImpl(
|
||||
}
|
||||
|
||||
override suspend fun resendVerificationCodeEmail(body: ResendEmailRequestJson): Result<Unit> =
|
||||
accountsApi.resendVerificationCodeEmail(body = body)
|
||||
unauthenticatedAccountsApi.resendVerificationCodeEmail(body = body)
|
||||
|
||||
override suspend fun resetPassword(body: ResetPasswordRequestJson): Result<Unit> {
|
||||
return if (body.currentPasswordHash == null) {
|
||||
@@ -93,7 +111,44 @@ class AccountsServiceImpl(
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun setKeyConnectorKey(
|
||||
accessToken: String,
|
||||
body: KeyConnectorKeyRequestJson,
|
||||
): Result<Unit> = unauthenticatedAccountsApi.setKeyConnectorKey(
|
||||
body = body,
|
||||
bearerToken = "$HEADER_VALUE_BEARER_PREFIX$accessToken",
|
||||
)
|
||||
|
||||
override suspend fun setPassword(
|
||||
body: SetPasswordRequestJson,
|
||||
): Result<Unit> = authenticatedAccountsApi.setPassword(body)
|
||||
|
||||
override suspend fun getMasterKeyFromKeyConnector(
|
||||
url: String,
|
||||
accessToken: String,
|
||||
): Result<KeyConnectorMasterKeyResponseJson> =
|
||||
unauthenticatedKeyConnectorApi.getMasterKeyFromKeyConnector(
|
||||
url = "$url/user-keys",
|
||||
bearerToken = "$HEADER_VALUE_BEARER_PREFIX$accessToken",
|
||||
)
|
||||
|
||||
override suspend fun storeMasterKeyToKeyConnector(
|
||||
url: String,
|
||||
masterKey: String,
|
||||
): Result<Unit> =
|
||||
authenticatedKeyConnectorApi.storeMasterKeyToKeyConnector(
|
||||
url = "$url/user-keys",
|
||||
body = KeyConnectorMasterKeyRequestJson(masterKey = masterKey),
|
||||
)
|
||||
|
||||
override suspend fun storeMasterKeyToKeyConnector(
|
||||
url: String,
|
||||
accessToken: String,
|
||||
masterKey: String,
|
||||
): Result<Unit> =
|
||||
unauthenticatedKeyConnectorApi.storeMasterKeyToKeyConnector(
|
||||
url = "$url/user-keys",
|
||||
bearerToken = "$HEADER_VALUE_BEARER_PREFIX$accessToken",
|
||||
body = KeyConnectorMasterKeyRequestJson(masterKey = masterKey),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
}
|
||||
|
||||
@@ -1,14 +1,16 @@
|
||||
package com.x8bit.bitwarden.data.auth.datasource.network.service
|
||||
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.api.IdentityApi
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.api.UnauthenticatedIdentityApi
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.GetTokenResponseJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.IdentityTokenAuthModel
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.PreLoginRequestJson
|
||||
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
|
||||
@@ -18,30 +20,30 @@ import com.x8bit.bitwarden.data.platform.util.DeviceModelProvider
|
||||
import kotlinx.serialization.json.Json
|
||||
|
||||
class IdentityServiceImpl(
|
||||
private val api: IdentityApi,
|
||||
private val unauthenticatedIdentityApi: UnauthenticatedIdentityApi,
|
||||
private val json: Json,
|
||||
private val deviceModelProvider: DeviceModelProvider = DeviceModelProvider(),
|
||||
) : IdentityService {
|
||||
|
||||
override suspend fun preLogin(email: String): Result<PreLoginResponseJson> =
|
||||
api.preLogin(PreLoginRequestJson(email = email))
|
||||
unauthenticatedIdentityApi.preLogin(PreLoginRequestJson(email = email))
|
||||
|
||||
@Suppress("MagicNumber")
|
||||
override suspend fun register(body: RegisterRequestJson): Result<RegisterResponseJson> =
|
||||
api
|
||||
unauthenticatedIdentityApi
|
||||
.register(body)
|
||||
.recoverCatching { throwable ->
|
||||
val bitwardenError = throwable.toBitwardenError()
|
||||
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,
|
||||
)
|
||||
?: throw throwable
|
||||
}
|
||||
|
||||
@Suppress("MagicNumber")
|
||||
@@ -51,7 +53,7 @@ class IdentityServiceImpl(
|
||||
authModel: IdentityTokenAuthModel,
|
||||
captchaToken: String?,
|
||||
twoFactorData: TwoFactorDataModel?,
|
||||
): Result<GetTokenResponseJson> = api
|
||||
): Result<GetTokenResponseJson> = unauthenticatedIdentityApi
|
||||
.getToken(
|
||||
scope = "api offline_access",
|
||||
clientId = "mobile",
|
||||
@@ -87,18 +89,42 @@ class IdentityServiceImpl(
|
||||
|
||||
override suspend fun prevalidateSso(
|
||||
organizationIdentifier: String,
|
||||
): Result<PrevalidateSsoResponseJson> = api
|
||||
): Result<PrevalidateSsoResponseJson> = unauthenticatedIdentityApi
|
||||
.prevalidateSso(
|
||||
organizationIdentifier = organizationIdentifier,
|
||||
)
|
||||
|
||||
override fun refreshTokenSynchronously(
|
||||
refreshToken: String,
|
||||
): Result<RefreshTokenResponseJson> = api
|
||||
): Result<RefreshTokenResponseJson> = unauthenticatedIdentityApi
|
||||
.refreshTokenCall(
|
||||
clientId = "mobile",
|
||||
grantType = "refresh_token",
|
||||
refreshToken = refreshToken,
|
||||
)
|
||||
.executeForResult()
|
||||
|
||||
@Suppress("MagicNumber")
|
||||
override suspend fun registerFinish(
|
||||
body: RegisterFinishRequestJson,
|
||||
): Result<RegisterResponseJson> =
|
||||
unauthenticatedIdentityApi
|
||||
.registerFinish(body)
|
||||
.recoverCatching { throwable ->
|
||||
val bitwardenError = throwable.toBitwardenError()
|
||||
bitwardenError
|
||||
.parseErrorBodyOrNull<RegisterResponseJson.Invalid>(
|
||||
codes = listOf(400, 429),
|
||||
json = json,
|
||||
)
|
||||
?: throw throwable
|
||||
}
|
||||
|
||||
override suspend fun sendVerificationEmail(
|
||||
body: SendVerificationEmailRequestJson,
|
||||
): Result<String?> {
|
||||
return unauthenticatedIdentityApi
|
||||
.sendVerificationEmail(body = body)
|
||||
.map { it?.content }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package com.x8bit.bitwarden.data.auth.datasource.network.service
|
||||
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.api.AuthenticatedOrganizationApi
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.api.OrganizationApi
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.api.UnauthenticatedOrganizationApi
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.OrganizationAutoEnrollStatusResponseJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.OrganizationDomainSsoDetailsRequestJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.OrganizationDomainSsoDetailsResponseJson
|
||||
@@ -13,7 +13,7 @@ import com.x8bit.bitwarden.data.auth.datasource.network.model.OrganizationResetP
|
||||
*/
|
||||
class OrganizationServiceImpl(
|
||||
private val authenticatedOrganizationApi: AuthenticatedOrganizationApi,
|
||||
private val organizationApi: OrganizationApi,
|
||||
private val unauthenticatedOrganizationApi: UnauthenticatedOrganizationApi,
|
||||
) : OrganizationService {
|
||||
override suspend fun organizationResetPasswordEnroll(
|
||||
organizationId: String,
|
||||
@@ -32,7 +32,7 @@ class OrganizationServiceImpl(
|
||||
|
||||
override suspend fun getOrganizationDomainSsoDetails(
|
||||
email: String,
|
||||
): Result<OrganizationDomainSsoDetailsResponseJson> = organizationApi
|
||||
): Result<OrganizationDomainSsoDetailsResponseJson> = unauthenticatedOrganizationApi
|
||||
.getClaimedDomainOrganizationDetails(
|
||||
body = OrganizationDomainSsoDetailsRequestJson(
|
||||
email = email,
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
package com.x8bit.bitwarden.data.auth.datasource.sdk
|
||||
|
||||
import com.bitwarden.bitwarden.AuthRequestResponse
|
||||
import com.bitwarden.bitwarden.MasterPasswordPolicyOptions
|
||||
import com.bitwarden.bitwarden.RegisterKeyResponse
|
||||
import com.bitwarden.bitwarden.RegisterTdeKeyResponse
|
||||
import com.bitwarden.core.AuthRequestResponse
|
||||
import com.bitwarden.core.KeyConnectorResponse
|
||||
import com.bitwarden.core.MasterPasswordPolicyOptions
|
||||
import com.bitwarden.core.RegisterKeyResponse
|
||||
import com.bitwarden.core.RegisterTdeKeyResponse
|
||||
import com.bitwarden.crypto.HashPurpose
|
||||
import com.bitwarden.crypto.Kdf
|
||||
import com.x8bit.bitwarden.data.auth.datasource.sdk.model.PasswordStrength
|
||||
@@ -37,6 +38,11 @@ interface AuthSdkSource {
|
||||
purpose: HashPurpose,
|
||||
): Result<String>
|
||||
|
||||
/**
|
||||
* Creates a set of encryption key information for use with a key connector.
|
||||
*/
|
||||
suspend fun makeKeyConnectorKeys(): Result<KeyConnectorResponse>
|
||||
|
||||
/**
|
||||
* Creates a set of encryption key information for registration.
|
||||
*/
|
||||
|
||||
@@ -1,17 +1,18 @@
|
||||
package com.x8bit.bitwarden.data.auth.datasource.sdk
|
||||
|
||||
import com.bitwarden.bitwarden.AuthRequestResponse
|
||||
import com.bitwarden.bitwarden.FingerprintRequest
|
||||
import com.bitwarden.bitwarden.MasterPasswordPolicyOptions
|
||||
import com.bitwarden.bitwarden.RegisterKeyResponse
|
||||
import com.bitwarden.bitwarden.RegisterTdeKeyResponse
|
||||
import com.bitwarden.core.AuthRequestResponse
|
||||
import com.bitwarden.core.FingerprintRequest
|
||||
import com.bitwarden.core.KeyConnectorResponse
|
||||
import com.bitwarden.core.MasterPasswordPolicyOptions
|
||||
import com.bitwarden.core.RegisterKeyResponse
|
||||
import com.bitwarden.core.RegisterTdeKeyResponse
|
||||
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 +20,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 +37,7 @@ class AuthSdkSourceImpl(
|
||||
override suspend fun getUserFingerprint(
|
||||
email: String,
|
||||
publicKey: String,
|
||||
): Result<String> = runCatching {
|
||||
): Result<String> = runCatchingWithLogs {
|
||||
getClient()
|
||||
.platform()
|
||||
.fingerprint(
|
||||
@@ -51,7 +53,7 @@ class AuthSdkSourceImpl(
|
||||
password: String,
|
||||
kdf: Kdf,
|
||||
purpose: HashPurpose,
|
||||
): Result<String> = runCatching {
|
||||
): Result<String> = runCatchingWithLogs {
|
||||
getClient()
|
||||
.auth()
|
||||
.hashPassword(
|
||||
@@ -62,11 +64,18 @@ class AuthSdkSourceImpl(
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun makeKeyConnectorKeys(): Result<KeyConnectorResponse> =
|
||||
runCatchingWithLogs {
|
||||
getClient()
|
||||
.auth()
|
||||
.makeKeyConnectorKeys()
|
||||
}
|
||||
|
||||
override suspend fun makeRegisterKeys(
|
||||
email: String,
|
||||
password: String,
|
||||
kdf: Kdf,
|
||||
): Result<RegisterKeyResponse> = runCatching {
|
||||
): Result<RegisterKeyResponse> = runCatchingWithLogs {
|
||||
getClient()
|
||||
.auth()
|
||||
.makeRegisterKeys(
|
||||
@@ -81,7 +90,7 @@ class AuthSdkSourceImpl(
|
||||
email: String,
|
||||
orgPublicKey: String,
|
||||
rememberDevice: Boolean,
|
||||
): Result<RegisterTdeKeyResponse> = runCatching {
|
||||
): Result<RegisterTdeKeyResponse> = runCatchingWithLogs {
|
||||
getClient(userId = userId)
|
||||
.auth()
|
||||
.makeRegisterTdeKeys(
|
||||
@@ -95,7 +104,7 @@ class AuthSdkSourceImpl(
|
||||
email: String,
|
||||
password: String,
|
||||
additionalInputs: List<String>,
|
||||
): Result<PasswordStrength> = runCatching {
|
||||
): Result<PasswordStrength> = runCatchingWithLogs {
|
||||
@Suppress("UnsafeCallOnNullableType")
|
||||
getClient()
|
||||
.auth()
|
||||
@@ -111,7 +120,7 @@ class AuthSdkSourceImpl(
|
||||
password: String,
|
||||
passwordStrength: PasswordStrength,
|
||||
policy: MasterPasswordPolicyOptions,
|
||||
): Result<Boolean> = runCatching {
|
||||
): Result<Boolean> = runCatchingWithLogs {
|
||||
getClient()
|
||||
.auth()
|
||||
.satisfiesPolicy(
|
||||
@@ -120,8 +129,4 @@ class AuthSdkSourceImpl(
|
||||
policy = policy,
|
||||
)
|
||||
}
|
||||
|
||||
private suspend fun getClient(
|
||||
userId: String? = null,
|
||||
): Client = sdkClientManager.getOrCreateClient(userId = userId)
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
package com.x8bit.bitwarden.data.auth.manager
|
||||
|
||||
import com.bitwarden.bitwarden.AuthRequestResponse
|
||||
import com.bitwarden.core.AuthRequestResponse
|
||||
import com.x8bit.bitwarden.data.auth.datasource.disk.AuthDiskSource
|
||||
import com.x8bit.bitwarden.data.auth.datasource.disk.model.PendingAuthRequestJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.AuthRequestTypeJson
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
package com.x8bit.bitwarden.data.auth.manager
|
||||
|
||||
import com.bitwarden.core.KeyConnectorResponse
|
||||
import com.bitwarden.crypto.Kdf
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.KdfTypeJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.KeyConnectorMasterKeyResponseJson
|
||||
|
||||
/**
|
||||
* Manager used to interface with a key connector.
|
||||
*/
|
||||
interface KeyConnectorManager {
|
||||
/**
|
||||
* Retrieves the master key from the key connector.
|
||||
*/
|
||||
suspend fun getMasterKeyFromKeyConnector(
|
||||
url: String,
|
||||
accessToken: String,
|
||||
): Result<KeyConnectorMasterKeyResponseJson>
|
||||
|
||||
/**
|
||||
* Migrates an existing user to use the key connector.
|
||||
*/
|
||||
@Suppress("LongParameterList")
|
||||
suspend fun migrateExistingUserToKeyConnector(
|
||||
userId: String,
|
||||
url: String,
|
||||
userKeyEncrypted: String,
|
||||
email: String,
|
||||
masterPassword: String,
|
||||
kdf: Kdf,
|
||||
): Result<Unit>
|
||||
|
||||
/**
|
||||
* Migrates a new user to use the key connector.
|
||||
*/
|
||||
@Suppress("LongParameterList")
|
||||
suspend fun migrateNewUserToKeyConnector(
|
||||
url: String,
|
||||
accessToken: String,
|
||||
kdfType: KdfTypeJson,
|
||||
kdfIterations: Int?,
|
||||
kdfMemory: Int?,
|
||||
kdfParallelism: Int?,
|
||||
organizationIdentifier: String,
|
||||
): Result<KeyConnectorResponse>
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
package com.x8bit.bitwarden.data.auth.manager
|
||||
|
||||
import com.bitwarden.core.KeyConnectorResponse
|
||||
import com.bitwarden.crypto.Kdf
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.KdfTypeJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.KeyConnectorKeyRequestJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.KeyConnectorMasterKeyResponseJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.service.AccountsService
|
||||
import com.x8bit.bitwarden.data.auth.datasource.sdk.AuthSdkSource
|
||||
import com.x8bit.bitwarden.data.platform.util.flatMap
|
||||
import com.x8bit.bitwarden.data.vault.datasource.sdk.VaultSdkSource
|
||||
|
||||
/**
|
||||
* The default implementation of the [KeyConnectorManager].
|
||||
*/
|
||||
class KeyConnectorManagerImpl(
|
||||
private val accountsService: AccountsService,
|
||||
private val authSdkSource: AuthSdkSource,
|
||||
private val vaultSdkSource: VaultSdkSource,
|
||||
) : KeyConnectorManager {
|
||||
override suspend fun getMasterKeyFromKeyConnector(
|
||||
url: String,
|
||||
accessToken: String,
|
||||
): Result<KeyConnectorMasterKeyResponseJson> =
|
||||
accountsService.getMasterKeyFromKeyConnector(
|
||||
url = url,
|
||||
accessToken = accessToken,
|
||||
)
|
||||
|
||||
override suspend fun migrateExistingUserToKeyConnector(
|
||||
userId: String,
|
||||
url: String,
|
||||
userKeyEncrypted: String,
|
||||
email: String,
|
||||
masterPassword: String,
|
||||
kdf: Kdf,
|
||||
): Result<Unit> =
|
||||
vaultSdkSource
|
||||
.deriveKeyConnector(
|
||||
userId = userId,
|
||||
userKeyEncrypted = userKeyEncrypted,
|
||||
email = email,
|
||||
password = masterPassword,
|
||||
kdf = kdf,
|
||||
)
|
||||
.flatMap { masterKey ->
|
||||
accountsService.storeMasterKeyToKeyConnector(url = url, masterKey = masterKey)
|
||||
}
|
||||
.flatMap { accountsService.convertToKeyConnector() }
|
||||
|
||||
override suspend fun migrateNewUserToKeyConnector(
|
||||
url: String,
|
||||
accessToken: String,
|
||||
kdfType: KdfTypeJson,
|
||||
kdfIterations: Int?,
|
||||
kdfMemory: Int?,
|
||||
kdfParallelism: Int?,
|
||||
organizationIdentifier: String,
|
||||
): Result<KeyConnectorResponse> =
|
||||
authSdkSource
|
||||
.makeKeyConnectorKeys()
|
||||
.flatMap { keyConnectorResponse ->
|
||||
accountsService
|
||||
.storeMasterKeyToKeyConnector(
|
||||
url = url,
|
||||
accessToken = accessToken,
|
||||
masterKey = keyConnectorResponse.masterKey,
|
||||
)
|
||||
.flatMap {
|
||||
accountsService.setKeyConnectorKey(
|
||||
accessToken = accessToken,
|
||||
body = KeyConnectorKeyRequestJson(
|
||||
userKey = keyConnectorResponse.encryptedUserKey,
|
||||
keys = KeyConnectorKeyRequestJson.Keys(
|
||||
publicKey = keyConnectorResponse.keys.public,
|
||||
encryptedPrivateKey = keyConnectorResponse.keys.private,
|
||||
),
|
||||
kdfType = kdfType,
|
||||
kdfIterations = kdfIterations,
|
||||
kdfMemory = kdfMemory,
|
||||
kdfParallelism = kdfParallelism,
|
||||
organizationIdentifier = organizationIdentifier,
|
||||
),
|
||||
)
|
||||
}
|
||||
.map { keyConnectorResponse }
|
||||
}
|
||||
}
|
||||
@@ -17,7 +17,8 @@ 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) {
|
||||
authDiskSource.storeIsTdeLoginComplete(userId = userId, isTdeLoginComplete = true)
|
||||
false.asSuccess()
|
||||
} else {
|
||||
vaultSdkSource
|
||||
@@ -51,6 +52,7 @@ class TrustedDeviceManagerImpl(
|
||||
userId = userId,
|
||||
previousUserState = requireNotNull(authDiskSource.userState),
|
||||
)
|
||||
authDiskSource.storeIsTdeLoginComplete(userId = userId, isTdeLoginComplete = true)
|
||||
}
|
||||
.also { authDiskSource.storeShouldTrustDevice(userId = userId, shouldTrustDevice = null) }
|
||||
.map { Unit }
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package com.x8bit.bitwarden.data.auth.manager.di
|
||||
|
||||
import android.content.Context
|
||||
import com.x8bit.bitwarden.data.auth.datasource.disk.AuthDiskSource
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.service.AccountsService
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.service.AuthRequestsService
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.service.DevicesService
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.service.NewAuthRequestService
|
||||
@@ -10,6 +11,8 @@ import com.x8bit.bitwarden.data.auth.manager.AuthRequestManager
|
||||
import com.x8bit.bitwarden.data.auth.manager.AuthRequestManagerImpl
|
||||
import com.x8bit.bitwarden.data.auth.manager.AuthRequestNotificationManager
|
||||
import com.x8bit.bitwarden.data.auth.manager.AuthRequestNotificationManagerImpl
|
||||
import com.x8bit.bitwarden.data.auth.manager.KeyConnectorManager
|
||||
import com.x8bit.bitwarden.data.auth.manager.KeyConnectorManagerImpl
|
||||
import com.x8bit.bitwarden.data.auth.manager.TrustedDeviceManager
|
||||
import com.x8bit.bitwarden.data.auth.manager.TrustedDeviceManagerImpl
|
||||
import com.x8bit.bitwarden.data.auth.manager.UserLogoutManager
|
||||
@@ -71,6 +74,19 @@ object AuthManagerModule {
|
||||
authDiskSource = authDiskSource,
|
||||
)
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideKeyConnectorManager(
|
||||
accountsService: AccountsService,
|
||||
authSdkSource: AuthSdkSource,
|
||||
vaultSdkSource: VaultSdkSource,
|
||||
): KeyConnectorManager =
|
||||
KeyConnectorManagerImpl(
|
||||
accountsService = accountsService,
|
||||
authSdkSource = authSdkSource,
|
||||
vaultSdkSource = vaultSdkSource,
|
||||
)
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideTrustedDeviceManager(
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
package com.x8bit.bitwarden.data.auth.manager.model
|
||||
|
||||
import com.bitwarden.bitwarden.AuthRequestResponse
|
||||
import com.bitwarden.core.AuthRequestResponse
|
||||
|
||||
/**
|
||||
* Models result of creating a new login approval request.
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
@@ -16,13 +16,16 @@ import com.x8bit.bitwarden.data.auth.repository.model.PasswordStrengthResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.PolicyInformation
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.PrevalidateSsoResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.RegisterResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.RemovePasswordResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.RequestOtpResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.ResendEmailResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.ResetPasswordResult
|
||||
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
|
||||
@@ -100,6 +103,11 @@ interface AuthRepository : AuthenticatorProvider, AuthRequestManager {
|
||||
*/
|
||||
var rememberedOrgIdentifier: String?
|
||||
|
||||
/**
|
||||
* The currently persisted state indicating whether the user has completed login via TDE.
|
||||
*/
|
||||
val tdeLoginComplete: Boolean?
|
||||
|
||||
/**
|
||||
* The currently persisted state indicating whether the user has trusted this device.
|
||||
*/
|
||||
@@ -129,6 +137,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 +252,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 +260,7 @@ interface AuthRepository : AuthenticatorProvider, AuthRequestManager {
|
||||
email: String,
|
||||
masterPassword: String,
|
||||
masterPasswordHint: String?,
|
||||
emailVerificationToken: String? = null,
|
||||
captchaToken: String?,
|
||||
shouldCheckDataBreaches: Boolean,
|
||||
isMasterPasswordStrong: Boolean,
|
||||
@@ -263,6 +273,12 @@ interface AuthRepository : AuthenticatorProvider, AuthRequestManager {
|
||||
email: String,
|
||||
): PasswordHintResult
|
||||
|
||||
/**
|
||||
* Removes the users password from the account. This used used when migrating from master
|
||||
* password login to key connector login.
|
||||
*/
|
||||
suspend fun removePassword(masterPassword: String): RemovePasswordResult
|
||||
|
||||
/**
|
||||
* Resets the users password from the [currentPassword] (or null for account recovery resets),
|
||||
* to the [newPassword] and optional [passwordHint].
|
||||
@@ -342,9 +358,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.bitwarden.AuthRequestMethod
|
||||
import com.bitwarden.bitwarden.InitUserCryptoMethod
|
||||
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
|
||||
@@ -32,6 +35,7 @@ import com.x8bit.bitwarden.data.auth.datasource.sdk.AuthSdkSource
|
||||
import com.x8bit.bitwarden.data.auth.datasource.sdk.util.toInt
|
||||
import com.x8bit.bitwarden.data.auth.datasource.sdk.util.toKdfTypeJson
|
||||
import com.x8bit.bitwarden.data.auth.manager.AuthRequestManager
|
||||
import com.x8bit.bitwarden.data.auth.manager.KeyConnectorManager
|
||||
import com.x8bit.bitwarden.data.auth.manager.TrustedDeviceManager
|
||||
import com.x8bit.bitwarden.data.auth.manager.UserLogoutManager
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.AuthState
|
||||
@@ -46,38 +50,47 @@ import com.x8bit.bitwarden.data.auth.repository.model.PasswordStrengthResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.PolicyInformation
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.PrevalidateSsoResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.RegisterResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.RemovePasswordResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.RequestOtpResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.ResendEmailResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.ResetPasswordResult
|
||||
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.UserKeyConnectorState
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.UserOrganizations
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.UserState
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.ValidatePasswordResult
|
||||
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
|
||||
import com.x8bit.bitwarden.data.auth.repository.util.WebAuthResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.util.activeUserIdChangesFlow
|
||||
import com.x8bit.bitwarden.data.auth.repository.util.policyInformation
|
||||
import com.x8bit.bitwarden.data.auth.repository.util.toRemovedPasswordUserStateJson
|
||||
import com.x8bit.bitwarden.data.auth.repository.util.toSdkParams
|
||||
import com.x8bit.bitwarden.data.auth.repository.util.toUserState
|
||||
import com.x8bit.bitwarden.data.auth.repository.util.toUserStateJson
|
||||
import com.x8bit.bitwarden.data.auth.repository.util.toUserStateJsonWithPassword
|
||||
import com.x8bit.bitwarden.data.auth.repository.util.userAccountTokens
|
||||
import com.x8bit.bitwarden.data.auth.repository.util.userAccountTokensFlow
|
||||
import com.x8bit.bitwarden.data.auth.repository.util.userKeyConnectorStateFlow
|
||||
import com.x8bit.bitwarden.data.auth.repository.util.userKeyConnectorStateList
|
||||
import com.x8bit.bitwarden.data.auth.repository.util.userOrganizationsList
|
||||
import com.x8bit.bitwarden.data.auth.repository.util.userOrganizationsListFlow
|
||||
import com.x8bit.bitwarden.data.auth.repository.util.userSwitchingChangesFlow
|
||||
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
|
||||
@@ -85,11 +98,14 @@ import com.x8bit.bitwarden.data.platform.repository.util.bufferedMutableSharedFl
|
||||
import com.x8bit.bitwarden.data.platform.util.asFailure
|
||||
import com.x8bit.bitwarden.data.platform.util.asSuccess
|
||||
import com.x8bit.bitwarden.data.platform.util.flatMap
|
||||
import com.x8bit.bitwarden.data.vault.datasource.network.model.OrganizationType
|
||||
import com.x8bit.bitwarden.data.vault.datasource.network.model.PolicyTypeJson
|
||||
import com.x8bit.bitwarden.data.vault.datasource.network.model.SyncResponseJson
|
||||
import com.x8bit.bitwarden.data.vault.datasource.sdk.VaultSdkSource
|
||||
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
|
||||
@@ -133,12 +149,13 @@ class AuthRepositoryImpl(
|
||||
private val settingsRepository: SettingsRepository,
|
||||
private val vaultRepository: VaultRepository,
|
||||
private val authRequestManager: AuthRequestManager,
|
||||
private val keyConnectorManager: KeyConnectorManager,
|
||||
private val trustedDeviceManager: TrustedDeviceManager,
|
||||
private val userLogoutManager: UserLogoutManager,
|
||||
private val policyManager: PolicyManager,
|
||||
private val featureFlagManager: FeatureFlagManager,
|
||||
pushManager: PushManager,
|
||||
dispatcherManager: DispatcherManager,
|
||||
private val elapsedRealtimeMillisProvider: () -> Long = { SystemClock.elapsedRealtime() },
|
||||
) : AuthRepository,
|
||||
AuthRequestManager by authRequestManager {
|
||||
/**
|
||||
@@ -229,6 +246,7 @@ class AuthRepositoryImpl(
|
||||
authDiskSource.userStateFlow,
|
||||
authDiskSource.userAccountTokensFlow,
|
||||
authDiskSource.userOrganizationsListFlow,
|
||||
authDiskSource.userKeyConnectorStateFlow,
|
||||
vaultRepository.vaultUnlockDataStateFlow,
|
||||
mutableHasPendingAccountAdditionStateFlow,
|
||||
// Ignore the data in the merge, but trigger an update when they emit.
|
||||
@@ -240,12 +258,14 @@ class AuthRepositoryImpl(
|
||||
val userStateJson = array[0] as UserStateJson?
|
||||
val userAccountTokens = array[1] as List<UserAccountTokens>
|
||||
val userOrganizationsList = array[2] as List<UserOrganizations>
|
||||
val vaultState = array[3] as List<VaultUnlockData>
|
||||
val hasPendingAccountAddition = array[4] as Boolean
|
||||
val userIsUsingKeyConnectorList = array[3] as List<UserKeyConnectorState>
|
||||
val vaultState = array[4] as List<VaultUnlockData>
|
||||
val hasPendingAccountAddition = array[5] as Boolean
|
||||
userStateJson?.toUserState(
|
||||
vaultState = vaultState,
|
||||
userAccountTokens = userAccountTokens,
|
||||
userOrganizationsList = userOrganizationsList,
|
||||
userIsUsingKeyConnectorList = userIsUsingKeyConnectorList,
|
||||
hasPendingAccountAddition = hasPendingAccountAddition,
|
||||
isBiometricsEnabledProvider = ::isBiometricsEnabled,
|
||||
vaultUnlockTypeProvider = ::getVaultUnlockType,
|
||||
@@ -263,6 +283,7 @@ class AuthRepositoryImpl(
|
||||
vaultState = vaultRepository.vaultUnlockDataStateFlow.value,
|
||||
userAccountTokens = authDiskSource.userAccountTokens,
|
||||
userOrganizationsList = authDiskSource.userOrganizationsList,
|
||||
userIsUsingKeyConnectorList = authDiskSource.userKeyConnectorStateList,
|
||||
hasPendingAccountAddition = mutableHasPendingAccountAdditionStateFlow.value,
|
||||
isBiometricsEnabledProvider = ::isBiometricsEnabled,
|
||||
vaultUnlockTypeProvider = ::getVaultUnlockType,
|
||||
@@ -291,6 +312,9 @@ class AuthRepositoryImpl(
|
||||
|
||||
override var rememberedOrgIdentifier: String? by authDiskSource::rememberedOrgIdentifier
|
||||
|
||||
override val tdeLoginComplete: Boolean?
|
||||
get() = activeUserId?.let { authDiskSource.getIsTdeLoginComplete(userId = it) }
|
||||
|
||||
override var shouldTrustDevice: Boolean
|
||||
get() = activeUserId?.let { authDiskSource.getShouldTrustDevice(userId = it) } ?: false
|
||||
set(value) {
|
||||
@@ -315,6 +339,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
|
||||
@@ -443,7 +471,6 @@ class AuthRepositoryImpl(
|
||||
},
|
||||
)
|
||||
|
||||
@Suppress("ReturnCount")
|
||||
override suspend fun createNewSsoUser(): NewSsoUserResult {
|
||||
val account = authDiskSource.userState?.activeAccount ?: return NewSsoUserResult.Failure
|
||||
val orgIdentifier = rememberedOrgIdentifier ?: return NewSsoUserResult.Failure
|
||||
@@ -458,7 +485,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 ->
|
||||
@@ -501,7 +529,6 @@ class AuthRepositoryImpl(
|
||||
)
|
||||
}
|
||||
|
||||
@Suppress("ReturnCount")
|
||||
override suspend fun completeTdeLogin(
|
||||
requestPrivateKey: String,
|
||||
asymmetricalKey: String,
|
||||
@@ -511,20 +538,22 @@ 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),
|
||||
)
|
||||
|
||||
authDiskSource.storeUserKey(userId = userId, userKey = asymmetricalKey)
|
||||
checkForVaultUnlockError(
|
||||
onVaultUnlockError = { error ->
|
||||
return error.toLoginErrorResult()
|
||||
},
|
||||
) {
|
||||
unlockVault(
|
||||
accountProfile = profile,
|
||||
privateKey = privateKey,
|
||||
initUserCryptoMethod = InitUserCryptoMethod.AuthRequest(
|
||||
requestPrivateKey = requestPrivateKey,
|
||||
method = AuthRequestMethod.UserKey(protectedUserKey = asymmetricalKey),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
vaultRepository.syncIfNecessary()
|
||||
return LoginResult.Success
|
||||
}
|
||||
@@ -627,19 +656,24 @@ class AuthRepositoryImpl(
|
||||
?: return IllegalStateException("Must be logged in.").asFailure()
|
||||
return identityService
|
||||
.refreshTokenSynchronously(refreshToken)
|
||||
.onSuccess {
|
||||
.flatMap { refreshTokenResponse ->
|
||||
// Check to make sure the user is still logged in after making the request
|
||||
authDiskSource
|
||||
.userState
|
||||
?.accounts
|
||||
?.get(userId)
|
||||
?.let { refreshTokenResponse.asSuccess() }
|
||||
?: IllegalStateException("Must be logged in.").asFailure()
|
||||
}
|
||||
.onSuccess { refreshTokenResponse ->
|
||||
// Update the existing UserState with updated token information
|
||||
authDiskSource.storeAccountTokens(
|
||||
userId = userId,
|
||||
accountTokens = AccountTokensJson(
|
||||
accessToken = it.accessToken,
|
||||
refreshToken = it.refreshToken,
|
||||
accessToken = refreshTokenResponse.accessToken,
|
||||
refreshToken = refreshTokenResponse.refreshToken,
|
||||
),
|
||||
)
|
||||
authDiskSource.userState = it.toUserStateJson(
|
||||
userId = userId,
|
||||
previousUserState = requireNotNull(authDiskSource.userState),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -678,7 +712,6 @@ class AuthRepositoryImpl(
|
||||
}
|
||||
?: ResendEmailResult.Error(message = null)
|
||||
|
||||
@Suppress("ReturnCount")
|
||||
override fun switchAccount(userId: String): SwitchAccountResult {
|
||||
val currentUserState = authDiskSource.userState ?: return SwitchAccountResult.NoChange
|
||||
val previousActiveUserId = currentUserState.activeUserId
|
||||
@@ -703,19 +736,12 @@ class AuthRepositoryImpl(
|
||||
return SwitchAccountResult.AccountSwitched
|
||||
}
|
||||
|
||||
override fun updateLastActiveTime() {
|
||||
val userId = activeUserId ?: return
|
||||
authDiskSource.storeLastActiveTimeMillis(
|
||||
userId = userId,
|
||||
lastActiveTimeMillis = elapsedRealtimeMillisProvider(),
|
||||
)
|
||||
}
|
||||
|
||||
@Suppress("ReturnCount", "LongMethod")
|
||||
@Suppress("LongMethod")
|
||||
override suspend fun register(
|
||||
email: String,
|
||||
masterPassword: String,
|
||||
masterPasswordHint: String?,
|
||||
emailVerificationToken: String?,
|
||||
captchaToken: String?,
|
||||
shouldCheckDataBreaches: Boolean,
|
||||
isMasterPasswordStrong: Boolean,
|
||||
@@ -744,21 +770,41 @@ 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) {
|
||||
// TODO PM-6675: Remove register call and service implementation
|
||||
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 = {
|
||||
@@ -784,10 +830,6 @@ class AuthRepositoryImpl(
|
||||
?: it.message,
|
||||
)
|
||||
}
|
||||
|
||||
is RegisterResponseJson.Error -> {
|
||||
RegisterResult.Error(it.message)
|
||||
}
|
||||
}
|
||||
},
|
||||
onFailure = { RegisterResult.Error(errorMessage = null) },
|
||||
@@ -806,7 +848,46 @@ class AuthRepositoryImpl(
|
||||
)
|
||||
}
|
||||
|
||||
@Suppress("ReturnCount")
|
||||
override suspend fun removePassword(masterPassword: String): RemovePasswordResult {
|
||||
val activeAccount = authDiskSource
|
||||
.userState
|
||||
?.activeAccount
|
||||
?: return RemovePasswordResult.Error
|
||||
val profile = activeAccount.profile
|
||||
val userId = profile.userId
|
||||
val userKey = authDiskSource
|
||||
.getUserKey(userId = userId)
|
||||
?: return RemovePasswordResult.Error
|
||||
val keyConnectorUrl = organizations
|
||||
.find {
|
||||
it.shouldUseKeyConnector &&
|
||||
it.type != OrganizationType.OWNER &&
|
||||
it.type != OrganizationType.ADMIN
|
||||
}
|
||||
?.keyConnectorUrl
|
||||
?: return RemovePasswordResult.Error
|
||||
return keyConnectorManager
|
||||
.migrateExistingUserToKeyConnector(
|
||||
userId = userId,
|
||||
url = keyConnectorUrl,
|
||||
userKeyEncrypted = userKey,
|
||||
email = profile.email,
|
||||
masterPassword = masterPassword,
|
||||
kdf = profile.toSdkParams(),
|
||||
)
|
||||
.onSuccess {
|
||||
authDiskSource.userState = authDiskSource
|
||||
.userState
|
||||
?.toRemovedPasswordUserStateJson(userId = userId)
|
||||
vaultRepository.sync()
|
||||
settingsRepository.setDefaultsIfNecessary(userId = userId)
|
||||
}
|
||||
.fold(
|
||||
onFailure = { RemovePasswordResult.Error },
|
||||
onSuccess = { RemovePasswordResult.Success },
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun resetPassword(
|
||||
currentPassword: String?,
|
||||
newPassword: String,
|
||||
@@ -954,9 +1035,9 @@ class AuthRepositoryImpl(
|
||||
)
|
||||
}
|
||||
|
||||
VaultUnlockResult.AuthenticationError,
|
||||
VaultUnlockResult.GenericError,
|
||||
is VaultUnlockResult.AuthenticationError,
|
||||
VaultUnlockResult.InvalidStateError,
|
||||
VaultUnlockResult.GenericError,
|
||||
-> {
|
||||
IllegalStateException("Failed to unlock vault").asFailure()
|
||||
}
|
||||
@@ -1059,7 +1140,6 @@ class AuthRepositoryImpl(
|
||||
onFailure = { PasswordStrengthResult.Error },
|
||||
)
|
||||
|
||||
@Suppress("ReturnCount")
|
||||
override suspend fun validatePassword(password: String): ValidatePasswordResult {
|
||||
val userId = activeUserId ?: return ValidatePasswordResult.Error
|
||||
return authDiskSource
|
||||
@@ -1104,12 +1184,79 @@ 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) }
|
||||
|
||||
@Suppress("CyclomaticComplexMethod", "ReturnCount")
|
||||
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,
|
||||
policy: PolicyInformation.MasterPassword,
|
||||
@@ -1317,7 +1464,65 @@ class AuthRepositoryImpl(
|
||||
previousUserState = authDiskSource.userState,
|
||||
environmentUrlData = environmentRepository.environment.environmentUrlData,
|
||||
)
|
||||
val userId = userStateJson.activeUserId
|
||||
val profile = userStateJson.activeAccount.profile
|
||||
val userId = profile.userId
|
||||
|
||||
checkForVaultUnlockError(
|
||||
onVaultUnlockError = { vaultUnlockError ->
|
||||
return@userStateTransaction vaultUnlockError.toLoginErrorResult()
|
||||
},
|
||||
) {
|
||||
val keyConnectorUrl = loginResponse
|
||||
.keyConnectorUrl
|
||||
?: loginResponse
|
||||
.userDecryptionOptions
|
||||
?.keyConnectorUserDecryptionOptions
|
||||
?.keyConnectorUrl
|
||||
val isDeviceUnlockAvailable = deviceData != null ||
|
||||
loginResponse.userDecryptionOptions?.trustedDeviceUserDecryptionOptions != null
|
||||
// if possible attempt to unlock the vault with trusted device data
|
||||
if (isDeviceUnlockAvailable) {
|
||||
unlockVaultWithTdeOnLoginSuccess(
|
||||
loginResponse = loginResponse,
|
||||
profile = profile,
|
||||
deviceData = deviceData,
|
||||
)
|
||||
} else if (keyConnectorUrl != null && orgIdentifier != null) {
|
||||
unlockVaultWithKeyConnectorOnLoginSuccess(
|
||||
profile = profile,
|
||||
keyConnectorUrl = keyConnectorUrl,
|
||||
orgIdentifier = orgIdentifier,
|
||||
loginResponse = loginResponse,
|
||||
)
|
||||
} else {
|
||||
unlockVaultWithPasswordOnLoginSuccess(
|
||||
loginResponse = loginResponse,
|
||||
profile = profile,
|
||||
password = password,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
password?.let {
|
||||
// Save the master password hash.
|
||||
authSdkSource
|
||||
.hashPassword(
|
||||
email = email,
|
||||
password = it,
|
||||
kdf = 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(
|
||||
@@ -1332,8 +1537,11 @@ class AuthRepositoryImpl(
|
||||
// when we completed the pending admin auth request.
|
||||
authDiskSource.storeUserKey(userId = userId, userKey = it)
|
||||
}
|
||||
authDiskSource.storePrivateKey(userId = userId, privateKey = loginResponse.privateKey)
|
||||
|
||||
loginResponse.privateKey?.let {
|
||||
// Only set the value if it's present, since we may have set it already
|
||||
// when we completed the key connector conversion.
|
||||
authDiskSource.storePrivateKey(userId = userId, privateKey = it)
|
||||
}
|
||||
// If the user just authenticated with a two-factor code and selected the option to
|
||||
// remember it, then the API response will return a token that will be used in place
|
||||
// of the two-factor code on the next login attempt.
|
||||
@@ -1346,162 +1554,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.
|
||||
*/
|
||||
@Suppress("ReturnCount")
|
||||
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.
|
||||
*/
|
||||
@@ -1527,6 +1590,259 @@ class AuthRepositoryImpl(
|
||||
return LoginResult.TwoFactorRequired
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempt to unlock the current user's vault with key connector data.
|
||||
*/
|
||||
private suspend fun unlockVaultWithKeyConnectorOnLoginSuccess(
|
||||
profile: AccountJson.Profile,
|
||||
keyConnectorUrl: String,
|
||||
orgIdentifier: String,
|
||||
loginResponse: GetTokenResponseJson.Success,
|
||||
): VaultUnlockResult? =
|
||||
if (loginResponse.userDecryptionOptions?.hasMasterPassword != false) {
|
||||
// This user has a master password, so we skip the key-connector logic as it is not
|
||||
// setup yet. The user can still unlock the vault with their master password.
|
||||
null
|
||||
} else if (loginResponse.key != null && loginResponse.privateKey != null) {
|
||||
// This is a returning user who should already have the key connector setup
|
||||
keyConnectorManager
|
||||
.getMasterKeyFromKeyConnector(
|
||||
url = keyConnectorUrl,
|
||||
accessToken = loginResponse.accessToken,
|
||||
)
|
||||
.map {
|
||||
unlockVault(
|
||||
accountProfile = profile,
|
||||
privateKey = loginResponse.privateKey,
|
||||
initUserCryptoMethod = InitUserCryptoMethod.KeyConnector(
|
||||
masterKey = it.masterKey,
|
||||
userKey = loginResponse.key,
|
||||
),
|
||||
)
|
||||
}
|
||||
.fold(
|
||||
// If the request failed, we want to abort the login process
|
||||
onFailure = { VaultUnlockResult.GenericError },
|
||||
onSuccess = { it },
|
||||
)
|
||||
} else {
|
||||
// This is a new user who needs to setup the key connector
|
||||
keyConnectorManager
|
||||
.migrateNewUserToKeyConnector(
|
||||
url = keyConnectorUrl,
|
||||
accessToken = loginResponse.accessToken,
|
||||
kdfType = loginResponse.kdfType,
|
||||
kdfIterations = loginResponse.kdfIterations,
|
||||
kdfMemory = loginResponse.kdfMemory,
|
||||
kdfParallelism = loginResponse.kdfParallelism,
|
||||
organizationIdentifier = orgIdentifier,
|
||||
)
|
||||
.map { keyConnectorResponse ->
|
||||
val result = unlockVault(
|
||||
accountProfile = profile,
|
||||
privateKey = keyConnectorResponse.keys.private,
|
||||
initUserCryptoMethod = InitUserCryptoMethod.KeyConnector(
|
||||
masterKey = keyConnectorResponse.masterKey,
|
||||
userKey = keyConnectorResponse.encryptedUserKey,
|
||||
),
|
||||
)
|
||||
if (result is VaultUnlockResult.Success) {
|
||||
// We now know that login/unlock was successful, so we store the userKey
|
||||
// and privateKey we now have since it didn't exist on the loginResponse
|
||||
authDiskSource.storeUserKey(
|
||||
userId = profile.userId,
|
||||
userKey = keyConnectorResponse.encryptedUserKey,
|
||||
)
|
||||
authDiskSource.storePrivateKey(
|
||||
userId = profile.userId,
|
||||
privateKey = keyConnectorResponse.keys.private,
|
||||
)
|
||||
}
|
||||
result
|
||||
}
|
||||
.fold(
|
||||
// If the request failed, we want to abort the login process
|
||||
onFailure = { VaultUnlockResult.GenericError },
|
||||
onSuccess = { it },
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempt to unlock the current user's vault with password data.
|
||||
*/
|
||||
private suspend fun unlockVaultWithPasswordOnLoginSuccess(
|
||||
loginResponse: GetTokenResponseJson.Success,
|
||||
profile: AccountJson.Profile,
|
||||
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 = 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,
|
||||
profile: AccountJson.Profile,
|
||||
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 = 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,
|
||||
profile = profile,
|
||||
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,
|
||||
profile: AccountJson.Profile,
|
||||
privateKey: String,
|
||||
): VaultUnlockResult? {
|
||||
var vaultUnlockResult: VaultUnlockResult? = null
|
||||
val userId = profile.userId
|
||||
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 = 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 = 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
|
||||
|
||||
/**
|
||||
|
||||
@@ -8,10 +8,12 @@ import com.x8bit.bitwarden.data.auth.datasource.network.service.IdentityService
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.service.OrganizationService
|
||||
import com.x8bit.bitwarden.data.auth.datasource.sdk.AuthSdkSource
|
||||
import com.x8bit.bitwarden.data.auth.manager.AuthRequestManager
|
||||
import com.x8bit.bitwarden.data.auth.manager.KeyConnectorManager
|
||||
import com.x8bit.bitwarden.data.auth.manager.TrustedDeviceManager
|
||||
import com.x8bit.bitwarden.data.auth.manager.UserLogoutManager
|
||||
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
|
||||
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
|
||||
@@ -34,7 +36,6 @@ object AuthRepositoryModule {
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
@Suppress("LongParameterList")
|
||||
fun providesAuthRepository(
|
||||
accountsService: AccountsService,
|
||||
devicesService: DevicesService,
|
||||
@@ -48,11 +49,13 @@ object AuthRepositoryModule {
|
||||
environmentRepository: EnvironmentRepository,
|
||||
settingsRepository: SettingsRepository,
|
||||
vaultRepository: VaultRepository,
|
||||
keyConnectorManager: KeyConnectorManager,
|
||||
authRequestManager: AuthRequestManager,
|
||||
trustedDeviceManager: TrustedDeviceManager,
|
||||
userLogoutManager: UserLogoutManager,
|
||||
pushManager: PushManager,
|
||||
policyManager: PolicyManager,
|
||||
featureFlagManager: FeatureFlagManager,
|
||||
): AuthRepository = AuthRepositoryImpl(
|
||||
accountsService = accountsService,
|
||||
devicesService = devicesService,
|
||||
@@ -66,10 +69,12 @@ object AuthRepositoryModule {
|
||||
environmentRepository = environmentRepository,
|
||||
settingsRepository = settingsRepository,
|
||||
vaultRepository = vaultRepository,
|
||||
keyConnectorManager = keyConnectorManager,
|
||||
authRequestManager = authRequestManager,
|
||||
trustedDeviceManager = trustedDeviceManager,
|
||||
userLogoutManager = userLogoutManager,
|
||||
pushManager = pushManager,
|
||||
policyManager = policyManager,
|
||||
featureFlagManager = featureFlagManager,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -37,4 +37,10 @@ data class JwtTokenDataJson(
|
||||
|
||||
@SerialName("amr")
|
||||
val authenticationMethodsReference: List<String>,
|
||||
)
|
||||
) {
|
||||
/**
|
||||
* Indicates that this is an external user. Mainly used for SSO users with a key connector.
|
||||
*/
|
||||
val isExternal: Boolean
|
||||
get() = authenticationMethodsReference.any { it == "external" }
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -1,12 +1,21 @@
|
||||
package com.x8bit.bitwarden.data.auth.repository.model
|
||||
|
||||
import com.x8bit.bitwarden.data.vault.datasource.network.model.OrganizationType
|
||||
|
||||
/**
|
||||
* Represents an organization a user may be a member of.
|
||||
*
|
||||
* @property id The ID of the organization.
|
||||
* @property name The name of the organization (if applicable).
|
||||
* @property shouldManageResetPassword Indicates that this user has the permission to manage their
|
||||
* own password.
|
||||
* @property shouldUseKeyConnector Indicates that the organization uses a key connector.
|
||||
* @property role The user's role in the organization.
|
||||
*/
|
||||
data class Organization(
|
||||
val id: String,
|
||||
val name: String?,
|
||||
val shouldManageResetPassword: Boolean,
|
||||
val shouldUseKeyConnector: Boolean,
|
||||
val role: OrganizationType,
|
||||
)
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
package com.x8bit.bitwarden.data.auth.repository.model
|
||||
|
||||
/**
|
||||
* Models result of removing a user's password.
|
||||
*/
|
||||
sealed class RemovePasswordResult {
|
||||
/**
|
||||
* The password was removed successfully.
|
||||
*/
|
||||
data object Success : RemovePasswordResult()
|
||||
|
||||
/**
|
||||
* There was an error removing the password.
|
||||
*/
|
||||
data object Error : RemovePasswordResult()
|
||||
}
|
||||
@@ -0,0 +1,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,9 @@
|
||||
package com.x8bit.bitwarden.data.auth.repository.model
|
||||
|
||||
/**
|
||||
* Associates [isUsingKeyConnector] with the given [userId].
|
||||
*/
|
||||
data class UserKeyConnectorState(
|
||||
val userId: String,
|
||||
val isUsingKeyConnector: Boolean?,
|
||||
)
|
||||
@@ -45,10 +45,12 @@ data class UserState(
|
||||
* they logged in using SSO and don't yet have one). NOTE: This should **not** be used to
|
||||
* determine whether a user has a master password. There are cases in which a user can both
|
||||
* not have a password but still not need one, such as TDE.
|
||||
* @property hasMasterPassword Indicates that the user does or does not have a master password.
|
||||
* @property organizations List of [Organization]s the user is associated with, if any.
|
||||
* @property isBiometricsEnabled Indicates that the biometrics mechanism for unlocking the
|
||||
* user's vault is enabled.
|
||||
* @property vaultUnlockType The mechanism by which the user's vault may be unlocked.
|
||||
* @property isUsingKeyConnector Indicates if the account is currently using a key connector.
|
||||
*/
|
||||
data class Account(
|
||||
val userId: String,
|
||||
@@ -61,16 +63,13 @@ data class UserState(
|
||||
val isVaultUnlocked: Boolean,
|
||||
val needsPasswordReset: Boolean,
|
||||
val needsMasterPassword: Boolean,
|
||||
val hasMasterPassword: Boolean,
|
||||
val trustedDevice: TrustedDevice?,
|
||||
val organizations: List<Organization>,
|
||||
val isBiometricsEnabled: Boolean,
|
||||
val vaultUnlockType: VaultUnlockType = VaultUnlockType.MASTER_PASSWORD,
|
||||
val isUsingKeyConnector: Boolean,
|
||||
) {
|
||||
/**
|
||||
* Indicates that the user does or does not have a master password.
|
||||
*/
|
||||
val hasMasterPassword: Boolean get() = trustedDevice?.hasMasterPassword != false
|
||||
|
||||
/**
|
||||
* Indicates that the user does or does not have a means to manually unlock the vault.
|
||||
*/
|
||||
@@ -86,7 +85,6 @@ data class UserState(
|
||||
*/
|
||||
data class TrustedDevice(
|
||||
val isDeviceTrusted: Boolean,
|
||||
val hasMasterPassword: Boolean,
|
||||
val hasAdminApproval: Boolean,
|
||||
val hasLoginApprovingDevice: Boolean,
|
||||
val hasResetPasswordPermission: Boolean,
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
@@ -2,6 +2,7 @@ package com.x8bit.bitwarden.data.auth.repository.util
|
||||
|
||||
import com.x8bit.bitwarden.data.auth.datasource.disk.AuthDiskSource
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.UserAccountTokens
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.UserKeyConnectorState
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.UserOrganizations
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.UserSwitchingData
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
@@ -100,6 +101,47 @@ val AuthDiskSource.userAccountTokensFlow: Flow<List<UserAccountTokens>>
|
||||
}
|
||||
.distinctUntilChanged()
|
||||
|
||||
/**
|
||||
* Returns the current list of [UserKeyConnectorState].
|
||||
*/
|
||||
val AuthDiskSource.userKeyConnectorStateList: List<UserKeyConnectorState>
|
||||
get() = this
|
||||
.userState
|
||||
?.accounts
|
||||
.orEmpty()
|
||||
.map { (userId, _) ->
|
||||
UserKeyConnectorState(
|
||||
userId = userId,
|
||||
isUsingKeyConnector = this.getShouldUseKeyConnector(userId = userId),
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a [Flow] that emits distinct updates to [UserKeyConnectorState].
|
||||
*/
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
val AuthDiskSource.userKeyConnectorStateFlow: Flow<List<UserKeyConnectorState>>
|
||||
get() = this
|
||||
.userStateFlow
|
||||
.flatMapLatest { userStateJson ->
|
||||
combine(
|
||||
userStateJson
|
||||
?.accounts
|
||||
.orEmpty()
|
||||
.map { (userId, _) ->
|
||||
this
|
||||
.getShouldUseKeyConnectorFlow(userId = userId)
|
||||
.map {
|
||||
UserKeyConnectorState(
|
||||
userId = userId,
|
||||
isUsingKeyConnector = it,
|
||||
)
|
||||
}
|
||||
},
|
||||
) { it.toList() }
|
||||
}
|
||||
.distinctUntilChanged()
|
||||
|
||||
/**
|
||||
* Returns a [Flow] that emits every time the active user is changed.
|
||||
*/
|
||||
|
||||
@@ -17,7 +17,7 @@ private val json: Json by lazy {
|
||||
/**
|
||||
* Parses a [JwtTokenDataJson] from the given [jwtToken], or `null` if this parsing is not possible.
|
||||
*/
|
||||
@Suppress("MagicNumber", "ReturnCount")
|
||||
@Suppress("MagicNumber")
|
||||
fun parseJwtTokenDataOrNull(jwtToken: String): JwtTokenDataJson? {
|
||||
val parts = jwtToken.split(".")
|
||||
if (parts.size != 3) return null
|
||||
|
||||
@@ -1,39 +0,0 @@
|
||||
package com.x8bit.bitwarden.data.auth.repository.util
|
||||
|
||||
import com.x8bit.bitwarden.data.auth.datasource.disk.model.UserStateJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.RefreshTokenResponseJson
|
||||
|
||||
/**
|
||||
* Converts the given [RefreshTokenResponseJson] to a [UserStateJson], given the following
|
||||
* additional information:
|
||||
*
|
||||
* - the [userId]
|
||||
* - the [previousUserState]
|
||||
*/
|
||||
fun RefreshTokenResponseJson.toUserStateJson(
|
||||
userId: String,
|
||||
previousUserState: UserStateJson,
|
||||
): UserStateJson {
|
||||
val refreshedAccount = requireNotNull(previousUserState.accounts[userId])
|
||||
val accessToken = this.accessToken
|
||||
val jwtTokenData = requireNotNull(parseJwtTokenDataOrNull(jwtToken = accessToken))
|
||||
|
||||
val account = refreshedAccount.copy(
|
||||
profile = refreshedAccount.profile.copy(
|
||||
userId = jwtTokenData.userId,
|
||||
email = jwtTokenData.email,
|
||||
isEmailVerified = jwtTokenData.isEmailVerified,
|
||||
name = jwtTokenData.name,
|
||||
),
|
||||
)
|
||||
|
||||
// Update the existing UserState.
|
||||
return previousUserState.copy(
|
||||
accounts = previousUserState
|
||||
.accounts
|
||||
.toMutableMap()
|
||||
.apply {
|
||||
put(userId, account)
|
||||
},
|
||||
)
|
||||
}
|
||||
@@ -14,6 +14,9 @@ fun SyncResponseJson.Profile.Organization.toOrganization(): Organization =
|
||||
Organization(
|
||||
id = this.id,
|
||||
name = this.name,
|
||||
shouldUseKeyConnector = this.shouldUseKeyConnector,
|
||||
role = this.type,
|
||||
shouldManageResetPassword = this.permissions.shouldManageResetPassword,
|
||||
)
|
||||
|
||||
/**
|
||||
|
||||
@@ -3,21 +3,48 @@ package com.x8bit.bitwarden.data.auth.repository.util
|
||||
import com.x8bit.bitwarden.data.auth.datasource.disk.model.UserStateJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.UserDecryptionOptionsJson
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.UserAccountTokens
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.UserKeyConnectorState
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.UserOrganizations
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.UserState
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.VaultUnlockType
|
||||
import com.x8bit.bitwarden.data.platform.repository.util.toEnvironmentUrlsOrDefault
|
||||
import com.x8bit.bitwarden.data.vault.datasource.network.model.OrganizationType
|
||||
import com.x8bit.bitwarden.data.vault.datasource.network.model.SyncResponseJson
|
||||
import com.x8bit.bitwarden.data.vault.repository.model.VaultUnlockData
|
||||
import com.x8bit.bitwarden.data.vault.repository.util.statusFor
|
||||
import com.x8bit.bitwarden.ui.platform.base.util.toHexColorRepresentation
|
||||
|
||||
/**
|
||||
* Updates the given [UserStateJson] with the data to indicate that the password has been removed.
|
||||
* The original will be returned if the [userId] does not match any accounts in the [UserStateJson].
|
||||
*/
|
||||
fun UserStateJson.toRemovedPasswordUserStateJson(
|
||||
userId: String,
|
||||
): UserStateJson {
|
||||
val account = this.accounts[userId] ?: return this
|
||||
val profile = account.profile
|
||||
val updatedUserDecryptionOptions = profile
|
||||
.userDecryptionOptions
|
||||
?.copy(hasMasterPassword = false)
|
||||
?: UserDecryptionOptionsJson(
|
||||
hasMasterPassword = false,
|
||||
trustedDeviceUserDecryptionOptions = null,
|
||||
keyConnectorUserDecryptionOptions = null,
|
||||
)
|
||||
val updatedProfile = profile.copy(userDecryptionOptions = updatedUserDecryptionOptions)
|
||||
val updatedAccount = account.copy(profile = updatedProfile)
|
||||
return this.copy(
|
||||
accounts = accounts
|
||||
.toMutableMap()
|
||||
.apply { replace(userId, updatedAccount) },
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the given [UserStateJson] with the data from the [syncResponse] to return a new
|
||||
* [UserStateJson]. The original will be returned if the sync response does not match any accounts
|
||||
* in the [UserStateJson].
|
||||
*/
|
||||
@Suppress("ReturnCount")
|
||||
fun UserStateJson.toUpdatedUserStateJson(
|
||||
syncResponse: SyncResponseJson,
|
||||
): UserStateJson {
|
||||
@@ -75,11 +102,12 @@ fun UserStateJson.toUserStateJsonWithPassword(): UserStateJson {
|
||||
/**
|
||||
* Converts the given [UserStateJson] to a [UserState] using the given [vaultState].
|
||||
*/
|
||||
@Suppress("LongParameterList")
|
||||
@Suppress("LongParameterList", "LongMethod")
|
||||
fun UserStateJson.toUserState(
|
||||
vaultState: List<VaultUnlockData>,
|
||||
userAccountTokens: List<UserAccountTokens>,
|
||||
userOrganizationsList: List<UserOrganizations>,
|
||||
userIsUsingKeyConnectorList: List<UserKeyConnectorState>,
|
||||
hasPendingAccountAddition: Boolean,
|
||||
isBiometricsEnabledProvider: (userId: String) -> Boolean,
|
||||
vaultUnlockTypeProvider: (userId: String) -> VaultUnlockType,
|
||||
@@ -98,13 +126,21 @@ fun UserStateJson.toUserState(
|
||||
val decryptionOptions = profile.userDecryptionOptions
|
||||
val trustedDeviceOptions = decryptionOptions?.trustedDeviceUserDecryptionOptions
|
||||
val keyConnectorOptions = decryptionOptions?.keyConnectorUserDecryptionOptions
|
||||
val organizations = userOrganizationsList
|
||||
.find { it.userId == userId }
|
||||
?.organizations
|
||||
.orEmpty()
|
||||
val hasManageResetPasswordPermission = organizations.any {
|
||||
it.role == OrganizationType.OWNER ||
|
||||
it.role == OrganizationType.ADMIN ||
|
||||
it.shouldManageResetPassword
|
||||
}
|
||||
val needsMasterPassword = decryptionOptions?.hasMasterPassword == false &&
|
||||
trustedDeviceOptions?.hasManageResetPasswordPermission != false &&
|
||||
hasManageResetPasswordPermission &&
|
||||
keyConnectorOptions == null
|
||||
val trustedDevice = trustedDeviceOptions?.let {
|
||||
UserState.TrustedDevice(
|
||||
isDeviceTrusted = isDeviceTrustedProvider(userId),
|
||||
hasMasterPassword = decryptionOptions.hasMasterPassword,
|
||||
hasAdminApproval = it.hasAdminApproval,
|
||||
hasLoginApprovingDevice = it.hasLoginApprovingDevice,
|
||||
hasResetPasswordPermission = it.hasManageResetPasswordPermission,
|
||||
@@ -126,14 +162,15 @@ fun UserStateJson.toUserState(
|
||||
?.isLoggedIn == true,
|
||||
isVaultUnlocked = vaultUnlocked,
|
||||
needsPasswordReset = needsPasswordReset,
|
||||
organizations = userOrganizationsList
|
||||
.find { it.userId == userId }
|
||||
?.organizations
|
||||
.orEmpty(),
|
||||
organizations = organizations,
|
||||
isBiometricsEnabled = isBiometricsEnabledProvider(userId),
|
||||
vaultUnlockType = vaultUnlockTypeProvider(userId),
|
||||
needsMasterPassword = needsMasterPassword,
|
||||
hasMasterPassword = decryptionOptions?.hasMasterPassword != false,
|
||||
trustedDevice = trustedDevice,
|
||||
isUsingKeyConnector = userIsUsingKeyConnectorList
|
||||
.find { it.userId == userId }
|
||||
?.isUsingKeyConnector == true,
|
||||
)
|
||||
},
|
||||
hasPendingAccountAddition = hasPendingAccountAddition,
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
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(
|
||||
oldValue = "/redirect-connector.html#",
|
||||
newValue = "/",
|
||||
ignoreCase = true,
|
||||
)
|
||||
val uri = runCatching { Uri.parse(sanitizedUriString) }.getOrNull() ?: return null
|
||||
uri.host ?: return null
|
||||
if (uri.path != "/finish-signup") return null
|
||||
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,
|
||||
)
|
||||
}
|
||||
@@ -10,6 +10,22 @@ import com.x8bit.bitwarden.data.autofill.model.FilledPartition
|
||||
import com.x8bit.bitwarden.data.autofill.provider.AutofillCipherProvider
|
||||
import com.x8bit.bitwarden.data.autofill.util.buildFilledItemOrNull
|
||||
|
||||
/**
|
||||
* The maximum amount of filled partitions the user will see. Viewing the rest will require opening
|
||||
* the vault.
|
||||
*
|
||||
* Note: The vault item is not included in this count.
|
||||
*/
|
||||
private const val MAX_FILLED_PARTITIONS_COUNT: Int = 20
|
||||
|
||||
/**
|
||||
* The maximum amount of inline suggestions the user will see. Viewing the rest will require
|
||||
* opening the vault.
|
||||
*
|
||||
* Note: The vault item is not included in this count.
|
||||
*/
|
||||
private const val MAX_INLINE_SUGGESTION_COUNT: Int = 5
|
||||
|
||||
/**
|
||||
* The default [FilledDataBuilder]. This converts parsed autofill data into filled data that is
|
||||
* ready to be loaded into an autofill response.
|
||||
@@ -21,7 +37,9 @@ class FilledDataBuilderImpl(
|
||||
val isVaultLocked = autofillCipherProvider.isVaultLocked()
|
||||
|
||||
// Subtract one to make sure there is space for the vault item.
|
||||
val maxCipherInlineSuggestionsCount = autofillRequest.maxInlineSuggestionsCount - 1
|
||||
val maxCipherInlineSuggestionsCount = (autofillRequest.maxInlineSuggestionsCount - 1)
|
||||
.coerceAtMost(maximumValue = MAX_INLINE_SUGGESTION_COUNT)
|
||||
|
||||
// Track the number of inline suggestions that have been added.
|
||||
var inlineSuggestionsAdded = 0
|
||||
|
||||
@@ -76,7 +94,7 @@ class FilledDataBuilderImpl(
|
||||
?.getOrLastOrNull(inlineSuggestionsAdded)
|
||||
|
||||
return FilledData(
|
||||
filledPartitions = filledPartitions,
|
||||
filledPartitions = filledPartitions.take(n = MAX_FILLED_PARTITIONS_COUNT),
|
||||
ignoreAutofillIds = autofillRequest.ignoreAutofillIds,
|
||||
originalPartition = autofillRequest.partition,
|
||||
uri = autofillRequest.uri,
|
||||
|
||||
@@ -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(),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -3,15 +3,17 @@ package com.x8bit.bitwarden.data.autofill.fido2.di
|
||||
import android.content.Context
|
||||
import android.os.Build
|
||||
import androidx.annotation.RequiresApi
|
||||
import com.bitwarden.sdk.Fido2CredentialStore
|
||||
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
|
||||
import com.x8bit.bitwarden.data.autofill.fido2.datasource.network.service.DigitalAssetLinkService
|
||||
import com.x8bit.bitwarden.data.autofill.fido2.manager.Fido2CredentialManager
|
||||
import com.x8bit.bitwarden.data.autofill.fido2.manager.Fido2CredentialManagerImpl
|
||||
import com.x8bit.bitwarden.data.autofill.fido2.processor.Fido2ProviderProcessor
|
||||
import com.x8bit.bitwarden.data.autofill.fido2.processor.Fido2ProviderProcessorImpl
|
||||
import com.x8bit.bitwarden.data.platform.annotation.OmitFromCoverage
|
||||
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
|
||||
@@ -19,12 +21,12 @@ import dagger.hilt.InstallIn
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import kotlinx.serialization.json.Json
|
||||
import java.time.Clock
|
||||
import javax.inject.Singleton
|
||||
|
||||
/**
|
||||
* Provides dependencies within the fido2 package.
|
||||
*/
|
||||
@OmitFromCoverage
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
object Fido2ProviderModule {
|
||||
@@ -35,13 +37,21 @@ object Fido2ProviderModule {
|
||||
fun provideCredentialProviderProcessor(
|
||||
@ApplicationContext context: Context,
|
||||
authRepository: AuthRepository,
|
||||
vaultRepository: VaultRepository,
|
||||
fido2CredentialStore: Fido2CredentialStore,
|
||||
fido2CredentialManager: Fido2CredentialManager,
|
||||
dispatcherManager: DispatcherManager,
|
||||
intentManager: IntentManager,
|
||||
clock: Clock,
|
||||
): Fido2ProviderProcessor =
|
||||
Fido2ProviderProcessorImpl(
|
||||
context,
|
||||
authRepository,
|
||||
vaultRepository,
|
||||
fido2CredentialStore,
|
||||
fido2CredentialManager,
|
||||
intentManager,
|
||||
clock,
|
||||
dispatcherManager,
|
||||
)
|
||||
|
||||
@@ -50,11 +60,15 @@ object Fido2ProviderModule {
|
||||
fun provideFido2CredentialManager(
|
||||
assetManager: AssetManager,
|
||||
digitalAssetLinkService: DigitalAssetLinkService,
|
||||
vaultSdkSource: VaultSdkSource,
|
||||
fido2CredentialStore: Fido2CredentialStore,
|
||||
json: Json,
|
||||
): Fido2CredentialManager =
|
||||
Fido2CredentialManagerImpl(
|
||||
assetManager = assetManager,
|
||||
digitalAssetLinkService = digitalAssetLinkService,
|
||||
vaultSdkSource = vaultSdkSource,
|
||||
fido2CredentialStore = fido2CredentialStore,
|
||||
json = json,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,15 +1,29 @@
|
||||
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.Fido2CreateCredentialResult
|
||||
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 creation and authentication.
|
||||
* 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].
|
||||
@@ -19,18 +33,39 @@ 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 create a FIDO2 credential from the given [credentialRequest] and associate it to
|
||||
* the given [cipherView].
|
||||
* Attempt to extract FIDO 2 passkey assertion options from the system [requestJson], or null.
|
||||
*/
|
||||
fun createCredentialForCipher(
|
||||
credentialRequest: Fido2CredentialRequest,
|
||||
cipherView: CipherView,
|
||||
): Fido2CreateCredentialResult
|
||||
fun getPasskeyAssertionOptionsOrNull(
|
||||
requestJson: String,
|
||||
): PasskeyAssertionOptions?
|
||||
|
||||
/**
|
||||
* Register a new FIDO 2 credential to a users vault.
|
||||
*/
|
||||
suspend fun registerFido2Credential(
|
||||
userId: String,
|
||||
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
|
||||
}
|
||||
|
||||
@@ -1,21 +1,33 @@
|
||||
package com.x8bit.bitwarden.data.autofill.fido2.manager
|
||||
|
||||
import androidx.credentials.exceptions.CreateCredentialUnknownException
|
||||
import androidx.credentials.provider.CallingAppInfo
|
||||
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.Fido2CreateCredentialResult
|
||||
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
|
||||
import com.x8bit.bitwarden.data.platform.util.flatMap
|
||||
import com.x8bit.bitwarden.data.platform.util.getCallingAppApkFingerprint
|
||||
import com.x8bit.bitwarden.data.platform.util.getAppOrigin
|
||||
import com.x8bit.bitwarden.data.platform.util.getAppSigningSignatureFingerprint
|
||||
import com.x8bit.bitwarden.data.platform.util.getSignatureFingerprintAsHexString
|
||||
import com.x8bit.bitwarden.data.platform.util.validatePrivilegedApp
|
||||
import com.x8bit.bitwarden.data.vault.datasource.sdk.VaultSdkSource
|
||||
import com.x8bit.bitwarden.data.vault.datasource.sdk.model.AuthenticateFido2CredentialRequest
|
||||
import com.x8bit.bitwarden.data.vault.datasource.sdk.model.RegisterFido2CredentialRequest
|
||||
import com.x8bit.bitwarden.data.vault.datasource.sdk.util.toAndroidAttestationResponse
|
||||
import com.x8bit.bitwarden.data.vault.datasource.sdk.util.toAndroidFido2PublicKeyCredential
|
||||
import kotlinx.serialization.SerializationException
|
||||
import kotlinx.serialization.encodeToString
|
||||
import kotlinx.serialization.json.Json
|
||||
|
||||
private const val ALLOW_LIST_FILE_NAME = "fido2_privileged_allow_list.json"
|
||||
@@ -23,11 +35,64 @@ 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,
|
||||
private val vaultSdkSource: VaultSdkSource,
|
||||
private val fido2CredentialStore: Fido2CredentialStore,
|
||||
private val json: Json,
|
||||
) : Fido2CredentialManager {
|
||||
) : Fido2CredentialManager,
|
||||
Fido2CredentialStore by fido2CredentialStore {
|
||||
|
||||
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
|
||||
.callingAppInfo
|
||||
.packageName,
|
||||
)
|
||||
}
|
||||
val origin = fido2CredentialRequest
|
||||
.origin
|
||||
?: getOriginUrlFromAttestationOptionsOrNull(fido2CredentialRequest.requestJson)
|
||||
?: return Fido2RegisterCredentialResult.Error
|
||||
|
||||
return vaultSdkSource
|
||||
.registerFido2Credential(
|
||||
request = RegisterFido2CredentialRequest(
|
||||
userId = userId,
|
||||
origin = origin,
|
||||
requestJson = """{"publicKey": ${fido2CredentialRequest.requestJson}}""",
|
||||
clientData = clientData,
|
||||
selectedCipherView = selectedCipherView,
|
||||
// User verification is handled prior to engaging the SDK. We always respond
|
||||
// `true` so that the SDK does not fail if the relying party requests UV.
|
||||
isUserVerificationSupported = true,
|
||||
),
|
||||
fido2CredentialStore = this,
|
||||
)
|
||||
.map { it.toAndroidAttestationResponse() }
|
||||
.mapCatching { json.encodeToString(it) }
|
||||
.fold(
|
||||
onSuccess = { Fido2RegisterCredentialResult.Success(it) },
|
||||
onFailure = { Fido2RegisterCredentialResult.Error },
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun validateOrigin(
|
||||
fido2CredentialRequest: Fido2CredentialRequest,
|
||||
@@ -40,18 +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
|
||||
}
|
||||
|
||||
@Suppress("ReturnCount")
|
||||
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 {
|
||||
@@ -73,10 +181,13 @@ class Fido2CredentialManagerImpl(
|
||||
?: return Fido2ValidateOriginResult.Error.ApplicationNotFound
|
||||
}
|
||||
.map { matchingStatements ->
|
||||
matchingStatements
|
||||
.filterMatchingAppSignaturesOrNull(
|
||||
signature = callingAppInfo.getCallingAppApkFingerprint(),
|
||||
)
|
||||
callingAppInfo.getSignatureFingerprintAsHexString()
|
||||
?.let { certificateFingerprint ->
|
||||
matchingStatements
|
||||
.filterMatchingAppSignaturesOrNull(
|
||||
signature = certificateFingerprint,
|
||||
)
|
||||
}
|
||||
?: return Fido2ValidateOriginResult.Error.ApplicationNotVerified
|
||||
}
|
||||
.fold(
|
||||
@@ -104,14 +215,6 @@ class Fido2CredentialManagerImpl(
|
||||
onFailure = { Fido2ValidateOriginResult.Error.Unknown },
|
||||
)
|
||||
|
||||
override fun createCredentialForCipher(
|
||||
credentialRequest: Fido2CredentialRequest,
|
||||
cipherView: CipherView,
|
||||
): Fido2CreateCredentialResult {
|
||||
// TODO [PM-8137]: Create and save passkey to cipher.
|
||||
return Fido2CreateCredentialResult.Error(CreateCredentialUnknownException())
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns statements targeting the calling Android application, or null.
|
||||
*/
|
||||
@@ -147,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()
|
||||
@@ -157,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,64 @@
|
||||
package com.x8bit.bitwarden.data.autofill.fido2.model
|
||||
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
/**
|
||||
* Represents the authenticator's response to a client’s request for the creation of a new public
|
||||
* key credential.
|
||||
*
|
||||
* Refer to https://w3c.github.io/webauthn/#iface-authenticatorattestationresponse for details.
|
||||
*/
|
||||
@Serializable
|
||||
data class Fido2AttestationResponse(
|
||||
@SerialName("id")
|
||||
val id: String,
|
||||
@SerialName("type")
|
||||
val type: String,
|
||||
@SerialName("rawId")
|
||||
val rawId: String,
|
||||
@SerialName("response")
|
||||
val response: RegistrationResponse,
|
||||
@SerialName("clientExtensionResults")
|
||||
val clientExtensionResults: ClientExtensionResults?,
|
||||
@SerialName("authenticatorAttachment")
|
||||
val authenticatorAttachment: String?,
|
||||
) {
|
||||
/**
|
||||
* Represents the registration result data expected from a FIDO2 credential registration
|
||||
* request.
|
||||
*/
|
||||
@Serializable
|
||||
data class RegistrationResponse(
|
||||
@SerialName("clientDataJSON")
|
||||
val clientDataJson: String,
|
||||
@SerialName("attestationObject")
|
||||
val attestationObject: String,
|
||||
@SerialName("transports")
|
||||
val transports: List<String>?,
|
||||
@SerialName("publicKeyAlgorithm")
|
||||
val publicKeyAlgorithm: Long,
|
||||
@SerialName("publicKey")
|
||||
val publicKey: String?,
|
||||
@SerialName("authenticatorData")
|
||||
val authenticatorData: String?,
|
||||
)
|
||||
|
||||
/**
|
||||
* Represents an extension processing result produced by the client.
|
||||
*/
|
||||
@Serializable
|
||||
data class ClientExtensionResults(
|
||||
@SerialName("credProps")
|
||||
val credentialProperties: CredentialProperties,
|
||||
) {
|
||||
/**
|
||||
* Represents properties for newly created credential.
|
||||
*/
|
||||
@Serializable
|
||||
data class CredentialProperties(
|
||||
@SerialName("rk")
|
||||
val residentKey: Boolean,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
package com.x8bit.bitwarden.data.autofill.fido2.model
|
||||
|
||||
import androidx.credentials.exceptions.CreateCredentialException
|
||||
|
||||
/**
|
||||
* Models the data returned from creating a FIDO 2 credential.
|
||||
*/
|
||||
sealed class Fido2CreateCredentialResult {
|
||||
|
||||
/**
|
||||
* Models a successful response for creating a credential.
|
||||
*/
|
||||
data class Success(
|
||||
val registrationResponse: String,
|
||||
) : Fido2CreateCredentialResult()
|
||||
|
||||
/**
|
||||
* Models an error response for creating a credential.
|
||||
*/
|
||||
data class Error(
|
||||
val exception: CreateCredentialException,
|
||||
) : Fido2CreateCredentialResult()
|
||||
}
|
||||
@@ -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,24 @@
|
||||
package com.x8bit.bitwarden.data.autofill.fido2.model
|
||||
|
||||
/**
|
||||
* Models the data returned from creating a FIDO 2 credential.
|
||||
*/
|
||||
sealed class Fido2RegisterCredentialResult {
|
||||
|
||||
/**
|
||||
* Indicates the credential has been successfully registered.
|
||||
*/
|
||||
data class Success(
|
||||
val registrationResponse: String,
|
||||
) : Fido2RegisterCredentialResult()
|
||||
|
||||
/**
|
||||
* Indicates there was an error and the credential was not registered.
|
||||
*/
|
||||
data object Error : Fido2RegisterCredentialResult()
|
||||
|
||||
/**
|
||||
* Indicates the user cancelled the request.
|
||||
*/
|
||||
data object Cancelled : Fido2RegisterCredentialResult()
|
||||
}
|
||||
@@ -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")
|
||||
@@ -30,7 +30,9 @@ data class PublicKeyCredentialCreationOptions(
|
||||
@SerialName("authenticatorAttachment")
|
||||
val authenticatorAttachment: AuthenticatorAttachment? = null,
|
||||
@SerialName("residentKey")
|
||||
val residentKey: ResidentKeyRequirement? = null,
|
||||
val residentKeyRequirement: ResidentKeyRequirement? = null,
|
||||
@SerialName("userVerification")
|
||||
val userVerification: UserVerificationRequirement = UserVerificationRequirement.PREFERRED,
|
||||
) {
|
||||
/**
|
||||
* Enum class representing the types of attachments associated with selection criteria.
|
||||
@@ -50,32 +52,19 @@ data class PublicKeyCredentialCreationOptions(
|
||||
@Serializable
|
||||
enum class ResidentKeyRequirement {
|
||||
/**
|
||||
* User verification is preferred during selection, if supported.
|
||||
* Resident keys are preferred during selection, if supported.
|
||||
*/
|
||||
@SerialName("preferred")
|
||||
PREFERRED,
|
||||
|
||||
/**
|
||||
* User verification is required during selection.
|
||||
* Resident keys are required during selection.
|
||||
*/
|
||||
@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,36 +10,55 @@ 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.time.Clock
|
||||
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,
|
||||
private val clock: Clock,
|
||||
dispatcherManager: DispatcherManager,
|
||||
) : Fido2ProviderProcessor {
|
||||
|
||||
@@ -51,22 +70,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(
|
||||
@@ -81,7 +101,6 @@ class Fido2ProviderProcessorImpl(
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("ReturnCount")
|
||||
private fun handleCreatePasskeyQuery(
|
||||
request: BeginCreatePublicKeyCredentialRequest,
|
||||
): BeginCreateCredentialResponse? {
|
||||
@@ -94,13 +113,14 @@ class Fido2ProviderProcessorImpl(
|
||||
val userState = authRepository.userStateFlow.value ?: return null
|
||||
|
||||
return BeginCreateCredentialResponse.Builder()
|
||||
.setCreateEntries(userState.accounts.toCreateEntries())
|
||||
.setCreateEntries(userState.accounts.toCreateEntries(userState.activeUserId))
|
||||
.build()
|
||||
}
|
||||
|
||||
private fun List<UserState.Account>.toCreateEntries() = map { it.toCreateEntry() }
|
||||
private fun List<UserState.Account>.toCreateEntries(activeUserId: String) =
|
||||
map { it.toCreateEntry(isActive = activeUserId == it.userId) }
|
||||
|
||||
private fun UserState.Account.toCreateEntry(): CreateEntry {
|
||||
private fun UserState.Account.toCreateEntry(isActive: Boolean): CreateEntry {
|
||||
val accountName = name ?: email
|
||||
return CreateEntry
|
||||
.Builder(
|
||||
@@ -117,6 +137,9 @@ class Fido2ProviderProcessorImpl(
|
||||
accountName,
|
||||
),
|
||||
)
|
||||
// Set the last used time to "now" so the active account is the default option in the
|
||||
// system prompt.
|
||||
.setLastUsedTime(if (isActive) clock.instant() else null)
|
||||
.build()
|
||||
}
|
||||
|
||||
@@ -125,10 +148,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,27 +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.
|
||||
*/
|
||||
@Suppress("ReturnCount")
|
||||
@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
|
||||
@@ -36,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,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -107,10 +107,11 @@ class AutofillCompletionManagerImpl(
|
||||
cipherView: CipherView,
|
||||
) {
|
||||
val isPremium = authRepository.userStateFlow.value?.activeAccount?.isPremium == true
|
||||
val totpAvailableViaPremiumOrOrganization = isPremium || cipherView.organizationUseTotp
|
||||
val totpCode = cipherView.login?.totp
|
||||
val isTotpDisabled = settingsRepository.isAutoCopyTotpDisabled
|
||||
|
||||
if (!isTotpDisabled && isPremium && totpCode != null) {
|
||||
if (!isTotpDisabled && totpAvailableViaPremiumOrOrganization && totpCode != null) {
|
||||
val totpResult = vaultRepository.generateTotp(
|
||||
time = DateTime.now(),
|
||||
totpCode = totpCode,
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
/**
|
||||
@@ -86,4 +88,11 @@ sealed class AutofillView {
|
||||
override val data: Data,
|
||||
) : Login()
|
||||
}
|
||||
|
||||
/**
|
||||
* A view that is an input field but does not correspond to any known autofill field.
|
||||
*/
|
||||
data class Unused(
|
||||
override val data: Data,
|
||||
) : AutofillView()
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
@@ -105,6 +117,12 @@ class AutofillParserImpl(
|
||||
views = autofillViews.filterIsInstance<AutofillView.Login>(),
|
||||
)
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
// Flatten the ignorable autofill ids.
|
||||
val ignoreAutofillIds = traversalDataList
|
||||
@@ -139,7 +157,82 @@ class AutofillParserImpl(
|
||||
private fun AssistStructure.traverse(): List<ViewNodeTraversalData> =
|
||||
(0 until windowNodeCount)
|
||||
.map { getWindowNodeAt(it) }
|
||||
.mapNotNull { windowNode -> windowNode.rootViewNode?.traverse() }
|
||||
.mapNotNull { windowNode ->
|
||||
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
|
||||
* fields but no username fields, we check to see if there are any unused fields directly above
|
||||
* the password fields and we assume that those are the missing username fields.
|
||||
*/
|
||||
private fun ViewNodeTraversalData.updateForMissingUsernameFields(): ViewNodeTraversalData {
|
||||
val passwordPositions = this.autofillViews.mapIndexedNotNull { index, autofillView ->
|
||||
(autofillView as? AutofillView.Login.Password)?.let { index }
|
||||
}
|
||||
return if (passwordPositions.any() &&
|
||||
this.autofillViews.none { it is AutofillView.Login.Username }
|
||||
) {
|
||||
this.copyAndMapAutofillViews { index, autofillView ->
|
||||
if (autofillView is AutofillView.Unused && passwordPositions.contains(index + 1)) {
|
||||
AutofillView.Login.Username(data = autofillView.data)
|
||||
} else {
|
||||
autofillView
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// We already have username fields available or there are no password fields, so no need
|
||||
// to search for them.
|
||||
this
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
|
||||
@@ -13,12 +13,13 @@ 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
|
||||
import com.x8bit.bitwarden.data.vault.datasource.network.model.PolicyTypeJson
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.cancel
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
/**
|
||||
@@ -34,6 +35,7 @@ class AutofillProcessorImpl(
|
||||
private val parser: AutofillParser,
|
||||
private val saveInfoBuilder: SaveInfoBuilder,
|
||||
private val settingsRepository: SettingsRepository,
|
||||
private val crashLogsManager: CrashLogsManager,
|
||||
) : AutofillProcessor {
|
||||
|
||||
/**
|
||||
@@ -41,6 +43,11 @@ class AutofillProcessorImpl(
|
||||
*/
|
||||
private val scope: CoroutineScope = CoroutineScope(dispatcherManager.unconfined)
|
||||
|
||||
/**
|
||||
* The job being used to process the fill request.
|
||||
*/
|
||||
private var job: Job = Job().apply { complete() }
|
||||
|
||||
override fun processFillRequest(
|
||||
autofillAppInfo: AutofillAppInfo,
|
||||
cancellationSignal: CancellationSignal,
|
||||
@@ -48,13 +55,16 @@ class AutofillProcessorImpl(
|
||||
request: FillRequest,
|
||||
) {
|
||||
// Set the listener so that any long running work is cancelled when it is no longer needed.
|
||||
cancellationSignal.setOnCancelListener { scope.cancel() }
|
||||
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(
|
||||
@@ -101,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,
|
||||
@@ -113,26 +123,30 @@ class AutofillProcessorImpl(
|
||||
)
|
||||
when (autofillRequest) {
|
||||
is AutofillRequest.Fillable -> {
|
||||
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
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ import com.x8bit.bitwarden.data.autofill.model.AutofillSelectionData
|
||||
import com.x8bit.bitwarden.data.autofill.model.AutofillTotpCopyData
|
||||
import com.x8bit.bitwarden.data.platform.annotation.OmitFromCoverage
|
||||
import com.x8bit.bitwarden.data.platform.util.getSafeParcelableExtra
|
||||
import kotlin.random.Random
|
||||
|
||||
private const val AUTOFILL_SAVE_ITEM_DATA_KEY = "autofill-save-item-data"
|
||||
private const val AUTOFILL_SELECTION_DATA_KEY = "autofill-selection-data"
|
||||
@@ -57,19 +58,16 @@ fun createTotpCopyIntentSender(
|
||||
context,
|
||||
AutofillTotpCopyActivity::class.java,
|
||||
)
|
||||
.apply {
|
||||
putExtra(
|
||||
AUTOFILL_BUNDLE_KEY,
|
||||
bundleOf(
|
||||
AUTOFILL_TOTP_COPY_DATA_KEY to AutofillTotpCopyData(cipherId = cipherId),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
.putExtra(
|
||||
AUTOFILL_BUNDLE_KEY,
|
||||
bundleOf(
|
||||
AUTOFILL_TOTP_COPY_DATA_KEY to AutofillTotpCopyData(cipherId = cipherId),
|
||||
),
|
||||
)
|
||||
return PendingIntent
|
||||
.getActivity(
|
||||
context,
|
||||
0,
|
||||
Random.nextInt(),
|
||||
intent,
|
||||
PendingIntent.FLAG_CANCEL_CURRENT.toPendingIntentMutabilityFlag(),
|
||||
)
|
||||
@@ -95,7 +93,7 @@ fun createAutofillSavedItemIntentSender(
|
||||
return PendingIntent
|
||||
.getActivity(
|
||||
autofillAppInfo.context,
|
||||
0,
|
||||
Random.nextInt(),
|
||||
intent,
|
||||
PendingIntent.FLAG_CANCEL_CURRENT.toPendingIntentMutabilityFlag(),
|
||||
)
|
||||
|
||||
@@ -26,7 +26,12 @@ fun AutofillValue.extractMonthValue(
|
||||
/**
|
||||
* Extract a text value from this [AutofillValue].
|
||||
*/
|
||||
fun AutofillValue.extractTextValue(): String? = this
|
||||
.textValue
|
||||
.takeIf { it.isNotBlank() }
|
||||
?.toString()
|
||||
fun AutofillValue.extractTextValue(): String? =
|
||||
if (this.isText) {
|
||||
this
|
||||
.textValue
|
||||
.takeIf { it.isNotBlank() }
|
||||
?.toString()
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
@@ -44,3 +44,9 @@ 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())
|
||||
|
||||
@@ -17,6 +17,7 @@ import com.x8bit.bitwarden.data.autofill.model.FilledData
|
||||
import com.x8bit.bitwarden.data.autofill.model.FilledItem
|
||||
import com.x8bit.bitwarden.ui.autofill.buildVaultItemAutofillRemoteViews
|
||||
import com.x8bit.bitwarden.ui.autofill.util.createVaultItemInlinePresentationOrNull
|
||||
import kotlin.random.Random
|
||||
|
||||
/**
|
||||
* Returns all the possible [AutofillId]s that were potentially fillable for the given [FilledData].
|
||||
@@ -43,7 +44,7 @@ fun FilledData.buildVaultItemDataset(
|
||||
val pendingIntent = PendingIntent
|
||||
.getActivity(
|
||||
autofillAppInfo.context,
|
||||
0,
|
||||
Random.nextInt(),
|
||||
intent,
|
||||
PendingIntent.FLAG_CANCEL_CURRENT.toPendingIntentMutabilityFlag(),
|
||||
)
|
||||
|
||||
@@ -2,14 +2,10 @@ package com.x8bit.bitwarden.data.autofill.util
|
||||
|
||||
import android.app.assist.AssistStructure
|
||||
import android.view.View
|
||||
import android.widget.EditText
|
||||
import com.x8bit.bitwarden.data.autofill.model.AutofillView
|
||||
import com.x8bit.bitwarden.ui.platform.base.util.orNullIfBlank
|
||||
|
||||
/**
|
||||
* The class name of the android edit text field.
|
||||
*/
|
||||
private const val ANDROID_EDIT_TEXT_CLASS_NAME: String = "android.widget.EditText"
|
||||
|
||||
/**
|
||||
* The default web URI scheme.
|
||||
*/
|
||||
@@ -59,7 +55,18 @@ private val SUPPORTED_VIEW_HINTS: List<String> = listOf(
|
||||
* Whether this [AssistStructure.ViewNode] represents an input field.
|
||||
*/
|
||||
private val AssistStructure.ViewNode.isInputField: Boolean
|
||||
get() = className == ANDROID_EDIT_TEXT_CLASS_NAME || htmlInfo.isInputField
|
||||
get() {
|
||||
val isEditText = className
|
||||
?.let {
|
||||
try {
|
||||
Class.forName(it)
|
||||
} catch (e: ClassNotFoundException) {
|
||||
null
|
||||
}
|
||||
}
|
||||
?.let { EditText::class.java.isAssignableFrom(it) } == true
|
||||
return isEditText || htmlInfo.isInputField
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempt to convert this [AssistStructure.ViewNode] into an [AutofillView]. If the view node
|
||||
@@ -87,6 +94,7 @@ fun AssistStructure.ViewNode.toAutofillView(): AutofillView? =
|
||||
autofillType = this.autofillType,
|
||||
isFocused = this.isFocused,
|
||||
textValue = this.autofillValue?.extractTextValue(),
|
||||
hasPasswordTerms = this.hasPasswordTerms(),
|
||||
)
|
||||
buildAutofillView(
|
||||
autofillOptions = autofillOptions,
|
||||
@@ -105,7 +113,7 @@ private fun AssistStructure.ViewNode.buildAutofillView(
|
||||
autofillOptions: List<String>,
|
||||
autofillViewData: AutofillView.Data,
|
||||
supportedHint: String?,
|
||||
): AutofillView? = when {
|
||||
): AutofillView = when {
|
||||
supportedHint == View.AUTOFILL_HINT_CREDIT_CARD_EXPIRATION_MONTH -> {
|
||||
val monthValue = this
|
||||
.autofillValue
|
||||
@@ -149,20 +157,21 @@ private fun AssistStructure.ViewNode.buildAutofillView(
|
||||
)
|
||||
}
|
||||
|
||||
else -> null
|
||||
else -> {
|
||||
AutofillView.Unused(
|
||||
data = autofillViewData,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether this [AssistStructure.ViewNode] represents a password field.
|
||||
*/
|
||||
@Suppress("ReturnCount")
|
||||
fun AssistStructure.ViewNode.isPasswordField(
|
||||
supportedHint: String?,
|
||||
): 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)
|
||||
@@ -173,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.
|
||||
*/
|
||||
|
||||
@@ -13,7 +13,6 @@ private const val ANDROID_APP_SCHEME: String = "androidapp"
|
||||
* Try and build a URI. First, try building a website from the list of [ViewNodeTraversalData]. If
|
||||
* that fails, try converting [packageName] into an Android app URI.
|
||||
*/
|
||||
@Suppress("ReturnCount")
|
||||
fun List<ViewNodeTraversalData>.buildUriOrNull(
|
||||
packageName: String?,
|
||||
): String? {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user