Compare commits

...

140 Commits

Author SHA1 Message Date
Dave Severns
1c10a94109 PM-11187 show import success bottom sheet after success import sync (#4125) 2024-10-24 14:44:14 +00:00
David Perez
6f535c0abe PM-13937: Replace tonal buttons with outline buttons (#4147) 2024-10-24 13:58:40 +00:00
Patrick Honkonen
bdb6136d36 [PM-13980] Add SSH Key Cipher Item Types feature flag (#4144) 2024-10-24 13:16:31 +00:00
Patrick Honkonen
2d9451cc34 [PM-13900] Track last database scheme change (#4124) 2024-10-24 13:15:54 +00:00
ifernandezdiaz
6217532237 QA-948: Adding missing testTags on SSO/TDE views (#4145) 2024-10-23 19:28:03 +00:00
David Perez
ef1e8403e1 PM-13939: Remove hyphen from auto-fill (#4141) 2024-10-23 17:58:28 +00:00
Patrick Honkonen
24c8406ed8 Upload test reports on test and build workflow failures (#4143) 2024-10-23 16:53:20 +00:00
David Perez
51c87625cb Ensure unmockk static is called in test teardown (#4142) 2024-10-23 16:18:45 +00:00
Andrew Haisting
fa248243b6 BITAU-112 Support deep link into add item flow from Authenticator app (#4128) 2024-10-23 16:17:31 +00:00
rad
f1d7d1a530 [PM-13857] Add Iceraven to privlieged browsers (#4122)
Co-authored-by: Patrick Honkonen <1883101+SaintPatrck@users.noreply.github.com>
2024-10-23 15:14:46 +00:00
David Perez
79ebd2ba33 User TImber instead of LogsManager directly (#4140) 2024-10-23 15:11:31 +00:00
Dave Severns
a23fc319de PM-13943 : PT1 Custom snackbar UI (#4135) 2024-10-23 14:44:59 +00:00
David Perez
c5a266dfc0 PM-13020: During totp flow master password reprompt should be honored (#4136) 2024-10-23 12:51:28 +00:00
David Perez
fca00d38f5 PM-13024: After saving cipher in totp flow, app should not close (#4137) 2024-10-22 22:12:36 +00:00
David Perez
65380095f0 Update Compose BOM and Androidx activity libs (#4134) 2024-10-22 19:37:55 +00:00
David Perez
002fd06b72 This PR adds Timber to the app (#4116) 2024-10-22 17:37:03 +00:00
David Perez
5d85060260 Update Firebase BOM to 33.5.0 (#4133) 2024-10-22 16:26:56 +00:00
David Perez
4dfee643a0 Update Junit 5 (5.11.3) (#4132) 2024-10-22 16:25:04 +00:00
David Perez
7aab846244 Apply the formatter to the entire app (#4129) 2024-10-22 14:00:10 +00:00
Dave Severns
c704cd2eca PM-13627 show action card on vault settings if applicable (#4101) 2024-10-21 20:51:49 +00:00
David Perez
09c11f4890 Fix authenticator test (#4127) 2024-10-21 20:46:30 +00:00
David Perez
df6e842201 Create single helper method to clean up the MainViewModelTest (#4126) 2024-10-21 20:42:11 +00:00
David Perez
b82614e5fa PM-13847: Totp click on search should go directly to edit screen (#4123) 2024-10-21 19:48:54 +00:00
David Perez
27beb25bf7 PM-13690: Add dialog before switching account during passwordless login (#4114) 2024-10-21 19:35:19 +00:00
Andrew Haisting
d1f13e49a4 Use array to define knownCerts for authenticator bridge permission (#4103) 2024-10-21 18:47:40 +00:00
David Perez
36a718753d PM-13021: Update no item found copy for totp (#4115) 2024-10-21 14:00:22 +00:00
Patrick Honkonen
be0ebb9b3f [PM-13396] Add support for legacy error response model in getToken (#4112) 2024-10-21 13:23:49 +00:00
Dave Severns
4fc01c77d1 PM-11186 Sync in progress for import logins and full screen loading. (#4117) 2024-10-18 18:37:06 +00:00
github-actions[bot]
258f25cd37 Autosync Crowdin Translations (#4118)
Co-authored-by: bitwarden-devops-bot <106330231+bitwarden-devops-bot@users.noreply.github.com>
Co-authored-by: Patrick Honkonen <1883101+SaintPatrck@users.noreply.github.com>
2024-10-18 18:18:01 +00:00
Patrick Honkonen
98c3ced191 [PM-13825] Update Google sourced FIDO 2 privileged app list (#4121) 2024-10-18 17:15:11 +00:00
Dave Severns
c26a7cdf28 PM-13464 show notification badge for vault settings if the showImport… (#4096) 2024-10-18 17:13:23 +00:00
Opeyemi
083578ec2b [BRE-372] - Clean up document start (#4111) 2024-10-17 18:39:12 +00:00
Patrick Honkonen
f73ce842fc [PM-13315] Prevent account switching during FIDO 2 unlock (#4054) 2024-10-17 18:20:49 +00:00
Dave Severns
56ad1ef05b PM-13464 and PM-13627 support (#4107) 2024-10-17 17:30:13 +00:00
Patrick Honkonen
5faa30e2f2 [PM-13396] Show error when logging into an unofficial Bitwarden server (#4088) 2024-10-17 15:11:13 +00:00
David Perez
a9b6f296d8 Allow CrashLogsManager to handle generic Throwables (#4106) 2024-10-17 13:55:41 +00:00
David Perez
655beb9dd6 PM-13688: Remove race condition from AuthTokenInterceptor (#4108) 2024-10-16 22:01:05 +00:00
David Perez
0d6a8513b2 Update Turbine to 1.2.0 (#4104) 2024-10-16 18:17:04 +00:00
Dave Severns
ab9d57b4f2 PM-11182 PM-11183 PM-11184 Add the instruction steps to logins import flow (#4089) 2024-10-16 18:15:13 +00:00
Dave Severns
c382227b6a PM-13648 Nav to new create account when email verification is on (#4092) 2024-10-16 17:51:23 +00:00
David Perez
cf3624264e PM-13726: Process cipher notifications without organizationIds or collectionIds (#4102) 2024-10-16 16:43:46 +00:00
Dave Severns
62cfd5e746 PM-13382 show contextual message for the level of Biometrics available (#4099) 2024-10-16 16:10:37 +00:00
Andrew Haisting
1446e43c46 BITAU-105 Add support for deep link to account security (#4063) 2024-10-16 14:45:10 +00:00
David Perez
43dc2f8116 Update PendingRequestsScreen image size (#4098) 2024-10-15 21:33:45 +00:00
David Perez
8eb408b140 Pin the segmented control to toolbar in AddSendScreen (#4093) 2024-10-15 21:13:29 +00:00
Dave Severns
970a1e14cd PM-13609 Navigate to new import flow from Vault settings when feature is enabled. (#4090) 2024-10-15 20:12:29 +00:00
David Perez
736912bd6c Remove launch icon and update BitwardenActionCard (#4097) 2024-10-15 19:41:19 +00:00
David Perez
ec47cb9ee2 AGP update v8.7.1 (#4095) 2024-10-15 17:52:52 +00:00
renovate[bot]
690de93e63 [deps]: Update gh minor (#4084)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-15 15:24:00 +00:00
renovate[bot]
9adb106a12 [deps]: Update kotlin (#4083)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-15 15:23:43 +00:00
David Perez
499ab2d2d0 PM-12296: Only match port when present on both uris (#4091) 2024-10-15 13:56:11 +00:00
Víctor
12afbea83e [PM-13387] Skip unneeded confirmation button when using passive biometrics such as face unlock (#4064) 2024-10-15 13:52:15 +00:00
David Perez
efbf84238d PM-11176: Update generator to use segmented control (#4075) 2024-10-14 20:43:17 +00:00
Andrew Haisting
2b87cdac9e BITAU-176 Filter out deleted ciphers from syncAccounts call (#4078) 2024-10-14 20:19:07 +00:00
David Perez
8eab74d458 PM-13635, PM-13636, PM-13637: Update icons (#4087) 2024-10-14 18:23:03 +00:00
Andrew Haisting
b465cc5078 BITAU-175 Remove lastSyncTime property from SharedAccountData (#4077) 2024-10-14 16:56:18 +00:00
aj-rosado
9b5c88e990 [PM-11982] On Passwordless flow switch activeAccount to match PasswordlessRequest userId (#4066) 2024-10-14 15:31:20 +00:00
Dave Severns
4756040c4a PM-13471 Remove instances deprecated ClickableText (#4076) 2024-10-14 14:26:11 +00:00
Dave Severns
bde47d7919 PM-11179 PM-11180 PM-11181 Add import logins screen and dialogs. (#4067) 2024-10-11 19:09:57 +00:00
David Perez
86db9bd3fa PM-12668: Update TopAppBars accross the app (#4074) 2024-10-11 19:05:24 +00:00
github-actions[bot]
cd9f4e8723 Autosync Crowdin Translations (#4070)
Co-authored-by: bitwarden-devops-bot <106330231+bitwarden-devops-bot@users.noreply.github.com>
Co-authored-by: Patrick Honkonen <1883101+SaintPatrck@users.noreply.github.com>
2024-10-11 16:32:29 +00:00
renovate[bot]
879c2b9107 [deps]: Lock file maintenance (#4072)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-11 16:14:43 +00:00
David Perez
cdb03f5649 Add enum for better control of TopAppBar divider (#4073) 2024-10-11 15:59:58 +00:00
Dave Severns
ba8e3a6c51 PM-11174 Action card for import logins flow (#4057) 2024-10-11 14:49:34 +00:00
David Perez
028242c4be Update the search top app bar divider thickness (#4069) 2024-10-11 13:57:20 +00:00
David Perez
3296477932 Fix dark mode toggle color (#4068) 2024-10-11 13:57:02 +00:00
David Perez
c3af26d83f PM-13286: Update segmented control to match the TopAppBar (#4058) 2024-10-10 21:14:13 +00:00
David Perez
3e9e45ba2f Simplify text field color and textstyles (#4065) 2024-10-10 20:06:10 +00:00
David Perez
22c0745993 PM-12668: Update TopAppBar divider (#4060) 2024-10-10 20:05:53 +00:00
David Perez
537281f6c3 PM-13301: Fix 2fa with key connector bug (#4059) 2024-10-09 21:16:20 +00:00
David Perez
79d2a00bf8 Add logic to identify root cause of flakey test (#4056) 2024-10-09 18:03:21 +00:00
aj-rosado
57d79cd51c [PM-12408] Updating password revision date on password change (#4044) 2024-10-09 17:09:03 +00:00
David Perez
57082ff7c1 Update Mockk to 1.13.13 (#4055) 2024-10-09 15:42:12 +00:00
David Perez
8a30f14dea Apply formatter to entire app (#4053) 2024-10-08 21:42:09 +00:00
David Perez
2af96988ab PM-13021: Update empty state for TOTP flow (#4051) 2024-10-08 19:20:05 +00:00
David Perez
ccb52ae6c5 Add shapes to the BitwardenTheme (#4052) 2024-10-08 18:35:59 +00:00
André Bispo
cda4e47414 [PM-12695] Add hidden field changes to password history (#4047) 2024-10-08 18:29:50 +00:00
Dave Severns
5e7dc26837 PM-13300 Adjust size and padding modifier order where needed. (#4050) 2024-10-08 18:18:44 +00:00
Dave Severns
b5658fda42 PM-11177 Update the empty state on the sends screen to v3 design (#4045) 2024-10-08 18:18:27 +00:00
David Perez
1539c2032e Add a bitwarden styles snackbar (#4049) 2024-10-08 17:41:40 +00:00
David Perez
94791b4256 Rename BitwardenPolicyWarning to BitwardenInfoCalloutCard (#4048) 2024-10-08 16:49:16 +00:00
Dave Severns
49d9a46917 PM-11175 update to new empty vault screen (#4046) 2024-10-08 16:00:40 +00:00
David Perez
e7450171cd This PR adds the TOTP matching flow to the app (#4042) 2024-10-08 15:10:18 +00:00
Dave Severns
641a48fe44 PM-13068 Navigate from settings to setup autofill screen. (#4034) 2024-10-08 14:29:40 +00:00
Dave Severns
bc057932a0 PM-12667 Final change, update the image files (#4043) 2024-10-08 13:58:07 +00:00
Dave Severns
0cb8e369ae PM-12667 Update the names of the the existing icon assets to match with design language (#4040) 2024-10-07 22:38:55 +00:00
Dave Severns
3d4c901039 PM-12667 Update the icons to match V3 designs (#4041) 2024-10-07 21:41:08 +00:00
David Perez
e62dc5dd21 Remove last references to MaterialTheme (#4038) 2024-10-07 17:58:00 +00:00
Andrew Haisting
f8592f4e17 Fix unused test (#4039) 2024-10-07 17:54:09 +00:00
David Perez
c4467f0cba PM-13019: Add special circumstance to navigate to the vault listing UI for TOTP code (#4033) 2024-10-07 15:04:58 +00:00
Andrew Haisting
8d578a9b57 Remove unused .aar (#4036) 2024-10-07 12:57:57 +00:00
Patrick Honkonen
73a802a483 [PM-13101] Validate FIDO2 privileged apps against community allow list (#4022) 2024-10-07 12:57:07 +00:00
Patrick Honkonen
60fce08c7e Trigger scan and test workflows on merge queue events (#4037) 2024-10-07 12:52:41 +00:00
Dave Severns
8ae6433906 PM-13067 Navigate to setup unlock screen from action card in security settings (#4023) 2024-10-04 14:29:47 -04:00
Dave Severns
83652c9699 PM-12773 show autofill card when user skipped this step in onboarding (#4021) 2024-10-04 14:21:40 -04:00
David Perez
a5cf4f49d7 Add logic for parting a TOTP code from a Uri or Intent (#4032) 2024-10-04 12:41:07 -05:00
David Perez
78d14547e4 Clean up special circumstances (#4031) 2024-10-04 11:23:17 -05:00
David Perez
29f00421bb Update to Junit 5.11.2 (#4028) 2024-10-04 10:54:42 -05:00
David Perez
8501db0eb2 Update compose bom to 2024.09.03 (#4030) 2024-10-04 10:54:27 -05:00
David Perez
c1e9759dae Update credential library (1.3.0) (#4029) 2024-10-04 10:54:16 -05:00
github-actions[bot]
8f24597bad Autosync Crowdin Translations (#4024) 2024-10-04 15:53:05 +00:00
Dave Severns
954a9acf92 PM-12764 update image assets (#3982) 2024-10-04 10:05:56 -04:00
David Perez
3dfe6adc05 PM-12322: Update color scheme (#3986) 2024-10-04 08:47:28 -05:00
mpbw2
1d84479cf3 [PM-9363] Disable cipher key encryption for older server versions (#4006) 2024-10-03 17:42:23 -04:00
Dave Severns
c8dcafe737 PM-10632 update the copy on setup complete (#4020) 2024-10-03 17:37:43 -04:00
Andrew Haisting
567c2ffb94 BITAU-99 Expose and protect AuthenticatorBridgeService (#3988) 2024-10-03 15:02:58 -05:00
Patrick Honkonen
488ec095bc [PM-13073] Handle Fido2 credential errors on vault unlock screen (#4010) 2024-10-03 15:40:03 -04:00
Andrew Haisting
32f2bfb29f BITAU-69 Check for OS version in AuthenticatorBridgeManager (#4019) 2024-10-03 14:06:15 -05:00
Lucas
20383f06a8 [PM-13011] Allow relevant browsers in the privacy/security/FOSS space to use auto-fill and passkeys (#4005) 2024-10-03 14:44:22 -04:00
Dave Severns
fd6b276cc8 PM-12683 SSO user needed password set bug (#4018) 2024-10-03 12:25:58 -04:00
Patrick Honkonen
e6eb626d85 [PM-13070] Add userId to Fido2 GetCredentials and CredentialAssertion requests (#4003) 2024-10-03 11:14:03 -04:00
Dave Severns
569ffc3583 PM-12760 Add way to re-show the onboarding carousel via debug menu (#3999) 2024-10-03 09:58:37 -04:00
aj-rosado
e2e5042be5 [PM-12739] adjusted generator length to not be lower than minimum length (#4016) 2024-10-03 10:30:54 +02:00
David Perez
0c83a1099f PM-10628: Update pin dialog title (#4017) 2024-10-02 12:57:34 -05:00
David Perez
36a5fee048 Update the Firebase BOM to 33.4.0 (#4015) 2024-10-02 12:57:17 -05:00
Patrick Honkonen
01ab047d9c [PM-13074] Explicitly sync FIDO2 credentials (#4012) 2024-10-02 13:50:18 -04:00
David Perez
4fd81ed3b8 PM-10628: Update Pin Input Dialog UI (#4013) 2024-10-02 08:59:26 -05:00
Dave Severns
8e092ef860 PM-12772 Add notification action card to security settings when applicable (#4008) 2024-10-02 08:27:45 -04:00
Andrew Haisting
9e4119fe32 BITAU-97 Add AuthenticatorBridgeManager (#3987) 2024-10-01 21:27:12 +00:00
David Perez
757baf0290 Clean up text field typography (#4011) 2024-10-01 16:18:55 -05:00
David Perez
53b1bec42b Update the Android Gradle Plugin and the Gradle Wrapper (#4009) 2024-10-01 13:57:12 -05:00
David Perez
e63c4806f1 Migrate all references of MaterialTheme Typography to BitwardenTheme (#4007) 2024-10-01 13:11:57 -05:00
David Perez
2224708fb1 Add singular BitwardenTypography to manage all text-styles (#4002) 2024-10-01 09:20:26 -05:00
aj-rosado
b3e885bcb1 [PM-12279] Update SDK reference and use Origin.Android on Fido2Credential (#3975) 2024-10-01 15:07:30 +02:00
David Perez
10bbab971f PM-12322: Add bitwarden color scheme to BitwardenTheme (#4000) 2024-09-30 16:47:08 -05:00
David Perez
8ec743736a Update to Junit 5.11.1 (#3998) 2024-09-30 13:51:04 -05:00
David Perez
2f05355487 PM-12322: New color scheme (#3995) 2024-09-30 12:56:32 -05:00
David Perez
b7c48c2e26 Update account item font and remove unused fonts (#3997) 2024-09-30 12:56:06 -05:00
renovate[bot]
1e9583b3be [deps]: Update org.jetbrains.kotlinx:kotlinx-serialization-json to v1.7.3 (#3990) 2024-09-30 13:49:35 -04:00
Patrick Honkonen
75819cce3c [PM-12322] Remove branch restriction for distributing to Firebase (#3996) 2024-09-30 14:42:12 -03:00
aj-rosado
d60c534e06 [PM-12739] Updated generator maximum number and specials (#3994) 2024-09-30 19:33:27 +02:00
github-actions[bot]
ad338a8fd6 Autosync Crowdin Translations (#3978) 2024-09-30 17:00:23 +00:00
renovate[bot]
290377af74 [deps]: Update ubuntu to v24 (#3992) 2024-09-30 12:51:05 -04:00
renovate[bot]
24195ddb90 [deps]: Lock file maintenance (#3993)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-09-30 11:23:07 -05:00
Oscar Hinton
73c5571d6b Fix linking url (#3979) 2024-09-30 17:26:51 +02:00
renovate[bot]
5beec9d687 [deps]: Update gh minor (#3991)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-09-30 10:52:17 -04:00
André Bispo
9d19a73fd6 [PM-9755] Change error message from a toast to a dialog (#3963) 2024-09-30 08:20:45 +01:00
Dave Severns
72cb9918ac PM-10616 update copy to match design (#3985) 2024-09-27 15:58:44 -04:00
David Perez
aa6762dc22 Add BitwardenOutlinedErrorButton and rename BitwardenErrorButton (#3984) 2024-09-27 11:42:12 -05:00
David Perez
b696964cb7 Add a reusable Navigation Bar Item (#3983) 2024-09-27 11:41:57 -05:00
697 changed files with 23173 additions and 6990 deletions

View File

@@ -33,17 +33,17 @@ env:
jobs:
build:
name: Build
runs-on: ubuntu-22.04
runs-on: ubuntu-24.04
steps:
- name: Check out repo
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1
- name: Validate Gradle wrapper
uses: gradle/actions/wrapper-validation@d156388eb19639ec20ade50009f3d199ce1e2808 # v4.1.0
- name: Cache Gradle files
uses: actions/cache@0c45773b623bea8c8e75f6c82b208c3cf94ea4f9 # v4.0.2
uses: actions/cache@3624ceb22c1c5a301c8db4169662070a689d9ea8 # v4.1.1
with:
path: |
~/.gradle/caches
@@ -53,7 +53,7 @@ jobs:
${{ runner.os }}-gradle-v2-
- name: Cache build output
uses: actions/cache@0c45773b623bea8c8e75f6c82b208c3cf94ea4f9 # v4.0.2
uses: actions/cache@3624ceb22c1c5a301c8db4169662070a689d9ea8 # v4.1.1
with:
path: |
${{ github.workspace }}/build-cache
@@ -62,13 +62,13 @@ jobs:
${{ runner.os }}-build-
- name: Configure JDK
uses: actions/setup-java@2dfa2011c5b2a0f1489bf9e433881c92c1631f88 # v4.3.0
uses: actions/setup-java@b36c23c0d998641eff861008f374ee103c25ac73 # v4.4.0
with:
distribution: "temurin"
java-version: ${{ env.JAVA_VERSION }}
- name: Configure Ruby
uses: ruby/setup-ruby@f321cf5a4d1533575411f8752cf25b86478b0442 # v1.193.0
uses: ruby/setup-ruby@f26937343756480a8cb3ae1f623b9c8d89ed6984 # v1.196.0
with:
bundler-cache: true
@@ -84,11 +84,18 @@ jobs:
- name: Build
run: bundle exec fastlane assembleDebugApks
- name: Upload test reports on failure
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
if: failure()
with:
name: test-reports
path: app/build/reports/tests/
publish_playstore:
name: Publish Play Store artifacts
needs:
- build
runs-on: ubuntu-22.04
runs-on: ubuntu-24.04
strategy:
fail-fast: false
matrix:
@@ -96,10 +103,10 @@ jobs:
artifact: ["apk", "aab"]
steps:
- name: Check out repo
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1
- name: Configure Ruby
uses: ruby/setup-ruby@f321cf5a4d1533575411f8752cf25b86478b0442 # v1.193.0
uses: ruby/setup-ruby@f26937343756480a8cb3ae1f623b9c8d89ed6984 # v1.196.0
with:
bundler-cache: true
@@ -153,7 +160,7 @@ jobs:
uses: gradle/actions/wrapper-validation@d156388eb19639ec20ade50009f3d199ce1e2808 # v4.1.0
- name: Cache Gradle files
uses: actions/cache@0c45773b623bea8c8e75f6c82b208c3cf94ea4f9 # v4.0.2
uses: actions/cache@3624ceb22c1c5a301c8db4169662070a689d9ea8 # v4.1.1
with:
path: |
~/.gradle/caches
@@ -163,7 +170,7 @@ jobs:
${{ runner.os }}-gradle-v2-
- name: Cache build output
uses: actions/cache@0c45773b623bea8c8e75f6c82b208c3cf94ea4f9 # v4.0.2
uses: actions/cache@3624ceb22c1c5a301c8db4169662070a689d9ea8 # v4.1.1
with:
path: |
${{ github.workspace }}/build-cache
@@ -172,7 +179,7 @@ jobs:
${{ runner.os }}-build-
- name: Configure JDK
uses: actions/setup-java@2dfa2011c5b2a0f1489bf9e433881c92c1631f88 # v4.3.0
uses: actions/setup-java@b36c23c0d998641eff861008f374ee103c25ac73 # v4.4.0
with:
distribution: "temurin"
java-version: ${{ env.JAVA_VERSION }}
@@ -237,7 +244,7 @@ jobs:
- name: Upload release Play Store .aab artifact
if: ${{ (matrix.variant == 'prod') && (matrix.artifact == 'aab') }}
uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
with:
name: com.x8bit.bitwarden.aab
path: app/build/outputs/bundle/standardRelease/com.x8bit.bitwarden-standard-release.aab
@@ -245,7 +252,7 @@ jobs:
- name: Upload beta Play Store .aab artifact
if: ${{ (matrix.variant == 'prod') && (matrix.artifact == 'aab') }}
uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
with:
name: com.x8bit.bitwarden.beta.aab
path: app/build/outputs/bundle/standardBeta/com.x8bit.bitwarden-standard-beta.aab
@@ -253,7 +260,7 @@ jobs:
- name: Upload release .apk artifact
if: ${{ (matrix.variant == 'prod') && (matrix.artifact == 'apk') }}
uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
with:
name: com.x8bit.bitwarden.apk
path: app/build/outputs/apk/standard/release/com.x8bit.bitwarden-standard-release.apk
@@ -261,7 +268,7 @@ jobs:
- name: Upload beta .apk artifact
if: ${{ (matrix.variant == 'prod') && (matrix.artifact == 'apk') }}
uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
with:
name: com.x8bit.bitwarden.beta.apk
path: app/build/outputs/apk/standard/beta/com.x8bit.bitwarden-standard-beta.apk
@@ -270,7 +277,7 @@ jobs:
# When building variants other than 'prod'
- name: Upload debug .apk artifact
if: ${{ (matrix.variant != 'prod') && (matrix.artifact == 'apk') }}
uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
with:
name: com.x8bit.bitwarden.${{ matrix.variant }}.apk
path: app/build/outputs/apk/standard/debug/com.x8bit.bitwarden-standard-debug.apk
@@ -308,7 +315,7 @@ jobs:
- name: Upload .apk SHA file for release
if: ${{ (matrix.variant == 'prod') && (matrix.artifact == 'apk') }}
uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
with:
name: com.x8bit.bitwarden.apk-sha256.txt
path: ./com.x8bit.bitwarden.apk-sha256.txt
@@ -316,7 +323,7 @@ jobs:
- name: Upload .apk SHA file for beta
if: ${{ (matrix.variant == 'prod') && (matrix.artifact == 'apk') }}
uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
with:
name: com.x8bit.bitwarden.beta.apk-sha256.txt
path: ./com.x8bit.bitwarden.beta.apk-sha256.txt
@@ -324,7 +331,7 @@ jobs:
- name: Upload .aab SHA file for release
if: ${{ (matrix.variant == 'prod') && (matrix.artifact == 'aab') }}
uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
with:
name: com.x8bit.bitwarden.aab-sha256.txt
path: ./com.x8bit.bitwarden.aab-sha256.txt
@@ -332,7 +339,7 @@ jobs:
- name: Upload .aab SHA file for beta
if: ${{ (matrix.variant == 'prod') && (matrix.artifact == 'aab') }}
uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
with:
name: com.x8bit.bitwarden.beta.aab-sha256.txt
path: ./com.x8bit.bitwarden.beta.aab-sha256.txt
@@ -340,18 +347,18 @@ jobs:
- name: Upload .apk SHA file for debug
if: ${{ (matrix.variant != 'prod') && (matrix.artifact == 'apk') }}
uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
with:
name: com.x8bit.bitwarden.${{ matrix.variant }}.apk-sha256.txt
path: ./com.x8bit.bitwarden.${{ matrix.variant }}.apk-sha256.txt
if-no-files-found: error
- name: Install Firebase app distribution plugin
if: ${{ matrix.variant == 'prod' && github.ref_name == 'main' && (inputs.distribute-to-firebase || github.event_name == 'push') }}
if: ${{ matrix.variant == 'prod' && (inputs.distribute-to-firebase || github.event_name == 'push') }}
run: bundle exec fastlane add_plugin firebase_app_distribution
- name: Publish release artifacts to Firebase
if: ${{ matrix.variant == 'prod' && matrix.artifact == 'apk' && github.ref_name == 'main' && (inputs.distribute-to-firebase || github.event_name == 'push') }}
if: ${{ matrix.variant == 'prod' && matrix.artifact == 'apk' && (inputs.distribute-to-firebase || github.event_name == 'push') }}
env:
APP_PLAY_FIREBASE_CREDS_PATH: ${{ github.workspace }}/secrets/app_play_prod_firebase-creds.json
run: |
@@ -360,7 +367,7 @@ jobs:
service_credentials_file:${{ env.APP_PLAY_FIREBASE_CREDS_PATH }}
- name: Publish beta artifacts to Firebase
if: ${{ (matrix.variant == 'prod' && matrix.artifact == 'apk') && github.ref_name == 'main' && (inputs.distribute-to-firebase || github.event_name == 'push') }}
if: ${{ (matrix.variant == 'prod' && matrix.artifact == 'apk') && (inputs.distribute-to-firebase || github.event_name == 'push') }}
env:
APP_PLAY_FIREBASE_CREDS_PATH: ${{ github.workspace }}/secrets/app_play_prod_firebase-creds.json
run: |
@@ -381,13 +388,13 @@ jobs:
name: Publish F-Droid artifacts
needs:
- build
runs-on: ubuntu-22.04
runs-on: ubuntu-24.04
steps:
- name: Check out repo
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1
- name: Configure Ruby
uses: ruby/setup-ruby@f321cf5a4d1533575411f8752cf25b86478b0442 # v1.193.0
uses: ruby/setup-ruby@f26937343756480a8cb3ae1f623b9c8d89ed6984 # v1.196.0
with:
bundler-cache: true
@@ -427,7 +434,7 @@ jobs:
uses: gradle/actions/wrapper-validation@d156388eb19639ec20ade50009f3d199ce1e2808 # v4.1.0
- name: Cache Gradle files
uses: actions/cache@0c45773b623bea8c8e75f6c82b208c3cf94ea4f9 # v4.0.2
uses: actions/cache@3624ceb22c1c5a301c8db4169662070a689d9ea8 # v4.1.1
with:
path: |
~/.gradle/caches
@@ -437,7 +444,7 @@ jobs:
${{ runner.os }}-gradle-v2-
- name: Cache build output
uses: actions/cache@0c45773b623bea8c8e75f6c82b208c3cf94ea4f9 # v4.0.2
uses: actions/cache@3624ceb22c1c5a301c8db4169662070a689d9ea8 # v4.1.1
with:
path: |
${{ github.workspace }}/build-cache
@@ -446,7 +453,7 @@ jobs:
${{ runner.os }}-build-
- name: Configure JDK
uses: actions/setup-java@2dfa2011c5b2a0f1489bf9e433881c92c1631f88 # v4.3.0
uses: actions/setup-java@b36c23c0d998641eff861008f374ee103c25ac73 # v4.4.0
with:
distribution: "temurin"
java-version: ${{ env.JAVA_VERSION }}
@@ -481,7 +488,7 @@ jobs:
keyPassword:"${{ env.FDROID_BETA_KEY_PASSWORD }}"
- name: Upload F-Droid .apk artifact
uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
with:
name: com.x8bit.bitwarden-fdroid.apk
path: app/build/outputs/apk/fdroid/release/com.x8bit.bitwarden-fdroid-release.apk
@@ -493,14 +500,14 @@ jobs:
> ./com.x8bit.bitwarden-fdroid.apk-sha256.txt
- name: Upload F-Droid SHA file
uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
with:
name: com.x8bit.bitwarden-fdroid.apk-sha256.txt
path: ./com.x8bit.bitwarden-fdroid.apk-sha256.txt
if-no-files-found: error
- name: Upload F-Droid Beta .apk artifact
uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
with:
name: com.x8bit.bitwarden.beta-fdroid.apk
path: app/build/outputs/apk/fdroid/beta/com.x8bit.bitwarden-fdroid-beta.apk
@@ -512,18 +519,18 @@ jobs:
> ./com.x8bit.bitwarden.beta-fdroid.apk-sha256.txt
- name: Upload F-Droid Beta SHA file
uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
with:
name: com.x8bit.bitwarden.beta-fdroid.apk-sha256.txt
path: ./com.x8bit.bitwarden.beta-fdroid.apk-sha256.txt
if-no-files-found: error
- name: Install Firebase app distribution plugin
if: ${{ github.ref_name == 'main' && inputs.distribute_to_firebase }}
if: ${{ inputs.distribute_to_firebase || github.event_name == 'push' }}
run: bundle exec fastlane add_plugin firebase_app_distribution
- name: Publish release F-Droid artifacts to Firebase
if: ${{ github.ref_name == 'main' && inputs.distribute_to_firebase }}
if: ${{ inputs.distribute_to_firebase || github.event_name == 'push' }}
env:
APP_FDROID_FIREBASE_CREDS_PATH: ${{ github.workspace }}/secrets/app_fdroid_firebase-creds.json
run: |

View File

@@ -1,4 +1,3 @@
---
name: Crowdin Sync
on:
@@ -10,12 +9,12 @@ on:
jobs:
crowdin-sync:
name: Autosync
runs-on: ubuntu-22.04
runs-on: ubuntu-24.04
env:
_CROWDIN_PROJECT_ID: "269690"
steps:
- name: Checkout repo
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1
- name: Login to Azure - CI Subscription
uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0
@@ -30,7 +29,7 @@ jobs:
secrets: "crowdin-api-token, github-gpg-private-key, github-gpg-private-key-passphrase"
- name: Download translations
uses: crowdin/github-action@91d52b545f82cb88e86c3002a443de22df77fa16 # v2.1.3
uses: crowdin/github-action@95d6e895e871c3c7acf0cfb962f296baa41e63c6 # v2.2.0
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
CROWDIN_API_TOKEN: ${{ steps.retrieve-secrets.outputs.crowdin-api-token }}

View File

@@ -9,12 +9,12 @@ on:
jobs:
crowdin-push:
name: Crowdin Push
runs-on: ubuntu-22.04
runs-on: ubuntu-24.04
env:
_CROWDIN_PROJECT_ID: "269690"
steps:
- name: Check out repo
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1
- name: Log in to Azure
uses: Azure/login@cb79c773a3cfa27f31f25eb3f677781210c9ce3d # v1.6.1
@@ -29,7 +29,7 @@ jobs:
secrets: "crowdin-api-token"
- name: Upload sources
uses: crowdin/github-action@91d52b545f82cb88e86c3002a443de22df77fa16 # v2.1.3
uses: crowdin/github-action@95d6e895e871c3c7acf0cfb962f296baa41e63c6 # v2.2.0
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
CROWDIN_API_TOKEN: ${{ steps.retrieve-secrets.outputs.crowdin-api-token }}

View File

@@ -9,6 +9,8 @@ on:
- "hotfix-rc"
pull_request_target:
types: [opened, synchronize]
merge_group:
types: [checks_requested]
jobs:
check-run:
@@ -17,7 +19,7 @@ jobs:
sast:
name: SAST scan
runs-on: ubuntu-22.04
runs-on: ubuntu-24.04
needs: check-run
permissions:
contents: read
@@ -26,12 +28,12 @@ jobs:
steps:
- name: Check out repo
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1
with:
ref: ${{ github.event.pull_request.head.sha }}
- name: Scan with Checkmarx
uses: checkmarx/ast-github-action@9fda5a4a2c297608117a5a56af424502a9192e57 # 2.0.34
uses: checkmarx/ast-github-action@f0869bd1a37fddc06499a096101e6c900e815d81 # 2.0.36
env:
INCREMENTAL: "${{ contains(github.event_name, 'pull_request') && '--sast-incremental' || '' }}"
with:
@@ -46,13 +48,13 @@ jobs:
--output-path . ${{ env.INCREMENTAL }}
- name: Upload Checkmarx results to GitHub
uses: github/codeql-action/upload-sarif@294a9d92911152fe08befb9ec03e240add280cb3 # v3.26.8
uses: github/codeql-action/upload-sarif@f779452ac5af1c261dce0346a8f964149f49322b # v3.26.13
with:
sarif_file: cx_result.sarif
quality:
name: Quality scan
runs-on: ubuntu-22.04
runs-on: ubuntu-24.04
needs: check-run
permissions:
contents: read
@@ -60,13 +62,13 @@ jobs:
steps:
- name: Check out repo
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1
with:
fetch-depth: 0
ref: ${{ github.event.pull_request.head.sha }}
- name: Scan with SonarCloud
uses: sonarsource/sonarcloud-github-action@eb211723266fe8e83102bac7361f0a05c3ac1d1b # v3.0.0
uses: sonarsource/sonarcloud-github-action@383f7e52eae3ab0510c3cb0e7d9d150bbaeab838 # v3.1.0
env:
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

View File

@@ -8,6 +8,8 @@ on:
- "hotfix-rc"
pull_request_target:
types: [opened, synchronize]
merge_group:
type: [checks_requested]
workflow_dispatch:
env:
@@ -21,7 +23,7 @@ jobs:
test:
name: Test
runs-on: ubuntu-22.04
runs-on: ubuntu-24.04
needs: check-run
permissions:
contents: read
@@ -31,7 +33,7 @@ jobs:
steps:
- name: Check out repo
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1
with:
ref: ${{ github.event.pull_request.head.sha }}
@@ -39,7 +41,7 @@ jobs:
uses: gradle/actions/wrapper-validation@d156388eb19639ec20ade50009f3d199ce1e2808 # v4.1.0
- name: Cache Gradle files
uses: actions/cache@0c45773b623bea8c8e75f6c82b208c3cf94ea4f9 # v4.0.2
uses: actions/cache@3624ceb22c1c5a301c8db4169662070a689d9ea8 # v4.1.1
with:
path: |
~/.gradle/caches
@@ -49,7 +51,7 @@ jobs:
${{ runner.os }}-gradle-v2-
- name: Cache build output
uses: actions/cache@0c45773b623bea8c8e75f6c82b208c3cf94ea4f9 # v4.0.2
uses: actions/cache@3624ceb22c1c5a301c8db4169662070a689d9ea8 # v4.1.1
with:
path: |
${{ github.workspace }}/build-cache
@@ -58,12 +60,12 @@ jobs:
${{ runner.os }}-build-
- name: Configure Ruby
uses: ruby/setup-ruby@f321cf5a4d1533575411f8752cf25b86478b0442 # v1.193.0
uses: ruby/setup-ruby@f26937343756480a8cb3ae1f623b9c8d89ed6984 # v1.196.0
with:
bundler-cache: true
- name: Configure JDK
uses: actions/setup-java@2dfa2011c5b2a0f1489bf9e433881c92c1631f88 # v4.3.0
uses: actions/setup-java@b36c23c0d998641eff861008f374ee103c25ac73 # v4.4.0
with:
distribution: "temurin"
java-version: ${{ env.JAVA_VERSION }}
@@ -78,8 +80,15 @@ jobs:
run: |
bundle exec fastlane check
- name: Upload test reports on failure
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
if: failure()
with:
name: test-reports
path: app/build/reports/tests/
- name: Upload to codecov.io
uses: codecov/codecov-action@e28ff129e5465c2c0dcc6f003fc735cb6ae0c673 # v4.5.0
uses: codecov/codecov-action@b9fd7d16f6d7d1b5d2bec1a2887e65ceed900238 # v4.6.0
with:
file: app/build/reports/kover/reportStandardDebug.xml
env:

View File

@@ -10,20 +10,20 @@ GEM
artifactory (3.0.17)
atomos (0.1.3)
aws-eventstream (1.3.0)
aws-partitions (1.975.0)
aws-sdk-core (3.205.0)
aws-partitions (1.989.0)
aws-sdk-core (3.209.1)
aws-eventstream (~> 1, >= 1.3.0)
aws-partitions (~> 1, >= 1.651.0)
aws-sigv4 (~> 1.9)
jmespath (~> 1, >= 1.6.1)
aws-sdk-kms (1.91.0)
aws-sdk-core (~> 3, >= 3.205.0)
aws-sdk-kms (1.94.0)
aws-sdk-core (~> 3, >= 3.207.0)
aws-sigv4 (~> 1.5)
aws-sdk-s3 (1.162.0)
aws-sdk-core (~> 3, >= 3.205.0)
aws-sdk-s3 (1.167.0)
aws-sdk-core (~> 3, >= 3.207.0)
aws-sdk-kms (~> 1)
aws-sigv4 (~> 1.5)
aws-sigv4 (1.9.1)
aws-sigv4 (1.10.0)
aws-eventstream (~> 1, >= 1.0.2)
babosa (1.0.4)
base64 (0.2.0)
@@ -39,8 +39,8 @@ GEM
domain_name (0.6.20240107)
dotenv (2.8.1)
emoji_regex (3.2.3)
excon (0.111.0)
faraday (1.10.3)
excon (0.112.0)
faraday (1.10.4)
faraday-em_http (~> 1.0)
faraday-em_synchrony (~> 1.0)
faraday-excon (~> 1.1)
@@ -66,10 +66,10 @@ GEM
faraday-patron (1.0.0)
faraday-rack (1.0.0)
faraday-retry (1.0.3)
faraday_middleware (1.2.0)
faraday_middleware (1.2.1)
faraday (~> 1.0)
fastimage (2.3.1)
fastlane (2.222.0)
fastlane (2.224.0)
CFPropertyList (>= 2.3, < 4.0.0)
addressable (>= 2.8, < 3.0.0)
artifactory (~> 3.0)
@@ -160,7 +160,7 @@ GEM
httpclient (2.8.3)
jmespath (1.6.2)
json (2.7.2)
jwt (2.9.0)
jwt (2.9.3)
base64
mini_magick (4.13.2)
mini_mime (1.1.5)
@@ -179,7 +179,7 @@ GEM
trailblazer-option (>= 0.1.1, < 0.2.0)
uber (< 0.2.0)
retriable (3.1.2)
rexml (3.3.7)
rexml (3.3.8)
rouge (2.0.7)
ruby2_keywords (0.0.5)
rubyzip (2.3.2)
@@ -205,13 +205,13 @@ GEM
uber (0.1.0)
unicode-display_width (2.6.0)
word_wrap (1.0.0)
xcodeproj (1.25.0)
xcodeproj (1.25.1)
CFPropertyList (>= 2.3.3, < 4.0)
atomos (~> 0.1.3)
claide (>= 1.0.2, < 2.0)
colored2 (~> 3.1)
nanaimo (~> 0.3.0)
rexml (>= 3.3.2, < 4.0)
rexml (>= 3.3.6, < 4.0)
xcpretty (0.3.0)
rouge (~> 2.0.7)
xcpretty-travis-formatter (1.0.1)

View File

@@ -28,7 +28,7 @@
2. Create a `user.properties` file in the root directory of the project and add the following properties:
- `gitHubToken`: A "classic" Github Personal Access Token (PAT) with the `read:packages` scope (ex: `gitHubToken=gph_xx...xx`). These can be generated by going to the [Github tokens page](https://github.com/settings/tokens). See [the Github Packages user documentation concerning authentication](https://docs.github.com/en/packages/working-with-a-github-packages-registry/working-with-the-gradle-registry#authenticating-to-github-packages) for more details.
- `localSdk`: A boolean value to determine if the SDK should be loaded from the local maven artifactory (ex: `localSdk=true`). This is particularly useful when developing new SDK capabilities. Review [Linking SDK to clients](https://contributing-docs.pages.dev/getting-started/sdk/#linking-sdk-to-clients) for more details.
- `localSdk`: A boolean value to determine if the SDK should be loaded from the local maven artifactory (ex: `localSdk=true`). This is particularly useful when developing new SDK capabilities. Review [Linking SDK to clients](https://contributing.bitwarden.com/getting-started/sdk/#linking-the-sdk-to-clients) for more details.
3. Setup the code style formatter:
@@ -171,6 +171,11 @@ The following is a list of all third-party dependencies included as part of the
- Purpose: A networking layer interface.
- License: Apache 2.0
- **Timber**
- https://github.com/JakeWharton/timber
- Purpose: Extensible logging library for Android.
- License: Apache 2.0
- **zxcvbn4j**
- https://github.com/nulab/zxcvbn4j
- Purpose: Password strength estimation.

View File

@@ -75,6 +75,7 @@ android {
isMinifyEnabled = false
buildConfigField(type = "boolean", name = "HAS_DEBUG_MENU", value = "true")
buildConfigField(type = "boolean", name = "HAS_LOGS_ENABLED", value = "true")
}
// Beta and Release variants are identical except beta has a different package name
@@ -88,6 +89,7 @@ android {
)
buildConfigField(type = "boolean", name = "HAS_DEBUG_MENU", value = "false")
buildConfigField(type = "boolean", name = "HAS_LOGS_ENABLED", value = "false")
}
release {
isDebuggable = false
@@ -98,6 +100,7 @@ android {
)
buildConfigField(type = "boolean", name = "HAS_DEBUG_MENU", value = "false")
buildConfigField(type = "boolean", name = "HAS_LOGS_ENABLED", value = "false")
}
}
@@ -200,6 +203,7 @@ dependencies {
implementation(platform(libs.square.retrofit.bom))
implementation(libs.square.retrofit)
implementation(libs.square.retrofit.kotlinx.serialization)
implementation(libs.timber)
implementation(libs.zxing.zxing.core)
// For now we are restricted to running Compose tests for debug builds only

View File

@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- For beta variant, we don't have a matching variant of the Bitwarden Authenticator app.
Therefore, we leave the known app cert null here so that no clients can connect to
AuthenticatorBridgeService in the beta variant. If later another variant of the
Bitwarden Authenticator app is added, a SHA-256 digest of that variant's APK can be added here.
-->
<string-array name="known_authenticator_app_certs" />
</resources>

View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string-array name="known_authenticator_app_certs">
<!-- This is the SHA-256 digest for the Authenticator App debug variant:-->
<item>13144ab52af797a88c2fe292674461ef1715e0e1e4f5f538f63f1c174696f476</item>
</string-array>
</resources>

View File

@@ -1,16 +0,0 @@
package com.x8bit.bitwarden.data.platform.manager
import com.x8bit.bitwarden.data.platform.datasource.disk.legacy.LegacyAppCenterMigrator
import com.x8bit.bitwarden.data.platform.repository.SettingsRepository
/**
* CrashLogsManager implementation for F-droid flavor builds.
*/
class CrashLogsManagerImpl(
settingsRepository: SettingsRepository,
legacyAppCenterMigrator: LegacyAppCenterMigrator,
) : CrashLogsManager {
override var isEnabled: Boolean = true
override fun trackNonFatalException(e: Exception) = Unit
}

View File

@@ -0,0 +1,27 @@
package com.x8bit.bitwarden.data.platform.manager
import com.x8bit.bitwarden.BuildConfig
import com.x8bit.bitwarden.data.platform.datasource.disk.legacy.LegacyAppCenterMigrator
import com.x8bit.bitwarden.data.platform.repository.SettingsRepository
import com.x8bit.bitwarden.data.platform.repository.model.Environment
import timber.log.Timber
/**
* [LogsManager] implementation for F-droid flavor builds.
*/
class LogsManagerImpl(
settingsRepository: SettingsRepository,
legacyAppCenterMigrator: LegacyAppCenterMigrator,
) : LogsManager {
init {
if (BuildConfig.HAS_LOGS_ENABLED) {
Timber.plant(Timber.DebugTree())
}
}
override var isEnabled: Boolean = false
override fun setUserData(userId: String?, environmentType: Environment.Type) = Unit
override fun trackNonFatalException(throwable: Throwable) = Unit
}

View File

@@ -16,6 +16,20 @@
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<!-- Protect access to AuthenticatorBridgeService using this custom permission.
Note that each build type uses a different value for knownCerts.
This in effect means that the only application that can connect to the debug/release/etc
variant AuthenticatorBridgeService is the debug/release/etc variant Bitwarden Authenticator
app. -->
<permission
android:name="${applicationId}.permission.AUTHENTICATOR_BRIDGE_SERVICE"
android:knownCerts="@array/known_authenticator_app_certs"
android:label="Bitwarden Bridge"
android:protectionLevel="signature|knownSigner"
tools:targetApi="s" />
<application
android:name=".BitwardenApplication"
android:allowBackup="false"
@@ -75,6 +89,20 @@
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="otpauth" />
<data android:host="totp" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<data android:scheme="bitwarden" />
</intent-filter>
</activity>
<activity
@@ -277,6 +305,11 @@
android:name="android.content.APP_RESTRICTIONS"
android:resource="@xml/app_restrictions" />
<service
android:name="com.x8bit.bitwarden.data.platform.service.AuthenticatorBridgeService"
android:exported="true"
android:permission="${applicationId}.permission.AUTHENTICATOR_BRIDGE_SERVICE" />
</application>
<queries>

View File

@@ -0,0 +1,80 @@
{
"apps": [
{
"type": "android",
"info": {
"package_name": "org.chromium.chrome",
"signatures": [
{
"build": "release",
"cert_fingerprint_sha256": "A8:56:48:50:79:BC:B3:57:BF:BE:69:BA:19:A9:BA:43:CD:0A:D9:AB:22:67:52:C7:80:B6:88:8A:FD:48:21:6B"
}
]
}
},
{
"type": "android",
"info": {
"package_name": "org.cromite.cromite",
"signatures": [
{
"build": "release",
"cert_fingerprint_sha256": "63:3F:A4:1D:82:11:D6:D0:91:6A:81:9B:89:66:8C:6D:E9:2E:64:23:2D:A6:7F:9D:16:FD:81:C3:B7:E9:23:FF"
}
]
}
},
{
"type": "android",
"info": {
"package_name": "org.mozilla.fennec_fdroid",
"signatures": [
{
"build": "release",
"cert_fingerprint_sha256": "06:66:53:58:EF:D8:BA:05:BE:23:6A:47:A1:2C:B0:95:8D:7D:75:DD:93:9D:77:C2:B3:1F:53:98:53:7E:BD:C5"
}
]
}
},
{
"type": "android",
"info": {
"package_name": "us.spotco.fennec_dos",
"signatures": [
{
"build": "release",
"cert_fingerprint_sha256": "26:0E:0A:49:67:8C:78:B7:0C:02:D6:53:7A:DD:3B:6D:C0:A1:71:71:BB:DE:8C:E7:5F:D4:02:6A:8A:3E:18:D2"
},
{
"build": "release",
"cert_fingerprint_sha256": "FF:81:F5:BE:56:39:65:94:EE:E7:0F:EF:28:32:25:6E:15:21:41:22:E2:BA:9C:ED:D2:60:05:FF:D4:BC:AA:A8"
}
]
}
},
{
"type": "android",
"info": {
"package_name": "us.spotco.mulch",
"signatures": [
{
"build": "release",
"cert_fingerprint_sha256": "26:0E:0A:49:67:8C:78:B7:0C:02:D6:53:7A:DD:3B:6D:C0:A1:71:71:BB:DE:8C:E7:5F:D4:02:6A:8A:3E:18:D2"
}
]
}
},
{
"type": "android",
"info": {
"package_name": "io.github.forkmaintainers.iceraven",
"signatures": [
{
"build": "release",
"cert_fingerprint_sha256": "9C:0D:22:37:9F:48:7B:70:A4:F9:F8:BE:C0:17:3C:F9:1A:16:44:F0:8F:93:38:5B:5B:78:2C:E3:76:60:BA:81"
}
]
}
}
]
}

View File

@@ -475,7 +475,102 @@
}
]
}
},
{
"type": "android",
"info": {
"package_name": "com.talonsec.talon",
"signatures": [
{
"build": "release",
"cert_fingerprint_sha256": "A3:66:03:44:A6:F6:AF:CA:81:8C:BF:43:96:A2:3C:CF:D5:ED:7A:78:1B:B4:A3:D1:85:03:01:E2:F4:6D:23:83"
},
{
"build": "release",
"cert_fingerprint_sha256": "E2:A5:64:74:EA:23:7B:06:67:B6:F5:2C:DC:E9:04:5E:24:88:3B:AE:D0:82:59:9A:A2:DF:0B:60:3A:CF:6A:3B"
}
]
}
},
{
"type": "android",
"info": {
"package_name": "com.talonsec.talon_beta",
"signatures": [
{
"build": "release",
"cert_fingerprint_sha256": "F5:86:62:7A:32:C8:9F:E6:7E:00:6D:B1:8C:34:31:9E:01:7F:B3:B2:BE:D6:9D:01:01:B7:F9:43:E7:7C:48:AE"
},
{
"build": "release",
"cert_fingerprint_sha256": "9A:A1:25:D5:E5:5E:3F:B0:DE:96:72:D9:A9:5D:04:65:3F:49:4A:1E:C3:EE:76:1E:94:C4:4E:5D:2F:65:8E:2F"
}
]
}
},
{
"type": "android",
"info": {
"package_name": "com.duckduckgo.mobile.android.debug",
"signatures": [
{
"build": "release",
"cert_fingerprint_sha256": "C4:F0:9E:2B:D7:25:AD:F5:AD:92:0B:A2:80:27:66:AC:16:4A:C1:53:B3:EA:9E:08:48:B0:57:98:37:F7:6A:29"
}
]
}
},
{
"type": "android",
"info": {
"package_name": "com.duckduckgo.mobile.android",
"signatures": [
{
"build": "release",
"cert_fingerprint_sha256": "BB:7B:B3:1C:57:3C:46:A1:DA:7F:C5:C5:28:A6:AC:F4:32:10:84:56:FE:EC:50:81:0C:7F:33:69:4E:B3:D2:D4"
}
]
}
},
{
"type": "android",
"info": {
"package_name": "com.naver.whale",
"signatures": [
{
"build": "release",
"cert_fingerprint_sha256": "0B:8B:85:23:BB:4A:EF:FA:34:6E:4B:DD:4F:BF:7D:19:34:50:56:9A:A1:4A:AA:D4:AD:FD:94:A3:F7:B2:27:BB"
}
]
}
},
{
"type": "android",
"info": {
"package_name": "com.fido.fido2client",
"signatures": [
{
"build": "release",
"cert_fingerprint_sha256": "FC:98:DA:E6:3A:D3:96:26:C8:C6:7F:BE:83:F2:F0:6F:74:93:2A:9C:D1:46:B9:2C:EC:FC:6A:04:7A:90:43:86"
}
]
}
},
{
"type": "android",
"info": {
"package_name": "com.heytap.browser",
"signatures": [
{
"build": "release",
"cert_fingerprint_sha256": "AF:F8:A7:49:CF:0E:7D:75:44:65:D0:FB:FA:7B:8D:0C:64:5E:22:5C:10:C6:E2:32:AD:A0:D9:74:88:36:B8:E5"
},
{
"build": "release",
"cert_fingerprint_sha256": "A8:FE:A4:CA:FB:93:32:DA:26:B8:E6:81:08:17:C1:DA:90:A5:03:0E:35:A6:0A:79:E0:6C:90:97:AA:C6:A4:42"
}
]
}
}
]
}

View File

@@ -3,7 +3,7 @@ package com.x8bit.bitwarden
import android.app.Application
import com.x8bit.bitwarden.data.auth.manager.AuthRequestNotificationManager
import com.x8bit.bitwarden.data.platform.annotation.OmitFromCoverage
import com.x8bit.bitwarden.data.platform.manager.CrashLogsManager
import com.x8bit.bitwarden.data.platform.manager.LogsManager
import com.x8bit.bitwarden.data.platform.manager.NetworkConfigManager
import com.x8bit.bitwarden.data.platform.manager.event.OrganizationEventManager
import com.x8bit.bitwarden.data.platform.manager.restriction.RestrictionManager
@@ -19,10 +19,10 @@ class BitwardenApplication : Application() {
// Inject classes here that must be triggered on startup but are not otherwise consumed by
// other callers.
@Inject
lateinit var networkConfigManager: NetworkConfigManager
lateinit var logsManager: LogsManager
@Inject
lateinit var crashLogsManager: CrashLogsManager
lateinit var networkConfigManager: NetworkConfigManager
@Inject
lateinit var authRequestNotificationManager: AuthRequestNotificationManager

View File

@@ -5,6 +5,7 @@ import android.os.Parcelable
import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.viewModelScope
import com.bitwarden.vault.CipherView
import com.x8bit.bitwarden.data.auth.manager.AddTotpItemFromAuthenticatorManager
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
import com.x8bit.bitwarden.data.auth.repository.model.EmailTokenResult
import com.x8bit.bitwarden.data.auth.util.getCompleteRegistrationDataIntentOrNull
@@ -23,6 +24,7 @@ import com.x8bit.bitwarden.data.platform.manager.model.CompleteRegistrationData
import com.x8bit.bitwarden.data.platform.manager.model.SpecialCircumstance
import com.x8bit.bitwarden.data.platform.repository.EnvironmentRepository
import com.x8bit.bitwarden.data.platform.repository.SettingsRepository
import com.x8bit.bitwarden.data.platform.util.isAddTotpLoginItemFromAuthenticator
import com.x8bit.bitwarden.data.vault.manager.model.VaultStateEvent
import com.x8bit.bitwarden.data.vault.repository.VaultRepository
import com.x8bit.bitwarden.ui.platform.base.BaseViewModel
@@ -30,8 +32,11 @@ import com.x8bit.bitwarden.ui.platform.base.util.Text
import com.x8bit.bitwarden.ui.platform.base.util.asText
import com.x8bit.bitwarden.ui.platform.feature.settings.appearance.model.AppTheme
import com.x8bit.bitwarden.ui.platform.manager.intent.IntentManager
import com.x8bit.bitwarden.ui.platform.util.isAccountSecurityShortcut
import com.x8bit.bitwarden.ui.platform.util.isMyVaultShortcut
import com.x8bit.bitwarden.ui.platform.util.isPasswordGeneratorShortcut
import com.x8bit.bitwarden.ui.vault.model.TotpData
import com.x8bit.bitwarden.ui.vault.util.getTotpDataOrNull
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.distinctUntilChanged
@@ -56,12 +61,13 @@ private const val SPECIAL_CIRCUMSTANCE_KEY = "special-circumstance"
class MainViewModel @Inject constructor(
accessibilitySelectionManager: AccessibilitySelectionManager,
autofillSelectionManager: AutofillSelectionManager,
private val addTotpItemFromAuthenticatorManager: AddTotpItemFromAuthenticatorManager,
private val specialCircumstanceManager: SpecialCircumstanceManager,
private val garbageCollectionManager: GarbageCollectionManager,
private val fido2CredentialManager: Fido2CredentialManager,
private val intentManager: IntentManager,
settingsRepository: SettingsRepository,
vaultRepository: VaultRepository,
private val vaultRepository: VaultRepository,
private val authRepository: AuthRepository,
private val environmentRepository: EnvironmentRepository,
private val savedStateHandle: SavedStateHandle,
@@ -223,7 +229,7 @@ class MainViewModel @Inject constructor(
)
}
@Suppress("LongMethod")
@Suppress("LongMethod", "CyclomaticComplexMethod")
private fun handleIntent(
intent: Intent,
isFirstIntent: Boolean,
@@ -232,14 +238,39 @@ class MainViewModel @Inject constructor(
val autofillSaveItem = intent.getAutofillSaveItemOrNull()
val autofillSelectionData = intent.getAutofillSelectionDataOrNull()
val shareData = intentManager.getShareDataFromIntent(intent)
val totpData: TotpData? =
// First grab TOTP URI directly from the intent data:
intent.getTotpDataOrNull()
?: run {
// Then check to see if the intent is coming from the Authenticator app:
if (intent.isAddTotpLoginItemFromAuthenticator()) {
addTotpItemFromAuthenticatorManager.pendingAddTotpLoginItemData.also {
// Clear pending add TOTP data so it is only handled once:
addTotpItemFromAuthenticatorManager.pendingAddTotpLoginItemData = null
}
} else {
null
}
}
val hasGeneratorShortcut = intent.isPasswordGeneratorShortcut
val hasVaultShortcut = intent.isMyVaultShortcut
val hasAccountSecurityShortcut = intent.isAccountSecurityShortcut
val fido2CredentialRequestData = intent.getFido2CredentialRequestOrNull()
val completeRegistrationData = intent.getCompleteRegistrationDataIntentOrNull()
val fido2CredentialAssertionRequest = intent.getFido2AssertionRequestOrNull()
val fido2GetCredentialsRequest = intent.getFido2GetCredentialsRequestOrNull()
when {
passwordlessRequestData != null -> {
authRepository.activeUserId?.let {
if (it != passwordlessRequestData.userId &&
!vaultRepository.isVaultUnlocked(it)
) {
// We only switch the account here if the current user's vault is not
// unlocked, otherwise prompt the user to allow us to change the account
// in the LoginApprovalScreen
authRepository.switchAccount(passwordlessRequestData.userId)
}
}
specialCircumstanceManager.specialCircumstance =
SpecialCircumstance.PasswordlessRequest(
passwordlessRequestData = passwordlessRequestData,
@@ -270,6 +301,11 @@ class MainViewModel @Inject constructor(
)
}
totpData != null -> {
specialCircumstanceManager.specialCircumstance =
SpecialCircumstance.AddTotpLoginItem(data = totpData)
}
shareData != null -> {
specialCircumstanceManager.specialCircumstance =
SpecialCircumstance.ShareNewSend(
@@ -320,6 +356,11 @@ class MainViewModel @Inject constructor(
hasVaultShortcut -> {
specialCircumstanceManager.specialCircumstance = SpecialCircumstance.VaultShortcut
}
hasAccountSecurityShortcut -> {
specialCircumstanceManager.specialCircumstance =
SpecialCircumstance.AccountSecurityShortcut
}
}
}

View File

@@ -306,4 +306,19 @@ interface AuthDiskSource {
* if any exists.
*/
fun getOnboardingStatusFlow(userId: String): Flow<OnboardingStatus?>
/**
* Gets the show import logins flag for the given [userId].
*/
fun getShowImportLogins(userId: String): Boolean?
/**
* Stores the show import logins flag for the given [userId].
*/
fun storeShowImportLogins(userId: String, showImportLogins: Boolean?)
/**
* Emits updates that track [getShowImportLogins]. This will replay the last known value,
*/
fun getShowImportLoginsFlow(userId: String): Flow<Boolean?>
}

View File

@@ -45,6 +45,7 @@ private const val SHOULD_TRUST_DEVICE_KEY = "shouldTrustDevice"
private const val TDE_LOGIN_COMPLETE = "tdeLoginComplete"
private const val USES_KEY_CONNECTOR = "usesKeyConnector"
private const val ONBOARDING_STATUS_KEY = "onboardingStatus"
private const val SHOW_IMPORT_LOGINS_KEY = "showImportLogins"
/**
* Primary implementation of [AuthDiskSource].
@@ -72,6 +73,7 @@ class AuthDiskSourceImpl(
mutableMapOf<String, MutableSharedFlow<AccountTokensJson?>>()
private val mutableOnboardingStatusFlowMap =
mutableMapOf<String, MutableSharedFlow<OnboardingStatus?>>()
private val mutableShowImportLoginsFlowMap = mutableMapOf<String, MutableSharedFlow<Boolean?>>()
private val mutableUserStateFlow = bufferedMutableSharedFlow<UserStateJson?>(replay = 1)
override var userState: UserStateJson?
@@ -143,9 +145,11 @@ class AuthDiskSourceImpl(
storeShouldUseKeyConnector(userId = userId, shouldUseKeyConnector = null)
storeIsTdeLoginComplete(userId = userId, isTdeLoginComplete = null)
storeAuthenticatorSyncUnlockKey(userId = userId, authenticatorSyncUnlockKey = null)
storeShowImportLogins(userId = userId, showImportLogins = null)
// Do not remove the DeviceKey or PendingAuthRequest on logout, these are persisted
// indefinitely unless the TDE flow explicitly removes them.
// Do not remove OnboardingStatus we want to keep track of this even after logout.
}
override fun getAuthenticatorSyncUnlockKey(userId: String): String? =
@@ -437,6 +441,22 @@ class AuthDiskSourceImpl(
.onSubscription { emit(getOnboardingStatus(userId = userId)) }
}
override fun getShowImportLogins(userId: String): Boolean? {
return getBoolean(SHOW_IMPORT_LOGINS_KEY.appendIdentifier(userId))
}
override fun storeShowImportLogins(userId: String, showImportLogins: Boolean?) {
putBoolean(
key = SHOW_IMPORT_LOGINS_KEY.appendIdentifier(userId),
value = showImportLogins,
)
getMutableShowImportLoginsFlow(userId = userId).tryEmit(showImportLogins)
}
override fun getShowImportLoginsFlow(userId: String): Flow<Boolean?> =
getMutableShowImportLoginsFlow(userId)
.onSubscription { emit(getShowImportLogins(userId)) }
private fun generateAndStoreUniqueAppId(): String =
UUID
.randomUUID()
@@ -480,6 +500,12 @@ class AuthDiskSourceImpl(
bufferedMutableSharedFlow(replay = 1)
}
private fun getMutableShowImportLoginsFlow(
userId: String,
): MutableSharedFlow<Boolean?> = mutableShowImportLoginsFlowMap.getOrPut(userId) {
bufferedMutableSharedFlow(replay = 1)
}
private fun migrateAccountTokens() {
userState
?.accounts

View File

@@ -96,9 +96,17 @@ sealed class GetTokenResponseJson {
@Serializable
data class Invalid(
@SerialName("ErrorModel")
val errorModel: ErrorModel,
val errorModel: ErrorModel?,
@SerialName("errorModel")
val legacyErrorModel: LegacyErrorModel?,
) : GetTokenResponseJson() {
/**
* The error message returned from the server, or null.
*/
val errorMessage: String?
get() = errorModel?.errorMessage ?: legacyErrorModel?.errorMessage
/**
* The error body of an invalid request containing a message.
*/
@@ -107,6 +115,18 @@ sealed class GetTokenResponseJson {
@SerialName("Message")
val errorMessage: String,
)
/**
* The legacy error body of an invalid request containing a message.
*
* This model is used to support older versions of the error response model that used
* lower-case keys.
*/
@Serializable
data class LegacyErrorModel(
@SerialName("message")
val errorMessage: String,
)
}
/**

View File

@@ -0,0 +1,15 @@
package com.x8bit.bitwarden.data.auth.manager
import com.x8bit.bitwarden.ui.vault.model.TotpData
/**
* Manager for keeping track of requests from the Bitwarden Authenticator app to add a TOTP
* item.
*/
interface AddTotpItemFromAuthenticatorManager {
/**
* Current pending [TotpData] to be added from the Authenticator app.
*/
var pendingAddTotpLoginItemData: TotpData?
}

View File

@@ -0,0 +1,11 @@
package com.x8bit.bitwarden.data.auth.manager
import com.x8bit.bitwarden.ui.vault.model.TotpData
/**
* Default in memory implementation for [AddTotpItemFromAuthenticatorManager].
*/
class AddTotpItemFromAuthenticatorManagerImpl : AddTotpItemFromAuthenticatorManager {
override var pendingAddTotpLoginItemData: TotpData? = null
}

View File

@@ -7,6 +7,8 @@ import com.x8bit.bitwarden.data.auth.datasource.network.service.AuthRequestsServ
import com.x8bit.bitwarden.data.auth.datasource.network.service.DevicesService
import com.x8bit.bitwarden.data.auth.datasource.network.service.NewAuthRequestService
import com.x8bit.bitwarden.data.auth.datasource.sdk.AuthSdkSource
import com.x8bit.bitwarden.data.auth.manager.AddTotpItemFromAuthenticatorManager
import com.x8bit.bitwarden.data.auth.manager.AddTotpItemFromAuthenticatorManagerImpl
import com.x8bit.bitwarden.data.auth.manager.AuthRequestManager
import com.x8bit.bitwarden.data.auth.manager.AuthRequestManagerImpl
import com.x8bit.bitwarden.data.auth.manager.AuthRequestNotificationManager
@@ -124,4 +126,9 @@ object AuthManagerModule {
vaultSdkSource = vaultSdkSource,
dispatcherManager = dispatcherManager,
)
@Provides
@Singleton
fun providesAddTotpItemFromAuthenticatorManager(): AddTotpItemFromAuthenticatorManager =
AddTotpItemFromAuthenticatorManagerImpl()
}

View File

@@ -11,7 +11,7 @@ val AuthRequestType.isSso: Boolean
AuthRequestType.OTHER_DEVICE -> false
AuthRequestType.SSO_OTHER_DEVICE,
AuthRequestType.SSO_ADMIN_APPROVAL,
-> true
-> true
}
/**
@@ -21,7 +21,7 @@ fun AuthRequestType.toAuthRequestTypeJson(): AuthRequestTypeJson =
when (this) {
AuthRequestType.OTHER_DEVICE,
AuthRequestType.SSO_OTHER_DEVICE,
-> AuthRequestTypeJson.LOGIN_WITH_DEVICE
-> AuthRequestTypeJson.LOGIN_WITH_DEVICE
AuthRequestType.SSO_ADMIN_APPROVAL -> AuthRequestTypeJson.ADMIN_APPROVAL
}

View File

@@ -212,6 +212,7 @@ interface AuthRepository : AuthenticatorProvider, AuthRequestManager {
password: String?,
twoFactorData: TwoFactorDataModel,
captchaToken: String?,
orgIdentifier: String?,
): LoginResult
/**
@@ -392,4 +393,9 @@ interface AuthRepository : AuthenticatorProvider, AuthRequestManager {
* Update the value of the onboarding status for the user.
*/
fun setOnboardingStatus(userId: String, status: OnboardingStatus?)
/**
* Update the value of the showImportLogins status for the user.
*/
fun setShowImportLogins(showImportLogins: Boolean)
}

View File

@@ -92,15 +92,20 @@ import com.x8bit.bitwarden.data.auth.repository.util.userSwitchingChangesFlow
import com.x8bit.bitwarden.data.auth.util.KdfParamsConstants.DEFAULT_PBKDF2_ITERATIONS
import com.x8bit.bitwarden.data.auth.util.YubiKeyResult
import com.x8bit.bitwarden.data.auth.util.toSdkParams
import com.x8bit.bitwarden.data.platform.datasource.disk.ConfigDiskSource
import com.x8bit.bitwarden.data.platform.manager.FeatureFlagManager
import com.x8bit.bitwarden.data.platform.manager.FirstTimeActionManager
import com.x8bit.bitwarden.data.platform.manager.LogsManager
import com.x8bit.bitwarden.data.platform.manager.PolicyManager
import com.x8bit.bitwarden.data.platform.manager.PushManager
import com.x8bit.bitwarden.data.platform.manager.dispatcher.DispatcherManager
import com.x8bit.bitwarden.data.platform.manager.model.FirstTimeState
import com.x8bit.bitwarden.data.platform.manager.model.FlagKey
import com.x8bit.bitwarden.data.platform.manager.util.getActivePolicies
import com.x8bit.bitwarden.data.platform.repository.EnvironmentRepository
import com.x8bit.bitwarden.data.platform.repository.SettingsRepository
import com.x8bit.bitwarden.data.platform.repository.util.bufferedMutableSharedFlow
import com.x8bit.bitwarden.data.platform.repository.util.toEnvironmentUrls
import com.x8bit.bitwarden.data.platform.util.asFailure
import com.x8bit.bitwarden.data.platform.util.asSuccess
import com.x8bit.bitwarden.data.platform.util.flatMap
@@ -151,6 +156,7 @@ class AuthRepositoryImpl(
private val authSdkSource: AuthSdkSource,
private val vaultSdkSource: VaultSdkSource,
private val authDiskSource: AuthDiskSource,
private val configDiskSource: ConfigDiskSource,
private val environmentRepository: EnvironmentRepository,
private val settingsRepository: SettingsRepository,
private val vaultRepository: VaultRepository,
@@ -160,6 +166,8 @@ class AuthRepositoryImpl(
private val userLogoutManager: UserLogoutManager,
private val policyManager: PolicyManager,
private val featureFlagManager: FeatureFlagManager,
firstTimeActionManager: FirstTimeActionManager,
logsManager: LogsManager,
pushManager: PushManager,
dispatcherManager: DispatcherManager,
) : AuthRepository,
@@ -254,6 +262,7 @@ class AuthRepositoryImpl(
authDiskSource.userOrganizationsListFlow,
authDiskSource.userKeyConnectorStateFlow,
authDiskSource.onboardingStatusChangesFlow,
firstTimeActionManager.firstTimeStateFlow,
vaultRepository.vaultUnlockDataStateFlow,
mutableHasPendingAccountAdditionStateFlow,
// Ignore the data in the merge, but trigger an update when they emit.
@@ -267,8 +276,9 @@ class AuthRepositoryImpl(
val userOrganizationsList = array[2] as List<UserOrganizations>
val userIsUsingKeyConnectorList = array[3] as List<UserKeyConnectorState>
val onboardingStatus = array[4] as OnboardingStatus?
val vaultState = array[5] as List<VaultUnlockData>
val hasPendingAccountAddition = array[6] as Boolean
val firstTimeState = array[5] as FirstTimeState
val vaultState = array[6] as List<VaultUnlockData>
val hasPendingAccountAddition = array[7] as Boolean
userStateJson?.toUserState(
vaultState = vaultState,
userAccountTokens = userAccountTokens,
@@ -279,6 +289,7 @@ class AuthRepositoryImpl(
isBiometricsEnabledProvider = ::isBiometricsEnabled,
vaultUnlockTypeProvider = ::getVaultUnlockType,
isDeviceTrustedProvider = ::isDeviceTrusted,
firstTimeState = firstTimeState,
)
}
.filterNot { mutableHasPendingAccountDeletionStateFlow.value }
@@ -298,6 +309,7 @@ class AuthRepositoryImpl(
isBiometricsEnabledProvider = ::isBiometricsEnabled,
vaultUnlockTypeProvider = ::getVaultUnlockType,
isDeviceTrustedProvider = ::isDeviceTrusted,
firstTimeState = firstTimeActionManager.currentOrDefaultUserFirstTimeState,
),
)
@@ -354,6 +366,24 @@ class AuthRepositoryImpl(
featureFlagManager.getFeatureFlag(FlagKey.OnboardingCarousel)
init {
combine(
mutableHasPendingAccountAdditionStateFlow,
authDiskSource.userStateFlow,
environmentRepository.environmentStateFlow,
) { hasPendingAddition, userState, environment ->
logsManager.setUserData(
userId = userState?.activeUserId.takeUnless { hasPendingAddition },
environmentType = userState
?.activeAccount
?.settings
?.environmentUrlData
?.toEnvironmentUrls()
?.type
.takeUnless { hasPendingAddition }
?: environment.type,
)
}
.launchIn(unconfinedScope)
pushManager
.syncOrgKeysFlow
.onEach {
@@ -628,6 +658,7 @@ class AuthRepositoryImpl(
password: String?,
twoFactorData: TwoFactorDataModel,
captchaToken: String?,
orgIdentifier: String?,
): LoginResult = identityTokenAuthModel
?.let {
loginCommon(
@@ -637,6 +668,7 @@ class AuthRepositoryImpl(
twoFactorData = twoFactorData,
captchaToken = captchaToken ?: twoFactorResponse?.captchaToken,
deviceData = twoFactorDeviceData,
orgIdentifier = orgIdentifier,
)
}
?: LoginResult.Error(errorMessage = null)
@@ -998,7 +1030,7 @@ class AuthRepositoryImpl(
ForcePasswordResetReason.ADMIN_FORCE_PASSWORD_RESET,
ForcePasswordResetReason.WEAK_MASTER_PASSWORD_ON_LOGIN,
null,
-> {
-> {
authSdkSource
.makeRegisterKeys(
email = activeAccount.profile.email,
@@ -1048,7 +1080,7 @@ class AuthRepositoryImpl(
is VaultUnlockResult.AuthenticationError,
VaultUnlockResult.InvalidStateError,
VaultUnlockResult.GenericError,
-> {
-> {
IllegalStateException("Failed to unlock vault").asFailure()
}
}
@@ -1295,6 +1327,11 @@ class AuthRepositoryImpl(
authDiskSource.storeOnboardingStatus(userId = userId, onboardingStatus = status)
}
override fun setShowImportLogins(showImportLogins: Boolean) {
val userId: String = activeUserId ?: return
authDiskSource.storeShowImportLogins(userId = userId, showImportLogins = showImportLogins)
}
@Suppress("CyclomaticComplexMethod")
private suspend fun validatePasswordAgainstPolicy(
password: String,
@@ -1459,7 +1496,12 @@ class AuthRepositoryImpl(
captchaToken = captchaToken,
)
.fold(
onFailure = { LoginResult.Error(errorMessage = null) },
onFailure = {
when (configDiskSource.serverConfig?.isOfficialBitwardenServer) {
false -> LoginResult.UnofficialServerError
else -> LoginResult.Error(errorMessage = null)
}
},
onSuccess = { loginResponse ->
when (loginResponse) {
is GetTokenResponseJson.CaptchaRequired -> LoginResult.CaptchaRequired(
@@ -1482,7 +1524,7 @@ class AuthRepositoryImpl(
)
is GetTokenResponseJson.Invalid -> LoginResult.Error(
errorMessage = loginResponse.errorModel.errorMessage,
errorMessage = loginResponse.errorMessage,
)
}
},

View File

@@ -13,7 +13,10 @@ import com.x8bit.bitwarden.data.auth.manager.TrustedDeviceManager
import com.x8bit.bitwarden.data.auth.manager.UserLogoutManager
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
import com.x8bit.bitwarden.data.auth.repository.AuthRepositoryImpl
import com.x8bit.bitwarden.data.platform.datasource.disk.ConfigDiskSource
import com.x8bit.bitwarden.data.platform.manager.FeatureFlagManager
import com.x8bit.bitwarden.data.platform.manager.FirstTimeActionManager
import com.x8bit.bitwarden.data.platform.manager.LogsManager
import com.x8bit.bitwarden.data.platform.manager.PolicyManager
import com.x8bit.bitwarden.data.platform.manager.PushManager
import com.x8bit.bitwarden.data.platform.manager.dispatcher.DispatcherManager
@@ -45,6 +48,7 @@ object AuthRepositoryModule {
authSdkSource: AuthSdkSource,
vaultSdkSource: VaultSdkSource,
authDiskSource: AuthDiskSource,
configDiskSource: ConfigDiskSource,
dispatcherManager: DispatcherManager,
environmentRepository: EnvironmentRepository,
settingsRepository: SettingsRepository,
@@ -56,6 +60,8 @@ object AuthRepositoryModule {
pushManager: PushManager,
policyManager: PolicyManager,
featureFlagManager: FeatureFlagManager,
firstTimeActionManager: FirstTimeActionManager,
logsManager: LogsManager,
): AuthRepository = AuthRepositoryImpl(
accountsService = accountsService,
devicesService = devicesService,
@@ -64,6 +70,7 @@ object AuthRepositoryModule {
authSdkSource = authSdkSource,
vaultSdkSource = vaultSdkSource,
authDiskSource = authDiskSource,
configDiskSource = configDiskSource,
haveIBeenPwnedService = haveIBeenPwnedService,
dispatcherManager = dispatcherManager,
environmentRepository = environmentRepository,
@@ -76,5 +83,7 @@ object AuthRepositoryModule {
pushManager = pushManager,
policyManager = policyManager,
featureFlagManager = featureFlagManager,
firstTimeActionManager = firstTimeActionManager,
logsManager = logsManager,
)
}

View File

@@ -23,4 +23,9 @@ sealed class LoginResult {
* There was an error logging in.
*/
data class Error(val errorMessage: String?) : LoginResult()
/**
* There was an error while logging into an unofficial Bitwarden server.
*/
data object UnofficialServerError : LoginResult()
}

View File

@@ -11,5 +11,5 @@ fun VaultUnlockError.toLoginErrorResult(): LoginResult.Error = when (this) {
is VaultUnlockResult.AuthenticationError -> LoginResult.Error(this.message)
VaultUnlockResult.GenericError,
VaultUnlockResult.InvalidStateError,
-> LoginResult.Error(errorMessage = null)
-> LoginResult.Error(errorMessage = null)
}

View File

@@ -1,6 +1,5 @@
package com.x8bit.bitwarden.data.auth.repository.model
import com.x8bit.bitwarden.data.vault.datasource.network.model.SyncResponseJson
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable

View File

@@ -2,6 +2,7 @@ package com.x8bit.bitwarden.data.auth.repository.model
import com.x8bit.bitwarden.data.auth.datasource.disk.model.OnboardingStatus
import com.x8bit.bitwarden.data.auth.repository.model.UserState.Account
import com.x8bit.bitwarden.data.platform.manager.model.FirstTimeState
import com.x8bit.bitwarden.data.platform.repository.model.Environment
/**
@@ -29,6 +30,9 @@ data class UserState(
val activeAccount: Account
get() = accounts.first { it.userId == activeUserId }
val activeUserFirstTimeState: FirstTimeState
get() = activeAccount.firstTimeState
/**
* Basic account information about a given user.
*
@@ -71,6 +75,7 @@ data class UserState(
val vaultUnlockType: VaultUnlockType = VaultUnlockType.MASTER_PASSWORD,
val isUsingKeyConnector: Boolean,
val onboardingStatus: OnboardingStatus,
val firstTimeState: FirstTimeState,
) {
/**
* Indicates that the user does or does not have a means to manually unlock the vault.

View File

@@ -176,13 +176,16 @@ val AuthDiskSource.activeUserIdChangesFlow: Flow<String?>
@OptIn(ExperimentalCoroutinesApi::class)
val AuthDiskSource.onboardingStatusChangesFlow: Flow<OnboardingStatus?>
get() = activeUserIdChangesFlow
.flatMapLatest { activeUserId ->
activeUserId
?.let { this.getOnboardingStatusFlow(userId = it) }
?: flowOf(null)
}
.distinctUntilChanged()
.flatMapLatest { activeUserId ->
activeUserId
?.let { this.getOnboardingStatusFlow(userId = it) }
?: flowOf(null)
}
.distinctUntilChanged()
/**
* Returns the current [OnboardingStatus] of the active user.
*/
val AuthDiskSource.currentOnboardingStatus: OnboardingStatus?
get() = this
.userState

View File

@@ -8,6 +8,7 @@ import com.x8bit.bitwarden.data.auth.repository.model.UserKeyConnectorState
import com.x8bit.bitwarden.data.auth.repository.model.UserOrganizations
import com.x8bit.bitwarden.data.auth.repository.model.UserState
import com.x8bit.bitwarden.data.auth.repository.model.VaultUnlockType
import com.x8bit.bitwarden.data.platform.manager.model.FirstTimeState
import com.x8bit.bitwarden.data.platform.repository.util.toEnvironmentUrlsOrDefault
import com.x8bit.bitwarden.data.vault.datasource.network.model.OrganizationType
import com.x8bit.bitwarden.data.vault.datasource.network.model.SyncResponseJson
@@ -111,6 +112,7 @@ fun UserStateJson.toUserState(
userIsUsingKeyConnectorList: List<UserKeyConnectorState>,
hasPendingAccountAddition: Boolean,
onboardingStatus: OnboardingStatus?,
firstTimeState: FirstTimeState,
isBiometricsEnabledProvider: (userId: String) -> Boolean,
vaultUnlockTypeProvider: (userId: String) -> VaultUnlockType,
isDeviceTrustedProvider: (userId: String) -> Boolean,
@@ -137,9 +139,6 @@ fun UserStateJson.toUserState(
it.role == OrganizationType.ADMIN ||
it.shouldManageResetPassword
}
val needsMasterPassword = decryptionOptions?.hasMasterPassword == false &&
hasManageResetPasswordPermission &&
keyConnectorOptions == null
val trustedDevice = trustedDeviceOptions?.let {
UserState.TrustedDevice(
isDeviceTrusted = isDeviceTrustedProvider(userId),
@@ -148,7 +147,14 @@ fun UserStateJson.toUserState(
hasResetPasswordPermission = it.hasManageResetPasswordPermission,
)
}
// If a user does not have a Master Password we want to check if they have another
// method for unlocking the vault. In the case of a TDE user we check if they
// have the reset password permission via their organization(S). If the user does
// not belong to a TDE or we check to see if they user key connector.
val tdeUserNeedsMasterPassword =
hasManageResetPasswordPermission.takeIf { trustedDevice != null }
val needsMasterPassword = decryptionOptions?.hasMasterPassword == false &&
(tdeUserNeedsMasterPassword ?: (keyConnectorOptions == null))
UserState.Account(
userId = userId,
name = profile.name,
@@ -176,6 +182,7 @@ fun UserStateJson.toUserState(
// If the user exists with no onboarding status we can assume they have been
// using the app prior to the release of the onboarding flow.
onboardingStatus = onboardingStatus ?: OnboardingStatus.COMPLETE,
firstTimeState = firstTimeState,
)
},
hasPendingAccountAddition = hasPendingAccountAddition,

View File

@@ -1,11 +1,10 @@
package com.x8bit.bitwarden.data.autofill.accessibility.manager
import android.net.Uri
import android.util.Log
import android.view.accessibility.AccessibilityNodeInfo
import com.x8bit.bitwarden.BuildConfig
import com.x8bit.bitwarden.data.autofill.accessibility.util.getKnownUsernameFieldNull
import com.x8bit.bitwarden.data.autofill.accessibility.util.isUsername
import timber.log.Timber
private const val MAX_NODE_COUNT: Int = 100
@@ -90,7 +89,6 @@ class AccessibilityNodeInfoManagerImpl : AccessibilityNodeInfoManager {
?.let { allNodes.getOrNull(index = allNodes.indexOf(element = it) - 1) }
private fun log(message: String) {
if (!BuildConfig.DEBUG) return
Log.i("AccessibilityNodeInfoManager", message)
Timber.i(message)
}
}

View File

@@ -21,7 +21,6 @@ import com.x8bit.bitwarden.data.autofill.processor.AutofillProcessor
import com.x8bit.bitwarden.data.autofill.processor.AutofillProcessorImpl
import com.x8bit.bitwarden.data.autofill.provider.AutofillCipherProvider
import com.x8bit.bitwarden.data.autofill.provider.AutofillCipherProviderImpl
import com.x8bit.bitwarden.data.platform.manager.CrashLogsManager
import com.x8bit.bitwarden.data.platform.manager.PolicyManager
import com.x8bit.bitwarden.data.platform.manager.ciphermatching.CipherMatchingManager
import com.x8bit.bitwarden.data.platform.manager.clipboard.BitwardenClipboardManager
@@ -121,7 +120,6 @@ object AutofillModule {
policyManager: PolicyManager,
saveInfoBuilder: SaveInfoBuilder,
settingsRepository: SettingsRepository,
crashLogsManager: CrashLogsManager,
): AutofillProcessor =
AutofillProcessorImpl(
dispatcherManager = dispatcherManager,
@@ -131,7 +129,6 @@ object AutofillModule {
policyManager = policyManager,
saveInfoBuilder = saveInfoBuilder,
settingsRepository = settingsRepository,
crashLogsManager = crashLogsManager,
)
@Singleton

View File

@@ -2,6 +2,8 @@ package com.x8bit.bitwarden.data.autofill.fido2.manager
import androidx.credentials.provider.CallingAppInfo
import com.bitwarden.fido.ClientData
import com.bitwarden.fido.Origin
import com.bitwarden.fido.UnverifiedAssetLink
import com.bitwarden.sdk.Fido2CredentialStore
import com.bitwarden.vault.CipherView
import com.x8bit.bitwarden.data.autofill.fido2.datasource.network.model.DigitalAssetLinkResponseJson
@@ -24,11 +26,13 @@ import com.x8bit.bitwarden.data.vault.datasource.sdk.model.AuthenticateFido2Cred
import com.x8bit.bitwarden.data.vault.datasource.sdk.model.RegisterFido2CredentialRequest
import com.x8bit.bitwarden.data.vault.datasource.sdk.util.toAndroidAttestationResponse
import com.x8bit.bitwarden.data.vault.datasource.sdk.util.toAndroidFido2PublicKeyCredential
import com.x8bit.bitwarden.ui.platform.base.util.toHostOrPathOrNull
import kotlinx.serialization.SerializationException
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
private const val ALLOW_LIST_FILE_NAME = "fido2_privileged_allow_list.json"
private const val GOOGLE_ALLOW_LIST_FILE_NAME = "fido2_privileged_google.json"
private const val COMMUNITY_ALLOW_LIST_FILE_NAME = "fido2_privileged_community.json"
/**
* Primary implementation of [Fido2CredentialManager].
@@ -65,11 +69,23 @@ class Fido2CredentialManagerImpl(
.packageName,
)
}
val origin = fido2CredentialRequest
val assetLinkUrl = fido2CredentialRequest
.origin
?: getOriginUrlFromAttestationOptionsOrNull(fido2CredentialRequest.requestJson)
?: return Fido2RegisterCredentialResult.Error
val origin = Origin.Android(
UnverifiedAssetLink(
packageName = fido2CredentialRequest.packageName,
sha256CertFingerprint = fido2CredentialRequest
.callingAppInfo
.getSignatureFingerprintAsHexString()
?: return Fido2RegisterCredentialResult.Error,
host = assetLinkUrl.toHostOrPathOrNull()
?: return Fido2RegisterCredentialResult.Error,
assetLinkUrl = assetLinkUrl,
),
)
return vaultSdkSource
.registerFido2Credential(
request = RegisterFido2CredentialRequest(
@@ -157,7 +173,16 @@ class Fido2CredentialManagerImpl(
.authenticateFido2Credential(
request = AuthenticateFido2CredentialRequest(
userId = userId,
origin = origin,
origin = Origin.Android(
UnverifiedAssetLink(
callingAppInfo.packageName,
callingAppInfo.getSignatureFingerprintAsHexString()
?: return Fido2CredentialAssertionResult.Error,
origin.toHostOrPathOrNull()
?: return Fido2CredentialAssertionResult.Error,
origin,
),
),
requestJson = """{"publicKey": ${request.requestJson}}""",
clientData = clientData,
selectedCipherView = selectedCipherView,
@@ -179,7 +204,8 @@ class Fido2CredentialManagerImpl(
callingAppInfo: CallingAppInfo,
relyingPartyId: String,
): Fido2ValidateOriginResult {
return digitalAssetLinkService.getDigitalAssetLinkForRp(relyingParty = relyingPartyId)
return digitalAssetLinkService
.getDigitalAssetLinkForRp(relyingParty = relyingPartyId)
.onFailure {
return Fido2ValidateOriginResult.Error.AssetLinkNotFound
}
@@ -191,7 +217,8 @@ class Fido2CredentialManagerImpl(
?: return Fido2ValidateOriginResult.Error.ApplicationNotFound
}
.map { matchingStatements ->
callingAppInfo.getSignatureFingerprintAsHexString()
callingAppInfo
.getSignatureFingerprintAsHexString()
?.let { certificateFingerprint ->
matchingStatements
.filterMatchingAppSignaturesOrNull(
@@ -212,9 +239,46 @@ class Fido2CredentialManagerImpl(
private suspend fun validatePrivilegedAppOrigin(
callingAppInfo: CallingAppInfo,
): Fido2ValidateOriginResult {
val googleAllowListResult =
validatePrivilegedAppSignatureWithGoogleList(callingAppInfo)
return when (googleAllowListResult) {
is Fido2ValidateOriginResult.Success -> {
// Application was found and successfully validated against the Google allow list so
// we can return the result as the final validation result.
googleAllowListResult
}
is Fido2ValidateOriginResult.Error -> {
// Check the community allow list if the Google allow list failed, and return the
// result as the final validation result.
validatePrivilegedAppSignatureWithCommunityList(callingAppInfo)
}
}
}
private suspend fun validatePrivilegedAppSignatureWithGoogleList(
callingAppInfo: CallingAppInfo,
): Fido2ValidateOriginResult =
validatePrivilegedAppSignatureWithAllowList(
callingAppInfo = callingAppInfo,
fileName = GOOGLE_ALLOW_LIST_FILE_NAME,
)
private suspend fun validatePrivilegedAppSignatureWithCommunityList(
callingAppInfo: CallingAppInfo,
): Fido2ValidateOriginResult =
validatePrivilegedAppSignatureWithAllowList(
callingAppInfo = callingAppInfo,
fileName = COMMUNITY_ALLOW_LIST_FILE_NAME,
)
private suspend fun validatePrivilegedAppSignatureWithAllowList(
callingAppInfo: CallingAppInfo,
fileName: String,
): Fido2ValidateOriginResult =
assetManager
.readAsset(ALLOW_LIST_FILE_NAME)
.readAsset(fileName)
.map { allowList ->
callingAppInfo.validatePrivilegedApp(
allowList = allowList,

View File

@@ -10,6 +10,7 @@ import kotlinx.parcelize.Parcelize
*/
@Parcelize
data class Fido2CredentialAssertionRequest(
val userId: String,
val cipherId: String?,
val credentialId: String?,
val requestJson: String,

View File

@@ -14,6 +14,7 @@ import kotlinx.parcelize.Parcelize
data class Fido2GetCredentialsRequest(
val candidateQueryData: Bundle,
val id: String,
val userId: String,
val requestJson: String,
val clientDataHash: ByteArray? = null,
val packageName: String,

View File

@@ -10,11 +10,13 @@ sealed class Fido2GetCredentialsResult {
/**
* Indicates credentials were successfully queried.
*
* @param userId ID of the user whose credentials were queried.
* @param options Original request options provided by the relying party.
* @param credentials Collection of [Fido2CredentialAutofillView]s matching the original request
* parameters. This may be an empty list if no matching values were found.
*/
data class Success(
val userId: String,
val options: BeginGetPublicKeyCredentialOption,
val credentials: List<Fido2CredentialAutofillView>,
) : Fido2GetCredentialsResult()

View File

@@ -161,6 +161,7 @@ class Fido2ProviderProcessorImpl(
title = context.getString(R.string.unlock),
pendingIntent = intentManager.createFido2UnlockPendingIntent(
action = UNLOCK_ACCOUNT_INTENT,
userId = userState.activeUserId,
requestCode = requestCode.getAndIncrement(),
),
)
@@ -209,13 +210,14 @@ class Fido2ProviderProcessorImpl(
.getPasskeyAssertionOptionsOrNull(requestJson = option.requestJson)
?.relyingPartyId
?: throw GetCredentialUnknownException("Invalid data.")
buildCredentialEntries(relyingPartyId, option)
buildCredentialEntries(userId, relyingPartyId, option)
} else {
throw GetCredentialUnsupportedException("Unsupported option.")
}
}
private suspend fun buildCredentialEntries(
userId: String,
relyingPartyId: String,
option: BeginGetPublicKeyCredentialOption,
): List<CredentialEntry> {
@@ -236,12 +238,16 @@ class Fido2ProviderProcessorImpl(
result
.fido2CredentialAutofillViews
.filter { it.rpId == relyingPartyId }
.toCredentialEntries(option)
.toCredentialEntries(
userId = userId,
option = option,
)
}
}
}
private fun List<Fido2CredentialAutofillView>.toCredentialEntries(
userId: String,
option: BeginGetPublicKeyCredentialOption,
): List<CredentialEntry> =
this
@@ -253,6 +259,7 @@ class Fido2ProviderProcessorImpl(
pendingIntent = intentManager
.createFido2GetCredentialPendingIntent(
action = GET_PASSKEY_INTENT,
userId = userId,
credentialId = it.credentialId.toString(),
cipherId = it.cipherId,
requestCode = requestCode.getAndIncrement(),

View File

@@ -64,7 +64,11 @@ fun Intent.getFido2AssertionRequestOrNull(): Fido2CredentialAssertionRequest? {
val cipherId = getStringExtra(EXTRA_KEY_CIPHER_ID)
?: return null
val userId: String = getStringExtra(EXTRA_KEY_USER_ID)
?: return null
return Fido2CredentialAssertionRequest(
userId = userId,
cipherId = cipherId,
credentialId = credentialId,
requestJson = option.requestJson,
@@ -95,9 +99,13 @@ fun Intent.getFido2GetCredentialsRequestOrNull(): Fido2GetCredentialsRequest? {
.callingAppInfo
?: return null
val userId: String = getStringExtra(EXTRA_KEY_USER_ID)
?: return null
return Fido2GetCredentialsRequest(
candidateQueryData = option.candidateQueryData,
id = option.id,
userId = userId,
requestJson = option.requestJson,
clientDataHash = option.clientDataHash,
packageName = callingAppInfo.packageName,

View File

@@ -48,7 +48,7 @@ sealed class AutofillCipher {
val number: String,
) : AutofillCipher() {
override val iconRes: Int
@DrawableRes get() = R.drawable.ic_card_item
@DrawableRes get() = R.drawable.ic_payment_card
override val isTotpEnabled: Boolean
get() = false
@@ -67,6 +67,6 @@ sealed class AutofillCipher {
val username: String,
) : AutofillCipher() {
override val iconRes: Int
@DrawableRes get() = R.drawable.ic_login_item
@DrawableRes get() = R.drawable.ic_globe
}
}

View File

@@ -13,7 +13,6 @@ import com.x8bit.bitwarden.data.autofill.model.AutofillRequest
import com.x8bit.bitwarden.data.autofill.parser.AutofillParser
import com.x8bit.bitwarden.data.autofill.util.createAutofillSavedItemIntentSender
import com.x8bit.bitwarden.data.autofill.util.toAutofillSaveItem
import com.x8bit.bitwarden.data.platform.manager.CrashLogsManager
import com.x8bit.bitwarden.data.platform.manager.PolicyManager
import com.x8bit.bitwarden.data.platform.manager.dispatcher.DispatcherManager
import com.x8bit.bitwarden.data.platform.repository.SettingsRepository
@@ -21,6 +20,7 @@ import com.x8bit.bitwarden.data.vault.datasource.network.model.PolicyTypeJson
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
import timber.log.Timber
/**
* The default implementation of [AutofillProcessor]. Its purpose is to handle autofill related
@@ -35,7 +35,6 @@ class AutofillProcessorImpl(
private val parser: AutofillParser,
private val saveInfoBuilder: SaveInfoBuilder,
private val settingsRepository: SettingsRepository,
private val crashLogsManager: CrashLogsManager,
) : AutofillProcessor {
/**
@@ -146,7 +145,7 @@ class AutofillProcessorImpl(
} catch (e: RuntimeException) {
// This is to catch any TransactionTooLargeExceptions that could occur here.
// These exceptions get wrapped as a RuntimeException.
crashLogsManager.trackNonFatalException(e)
Timber.e(e, "Autofill Error")
}
}

View File

@@ -68,6 +68,12 @@ interface SettingsDiskSource {
*/
val hasUserLoggedInOrCreatedAccountFlow: Flow<Boolean?>
/**
* The instant when the last database scheme change was applied. `null` if no scheme changes
* have been applied yet.
*/
var lastDatabaseSchemeChangeInstant: Instant?
/**
* Clears all the settings data for the given user.
*/

View File

@@ -35,6 +35,7 @@ private const val INITIAL_AUTOFILL_DIALOG_SHOWN = "addSitePromptShown"
private const val HAS_USER_LOGGED_IN_OR_CREATED_AN_ACCOUNT_KEY = "hasUserLoggedInOrCreatedAccount"
private const val SHOW_AUTOFILL_SETTING_BADGE = "showAutofillSettingBadge"
private const val SHOW_UNLOCK_SETTING_BADGE = "showUnlockSettingBadge"
private const val LAST_SCHEME_CHANGE_INSTANT = "lastDatabaseSchemeChangeInstant"
/**
* Primary implementation of [SettingsDiskSource].
@@ -151,6 +152,10 @@ class SettingsDiskSourceImpl(
get() = mutableHasUserLoggedInOrCreatedAccountFlow
.onSubscription { emit(getBoolean(HAS_USER_LOGGED_IN_OR_CREATED_AN_ACCOUNT_KEY)) }
override var lastDatabaseSchemeChangeInstant: Instant?
get() = getLong(LAST_SCHEME_CHANGE_INSTANT)?.let { Instant.ofEpochMilli(it) }
set(value) = putLong(LAST_SCHEME_CHANGE_INSTANT, value?.toEpochMilli())
override fun clearData(userId: String) {
storeVaultTimeoutInMinutes(userId = userId, vaultTimeoutInMinutes = null)
storeVaultTimeoutAction(userId = userId, vaultTimeoutAction = null)
@@ -167,6 +172,8 @@ class SettingsDiskSourceImpl(
// The following are intentionally not cleared so they can be
// restored after logging out and back in:
// - screen capture allowed
// - show autofill setting badge
// - show unlock setting badge
}
override fun getAccountBiometricIntegrityValidity(

View File

@@ -26,8 +26,10 @@ import com.x8bit.bitwarden.data.platform.datasource.disk.legacy.LegacySecureStor
import com.x8bit.bitwarden.data.platform.datasource.disk.legacy.LegacySecureStorageImpl
import com.x8bit.bitwarden.data.platform.datasource.disk.legacy.LegacySecureStorageMigrator
import com.x8bit.bitwarden.data.platform.datasource.disk.legacy.LegacySecureStorageMigratorImpl
import com.x8bit.bitwarden.data.platform.manager.DatabaseSchemeManager
import com.x8bit.bitwarden.data.platform.manager.dispatcher.DispatcherManager
import com.x8bit.bitwarden.data.platform.repository.SettingsRepository
import com.x8bit.bitwarden.data.vault.datasource.disk.callback.DatabaseSchemeCallback
import com.x8bit.bitwarden.data.vault.datasource.disk.convertor.ZonedDateTimeTypeConverter
import dagger.Module
import dagger.Provides
@@ -35,6 +37,7 @@ import dagger.hilt.InstallIn
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent
import kotlinx.serialization.json.Json
import java.time.Clock
import javax.inject.Singleton
/**
@@ -68,7 +71,11 @@ object PlatformDiskModule {
@Provides
@Singleton
fun provideEventDatabase(app: Application): PlatformDatabase =
fun provideEventDatabase(
app: Application,
databaseSchemeManager: DatabaseSchemeManager,
clock: Clock,
): PlatformDatabase =
Room
.databaseBuilder(
context = app,
@@ -77,6 +84,12 @@ object PlatformDiskModule {
)
.fallbackToDestructiveMigration()
.addTypeConverter(ZonedDateTimeTypeConverter())
.addCallback(
DatabaseSchemeCallback(
databaseSchemeManager = databaseSchemeManager,
clock = clock,
),
)
.build()
@Provides

View File

@@ -19,4 +19,10 @@ data class ServerConfig(
@SerialName("serverData")
val serverData: ConfigResponseJson,
)
) {
/**
* Whether the server is an official Bitwarden server or not.
*/
val isOfficialBitwardenServer: Boolean
get() = serverData.server == null
}

View File

@@ -9,6 +9,7 @@ import retrofit2.Call
import retrofit2.Callback
import retrofit2.HttpException
import retrofit2.Response
import timber.log.Timber
import java.lang.reflect.Type
/**
@@ -19,6 +20,7 @@ private const val NO_CONTENT_RESPONSE_CODE: Int = 204
/**
* A [Call] for wrapping a network request into a [Result].
*/
@Suppress("TooManyFunctions")
class ResultCall<T>(
private val backingCall: Call<T>,
private val successType: Type,
@@ -34,7 +36,7 @@ class ResultCall<T>(
}
override fun onFailure(call: Call<T>, t: Throwable) {
callback.onResponse(this@ResultCall, Response.success(t.asFailure()))
callback.onResponse(this@ResultCall, Response.success(t.toFailure()))
}
},
)
@@ -44,9 +46,9 @@ class ResultCall<T>(
try {
Response.success(backingCall.execute().toResult())
} catch (ioException: IOException) {
Response.success(ioException.asFailure())
Response.success(ioException.toFailure())
} catch (runtimeException: RuntimeException) {
Response.success(runtimeException.asFailure())
Response.success(runtimeException.toFailure())
}
override fun isCanceled(): Boolean = backingCall.isCanceled
@@ -62,9 +64,14 @@ class ResultCall<T>(
*/
fun executeForResult(): Result<T> = requireNotNull(execute().body())
private fun Throwable.toFailure(): Result<T> =
this
.also { Timber.w(it, "Network Error: ${backingCall.request().url}") }
.asFailure()
private fun Response<T>.toResult(): Result<T> =
if (!this.isSuccessful) {
HttpException(this).asFailure()
HttpException(this).toFailure()
} else {
val body = this.body()
@Suppress("UNCHECKED_CAST")
@@ -76,7 +83,7 @@ class ResultCall<T>(
// We allow null for 204's, just return null.
this.code() == NO_CONTENT_RESPONSE_CODE -> (null as T).asSuccess()
// All other null bodies result in an error.
else -> IllegalStateException("Unexpected null body!").asFailure()
else -> IllegalStateException("Unexpected null body!").toFailure()
}
}
}

View File

@@ -58,7 +58,9 @@ object PlatformNetworkModule {
@Provides
@Singleton
fun providesAuthTokenInterceptor(): AuthTokenInterceptor = AuthTokenInterceptor()
fun providesAuthTokenInterceptor(
authDiskSource: AuthDiskSource,
): AuthTokenInterceptor = AuthTokenInterceptor(authDiskSource = authDiskSource)
@Provides
@Singleton

View File

@@ -1,5 +1,6 @@
package com.x8bit.bitwarden.data.platform.datasource.network.interceptor
import com.x8bit.bitwarden.data.auth.datasource.disk.AuthDiskSource
import com.x8bit.bitwarden.data.platform.datasource.network.util.HEADER_KEY_AUTHORIZATION
import com.x8bit.bitwarden.data.platform.datasource.network.util.HEADER_VALUE_BEARER_PREFIX
import okhttp3.Interceptor
@@ -11,11 +12,20 @@ import javax.inject.Singleton
* Interceptor responsible for adding the auth token(Bearer) to API requests.
*/
@Singleton
class AuthTokenInterceptor : Interceptor {
class AuthTokenInterceptor(
private val authDiskSource: AuthDiskSource,
) : Interceptor {
/**
* The auth token to be added to API requests.
*
* Note: This is done on demand to ensure that no race conditions can exist when retrieving the
* token.
*/
var authToken: String? = null
private val authToken: String?
get() = authDiskSource
.userState
?.activeUserId
?.let { userId -> authDiskSource.getAccountTokens(userId = userId)?.accessToken }
private val missingTokenMessage = "Auth token is missing!"

View File

@@ -1,7 +1,5 @@
package com.x8bit.bitwarden.data.platform.datasource.network.retrofit
import android.util.Log
import com.x8bit.bitwarden.BuildConfig
import com.x8bit.bitwarden.data.platform.datasource.network.authenticator.RefreshAuthenticator
import com.x8bit.bitwarden.data.platform.datasource.network.core.ResultCallAdapterFactory
import com.x8bit.bitwarden.data.platform.datasource.network.interceptor.AuthTokenInterceptor
@@ -15,8 +13,7 @@ import okhttp3.OkHttpClient
import okhttp3.logging.HttpLoggingInterceptor
import retrofit2.Retrofit
import retrofit2.converter.kotlinx.serialization.asConverterFactory
private const val MAX_LOG_MESSAGE_LENGTH: Int = 4000
import timber.log.Timber
/**
* Primary implementation of [Retrofits].
@@ -79,20 +76,10 @@ class RetrofitsImpl(
//region Helper properties and functions
private val loggingInterceptor: HttpLoggingInterceptor by lazy {
HttpLoggingInterceptor { message ->
message.chunked(size = MAX_LOG_MESSAGE_LENGTH).forEach { chunk ->
Log.d("BitwardenNetworkClient", chunk)
}
}
HttpLoggingInterceptor { message -> Timber.tag("BitwardenNetworkClient").d(message) }
.apply {
redactHeader(name = HEADER_KEY_AUTHORIZATION)
setLevel(
if (BuildConfig.DEBUG) {
HttpLoggingInterceptor.Level.BODY
} else {
HttpLoggingInterceptor.Level.NONE
},
)
setLevel(HttpLoggingInterceptor.Level.BODY)
}
}

View File

@@ -1,9 +1,8 @@
package com.x8bit.bitwarden.data.platform.datasource.sdk
import android.util.Log
import com.bitwarden.sdk.Client
import com.x8bit.bitwarden.BuildConfig
import com.x8bit.bitwarden.data.platform.manager.SdkClientManager
import timber.log.Timber
/**
* Base class for simplifying sdk interactions.
@@ -27,9 +26,5 @@ abstract class BaseSdkSource(
protected inline fun <T, R> T.runCatchingWithLogs(
block: T.() -> R,
): Result<R> = runCatching(block = block)
.onFailure {
if (BuildConfig.DEBUG) {
Log.w(this@BaseSdkSource::class.java.simpleName, it)
}
}
.onFailure { Timber.w(it) }
}

View File

@@ -55,9 +55,9 @@ class BiometricsEncryptionManagerImpl(
}
val cipher = try {
Cipher.getInstance(CIPHER_TRANSFORMATION)
} catch (e: NoSuchAlgorithmException) {
} catch (_: NoSuchAlgorithmException) {
return null
} catch (e: NoSuchPaddingException) {
} catch (_: NoSuchPaddingException) {
return null
}
// This should never fail to initialize / return false because the cipher is newly generated
@@ -116,20 +116,20 @@ class BiometricsEncryptionManagerImpl(
KeyProperties.KEY_ALGORITHM_AES,
ENCRYPTION_KEYSTORE_NAME,
)
} catch (e: NoSuchAlgorithmException) {
} catch (_: NoSuchAlgorithmException) {
return null
} catch (e: NoSuchProviderException) {
} catch (_: NoSuchProviderException) {
return null
} catch (e: IllegalArgumentException) {
} catch (_: IllegalArgumentException) {
return null
}
try {
keyGen.init(keyGenParameterSpec)
keyGen.generateKey()
} catch (e: InvalidAlgorithmParameterException) {
} catch (_: InvalidAlgorithmParameterException) {
return null
} catch (e: ProviderException) {
} catch (_: ProviderException) {
return null
}
@@ -142,29 +142,29 @@ class BiometricsEncryptionManagerImpl(
private fun getSecretKeyOrNull(): SecretKey? {
try {
keystore.load(null)
} catch (e: IllegalArgumentException) {
} catch (_: IllegalArgumentException) {
// keystore could not be loaded because [param] is unrecognized.
return null
} catch (e: IOException) {
} catch (_: IOException) {
// keystore data format is invalid or the password is incorrect.
return null
} catch (e: NoSuchAlgorithmException) {
} catch (_: NoSuchAlgorithmException) {
// keystore integrity could not be checked due to missing algorithm.
return null
} catch (e: CertificateException) {
} catch (_: CertificateException) {
// keystore certificates could not be loaded
return null
}
return try {
keystore.getKey(ENCRYPTION_KEY_NAME, null) as? SecretKey
} catch (e: KeyStoreException) {
} catch (_: KeyStoreException) {
// keystore was not loaded
null
} catch (e: NoSuchAlgorithmException) {
} catch (_: NoSuchAlgorithmException) {
// keystore algorithm cannot be found
null
} catch (e: UnrecoverableKeyException) {
} catch (_: UnrecoverableKeyException) {
// key could not be recovered
null
}
@@ -181,15 +181,15 @@ class BiometricsEncryptionManagerImpl(
try {
cipher.init(Cipher.ENCRYPT_MODE, secretKey)
true
} catch (e: KeyPermanentlyInvalidatedException) {
} catch (_: KeyPermanentlyInvalidatedException) {
// Biometric has changed
settingsDiskSource.systemBiometricIntegritySource = null
false
} catch (e: UnrecoverableKeyException) {
} catch (_: UnrecoverableKeyException) {
// Biometric was disabled and re-enabled
settingsDiskSource.systemBiometricIntegritySource = null
false
} catch (e: InvalidKeyException) {
} catch (_: InvalidKeyException) {
// Fallback for old Bitwarden users without a key
createIntegrityValues(userId)
true

View File

@@ -0,0 +1,17 @@
package com.x8bit.bitwarden.data.platform.manager
import java.time.Instant
/**
* Manager for tracking changes to database scheme(s).
*/
interface DatabaseSchemeManager {
/**
* The instant of the last database schema change performed on the database, if any.
*
* There is only a single scheme change instant tracked for all database schemes. It is expected
* that a scheme change to any database will update this value and trigger a sync.
*/
var lastDatabaseSchemeChangeInstant: Instant?
}

View File

@@ -0,0 +1,17 @@
package com.x8bit.bitwarden.data.platform.manager
import com.x8bit.bitwarden.data.platform.datasource.disk.SettingsDiskSource
import java.time.Instant
/**
* Primary implementation of [DatabaseSchemeManager].
*/
class DatabaseSchemeManagerImpl(
val settingsDiskSource: SettingsDiskSource,
) : DatabaseSchemeManager {
override var lastDatabaseSchemeChangeInstant: Instant?
get() = settingsDiskSource.lastDatabaseSchemeChangeInstant
set(value) {
settingsDiskSource.lastDatabaseSchemeChangeInstant = value
}
}

View File

@@ -3,10 +3,12 @@ package com.x8bit.bitwarden.data.platform.manager
import com.x8bit.bitwarden.data.platform.datasource.disk.model.ServerConfig
import com.x8bit.bitwarden.data.platform.manager.model.FlagKey
import com.x8bit.bitwarden.data.platform.repository.ServerConfigRepository
import com.x8bit.bitwarden.data.platform.util.isServerVersionAtLeast
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
private const val CIPHER_KEY_ENCRYPTION_KEY = "enableCipherKeyEncryption"
private const val CIPHER_KEY_ENC_MIN_SERVER_VERSION = "2024.2.0"
/**
* Primary implementation of [FeatureFlagManager].
@@ -16,7 +18,13 @@ class FeatureFlagManagerImpl(
) : FeatureFlagManager {
override val sdkFeatureFlags: Map<String, Boolean>
get() = mapOf(CIPHER_KEY_ENCRYPTION_KEY to true)
get() = mapOf(
CIPHER_KEY_ENCRYPTION_KEY to
isServerVersionAtLeast(
serverConfigRepository.serverConfigStateFlow.value,
CIPHER_KEY_ENC_MIN_SERVER_VERSION,
),
)
override fun <T : Any> getFeatureFlagFlow(key: FlagKey<T>): Flow<T> =
serverConfigRepository

View File

@@ -0,0 +1,47 @@
package com.x8bit.bitwarden.data.platform.manager
import com.x8bit.bitwarden.data.platform.manager.model.FirstTimeState
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.StateFlow
/**
* Manager for compiling the state of all first time actions and related information such
* as counts of notifications to show, etc.
*/
interface FirstTimeActionManager {
/**
* Returns an observable count of the number of settings items that have a badge to display
* for the current active user.
*/
val allSettingsBadgeCountFlow: StateFlow<Int>
/**
* Returns an observable count of the number of security settings items that have a badge to
* display for the current active user.
*/
val allSecuritySettingsBadgeCountFlow: StateFlow<Int>
/**
* Returns an observable count of the number of autofill settings items that have a badge to
* display for the current active user.
*/
val allAutofillSettingsBadgeCountFlow: StateFlow<Int>
/**
* Returns an observable count of the number of vault settings items that have a badge to
* display for the current active user.
*/
val allVaultSettingsBadgeCountFlow: StateFlow<Int>
/**
* Returns a [Flow] that emits every time the active user's first time state is changed.
*/
val firstTimeStateFlow: Flow<FirstTimeState>
/**
* Get the current [FirstTimeState] of the active user if available, otherwise return
* a default configuration.
*/
val currentOrDefaultUserFirstTimeState: FirstTimeState
}

View File

@@ -0,0 +1,186 @@
package com.x8bit.bitwarden.data.platform.manager
import com.x8bit.bitwarden.data.auth.datasource.disk.AuthDiskSource
import com.x8bit.bitwarden.data.auth.repository.util.activeUserIdChangesFlow
import com.x8bit.bitwarden.data.platform.datasource.disk.SettingsDiskSource
import com.x8bit.bitwarden.data.platform.manager.dispatcher.DispatcherManager
import com.x8bit.bitwarden.data.platform.manager.model.FirstTimeState
import com.x8bit.bitwarden.data.platform.manager.model.FlagKey
import com.x8bit.bitwarden.data.vault.datasource.disk.VaultDiskSource
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onStart
import kotlinx.coroutines.flow.stateIn
import javax.inject.Inject
/**
* Implementation of [FirstTimeActionManager]
*/
class FirstTimeActionManagerImpl @Inject constructor(
dispatcherManager: DispatcherManager,
private val authDiskSource: AuthDiskSource,
private val settingsDiskSource: SettingsDiskSource,
private val vaultDiskSource: VaultDiskSource,
private val featureFlagManager: FeatureFlagManager,
) : FirstTimeActionManager {
private val unconfinedScope = CoroutineScope(dispatcherManager.unconfined)
override val allSettingsBadgeCountFlow: StateFlow<Int>
get() = combine(
listOf(
allSecuritySettingsBadgeCountFlow,
allAutofillSettingsBadgeCountFlow,
allVaultSettingsBadgeCountFlow,
),
) {
it.sum()
}
.stateIn(
scope = unconfinedScope,
started = SharingStarted.Lazily,
initialValue = 0,
)
@OptIn(ExperimentalCoroutinesApi::class)
override val allSecuritySettingsBadgeCountFlow: StateFlow<Int>
get() = authDiskSource
.activeUserIdChangesFlow
.filterNotNull()
.flatMapLatest {
// can be expanded to support multiple security settings
settingsDiskSource.getShowUnlockSettingBadgeFlow(userId = it)
.map { showUnlockBadge ->
listOfNotNull(showUnlockBadge)
}
.map { list ->
list.count { badgeOnValue -> badgeOnValue }
}
}
.stateIn(
scope = unconfinedScope,
started = SharingStarted.Lazily,
initialValue = 0,
)
@OptIn(ExperimentalCoroutinesApi::class)
override val allAutofillSettingsBadgeCountFlow: StateFlow<Int>
get() = authDiskSource
.activeUserIdChangesFlow
.filterNotNull()
.flatMapLatest {
// Can be expanded to support multiple autofill settings
settingsDiskSource.getShowAutoFillSettingBadgeFlow(userId = it)
.map { showAutofillBadge ->
listOfNotNull(showAutofillBadge)
}
.map { list ->
list.count { showBadge -> showBadge }
}
}
.stateIn(
scope = unconfinedScope,
started = SharingStarted.Lazily,
initialValue = 0,
)
@OptIn(ExperimentalCoroutinesApi::class)
override val allVaultSettingsBadgeCountFlow: StateFlow<Int>
get() = authDiskSource
.activeUserIdChangesFlow
.filterNotNull()
.flatMapLatest {
combine(
getShowImportLoginsFlowInternal(userId = it),
featureFlagManager.getFeatureFlagFlow(FlagKey.ImportLoginsFlow),
) { showImportLogins, importLoginsEnabled ->
val shouldShowImportLogins = showImportLogins && importLoginsEnabled
listOf(shouldShowImportLogins)
}
.map { list ->
list.count { showImportLogins -> showImportLogins }
}
}
.stateIn(
scope = unconfinedScope,
started = SharingStarted.Lazily,
initialValue = 0,
)
/**
* Returns a [Flow] that emits every time the active user's first time state is changed.
*/
@OptIn(ExperimentalCoroutinesApi::class)
override val firstTimeStateFlow: Flow<FirstTimeState>
get() = authDiskSource
.activeUserIdChangesFlow
.filterNotNull()
.flatMapLatest { activeUserId ->
combine(
listOf(
getShowImportLoginsFlowInternal(userId = activeUserId),
settingsDiskSource.getShowUnlockSettingBadgeFlow(userId = activeUserId),
settingsDiskSource.getShowAutoFillSettingBadgeFlow(userId = activeUserId),
),
) {
FirstTimeState(
showImportLoginsCard = it[0],
showSetupUnlockCard = it[1],
showSetupAutofillCard = it[2],
)
}
}
.onStart {
emit(
FirstTimeState(
showImportLoginsCard = null,
showSetupUnlockCard = null,
showSetupAutofillCard = null,
),
)
}
.distinctUntilChanged()
/**
* Internal implementation to get a flow of the showImportLogins value which takes
* into account if the vault is empty.
*/
private fun getShowImportLoginsFlowInternal(userId: String): Flow<Boolean> {
return authDiskSource.getShowImportLoginsFlow(userId)
.combine(
vaultDiskSource.getCiphers(userId),
) { showImportLogins, ciphers ->
showImportLogins ?: true && ciphers.isEmpty()
}
}
/**
* Get the current [FirstTimeState] of the active user if available, otherwise return
* a default configuration.
*/
override val currentOrDefaultUserFirstTimeState: FirstTimeState
get() =
authDiskSource
.userState
?.activeUserId
?.let {
FirstTimeState(
showImportLoginsCard = authDiskSource.getShowImportLogins(it),
showSetupUnlockCard = settingsDiskSource.getShowUnlockSettingBadge(it),
showSetupAutofillCard = settingsDiskSource.getShowAutoFillSettingBadge(it),
)
}
?: FirstTimeState(
showImportLoginsCard = null,
showSetupUnlockCard = null,
showSetupAutofillCard = null,
)
}

View File

@@ -1,17 +1,24 @@
package com.x8bit.bitwarden.data.platform.manager
import com.x8bit.bitwarden.data.platform.repository.model.Environment
/**
* Implementations of this interface provide a way to enable or disable the collection of crash
* logs, giving control over whether crash logs are generated and stored.
*/
interface CrashLogsManager {
interface LogsManager {
/**
* Gets or sets whether the collection of crash logs is enabled.
*/
var isEnabled: Boolean
/**
* Tracks an exception if logs are enabled.
* Tracks a [Throwable] if logs are enabled.
*/
fun trackNonFatalException(e: Exception)
fun trackNonFatalException(throwable: Throwable)
/**
* Tracks the current user data.
*/
fun setUserData(userId: String?, environmentType: Environment.Type)
}

View File

@@ -1,9 +1,7 @@
package com.x8bit.bitwarden.data.platform.manager
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
import com.x8bit.bitwarden.data.auth.repository.model.AuthState
import com.x8bit.bitwarden.data.platform.datasource.network.authenticator.RefreshAuthenticator
import com.x8bit.bitwarden.data.platform.datasource.network.interceptor.AuthTokenInterceptor
import com.x8bit.bitwarden.data.platform.datasource.network.interceptor.BaseUrlInterceptors
import com.x8bit.bitwarden.data.platform.manager.dispatcher.DispatcherManager
import com.x8bit.bitwarden.data.platform.repository.EnvironmentRepository
@@ -18,10 +16,8 @@ private const val ENVIRONMENT_DEBOUNCE_TIMEOUT_MS: Long = 500L
/**
* Primary implementation of [NetworkConfigManager].
*/
@Suppress("LongParameterList")
class NetworkConfigManagerImpl(
authRepository: AuthRepository,
private val authTokenInterceptor: AuthTokenInterceptor,
environmentRepository: EnvironmentRepository,
serverConfigRepository: ServerConfigRepository,
private val baseUrlInterceptors: BaseUrlInterceptors,
@@ -32,17 +28,6 @@ class NetworkConfigManagerImpl(
private val collectionScope = CoroutineScope(dispatcherManager.unconfined)
init {
authRepository
.authStateFlow
.onEach { authState ->
authTokenInterceptor.authToken = when (authState) {
is AuthState.Authenticated -> authState.accessToken
is AuthState.Unauthenticated -> null
is AuthState.Uninitialized -> null
}
}
.launchIn(collectionScope)
@Suppress("OPT_IN_USAGE")
environmentRepository
.environmentStateFlow

View File

@@ -1,6 +1,7 @@
package com.x8bit.bitwarden.data.platform.manager
import com.x8bit.bitwarden.data.auth.datasource.disk.AuthDiskSource
import com.x8bit.bitwarden.data.auth.repository.util.activeUserIdChangesFlow
import com.x8bit.bitwarden.data.platform.datasource.disk.PushDiskSource
import com.x8bit.bitwarden.data.platform.datasource.network.model.PushTokenRequest
import com.x8bit.bitwarden.data.platform.datasource.network.service.PushService
@@ -21,7 +22,6 @@ import com.x8bit.bitwarden.data.platform.util.decodeFromStringOrNull
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.flow.asSharedFlow
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.mapNotNull
import kotlinx.coroutines.flow.onEach
@@ -100,9 +100,8 @@ class PushManagerImpl @Inject constructor(
init {
authDiskSource
.userStateFlow
.mapNotNull { it?.activeUserId }
.distinctUntilChanged()
.activeUserIdChangesFlow
.mapNotNull { it }
.onEach { registerStoredPushTokenIfNecessary() }
.launchIn(unconfinedScope)
}
@@ -129,7 +128,7 @@ class PushManagerImpl @Inject constructor(
when (val type = notification.notificationType) {
NotificationType.AUTH_REQUEST,
NotificationType.AUTH_REQUEST_RESPONSE,
-> {
-> {
json
.decodeFromString<NotificationPayload.PasswordlessRequestNotification>(
string = notification.payload,
@@ -156,25 +155,20 @@ class PushManagerImpl @Inject constructor(
NotificationType.SYNC_CIPHER_CREATE,
NotificationType.SYNC_CIPHER_UPDATE,
-> {
-> {
json
.decodeFromString<NotificationPayload.SyncCipherNotification>(
string = notification.payload,
)
.takeIf { isLoggedIn(userId) && it.userMatchesNotification(userId) }
?.takeIf {
it.cipherId != null &&
it.revisionDate != null &&
it.organizationId != null &&
it.collectionIds != null
}
?.takeIf { it.cipherId != null && it.revisionDate != null }
?.let {
mutableSyncCipherUpsertSharedFlow.tryEmit(
SyncCipherUpsertData(
cipherId = requireNotNull(it.cipherId),
revisionDate = requireNotNull(it.revisionDate),
organizationId = requireNotNull(it.organizationId),
collectionIds = requireNotNull(it.collectionIds),
organizationId = it.organizationId,
collectionIds = it.collectionIds,
isUpdate = type == NotificationType.SYNC_CIPHER_UPDATE,
),
)
@@ -183,7 +177,7 @@ class PushManagerImpl @Inject constructor(
NotificationType.SYNC_CIPHER_DELETE,
NotificationType.SYNC_LOGIN_DELETE,
-> {
-> {
json
.decodeFromString<NotificationPayload.SyncCipherNotification>(
string = notification.payload,
@@ -196,13 +190,13 @@ class PushManagerImpl @Inject constructor(
NotificationType.SYNC_CIPHERS,
NotificationType.SYNC_SETTINGS,
NotificationType.SYNC_VAULT,
-> {
-> {
mutableFullSyncSharedFlow.tryEmit(Unit)
}
NotificationType.SYNC_FOLDER_CREATE,
NotificationType.SYNC_FOLDER_UPDATE,
-> {
-> {
json
.decodeFromString<NotificationPayload.SyncFolderNotification>(
string = notification.payload,
@@ -238,7 +232,7 @@ class PushManagerImpl @Inject constructor(
NotificationType.SYNC_SEND_CREATE,
NotificationType.SYNC_SEND_UPDATE,
-> {
-> {
json
.decodeFromString<NotificationPayload.SyncSendNotification>(
string = notification.payload,
@@ -339,6 +333,6 @@ class PushManagerImpl @Inject constructor(
): Boolean = authDiskSource.getAccountTokens(userId)?.isLoggedIn == true
}
private fun NotificationPayload.userMatchesNotification(userId: String?): Boolean {
private fun NotificationPayload.userMatchesNotification(userId: String): Boolean {
return this.userId != null && this.userId == userId
}

View File

@@ -19,6 +19,7 @@ class SpecialCircumstanceManagerImpl(
) : SpecialCircumstanceManager {
private val mutableSpecialCircumstanceFlow = MutableStateFlow<SpecialCircumstance?>(null)
private val unconfinedScope = CoroutineScope(dispatcherManager.unconfined)
init {
authRepository
.userStateFlow

View File

@@ -7,8 +7,10 @@ import com.x8bit.bitwarden.data.platform.manager.ResourceCacheManager
import com.x8bit.bitwarden.data.platform.repository.SettingsRepository
import com.x8bit.bitwarden.data.platform.util.firstWithTimeoutOrNull
import com.x8bit.bitwarden.data.platform.util.getDomainOrNull
import com.x8bit.bitwarden.data.platform.util.getHostOrNull
import com.x8bit.bitwarden.data.platform.util.getHostWithPortOrNull
import com.x8bit.bitwarden.data.platform.util.getWebHostFromAndroidUriOrNull
import com.x8bit.bitwarden.data.platform.util.hasPort
import com.x8bit.bitwarden.data.platform.util.isAndroidApp
import com.x8bit.bitwarden.data.platform.util.regexOrNull
import com.x8bit.bitwarden.data.vault.repository.VaultRepository
@@ -186,6 +188,7 @@ private fun checkForCipherMatch(
* @param matchingDomains The set of domains that match the domain of [matchUri].
* @param matchUri The uri that this [LoginUriView] is being matched to.
*/
@Suppress("CyclomaticComplexMethod")
private fun LoginUriView.checkForMatch(
resourceCacheManager: ResourceCacheManager,
defaultUriMatchType: UriMatchType,
@@ -210,9 +213,15 @@ private fun LoginUriView.checkForMatch(
UriMatchType.EXACT -> exactIfTrue(loginViewUri == matchUri)
UriMatchType.HOST -> {
val loginUriHost = loginViewUri.getHostWithPortOrNull()
val matchUriHost = matchUri.getHostWithPortOrNull()
exactIfTrue(matchUriHost != null && loginUriHost == matchUriHost)
if (loginViewUri.hasPort() && matchUri.hasPort()) {
val loginUriHost = loginViewUri.getHostWithPortOrNull()
val matchUriHost = matchUri.getHostWithPortOrNull()
exactIfTrue(matchUriHost != null && loginUriHost == matchUriHost)
} else {
val loginUriHost = loginViewUri.getHostOrNull()
val matchUriHost = matchUri.getHostOrNull()
exactIfTrue(matchUriHost != null && loginUriHost == matchUriHost)
}
}
UriMatchType.NEVER -> MatchResult.NONE

View File

@@ -4,13 +4,13 @@ import android.app.Application
import android.content.Context
import androidx.core.content.getSystemService
import com.x8bit.bitwarden.data.auth.datasource.disk.AuthDiskSource
import com.x8bit.bitwarden.data.auth.manager.AddTotpItemFromAuthenticatorManager
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
import com.x8bit.bitwarden.data.platform.datasource.disk.EventDiskSource
import com.x8bit.bitwarden.data.platform.datasource.disk.PushDiskSource
import com.x8bit.bitwarden.data.platform.datasource.disk.SettingsDiskSource
import com.x8bit.bitwarden.data.platform.datasource.disk.legacy.LegacyAppCenterMigrator
import com.x8bit.bitwarden.data.platform.datasource.network.authenticator.RefreshAuthenticator
import com.x8bit.bitwarden.data.platform.datasource.network.interceptor.AuthTokenInterceptor
import com.x8bit.bitwarden.data.platform.datasource.network.interceptor.BaseUrlInterceptors
import com.x8bit.bitwarden.data.platform.datasource.network.service.EventService
import com.x8bit.bitwarden.data.platform.datasource.network.service.PushService
@@ -20,13 +20,15 @@ import com.x8bit.bitwarden.data.platform.manager.AssetManager
import com.x8bit.bitwarden.data.platform.manager.AssetManagerImpl
import com.x8bit.bitwarden.data.platform.manager.BiometricsEncryptionManager
import com.x8bit.bitwarden.data.platform.manager.BiometricsEncryptionManagerImpl
import com.x8bit.bitwarden.data.platform.processor.AuthenticatorBridgeProcessor
import com.x8bit.bitwarden.data.platform.processor.AuthenticatorBridgeProcessorImpl
import com.x8bit.bitwarden.data.platform.manager.CrashLogsManager
import com.x8bit.bitwarden.data.platform.manager.CrashLogsManagerImpl
import com.x8bit.bitwarden.data.platform.manager.DatabaseSchemeManager
import com.x8bit.bitwarden.data.platform.manager.DatabaseSchemeManagerImpl
import com.x8bit.bitwarden.data.platform.manager.DebugMenuFeatureFlagManagerImpl
import com.x8bit.bitwarden.data.platform.manager.FeatureFlagManager
import com.x8bit.bitwarden.data.platform.manager.FeatureFlagManagerImpl
import com.x8bit.bitwarden.data.platform.manager.FirstTimeActionManager
import com.x8bit.bitwarden.data.platform.manager.FirstTimeActionManagerImpl
import com.x8bit.bitwarden.data.platform.manager.LogsManager
import com.x8bit.bitwarden.data.platform.manager.LogsManagerImpl
import com.x8bit.bitwarden.data.platform.manager.NetworkConfigManager
import com.x8bit.bitwarden.data.platform.manager.NetworkConfigManagerImpl
import com.x8bit.bitwarden.data.platform.manager.NetworkConnectionManager
@@ -51,11 +53,14 @@ import com.x8bit.bitwarden.data.platform.manager.garbage.GarbageCollectionManage
import com.x8bit.bitwarden.data.platform.manager.garbage.GarbageCollectionManagerImpl
import com.x8bit.bitwarden.data.platform.manager.restriction.RestrictionManager
import com.x8bit.bitwarden.data.platform.manager.restriction.RestrictionManagerImpl
import com.x8bit.bitwarden.data.platform.processor.AuthenticatorBridgeProcessor
import com.x8bit.bitwarden.data.platform.processor.AuthenticatorBridgeProcessorImpl
import com.x8bit.bitwarden.data.platform.repository.AuthenticatorBridgeRepository
import com.x8bit.bitwarden.data.platform.repository.DebugMenuRepository
import com.x8bit.bitwarden.data.platform.repository.EnvironmentRepository
import com.x8bit.bitwarden.data.platform.repository.ServerConfigRepository
import com.x8bit.bitwarden.data.platform.repository.SettingsRepository
import com.x8bit.bitwarden.data.vault.datasource.disk.VaultDiskSource
import com.x8bit.bitwarden.data.vault.repository.VaultRepository
import dagger.Module
import dagger.Provides
@@ -82,10 +87,14 @@ object PlatformManagerModule {
@Singleton
fun provideAuthenticatorBridgeProcessor(
authenticatorBridgeRepository: AuthenticatorBridgeRepository,
addTotpItemFromAuthenticatorManager: AddTotpItemFromAuthenticatorManager,
@ApplicationContext context: Context,
dispatcherManager: DispatcherManager,
featureFlagManager: FeatureFlagManager,
): AuthenticatorBridgeProcessor = AuthenticatorBridgeProcessorImpl(
authenticatorBridgeRepository = authenticatorBridgeRepository,
addTotpItemFromAuthenticatorManager = addTotpItemFromAuthenticatorManager,
context = context,
dispatcherManager = dispatcherManager,
featureFlagManager = featureFlagManager,
)
@@ -185,7 +194,6 @@ object PlatformManagerModule {
@Singleton
fun provideNetworkConfigManager(
authRepository: AuthRepository,
authTokenInterceptor: AuthTokenInterceptor,
environmentRepository: EnvironmentRepository,
serverConfigRepository: ServerConfigRepository,
baseUrlInterceptors: BaseUrlInterceptors,
@@ -194,7 +202,6 @@ object PlatformManagerModule {
): NetworkConfigManager =
NetworkConfigManagerImpl(
authRepository = authRepository,
authTokenInterceptor = authTokenInterceptor,
environmentRepository = environmentRepository,
serverConfigRepository = serverConfigRepository,
baseUrlInterceptors = baseUrlInterceptors,
@@ -238,10 +245,10 @@ object PlatformManagerModule {
@Provides
@Singleton
fun provideCrashLogsManager(
fun provideLogsManager(
legacyAppCenterMigrator: LegacyAppCenterMigrator,
settingsRepository: SettingsRepository,
): CrashLogsManager = CrashLogsManagerImpl(
): LogsManager = LogsManagerImpl(
settingsRepository = settingsRepository,
legacyAppCenterMigrator = legacyAppCenterMigrator,
)
@@ -276,4 +283,28 @@ object PlatformManagerModule {
fun provideResourceCacheManager(
@ApplicationContext context: Context,
): ResourceCacheManager = ResourceCacheManagerImpl(context = context)
@Provides
@Singleton
fun provideFirstTimeActionManager(
authDiskSource: AuthDiskSource,
settingsDiskSource: SettingsDiskSource,
vaultDiskSource: VaultDiskSource,
dispatcherManager: DispatcherManager,
featureFlagManager: FeatureFlagManager,
): FirstTimeActionManager = FirstTimeActionManagerImpl(
authDiskSource = authDiskSource,
settingsDiskSource = settingsDiskSource,
vaultDiskSource = vaultDiskSource,
dispatcherManager = dispatcherManager,
featureFlagManager = featureFlagManager,
)
@Provides
@Singleton
fun provideDatabaseSchemeManager(
settingsDiskSource: SettingsDiskSource,
): DatabaseSchemeManager = DatabaseSchemeManagerImpl(
settingsDiskSource = settingsDiskSource,
)
}

View File

@@ -0,0 +1,24 @@
package com.x8bit.bitwarden.data.platform.manager.model
/**
* Model to encapsulate different states for a user's first time experience.
*/
data class FirstTimeState(
val showImportLoginsCard: Boolean,
val showSetupUnlockCard: Boolean,
val showSetupAutofillCard: Boolean,
) {
/**
* Constructs a [FirstTimeState] accepting nullable values. If a value is null, the default
* is used.
*/
constructor(
showImportLoginsCard: Boolean? = null,
showSetupUnlockCard: Boolean? = null,
showSetupAutofillCard: Boolean? = null,
) : this(
showImportLoginsCard = showImportLoginsCard ?: true,
showSetupUnlockCard = showSetupUnlockCard ?: false,
showSetupAutofillCard = showSetupAutofillCard ?: false,
)
}

View File

@@ -30,6 +30,8 @@ sealed class FlagKey<out T : Any> {
EmailVerification,
OnboardingFlow,
OnboardingCarousel,
ImportLoginsFlow,
SshKeyCipherItems,
)
}
}
@@ -70,6 +72,24 @@ sealed class FlagKey<out T : Any> {
override val isRemotelyConfigured: Boolean = false
}
/**
* Data object holding the feature flag key for the import logins feature.
*/
data object ImportLoginsFlow : FlagKey<Boolean>() {
override val keyName: String = "import-logins-flow"
override val defaultValue: Boolean = false
override val isRemotelyConfigured: Boolean = false
}
/**
* Data object holding the feature flag key for the SSH key cipher items feature.
*/
data object SshKeyCipherItems : FlagKey<Boolean>() {
override val keyName: String = "ssh-key-vault-item"
override val defaultValue: Boolean = false
override val isRemotelyConfigured: Boolean = true
}
/**
* Data object holding the key for a [Boolean] flag to be used in tests.
*/

View File

@@ -7,6 +7,7 @@ import com.x8bit.bitwarden.data.autofill.fido2.model.Fido2GetCredentialsRequest
import com.x8bit.bitwarden.data.autofill.model.AutofillSaveItem
import com.x8bit.bitwarden.data.autofill.model.AutofillSelectionData
import com.x8bit.bitwarden.ui.platform.manager.intent.IntentManager
import com.x8bit.bitwarden.ui.vault.model.TotpData
import kotlinx.parcelize.Parcelize
/**
@@ -14,6 +15,14 @@ import kotlinx.parcelize.Parcelize
* of navigation that is counter to what otherwise may happen based on the state of the app.
*/
sealed class SpecialCircumstance : Parcelable {
/**
* The app was launched in order to add a new TOTP to a cipher.
*/
@Parcelize
data class AddTotpLoginItem(
val data: TotpData,
) : SpecialCircumstance()
/**
* The app was launched in order to create/share a new Send using the given [data].
*/
@@ -89,6 +98,12 @@ sealed class SpecialCircumstance : Parcelable {
@Parcelize
data object VaultShortcut : SpecialCircumstance()
/**
* The app was launched via deeplink to the account security screen.
*/
@Parcelize
data object AccountSecurityShortcut : SpecialCircumstance()
/**
* A subset of [SpecialCircumstance] that are only relevant in a pre-login state and should be
* cleared after a successful login.

View File

@@ -6,6 +6,7 @@ import com.x8bit.bitwarden.data.autofill.fido2.model.Fido2GetCredentialsRequest
import com.x8bit.bitwarden.data.autofill.model.AutofillSaveItem
import com.x8bit.bitwarden.data.autofill.model.AutofillSelectionData
import com.x8bit.bitwarden.data.platform.manager.model.SpecialCircumstance
import com.x8bit.bitwarden.ui.vault.model.TotpData
/**
* Returns [AutofillSaveItem] when contained in the given [SpecialCircumstance].
@@ -13,16 +14,7 @@ import com.x8bit.bitwarden.data.platform.manager.model.SpecialCircumstance
fun SpecialCircumstance.toAutofillSaveItemOrNull(): AutofillSaveItem? =
when (this) {
is SpecialCircumstance.AutofillSave -> this.autofillSaveItem
is SpecialCircumstance.AutofillSelection -> null
is SpecialCircumstance.PasswordlessRequest -> null
is SpecialCircumstance.ShareNewSend -> null
SpecialCircumstance.GeneratorShortcut -> null
SpecialCircumstance.VaultShortcut -> null
is SpecialCircumstance.Fido2Save -> null
is SpecialCircumstance.Fido2Assertion -> null
is SpecialCircumstance.Fido2GetCredentials -> null
is SpecialCircumstance.RegistrationEvent.CompleteRegistration -> null
SpecialCircumstance.RegistrationEvent.ExpiredRegistrationLink -> null
else -> null
}
/**
@@ -30,17 +22,8 @@ fun SpecialCircumstance.toAutofillSaveItemOrNull(): AutofillSaveItem? =
*/
fun SpecialCircumstance.toAutofillSelectionDataOrNull(): AutofillSelectionData? =
when (this) {
is SpecialCircumstance.AutofillSave -> null
is SpecialCircumstance.AutofillSelection -> this.autofillSelectionData
is SpecialCircumstance.PasswordlessRequest -> null
is SpecialCircumstance.ShareNewSend -> null
SpecialCircumstance.GeneratorShortcut -> null
SpecialCircumstance.VaultShortcut -> null
is SpecialCircumstance.Fido2Save -> null
is SpecialCircumstance.Fido2Assertion -> null
is SpecialCircumstance.Fido2GetCredentials -> null
is SpecialCircumstance.RegistrationEvent.CompleteRegistration -> null
SpecialCircumstance.RegistrationEvent.ExpiredRegistrationLink -> null
else -> null
}
/**
@@ -69,3 +52,12 @@ fun SpecialCircumstance.toFido2GetCredentialsRequestOrNull(): Fido2GetCredential
is SpecialCircumstance.Fido2GetCredentials -> this.fido2GetCredentialsRequest
else -> null
}
/**
* Returns the [TotpData] when contained in the given [SpecialCircumstance].
*/
fun SpecialCircumstance.toTotpDataOrNull(): TotpData? =
when (this) {
is SpecialCircumstance.AddTotpLoginItem -> this.data
else -> null
}

View File

@@ -1,23 +1,28 @@
package com.x8bit.bitwarden.data.platform.processor
import android.content.Intent
import android.content.Context
import android.os.Build
import android.os.IInterface
import android.os.RemoteCallbackList
import androidx.core.net.toUri
import com.bitwarden.authenticatorbridge.IAuthenticatorBridgeService
import com.bitwarden.authenticatorbridge.IAuthenticatorBridgeServiceCallback
import com.bitwarden.authenticatorbridge.model.EncryptedAddTotpLoginItemData
import com.bitwarden.authenticatorbridge.model.SymmetricEncryptionKeyData
import com.bitwarden.authenticatorbridge.model.SymmetricEncryptionKeyFingerprintData
import com.bitwarden.authenticatorbridge.util.AUTHENTICATOR_BRIDGE_SDK_VERSION
import com.bitwarden.authenticatorbridge.util.decrypt
import com.bitwarden.authenticatorbridge.util.encrypt
import com.bitwarden.authenticatorbridge.util.toFingerprint
import com.bitwarden.authenticatorbridge.util.toSymmetricEncryptionKeyData
import com.x8bit.bitwarden.data.auth.manager.AddTotpItemFromAuthenticatorManager
import com.x8bit.bitwarden.data.platform.manager.FeatureFlagManager
import com.x8bit.bitwarden.data.platform.manager.dispatcher.DispatcherManager
import com.x8bit.bitwarden.data.platform.manager.model.FlagKey
import com.x8bit.bitwarden.data.platform.repository.AuthenticatorBridgeRepository
import com.x8bit.bitwarden.data.platform.util.createAddTotpItemFromAuthenticatorIntent
import com.x8bit.bitwarden.data.platform.util.isBuildVersionBelow
import com.x8bit.bitwarden.ui.vault.util.getTotpDataOrNull
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
@@ -26,10 +31,13 @@ import kotlinx.coroutines.launch
*/
class AuthenticatorBridgeProcessorImpl(
private val authenticatorBridgeRepository: AuthenticatorBridgeRepository,
private val addTotpItemFromAuthenticatorManager: AddTotpItemFromAuthenticatorManager,
private val featureFlagManager: FeatureFlagManager,
dispatcherManager: DispatcherManager,
context: Context,
) : AuthenticatorBridgeProcessor {
private val applicationContext = context.applicationContext
private val callbacks by lazy { RemoteCallbackList<IAuthenticatorBridgeServiceCallback>() }
private val scope by lazy { CoroutineScope(dispatcherManager.default) }
@@ -101,13 +109,18 @@ class AuthenticatorBridgeProcessorImpl(
}
}
override fun createAddTotpLoginItemIntent(): Intent {
// TODO: BITAU-112
return Intent()
}
override fun setPendingAddTotpLoginItemData(data: EncryptedAddTotpLoginItemData?) {
// TODO: BITAU-112
override fun startAddTotpLoginItemFlow(data: EncryptedAddTotpLoginItemData): Boolean {
val symmetricEncryptionKey = symmetricEncryptionKeyData ?: return false
val intent = createAddTotpItemFromAuthenticatorIntent(context = applicationContext)
val totpData = data.decrypt(symmetricEncryptionKey)
.getOrNull()
?.totpUri
?.toUri()
?.getTotpDataOrNull()
?: return false
addTotpItemFromAuthenticatorManager.pendingAddTotpLoginItemData = totpData
applicationContext.startActivity(intent)
return true
}
}
}

View File

@@ -3,7 +3,6 @@ package com.x8bit.bitwarden.data.platform.repository
import com.bitwarden.authenticatorbridge.model.SharedAccountData
import com.x8bit.bitwarden.data.auth.datasource.disk.AuthDiskSource
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
import com.x8bit.bitwarden.data.platform.datasource.disk.SettingsDiskSource
import com.x8bit.bitwarden.data.vault.datasource.disk.VaultDiskSource
import com.x8bit.bitwarden.data.vault.datasource.sdk.VaultSdkSource
import com.x8bit.bitwarden.data.vault.repository.VaultRepository
@@ -22,7 +21,6 @@ class AuthenticatorBridgeRepositoryImpl(
private val vaultRepository: VaultRepository,
private val vaultDiskSource: VaultDiskSource,
private val vaultSdkSource: VaultSdkSource,
private val settingsDiskSource: SettingsDiskSource,
) : AuthenticatorBridgeRepository {
override val authenticatorSyncSymmetricKey: ByteArray?
@@ -75,7 +73,7 @@ class AuthenticatorBridgeRepositoryImpl(
is VaultUnlockResult.AuthenticationError,
VaultUnlockResult.GenericError,
VaultUnlockResult.InvalidStateError,
-> {
-> {
// Not being able to unlock the user's vault with the
// decrypted unlock key is an unexpected case, but if it does
// happen we omit the account from list of shared accounts
@@ -97,8 +95,8 @@ class AuthenticatorBridgeRepositoryImpl(
val totpUris = vaultDiskSource
.getCiphers(userId)
.first()
// Filter out any ciphers without a totp item:
.filter { it.login?.totp != null }
// Filter out any ciphers without a totp item and also deleted ciphers:
.filter { it.login?.totp != null && it.deletedDate == null }
.mapNotNull {
// Decrypt each cipher and take just totp codes:
vaultSdkSource
@@ -111,9 +109,6 @@ class AuthenticatorBridgeRepositoryImpl(
?.totp
}
val lastSyncTime =
settingsDiskSource.getLastSyncTime(userId) ?: return@mapNotNull null
// Lock the user's vault if we unlocked it for this operation:
if (!isVaultAlreadyUnlocked) {
vaultRepository.lockVault(userId)
@@ -124,7 +119,6 @@ class AuthenticatorBridgeRepositoryImpl(
name = account.name,
email = account.email,
environmentLabel = account.environment.label,
lastSyncTime = lastSyncTime,
totpUris = totpUris,
)
}

View File

@@ -37,4 +37,11 @@ interface DebugMenuRepository {
* Resets the onboarding status to NOT_STARTED for the current active user, if applicable.
*/
fun resetOnboardingStatusForCurrentUser()
/**
* Manipulates the state to force showing the onboarding carousel.
*
* @param userStateUpdateTrigger A passable lambda to trigger a user state update.
*/
fun modifyStateToShowOnboardingCarousel(userStateUpdateTrigger: () -> Unit)
}

View File

@@ -4,6 +4,7 @@ import com.x8bit.bitwarden.BuildConfig
import com.x8bit.bitwarden.data.auth.datasource.disk.AuthDiskSource
import com.x8bit.bitwarden.data.auth.datasource.disk.model.OnboardingStatus
import com.x8bit.bitwarden.data.platform.datasource.disk.FeatureFlagOverrideDiskSource
import com.x8bit.bitwarden.data.platform.datasource.disk.SettingsDiskSource
import com.x8bit.bitwarden.data.platform.manager.getFlagValueOrDefault
import com.x8bit.bitwarden.data.platform.manager.model.FlagKey
import com.x8bit.bitwarden.data.platform.repository.util.bufferedMutableSharedFlow
@@ -16,6 +17,7 @@ import kotlinx.coroutines.flow.onSubscription
class DebugMenuRepositoryImpl(
private val featureFlagOverrideDiskSource: FeatureFlagOverrideDiskSource,
private val serverConfigRepository: ServerConfigRepository,
private val settingsDiskSource: SettingsDiskSource,
private val authDiskSource: AuthDiskSource,
) : DebugMenuRepository {
@@ -53,4 +55,11 @@ class DebugMenuRepositoryImpl(
onboardingStatus = OnboardingStatus.NOT_STARTED,
)
}
override fun modifyStateToShowOnboardingCarousel(
userStateUpdateTrigger: () -> Unit,
) {
settingsDiskSource.hasUserLoggedInOrCreatedAccount = false
userStateUpdateTrigger.invoke()
}
}

View File

@@ -160,24 +160,6 @@ interface SettingsRepository {
*/
val isScreenCaptureAllowedStateFlow: StateFlow<Boolean>
/**
* Returns an observable count of the number of settings items that have a badge to display
* for the current active user.
*/
val allSettingsBadgeCountFlow: StateFlow<Int>
/**
* Returns an observable count of the number of security settings items that have a badge to
* display for the current active user.
*/
val allSecuritySettingsBadgeCountFlow: StateFlow<Int>
/**
* Returns an observable count of the number of autofill settings items that have a badge to
* display for the current active user.
*/
val allAutofillSettingsBadgeCountFlow: StateFlow<Int>
/**
* Disables autofill if it is currently enabled.
*/

View File

@@ -6,7 +6,6 @@ import com.x8bit.bitwarden.BuildConfig
import com.x8bit.bitwarden.data.auth.datasource.disk.AuthDiskSource
import com.x8bit.bitwarden.data.auth.repository.model.PolicyInformation
import com.x8bit.bitwarden.data.auth.repository.model.UserFingerprintResult
import com.x8bit.bitwarden.data.auth.repository.util.activeUserIdChangesFlow
import com.x8bit.bitwarden.data.auth.repository.util.policyInformation
import com.x8bit.bitwarden.data.autofill.accessibility.manager.AccessibilityEnabledManager
import com.x8bit.bitwarden.data.autofill.manager.AutofillEnabledManager
@@ -30,8 +29,6 @@ import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.launchIn
@@ -340,60 +337,6 @@ class SettingsRepositoryImpl(
?: DEFAULT_IS_SCREEN_CAPTURE_ALLOWED,
)
override val allSettingsBadgeCountFlow: StateFlow<Int>
get() = combine(
allSecuritySettingsBadgeCountFlow,
allAutofillSettingsBadgeCountFlow,
transform = ::sumSettingsBadgeCount,
)
.stateIn(
scope = unconfinedScope,
started = SharingStarted.Lazily,
initialValue = 0,
)
@OptIn(ExperimentalCoroutinesApi::class)
override val allSecuritySettingsBadgeCountFlow: StateFlow<Int>
get() = authDiskSource
.activeUserIdChangesFlow
.filterNotNull()
.flatMapLatest {
// can be expanded to support multiple security settings
getShowUnlockBadgeFlow(userId = it)
.map { showUnlockBadge ->
listOf(showUnlockBadge)
}
.map { list ->
list.count { badgeOnValue -> badgeOnValue }
}
}
.stateIn(
scope = unconfinedScope,
started = SharingStarted.Lazily,
initialValue = 0,
)
@OptIn(ExperimentalCoroutinesApi::class)
override val allAutofillSettingsBadgeCountFlow: StateFlow<Int>
get() = authDiskSource
.activeUserIdChangesFlow
.filterNotNull()
.flatMapLatest {
// Can be expanded to support multiple autofill settings
getShowAutofillBadgeFlow(userId = it)
.map { showAutofillBadge ->
listOf(showAutofillBadge)
}
.map { list ->
list.count { showBadge -> showBadge }
}
}
.stateIn(
scope = unconfinedScope,
started = SharingStarted.Lazily,
initialValue = 0,
)
init {
policyManager
.getActivePoliciesFlow(type = PolicyTypeJson.MAXIMUM_VAULT_TIMEOUT)
@@ -660,10 +603,6 @@ class SettingsRepositoryImpl(
}
}
}
// helper function to sum badge counts from different settings sub-menus.
private fun sumSettingsBadgeCount(autoFillBadgeCount: Int, securityBadgeCount: Int) =
autoFillBadgeCount + securityBadgeCount
}
/**

View File

@@ -48,14 +48,12 @@ object PlatformRepositoryModule {
vaultRepository: VaultRepository,
vaultDiskSource: VaultDiskSource,
vaultSdkSource: VaultSdkSource,
settingsDiskSource: SettingsDiskSource,
): AuthenticatorBridgeRepository = AuthenticatorBridgeRepositoryImpl(
authRepository = authRepository,
authDiskSource = authDiskSource,
vaultRepository = vaultRepository,
vaultDiskSource = vaultDiskSource,
vaultSdkSource = vaultSdkSource,
settingsDiskSource = settingsDiskSource,
)
@Provides
@@ -117,9 +115,11 @@ object PlatformRepositoryModule {
featureFlagOverrideDiskSource: FeatureFlagOverrideDiskSource,
serverConfigRepository: ServerConfigRepository,
authDiskSource: AuthDiskSource,
settingsDiskSource: SettingsDiskSource,
): DebugMenuRepository = DebugMenuRepositoryImpl(
featureFlagOverrideDiskSource = featureFlagOverrideDiskSource,
serverConfigRepository = serverConfigRepository,
authDiskSource = authDiskSource,
settingsDiskSource = settingsDiskSource,
)
}

View File

@@ -125,11 +125,11 @@ fun EnvironmentUrlDataJson.toEnvironmentUrls(): Environment =
when (this) {
EnvironmentUrlDataJson.DEFAULT_US,
EnvironmentUrlDataJson.DEFAULT_LEGACY_US,
-> Environment.Us
-> Environment.Us
EnvironmentUrlDataJson.DEFAULT_EU,
EnvironmentUrlDataJson.DEFAULT_LEGACY_EU,
-> Environment.Eu
-> Environment.Eu
else -> Environment.SelfHosted(environmentUrlData = this)
}

View File

@@ -0,0 +1,41 @@
@file:OmitFromCoverage
package com.x8bit.bitwarden.data.platform.util
import android.content.Context
import android.content.Intent
import android.content.Intent.FLAG_ACTIVITY_NEW_TASK
import android.content.Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED
import android.content.Intent.FLAG_ACTIVITY_SINGLE_TOP
import com.x8bit.bitwarden.MainActivity
import com.x8bit.bitwarden.data.auth.manager.AddTotpItemFromAuthenticatorManager
import com.x8bit.bitwarden.data.platform.annotation.OmitFromCoverage
private const val ADD_TOTP_ITEM_FROM_AUTHENTICATOR_KEY = "add-totp-item-from-authenticator-key"
/**
* Creates an intent for launching add TOTP item flow from the Authenticator app.
*/
fun createAddTotpItemFromAuthenticatorIntent(
context: Context,
): Intent =
Intent(
context,
MainActivity::class.java,
)
.apply {
putExtra(
ADD_TOTP_ITEM_FROM_AUTHENTICATOR_KEY,
true,
)
addFlags(FLAG_ACTIVITY_NEW_TASK)
addFlags(FLAG_ACTIVITY_SINGLE_TOP)
addFlags(FLAG_ACTIVITY_RESET_TASK_IF_NEEDED)
}
/**
* Returns true if the Intent was started by the Authenticator app to add a TOTP item. The TOTP
* item can be found in [AddTotpItemFromAuthenticatorManager].
*/
fun Intent.isAddTotpLoginItemFromAuthenticator(): Boolean =
getBooleanExtra(ADD_TOTP_ITEM_FROM_AUTHENTICATOR_KEY, false)

View File

@@ -0,0 +1,51 @@
package com.x8bit.bitwarden.data.platform.util
import com.x8bit.bitwarden.data.platform.datasource.disk.model.ServerConfig
import kotlin.text.split
import kotlin.text.toIntOrNull
private const val VERSION_SEPARATOR = "."
private const val SUFFIX_SEPARATOR = "-"
/**
* Checks if the server version is greater than another provided version, returns true if it is.
*/
fun isServerVersionAtLeast(serverConfig: ServerConfig?, version: String): Boolean {
val serverVersion = serverConfig
?.serverData
?.version
if (serverVersion.isNullOrEmpty() || version.isEmpty()) {
return false
}
val serverVersionParts = getVersionComponents(serverVersion)
val otherVersionParts = getVersionComponents(version)
if (serverVersionParts.isNullOrEmpty() || otherVersionParts.isNullOrEmpty()) {
return false
}
// Must iterate through all indices to establish if versions are equal
for (i in serverVersionParts.indices) {
val serverPart = serverVersionParts.getOrNull(i)?.toIntOrNull() ?: 0
val otherPart = otherVersionParts.getOrNull(i)?.toIntOrNull() ?: 0
if (serverPart > otherPart) {
return true
} else if (serverPart < otherPart) {
return false
}
}
// Versions are equal
return true
}
/**
* Extracts the version components from a version string, disregarding any suffixes.
*/
private fun getVersionComponents(version: String?): List<String>? {
val versionComponents = version?.split(SUFFIX_SEPARATOR)?.first()
return versionComponents?.split(VERSION_SEPARATOR)
}

View File

@@ -58,6 +58,21 @@ fun String.getDomainOrNull(resourceCacheManager: ResourceCacheManager): String?
.toUriOrNull()
?.parseDomainOrNull(resourceCacheManager = resourceCacheManager)
/**
* Returns `true` if the [String] uri has a port, `false` otherwise.
*/
@OmitFromCoverage
fun String.hasPort(): Boolean {
val uri = this.toUriOrNull() ?: return false
return uri.port != -1
}
/**
* Extract the host from this [String] if possible, otherwise return null.
*/
@OmitFromCoverage
fun String.getHostOrNull(): String? = this.toUriOrNull()?.host
/**
* Extract the host with optional port from this [String] if possible, otherwise return null.
*/

View File

@@ -121,7 +121,7 @@ private fun parseDomainNameOrNullInternal(
val tldRange: IntRange? = when (largestMatch) {
is SuffixMatchType.Exception,
is SuffixMatchType.Normal,
-> {
-> {
host.findLastSubstringIndicesOrNull(largestMatch.partialDomain)
}

View File

@@ -4,17 +4,20 @@ import android.app.Application
import android.content.SharedPreferences
import androidx.room.Room
import com.x8bit.bitwarden.data.platform.datasource.di.UnencryptedPreferences
import com.x8bit.bitwarden.data.platform.manager.DatabaseSchemeManager
import com.x8bit.bitwarden.data.tools.generator.datasource.disk.GeneratorDiskSource
import com.x8bit.bitwarden.data.tools.generator.datasource.disk.GeneratorDiskSourceImpl
import com.x8bit.bitwarden.data.tools.generator.datasource.disk.PasswordHistoryDiskSource
import com.x8bit.bitwarden.data.tools.generator.datasource.disk.PasswordHistoryDiskSourceImpl
import com.x8bit.bitwarden.data.tools.generator.datasource.disk.dao.PasswordHistoryDao
import com.x8bit.bitwarden.data.tools.generator.datasource.disk.database.PasswordHistoryDatabase
import com.x8bit.bitwarden.data.vault.datasource.disk.callback.DatabaseSchemeCallback
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import kotlinx.serialization.json.Json
import java.time.Clock
import javax.inject.Singleton
/**
@@ -45,13 +48,23 @@ object GeneratorDiskModule {
@Provides
@Singleton
fun providePasswordHistoryDatabase(app: Application): PasswordHistoryDatabase {
fun providePasswordHistoryDatabase(
app: Application,
databaseSchemeManager: DatabaseSchemeManager,
clock: Clock,
): PasswordHistoryDatabase {
return Room
.databaseBuilder(
context = app,
klass = PasswordHistoryDatabase::class.java,
name = "passcode_history_database",
)
.addCallback(
DatabaseSchemeCallback(
databaseSchemeManager = databaseSchemeManager,
clock = clock,
),
)
.build()
}

View File

@@ -0,0 +1,18 @@
package com.x8bit.bitwarden.data.vault.datasource.disk.callback
import androidx.room.RoomDatabase
import androidx.sqlite.db.SupportSQLiteDatabase
import com.x8bit.bitwarden.data.platform.manager.DatabaseSchemeManager
import java.time.Clock
/**
* A [RoomDatabase.Callback] for tracking database scheme changes.
*/
class DatabaseSchemeCallback(
private val databaseSchemeManager: DatabaseSchemeManager,
private val clock: Clock,
) : RoomDatabase.Callback() {
override fun onDestructiveMigration(db: SupportSQLiteDatabase) {
databaseSchemeManager.lastDatabaseSchemeChangeInstant = clock.instant()
}
}

View File

@@ -2,9 +2,11 @@ package com.x8bit.bitwarden.data.vault.datasource.disk.di
import android.app.Application
import androidx.room.Room
import com.x8bit.bitwarden.data.platform.manager.DatabaseSchemeManager
import com.x8bit.bitwarden.data.platform.manager.dispatcher.DispatcherManager
import com.x8bit.bitwarden.data.vault.datasource.disk.VaultDiskSource
import com.x8bit.bitwarden.data.vault.datasource.disk.VaultDiskSourceImpl
import com.x8bit.bitwarden.data.vault.datasource.disk.callback.DatabaseSchemeCallback
import com.x8bit.bitwarden.data.vault.datasource.disk.convertor.ZonedDateTimeTypeConverter
import com.x8bit.bitwarden.data.vault.datasource.disk.dao.CiphersDao
import com.x8bit.bitwarden.data.vault.datasource.disk.dao.CollectionsDao
@@ -17,6 +19,7 @@ import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import kotlinx.serialization.json.Json
import java.time.Clock
import javax.inject.Singleton
/**
@@ -28,7 +31,11 @@ class VaultDiskModule {
@Provides
@Singleton
fun provideVaultDatabase(app: Application): VaultDatabase =
fun provideVaultDatabase(
app: Application,
databaseSchemeManager: DatabaseSchemeManager,
clock: Clock,
): VaultDatabase =
Room
.databaseBuilder(
context = app,
@@ -36,6 +43,7 @@ class VaultDiskModule {
name = "vault_database",
)
.fallbackToDestructiveMigration()
.addCallback(DatabaseSchemeCallback(databaseSchemeManager, clock))
.addTypeConverter(ZonedDateTimeTypeConverter())
.build()

View File

@@ -3,7 +3,6 @@ package com.x8bit.bitwarden.data.vault.datasource.sdk
import com.bitwarden.core.DateTime
import com.bitwarden.core.DerivePinKeyResponse
import com.bitwarden.core.InitOrgCryptoRequest
import com.bitwarden.core.InitUserCryptoMethod
import com.bitwarden.core.InitUserCryptoRequest
import com.bitwarden.core.UpdatePasswordResponse
import com.bitwarden.crypto.Kdf

View File

@@ -1,14 +1,14 @@
package com.x8bit.bitwarden.data.vault.datasource.sdk.model
import com.bitwarden.fido.ClientData
import com.bitwarden.fido.Origin
import com.bitwarden.vault.CipherView
/**
* Models a FIDO 2 authentication request to the Bitwarden SDK.
*
* @param userId User whom the credential is being authenticated for.
* @param origin Origin of the Relying Party. This can either be a Relying Party's URL or their
* application fingerprint.
* @param origin Origin of the Relying Party WebAuthn Request.
* @param requestJson Authentication request JSON received from the OS.
* @param clientData Metadata containing either privileged application certificate hash or Android
* package name of the Relying Party.
@@ -18,7 +18,7 @@ import com.bitwarden.vault.CipherView
*/
data class AuthenticateFido2CredentialRequest(
val userId: String,
val origin: String,
val origin: Origin,
val requestJson: String,
val clientData: ClientData,
val selectedCipherView: CipherView,

View File

@@ -9,6 +9,7 @@ import com.x8bit.bitwarden.data.autofill.util.isActiveWithFido2Credentials
import com.x8bit.bitwarden.data.platform.annotation.OmitFromCoverage
import com.x8bit.bitwarden.data.vault.datasource.sdk.VaultSdkSource
import com.x8bit.bitwarden.data.vault.repository.VaultRepository
import com.x8bit.bitwarden.data.vault.repository.model.SyncVaultDataResult
/**
* Primary implementation of [Fido2CredentialStore].
@@ -24,7 +25,12 @@ class Fido2CredentialStoreImpl(
* Return all active ciphers that contain FIDO 2 credentials.
*/
override suspend fun allCredentials(): List<CipherView> {
vaultRepository.sync()
val syncResult = vaultRepository.syncForResult()
if (syncResult is SyncVaultDataResult.Error) {
syncResult.throwable
?.let { throw it }
?: throw IllegalStateException("Sync failed.")
}
return vaultRepository.ciphersStateFlow.value.data
?.filter { it.isActiveWithFido2Credentials }
?: emptyList()
@@ -40,7 +46,12 @@ class Fido2CredentialStoreImpl(
override suspend fun findCredentials(ids: List<ByteArray>?, ripId: String): List<CipherView> {
val userId = getActiveUserIdOrThrow()
vaultRepository.sync()
val syncResult = vaultRepository.syncForResult()
if (syncResult is SyncVaultDataResult.Error) {
syncResult.throwable
?.let { throw it }
?: throw IllegalStateException("Sync failed.")
}
val ciphersWithFido2Credentials = vaultRepository.ciphersStateFlow.value.data
?.filter { it.isActiveWithFido2Credentials }

View File

@@ -1,6 +1,7 @@
package com.x8bit.bitwarden.data.vault.datasource.sdk.model
import com.bitwarden.fido.ClientData
import com.bitwarden.fido.Origin
import com.bitwarden.vault.CipherView
/**
@@ -18,7 +19,7 @@ import com.bitwarden.vault.CipherView
*/
data class RegisterFido2CredentialRequest(
val userId: String,
val origin: String,
val origin: Origin,
val requestJson: String,
val clientData: ClientData,
val selectedCipherView: CipherView,

View File

@@ -489,7 +489,7 @@ class VaultLockManagerImpl(
// User no longer active or engaging with the app.
CheckTimeoutReason.APP_BACKGROUNDED,
CheckTimeoutReason.USER_CHANGED,
-> {
-> {
handleTimeoutActionWithDelay(
userId = userId,
vaultTimeoutAction = vaultTimeoutAction,

View File

@@ -24,6 +24,7 @@ import com.x8bit.bitwarden.data.vault.repository.model.ExportVaultDataResult
import com.x8bit.bitwarden.data.vault.repository.model.GenerateTotpResult
import com.x8bit.bitwarden.data.vault.repository.model.RemovePasswordSendResult
import com.x8bit.bitwarden.data.vault.repository.model.SendData
import com.x8bit.bitwarden.data.vault.repository.model.SyncVaultDataResult
import com.x8bit.bitwarden.data.vault.repository.model.TotpCodeResult
import com.x8bit.bitwarden.data.vault.repository.model.UpdateFolderResult
import com.x8bit.bitwarden.data.vault.repository.model.UpdateSendResult
@@ -116,6 +117,12 @@ interface VaultRepository : CipherManager, VaultLockManager {
*/
fun syncIfNecessary()
/**
* Syncs the vault data for the current user. This is an explicit request to sync and will
* return the result of the sync as a [SyncVaultDataResult].
*/
suspend fun syncForResult(): SyncVaultDataResult
/**
* Flow that represents the data for a specific vault item as found by ID. This may emit `null`
* if the item cannot be found.

View File

@@ -21,6 +21,7 @@ import com.x8bit.bitwarden.data.auth.repository.util.toUpdatedUserStateJson
import com.x8bit.bitwarden.data.auth.repository.util.userSwitchingChangesFlow
import com.x8bit.bitwarden.data.platform.datasource.disk.SettingsDiskSource
import com.x8bit.bitwarden.data.platform.datasource.network.util.isNoConnectionError
import com.x8bit.bitwarden.data.platform.manager.DatabaseSchemeManager
import com.x8bit.bitwarden.data.platform.manager.PushManager
import com.x8bit.bitwarden.data.platform.manager.dispatcher.DispatcherManager
import com.x8bit.bitwarden.data.platform.manager.model.SyncCipherDeleteData
@@ -65,6 +66,7 @@ import com.x8bit.bitwarden.data.vault.repository.model.ExportVaultDataResult
import com.x8bit.bitwarden.data.vault.repository.model.GenerateTotpResult
import com.x8bit.bitwarden.data.vault.repository.model.RemovePasswordSendResult
import com.x8bit.bitwarden.data.vault.repository.model.SendData
import com.x8bit.bitwarden.data.vault.repository.model.SyncVaultDataResult
import com.x8bit.bitwarden.data.vault.repository.model.TotpCodeResult
import com.x8bit.bitwarden.data.vault.repository.model.UpdateFolderResult
import com.x8bit.bitwarden.data.vault.repository.model.UpdateSendResult
@@ -83,9 +85,11 @@ import com.x8bit.bitwarden.data.vault.repository.util.toEncryptedSdkSend
import com.x8bit.bitwarden.data.vault.repository.util.toEncryptedSdkSendList
import com.x8bit.bitwarden.ui.vault.feature.vault.model.VaultFilterType
import com.x8bit.bitwarden.ui.vault.feature.vault.util.toFilteredList
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.Job
import kotlinx.coroutines.async
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
@@ -135,6 +139,7 @@ class VaultRepositoryImpl(
private val vaultLockManager: VaultLockManager,
private val totpCodeManager: TotpCodeManager,
private val userLogoutManager: UserLogoutManager,
private val databaseSchemeManager: DatabaseSchemeManager,
pushManager: PushManager,
private val clock: Clock,
dispatcherManager: DispatcherManager,
@@ -312,7 +317,6 @@ class VaultRepositoryImpl(
}
}
@Suppress("LongMethod")
override fun sync() {
val userId = activeUserId ?: return
if (!syncJob.isCompleted) return
@@ -321,74 +325,7 @@ class VaultRepositoryImpl(
mutableFoldersStateFlow.updateToPendingOrLoading()
mutableCollectionsStateFlow.updateToPendingOrLoading()
mutableSendDataStateFlow.updateToPendingOrLoading()
syncJob = ioScope.launch {
val lastSyncInstant = settingsDiskSource
.getLastSyncTime(userId = userId)
?.toEpochMilli()
?: 0
syncService
.getAccountRevisionDateMillis()
.fold(
onSuccess = { serverRevisionDate ->
if (serverRevisionDate < lastSyncInstant) {
// We can skip the actual sync call if there is no new data
vaultDiskSource.resyncVaultData(userId)
settingsDiskSource.storeLastSyncTime(
userId = userId,
lastSyncTime = clock.instant(),
)
return@launch
}
},
onFailure = {
updateVaultStateFlowsToError(it)
return@launch
},
)
syncService
.sync()
.fold(
onSuccess = { syncResponse ->
val localSecurityStamp =
authDiskSource.userState?.activeAccount?.profile?.stamp
val serverSecurityStamp = syncResponse.profile.securityStamp
// Log the user out if the stamps do not match
localSecurityStamp?.let {
if (serverSecurityStamp != localSecurityStamp) {
userLogoutManager.softLogout(userId = userId, isExpired = true)
return@launch
}
}
// Update user information with additional information from sync response
authDiskSource.userState = authDiskSource
.userState
?.toUpdatedUserStateJson(
syncResponse = syncResponse,
)
unlockVaultForOrganizationsIfNecessary(syncResponse = syncResponse)
storeProfileData(syncResponse = syncResponse)
// Treat absent network policies as known empty data to
// distinguish between unknown null data.
authDiskSource.storePolicies(
userId = userId,
policies = syncResponse.policies.orEmpty(),
)
settingsDiskSource.storeLastSyncTime(
userId = userId,
lastSyncTime = clock.instant(),
)
vaultDiskSource.replaceVaultData(userId = userId, vault = syncResponse)
},
onFailure = { throwable ->
updateVaultStateFlowsToError(throwable)
},
)
}
syncJob = ioScope.launch { syncInternal(userId) }
}
@Suppress("MagicNumber")
@@ -396,15 +333,32 @@ class VaultRepositoryImpl(
val userId = activeUserId ?: return
val currentInstant = clock.instant()
val lastSyncInstant = settingsDiskSource.getLastSyncTime(userId = userId)
val lastDatabaseSchemeChangeInstant = databaseSchemeManager.lastDatabaseSchemeChangeInstant
// Sync if we have never done so or the last time was at last 30 minutes ago
// Sync if we have never done so, the last time was at last 30 minutes ago, or the database
// scheme changed since the last sync.
if (lastSyncInstant == null ||
currentInstant.isAfter(lastSyncInstant.plus(30, ChronoUnit.MINUTES))
currentInstant.isAfter(lastSyncInstant.plus(30, ChronoUnit.MINUTES)) ||
lastDatabaseSchemeChangeInstant?.isAfter(lastSyncInstant) == true
) {
sync()
}
}
override suspend fun syncForResult(): SyncVaultDataResult {
val userId = activeUserId
?: return SyncVaultDataResult.Error(throwable = null)
syncJob = ioScope
.async { syncInternal(userId) }
.also {
return try {
it.await()
} catch (e: CancellationException) {
SyncVaultDataResult.Error(throwable = e)
}
}
}
override fun getVaultItemStateFlow(itemId: String): StateFlow<DataState<CipherView?>> =
vaultDataStateFlow
.map { dataState ->
@@ -1355,6 +1309,86 @@ class VaultRepositoryImpl(
.onSuccess { vaultDiskSource.saveFolder(userId, it) }
}
//endregion Push Notification helpers
@Suppress("LongMethod")
private suspend fun syncInternal(userId: String): SyncVaultDataResult {
val lastSyncInstant = settingsDiskSource
.getLastSyncTime(userId = userId)
?.toEpochMilli()
?: 0
val lastDatabaseSchemeChangeInstant = databaseSchemeManager
.lastDatabaseSchemeChangeInstant
?.toEpochMilli()
?: 0
syncService
.getAccountRevisionDateMillis()
.fold(
onSuccess = { serverRevisionDate ->
if (serverRevisionDate < lastSyncInstant &&
lastDatabaseSchemeChangeInstant < lastSyncInstant
) {
// We can skip the actual sync call if there is no new data or database
// scheme changes since the last sync.
vaultDiskSource.resyncVaultData(userId)
settingsDiskSource.storeLastSyncTime(
userId = userId,
lastSyncTime = clock.instant(),
)
return SyncVaultDataResult.Success
}
},
onFailure = {
updateVaultStateFlowsToError(it)
return SyncVaultDataResult.Error(it)
},
)
syncService
.sync()
.fold(
onSuccess = { syncResponse ->
val localSecurityStamp =
authDiskSource.userState?.activeAccount?.profile?.stamp
val serverSecurityStamp = syncResponse.profile.securityStamp
// Log the user out if the stamps do not match
localSecurityStamp?.let {
if (serverSecurityStamp != localSecurityStamp) {
userLogoutManager.softLogout(userId = userId, isExpired = true)
return SyncVaultDataResult.Error(throwable = null)
}
}
// Update user information with additional information from sync response
authDiskSource.userState = authDiskSource
.userState
?.toUpdatedUserStateJson(
syncResponse = syncResponse,
)
unlockVaultForOrganizationsIfNecessary(syncResponse = syncResponse)
storeProfileData(syncResponse = syncResponse)
// Treat absent network policies as known empty data to
// distinguish between unknown null data.
authDiskSource.storePolicies(
userId = userId,
policies = syncResponse.policies.orEmpty(),
)
settingsDiskSource.storeLastSyncTime(
userId = userId,
lastSyncTime = clock.instant(),
)
vaultDiskSource.replaceVaultData(userId = userId, vault = syncResponse)
return SyncVaultDataResult.Success
},
onFailure = { throwable ->
updateVaultStateFlowsToError(throwable)
return SyncVaultDataResult.Error(throwable)
},
)
}
}
private fun <T> Throwable.toNetworkOrErrorState(data: T?): DataState<T> =

View File

@@ -3,6 +3,7 @@ package com.x8bit.bitwarden.data.vault.repository.di
import com.x8bit.bitwarden.data.auth.datasource.disk.AuthDiskSource
import com.x8bit.bitwarden.data.auth.manager.UserLogoutManager
import com.x8bit.bitwarden.data.platform.datasource.disk.SettingsDiskSource
import com.x8bit.bitwarden.data.platform.manager.DatabaseSchemeManager
import com.x8bit.bitwarden.data.platform.manager.PushManager
import com.x8bit.bitwarden.data.platform.manager.dispatcher.DispatcherManager
import com.x8bit.bitwarden.data.vault.datasource.disk.VaultDiskSource
@@ -49,6 +50,7 @@ object VaultRepositoryModule {
totpCodeManager: TotpCodeManager,
pushManager: PushManager,
userLogoutManager: UserLogoutManager,
databaseSchemeManager: DatabaseSchemeManager,
clock: Clock,
): VaultRepository = VaultRepositoryImpl(
syncService = syncService,
@@ -66,6 +68,7 @@ object VaultRepositoryModule {
totpCodeManager = totpCodeManager,
pushManager = pushManager,
userLogoutManager = userLogoutManager,
databaseSchemeManager = databaseSchemeManager,
clock = clock,
)
}

View File

@@ -0,0 +1,18 @@
package com.x8bit.bitwarden.data.vault.repository.model
/**
* Represents the result of a sync operation.
*/
sealed class SyncVaultDataResult {
/**
* Indicates a successful sync operation.
*/
data object Success : SyncVaultDataResult()
/**
* Indicates a failed sync operation.
*
* @property throwable The exception that caused the failure, if any.
*/
data class Error(val throwable: Throwable?) : SyncVaultDataResult()
}

View File

@@ -1,29 +1,79 @@
package com.x8bit.bitwarden.ui.auth.feature.accountsetup
import androidx.lifecycle.SavedStateHandle
import androidx.navigation.NavController
import androidx.navigation.NavGraphBuilder
import androidx.navigation.NavOptions
import androidx.navigation.NavType
import androidx.navigation.navArgument
import com.x8bit.bitwarden.data.platform.annotation.OmitFromCoverage
import com.x8bit.bitwarden.ui.platform.base.util.composableWithPushTransitions
import com.x8bit.bitwarden.ui.platform.base.util.composableWithSlideTransitions
/**
* Route name for [SetupAutoFillScreen].
* Route constant for navigating to the [SetupAutoFillScreen].
*/
const val SETUP_AUTO_FILL_ROUTE = "setup_auto_fill"
private const val SETUP_AUTO_FILL_PREFIX = "setup_auto_fill"
private const val SETUP_AUTO_FILL_AS_ROOT_PREFIX = "${SETUP_AUTO_FILL_PREFIX}_as_root"
private const val SETUP_AUTO_FILL_NAV_ARG = "isInitialSetup"
private const val SETUP_AUTO_FILL_ROUTE = "$SETUP_AUTO_FILL_PREFIX/{$SETUP_AUTO_FILL_NAV_ARG}"
const val SETUP_AUTO_FILL_AS_ROOT_ROUTE =
"$SETUP_AUTO_FILL_AS_ROOT_PREFIX/{$SETUP_AUTO_FILL_NAV_ARG}"
/**
* Arguments for the [SetupAutoFillScreen] using [SavedStateHandle].
*/
@OmitFromCoverage
data class SetupAutoFillScreenArgs(val isInitialSetup: Boolean) {
constructor(savedStateHandle: SavedStateHandle) : this(
isInitialSetup = requireNotNull(savedStateHandle[SETUP_AUTO_FILL_NAV_ARG]),
)
}
/**
* Navigate to the setup auto-fill screen.
*/
fun NavController.navigateToSetupAutoFillScreen(navOptions: NavOptions? = null) {
this.navigate(SETUP_AUTO_FILL_ROUTE, navOptions)
this.navigate("$SETUP_AUTO_FILL_PREFIX/false", navOptions)
}
/**
* Navigate to the setup auto-fill screen as the root.
*/
fun NavController.navigateToSetupAutoFillAsRootScreen(navOptions: NavOptions? = null) {
this.navigate("$SETUP_AUTO_FILL_AS_ROOT_PREFIX/true", navOptions)
}
/**
* Add the setup auto-fil screen to the nav graph.
*/
fun NavGraphBuilder.setupAutoFillDestination() {
composableWithPushTransitions(
fun NavGraphBuilder.setupAutoFillDestination(onNavigateBack: () -> Unit) {
composableWithSlideTransitions(
route = SETUP_AUTO_FILL_ROUTE,
arguments = setupAutofillNavArgs,
) {
SetupAutoFillScreen()
SetupAutoFillScreen(onNavigateBack = onNavigateBack)
}
}
/**
* Add the setup autofil screen to the root nav graph.
*/
fun NavGraphBuilder.setupAutoFillDestinationAsRoot() {
composableWithPushTransitions(
route = SETUP_AUTO_FILL_AS_ROOT_ROUTE,
arguments = setupAutofillNavArgs,
) {
SetupAutoFillScreen(
onNavigateBack = {
// No-Op
},
)
}
}
private val setupAutofillNavArgs = listOf(
navArgument(SETUP_AUTO_FILL_NAV_ARG) {
type = NavType.BoolType
},
)

View File

@@ -1,5 +1,7 @@
package com.x8bit.bitwarden.ui.auth.feature.accountsetup
import android.os.Parcelable
import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.viewModelScope
import com.x8bit.bitwarden.data.auth.datasource.disk.model.OnboardingStatus
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
@@ -10,20 +12,31 @@ import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.update
import kotlinx.parcelize.Parcelize
import javax.inject.Inject
private const val KEY_STATE = "state"
/**
* View model for the Auto-fill setup screen.
*/
@HiltViewModel
class SetupAutoFillViewModel @Inject constructor(
savedStateHandle: SavedStateHandle,
private val settingsRepository: SettingsRepository,
private val authRepository: AuthRepository,
) :
BaseViewModel<SetupAutoFillState, SetupAutoFillEvent, SetupAutoFillAction>(
initialState = run {
// We load the state from the savedStateHandle for testing purposes.
initialState = savedStateHandle[KEY_STATE] ?: run {
val userId = requireNotNull(authRepository.userStateFlow.value).activeUserId
SetupAutoFillState(userId = userId, dialogState = null, autofillEnabled = false)
val isInitialSetup = SetupAutoFillScreenArgs(savedStateHandle).isInitialSetup
SetupAutoFillState(
userId = userId,
dialogState = null,
autofillEnabled = false,
isInitialSetup = isInitialSetup,
)
},
) {
@@ -48,9 +61,15 @@ class SetupAutoFillViewModel @Inject constructor(
is SetupAutoFillAction.Internal.AutofillEnabledUpdateReceive -> {
handleAutofillEnabledUpdateReceive(action)
}
SetupAutoFillAction.CloseClick -> handleCloseClick()
}
}
private fun handleCloseClick() {
sendEvent(SetupAutoFillEvent.NavigateBack)
}
private fun handleAutofillEnabledUpdateReceive(
action: SetupAutoFillAction.Internal.AutofillEnabledUpdateReceive,
) {
@@ -83,7 +102,11 @@ class SetupAutoFillViewModel @Inject constructor(
}
private fun handleContinueClick() {
updateOnboardingStatusToNextStep()
if (state.isInitialSetup) {
updateOnboardingStatusToNextStep()
} else {
sendEvent(SetupAutoFillEvent.NavigateBack)
}
}
private fun handleAutofillServiceChanged(action: SetupAutoFillAction.AutofillServiceChanged) {
@@ -105,24 +128,28 @@ class SetupAutoFillViewModel @Inject constructor(
/**
* UI State for the Auto-fill setup screen.
*/
@Parcelize
data class SetupAutoFillState(
val userId: String,
val dialogState: SetupAutoFillDialogState?,
val autofillEnabled: Boolean,
)
val isInitialSetup: Boolean,
) : Parcelable
/**
* Dialog states for the Auto-fill setup screen.
*/
sealed class SetupAutoFillDialogState {
sealed class SetupAutoFillDialogState : Parcelable {
/**
* Represents the turn on later dialog.
*/
@Parcelize
data object TurnOnLaterDialog : SetupAutoFillDialogState()
/**
* Represents the autofill fallback dialog.
*/
@Parcelize
data object AutoFillFallbackDialog : SetupAutoFillDialogState()
}
@@ -135,6 +162,11 @@ sealed class SetupAutoFillEvent {
* Navigate to the autofill settings screen.
*/
data object NavigateToAutofillSettings : SetupAutoFillEvent()
/**
* Navigate back.
*/
data object NavigateBack : SetupAutoFillEvent()
}
/**
@@ -173,6 +205,11 @@ sealed class SetupAutoFillAction {
*/
data object AutoFillServiceFallback : SetupAutoFillAction()
/**
* The user has clicked the close button.
*/
data object CloseClick : SetupAutoFillAction()
/**
* Internal actions not send through UI.
*/

View File

@@ -15,11 +15,11 @@ import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
@@ -38,6 +38,7 @@ import com.x8bit.bitwarden.ui.platform.base.util.EventsEffect
import com.x8bit.bitwarden.ui.platform.base.util.asText
import com.x8bit.bitwarden.ui.platform.base.util.standardHorizontalMargin
import com.x8bit.bitwarden.ui.platform.components.appbar.BitwardenTopAppBar
import com.x8bit.bitwarden.ui.platform.components.appbar.NavigationIcon
import com.x8bit.bitwarden.ui.platform.components.button.BitwardenFilledButton
import com.x8bit.bitwarden.ui.platform.components.button.BitwardenTextButton
import com.x8bit.bitwarden.ui.platform.components.dialog.BasicDialogState
@@ -46,6 +47,7 @@ import com.x8bit.bitwarden.ui.platform.components.dialog.BitwardenTwoButtonDialo
import com.x8bit.bitwarden.ui.platform.components.image.BitwardenGifImage
import com.x8bit.bitwarden.ui.platform.components.scaffold.BitwardenScaffold
import com.x8bit.bitwarden.ui.platform.components.toggle.BitwardenWideSwitch
import com.x8bit.bitwarden.ui.platform.components.util.rememberVectorPainter
import com.x8bit.bitwarden.ui.platform.composition.LocalIntentManager
import com.x8bit.bitwarden.ui.platform.manager.intent.IntentManager
import com.x8bit.bitwarden.ui.platform.theme.BitwardenTheme
@@ -58,6 +60,7 @@ import com.x8bit.bitwarden.ui.platform.util.isPortrait
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun SetupAutoFillScreen(
onNavigateBack: () -> Unit,
intentManager: IntentManager = LocalIntentManager.current,
viewModel: SetupAutoFillViewModel = hiltViewModel(),
) {
@@ -71,6 +74,8 @@ fun SetupAutoFillScreen(
handler.sendAutoFillServiceFallback.invoke()
}
}
SetupAutoFillEvent.NavigateBack -> onNavigateBack()
}
}
when (state.dialogState) {
@@ -106,14 +111,32 @@ fun SetupAutoFillScreen(
.nestedScroll(scrollBehavior.nestedScrollConnection),
topBar = {
BitwardenTopAppBar(
title = stringResource(id = R.string.account_setup),
title = stringResource(
id = if (state.isInitialSetup) {
R.string.account_setup
} else {
R.string.turn_on_autofill
},
),
scrollBehavior = scrollBehavior,
navigationIcon = null,
navigationIcon = if (state.isInitialSetup) {
null
} else {
NavigationIcon(
navigationIcon = rememberVectorPainter(id = R.drawable.ic_close),
navigationIconContentDescription = stringResource(id = R.string.close),
onNavigationIconClick = remember(viewModel) {
{
viewModel.trySendAction(SetupAutoFillAction.CloseClick)
}
},
)
},
)
},
) { innerPadding ->
SetupAutoFillContent(
autofillEnabled = state.autofillEnabled,
state = state,
onAutofillServiceChanged = { handler.onAutofillServiceChanged(it) },
onContinueClick = handler.onContinueClick,
onTurnOnLaterClick = handler.onTurnOnLaterClick,
@@ -128,7 +151,7 @@ fun SetupAutoFillScreen(
@Suppress("LongMethod")
@Composable
private fun SetupAutoFillContent(
autofillEnabled: Boolean,
state: SetupAutoFillState,
onAutofillServiceChanged: (Boolean) -> Unit,
onContinueClick: () -> Unit,
onTurnOnLaterClick: () -> Unit,
@@ -148,7 +171,7 @@ private fun SetupAutoFillContent(
label = stringResource(
R.string.autofill_services,
),
isChecked = autofillEnabled,
isChecked = state.autofillEnabled,
onCheckedChange = onAutofillServiceChanged,
modifier = Modifier
.fillMaxWidth()
@@ -163,13 +186,15 @@ private fun SetupAutoFillContent(
.standardHorizontalMargin(),
)
Spacer(modifier = Modifier.height(12.dp))
BitwardenTextButton(
label = stringResource(R.string.turn_on_later),
onClick = onTurnOnLaterClick,
modifier = Modifier
.fillMaxWidth()
.standardHorizontalMargin(),
)
if (state.isInitialSetup) {
BitwardenTextButton(
label = stringResource(R.string.turn_on_later),
onClick = onTurnOnLaterClick,
modifier = Modifier
.fillMaxWidth()
.standardHorizontalMargin(),
)
}
Spacer(modifier = Modifier.navigationBarsPadding())
}
}
@@ -220,15 +245,15 @@ private fun OrderedHeaderContent() {
) {
Text(
text = stringResource(R.string.turn_on_autofill),
style = MaterialTheme.typography.titleMedium,
color = MaterialTheme.colorScheme.onSurface,
style = BitwardenTheme.typography.titleMedium,
color = BitwardenTheme.colorScheme.text.primary,
textAlign = TextAlign.Center,
)
Spacer(modifier = Modifier.height(8.dp))
Text(
text = stringResource(R.string.use_autofill_to_log_into_your_accounts),
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant,
style = BitwardenTheme.typography.bodyMedium,
color = BitwardenTheme.colorScheme.text.primary,
textAlign = TextAlign.Center,
// Apply similar line breaks to design
modifier = Modifier.sizeIn(maxWidth = 300.dp),
@@ -241,7 +266,12 @@ private fun OrderedHeaderContent() {
private fun SetupAutoFillContentDisabled_preview() {
BitwardenTheme {
SetupAutoFillContent(
autofillEnabled = false,
state = SetupAutoFillState(
userId = "disputationi",
dialogState = null,
autofillEnabled = false,
isInitialSetup = true,
),
onAutofillServiceChanged = {},
onContinueClick = {},
onTurnOnLaterClick = {},
@@ -254,7 +284,12 @@ private fun SetupAutoFillContentDisabled_preview() {
private fun SetupAutoFillContentEnabled_preview() {
BitwardenTheme {
SetupAutoFillContent(
autofillEnabled = true,
state = SetupAutoFillState(
userId = "disputationi",
dialogState = null,
autofillEnabled = true,
isInitialSetup = true,
),
onAutofillServiceChanged = {},
onContinueClick = {},
onTurnOnLaterClick = {},

View File

@@ -8,10 +8,10 @@ import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBarDefaults
@@ -86,13 +86,14 @@ private fun SetupCompleteContent(
contentDescription = null,
modifier = Modifier
.align(CenterHorizontally)
.standardHorizontalMargin(),
.standardHorizontalMargin()
.size(size = 100.dp),
)
Spacer(Modifier.height(24.dp))
Text(
text = stringResource(R.string.youre_all_set),
style = MaterialTheme.typography.titleMedium,
color = MaterialTheme.colorScheme.onSurface,
style = BitwardenTheme.typography.titleMedium,
color = BitwardenTheme.colorScheme.text.primary,
textAlign = TextAlign.Center,
modifier = Modifier
.align(CenterHorizontally)
@@ -101,8 +102,8 @@ private fun SetupCompleteContent(
Spacer(Modifier.height(8.dp))
Text(
text = stringResource(R.string.what_bitwarden_has_to_offer),
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant,
style = BitwardenTheme.typography.bodyMedium,
color = BitwardenTheme.colorScheme.text.primary,
textAlign = TextAlign.Center,
modifier = Modifier
.align(CenterHorizontally)

View File

@@ -1,29 +1,88 @@
package com.x8bit.bitwarden.ui.auth.feature.accountsetup
import androidx.lifecycle.SavedStateHandle
import androidx.navigation.NavController
import androidx.navigation.NavGraphBuilder
import androidx.navigation.NavOptions
import androidx.navigation.NavType
import androidx.navigation.navArgument
import com.x8bit.bitwarden.data.platform.annotation.OmitFromCoverage
import com.x8bit.bitwarden.ui.platform.base.util.composableWithPushTransitions
import com.x8bit.bitwarden.ui.platform.base.util.composableWithSlideTransitions
/**
* Route for [SetupUnlockScreen]
* Route constants for [SetupUnlockScreen]
*/
const val SETUP_UNLOCK_ROUTE = "setup_unlock"
private const val SETUP_UNLOCK_PREFIX = "setup_unlock"
private const val SETUP_UNLOCK_AS_ROOT_PREFIX = "${SETUP_UNLOCK_PREFIX}_as_root"
private const val SETUP_UNLOCK_INITIAL_SETUP_ARG = "isInitialSetup"
const val SETUP_UNLOCK_AS_ROOT_ROUTE = "$SETUP_UNLOCK_AS_ROOT_PREFIX/" +
"{$SETUP_UNLOCK_INITIAL_SETUP_ARG}"
private const val SETUP_UNLOCK_ROUTE = "$SETUP_UNLOCK_PREFIX/{$SETUP_UNLOCK_INITIAL_SETUP_ARG}"
/**
* Class to retrieve setup unlock arguments from the [SavedStateHandle].
*/
@OmitFromCoverage
data class SetupUnlockArgs(
val isInitialSetup: Boolean,
) {
constructor(savedStateHandle: SavedStateHandle) : this(
isInitialSetup = requireNotNull(savedStateHandle[SETUP_UNLOCK_INITIAL_SETUP_ARG]),
)
}
/**
* Navigate to the setup unlock screen.
*/
fun NavController.navigateToSetupUnlockScreen(navOptions: NavOptions? = null) {
this.navigate(SETUP_UNLOCK_ROUTE, navOptions)
this.navigate("$SETUP_UNLOCK_PREFIX/false", navOptions)
}
/**
* Add the setup unlock screen to the nav graph.
* Navigate to the setup unlock screen as root.
*/
fun NavGraphBuilder.setupUnlockDestination() {
composableWithPushTransitions(
fun NavController.navigateToSetupUnlockScreenAsRoot(navOptions: NavOptions? = null) {
this.navigate("$SETUP_UNLOCK_AS_ROOT_PREFIX/true", navOptions)
}
/**
* Add the setup unlock screen to a nav graph.
*/
fun NavGraphBuilder.setupUnlockDestination(
onNavigateBack: () -> Unit,
) {
composableWithSlideTransitions(
route = SETUP_UNLOCK_ROUTE,
arguments = setupUnlockArguments,
) {
SetupUnlockScreen()
SetupUnlockScreen(
onNavigateBack = onNavigateBack,
)
}
}
/**
* Add the setup unlock screen to the root nav graph.
*/
fun NavGraphBuilder.setupUnlockDestinationAsRoot() {
composableWithPushTransitions(
route = SETUP_UNLOCK_AS_ROOT_ROUTE,
arguments = setupUnlockArguments,
) {
SetupUnlockScreen(
onNavigateBack = {
// No-Op
},
)
}
}
private val setupUnlockArguments = listOf(
navArgument(
name = SETUP_UNLOCK_INITIAL_SETUP_ARG,
builder = {
type = NavType.BoolType
},
),
)

View File

@@ -16,7 +16,6 @@ import androidx.compose.foundation.layout.width
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.material3.rememberTopAppBarState
@@ -41,6 +40,7 @@ import com.x8bit.bitwarden.ui.auth.feature.accountsetup.handlers.SetupUnlockHand
import com.x8bit.bitwarden.ui.platform.base.util.EventsEffect
import com.x8bit.bitwarden.ui.platform.base.util.standardHorizontalMargin
import com.x8bit.bitwarden.ui.platform.components.appbar.BitwardenTopAppBar
import com.x8bit.bitwarden.ui.platform.components.appbar.NavigationIcon
import com.x8bit.bitwarden.ui.platform.components.button.BitwardenFilledButton
import com.x8bit.bitwarden.ui.platform.components.button.BitwardenTextButton
import com.x8bit.bitwarden.ui.platform.components.dialog.BasicDialogState
@@ -54,16 +54,19 @@ import com.x8bit.bitwarden.ui.platform.components.toggle.BitwardenUnlockWithPinS
import com.x8bit.bitwarden.ui.platform.components.util.rememberVectorPainter
import com.x8bit.bitwarden.ui.platform.composition.LocalBiometricsManager
import com.x8bit.bitwarden.ui.platform.manager.biometrics.BiometricsManager
import com.x8bit.bitwarden.ui.platform.theme.BitwardenTheme
import com.x8bit.bitwarden.ui.platform.util.isPortrait
/**
* Top level composable for the setup unlock screen.
*/
@OptIn(ExperimentalMaterial3Api::class)
@Suppress("LongMethod")
@Composable
fun SetupUnlockScreen(
viewModel: SetupUnlockViewModel = hiltViewModel(),
biometricsManager: BiometricsManager = LocalBiometricsManager.current,
onNavigateBack: () -> Unit,
) {
val state by viewModel.stateFlow.collectAsStateWithLifecycle()
val handler = remember(viewModel) { SetupUnlockHandler.create(viewModel = viewModel) }
@@ -83,6 +86,8 @@ fun SetupUnlockScreen(
cipher = event.cipher,
)
}
SetupUnlockEvent.NavigateBack -> onNavigateBack()
}
}
@@ -100,9 +105,27 @@ fun SetupUnlockScreen(
.nestedScroll(scrollBehavior.nestedScrollConnection),
topBar = {
BitwardenTopAppBar(
title = stringResource(id = R.string.account_setup),
title = stringResource(
id = if (state.isInitialSetup) {
R.string.account_setup
} else {
R.string.set_up_unlock
},
),
scrollBehavior = scrollBehavior,
navigationIcon = null,
navigationIcon = if (state.isInitialSetup) {
null
} else {
NavigationIcon(
navigationIcon = rememberVectorPainter(id = R.drawable.ic_close),
navigationIconContentDescription = stringResource(id = R.string.close),
onNavigationIconClick = remember(viewModel) {
{
viewModel.trySendAction(SetupUnlockAction.CloseClick)
}
},
)
},
)
},
) { innerPadding ->
@@ -138,7 +161,7 @@ private fun SetupUnlockScreenContent(
Spacer(modifier = Modifier.height(height = 24.dp))
BitwardenUnlockWithBiometricsSwitch(
isBiometricsSupported = biometricsManager.isBiometricsSupported,
biometricSupportStatus = biometricsManager.biometricSupportStatus,
isChecked = state.isUnlockWithBiometricsEnabled || showBiometricsPrompt,
onDisableBiometrics = handler.onDisableBiometrics,
onEnableBiometrics = handler.onEnableBiometrics,
@@ -169,14 +192,16 @@ private fun SetupUnlockScreenContent(
)
Spacer(modifier = Modifier.height(height = 12.dp))
SetUpLaterButton(
onConfirmClick = handler.onSetUpLaterClick,
modifier = Modifier
.fillMaxWidth()
.standardHorizontalMargin(),
)
if (state.isInitialSetup) {
SetUpLaterButton(
onConfirmClick = handler.onSetUpLaterClick,
modifier = Modifier
.fillMaxWidth()
.standardHorizontalMargin(),
)
Spacer(modifier = Modifier.height(height = 12.dp))
Spacer(modifier = Modifier.height(height = 12.dp))
}
Spacer(modifier = Modifier.navigationBarsPadding())
}
}
@@ -227,8 +252,8 @@ private fun ColumnScope.SetupUnlockHeaderPortrait() {
Spacer(modifier = Modifier.height(height = 24.dp))
Text(
text = stringResource(id = R.string.set_up_unlock),
style = MaterialTheme.typography.titleMedium,
color = MaterialTheme.colorScheme.onSurface,
style = BitwardenTheme.typography.titleMedium,
color = BitwardenTheme.colorScheme.text.primary,
textAlign = TextAlign.Center,
modifier = Modifier
.fillMaxWidth()
@@ -241,8 +266,8 @@ private fun ColumnScope.SetupUnlockHeaderPortrait() {
text = stringResource(
id = R.string.set_up_biometrics_or_choose_a_pin_code_to_quickly_access_your_vault_and_autofill_your_logins,
),
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurface,
style = BitwardenTheme.typography.bodyMedium,
color = BitwardenTheme.colorScheme.text.primary,
textAlign = TextAlign.Center,
modifier = Modifier
.fillMaxWidth()
@@ -273,8 +298,8 @@ private fun SetupUnlockHeaderLandscape(
) {
Text(
text = stringResource(id = R.string.set_up_unlock),
style = MaterialTheme.typography.titleMedium,
color = MaterialTheme.colorScheme.onSurface,
style = BitwardenTheme.typography.titleMedium,
color = BitwardenTheme.colorScheme.text.primary,
textAlign = TextAlign.Center,
modifier = Modifier.fillMaxWidth(),
)
@@ -285,8 +310,8 @@ private fun SetupUnlockHeaderLandscape(
text = stringResource(
id = R.string.set_up_biometrics_or_choose_a_pin_code_to_quickly_access_your_vault_and_autofill_your_logins,
),
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurface,
style = BitwardenTheme.typography.bodyMedium,
color = BitwardenTheme.colorScheme.text.primary,
textAlign = TextAlign.Center,
modifier = Modifier.fillMaxWidth(),
)

View File

@@ -25,6 +25,7 @@ private const val KEY_STATE = "state"
/**
* Models logic for the setup unlock screen.
*/
@Suppress("TooManyFunctions")
@HiltViewModel
class SetupUnlockViewModel @Inject constructor(
savedStateHandle: SavedStateHandle,
@@ -32,12 +33,15 @@ class SetupUnlockViewModel @Inject constructor(
private val settingsRepository: SettingsRepository,
private val biometricsEncryptionManager: BiometricsEncryptionManager,
) : BaseViewModel<SetupUnlockState, SetupUnlockEvent, SetupUnlockAction>(
// We load the state from the savedStateHandle for testing purposes.
initialState = savedStateHandle[KEY_STATE] ?: run {
val userId = requireNotNull(authRepository.userStateFlow.value).activeUserId
val isBiometricsValid = biometricsEncryptionManager.isBiometricIntegrityValid(
userId = userId,
cipher = biometricsEncryptionManager.getOrCreateCipher(userId = userId),
)
// whether or not the user has completed the initial setup prior to this.
val isInitialSetup = SetupUnlockArgs(savedStateHandle).isInitialSetup
SetupUnlockState(
userId = userId,
isUnlockWithPasswordEnabled = authRepository
@@ -49,6 +53,7 @@ class SetupUnlockViewModel @Inject constructor(
isUnlockWithBiometricsEnabled = settingsRepository.isUnlockWithBiometricsEnabled &&
isBiometricsValid,
dialogState = null,
isInitialSetup = isInitialSetup,
)
},
) {
@@ -64,11 +69,20 @@ class SetupUnlockViewModel @Inject constructor(
is SetupUnlockAction.UnlockWithPinToggle -> handleUnlockWithPinToggle(action)
is SetupUnlockAction.Internal -> handleInternalActions(action)
SetupUnlockAction.CloseClick -> handleCloseClick()
}
}
private fun handleCloseClick() {
sendEvent(SetupUnlockEvent.NavigateBack)
}
private fun handleContinueClick() {
updateOnboardingStatusToNextStep()
if (state.isInitialSetup) {
updateOnboardingStatusToNextStep()
} else {
sendEvent(SetupUnlockEvent.NavigateBack)
}
}
private fun handleEnableBiometricsClick() {
@@ -196,6 +210,7 @@ data class SetupUnlockState(
val isUnlockWithPinEnabled: Boolean,
val isUnlockWithBiometricsEnabled: Boolean,
val dialogState: DialogState?,
val isInitialSetup: Boolean,
) : Parcelable {
/**
* Indicates whether the continue button should be enabled or disabled.
@@ -237,6 +252,11 @@ sealed class SetupUnlockEvent {
data class ShowBiometricsPrompt(
val cipher: Cipher,
) : SetupUnlockEvent()
/**
* Navigates back to the previous screen.
*/
data object NavigateBack : SetupUnlockEvent()
}
/**
@@ -277,6 +297,11 @@ sealed class SetupUnlockAction {
*/
data object DismissDialog : SetupUnlockAction()
/**
* The user has clicked the close button.
*/
data object CloseClick : SetupUnlockAction()
/**
* Models actions that can be sent by the view model itself.
*/

Some files were not shown because too many files have changed in this diff Show More