Compare commits

..

244 Commits

Author SHA1 Message Date
Dave Severns
0413cdc70d PM-15412 🍒 Pull-to-refresh operations should not invoke a forced sync. (#4389) 2024-11-26 16:00:07 -05:00
David Perez
019bf8d0fa PM-15177: Improve destructive fallback logic (#4372) 2024-11-22 15:59:40 -06:00
David Perez
366c86da41 All user input syncs should be forced (#4370) 2024-11-22 13:24:38 -06:00
Dave Severns
a1881ce4d9 PM-15062 prevent account lockout when biometrics is only unlock method and no longer supported class. (#4341) 2024-11-20 11:23:18 -05:00
Dave Severns
13210343db PM-14429 Set the min and max range of the slider to match the restrictions not update the min with the computed min. (#4305) 2024-11-14 20:36:01 +00:00
Patrick Honkonen
3c4ac8b01a [PM-14596] Sync on database scheme change (#4304) 2024-11-14 19:18:25 +00:00
Patrick Honkonen
0a888a72c8 [PM-14553] Make canManage property of collections optional (#4284) 2024-11-14 19:18:04 +00:00
André Bispo
40f33dff89 [PM-11304] Ownership Not Defaulting To Org and Collection (#4254) 2024-11-14 08:15:01 +00:00
Álison Fernandes
5938e38070 [PM-11598] GitHub Release Workflow (#4285) 2024-11-13 22:34:36 +00:00
Álison Fernandes
31bc171d6b [PM-14879] Release Branch creation workflow (#4294) 2024-11-13 22:23:48 +00:00
David Perez
0967234ad8 PM-14411: Autofill logic to work better with QuickTile (#4300) 2024-11-13 21:38:08 +00:00
David Perez
911c9e4704 Update androidx dependecies and target API (#4212) 2024-11-13 16:22:55 +00:00
Dave Severns
072c3a992c PM-14414 hides autofill card for all users if autofill service is enabled. (#4297) 2024-11-13 15:41:14 +00:00
Álison Fernandes
1e0e4831b8 [PM-14897] Enhance build.yml run summary and fix f-droid distribution (#4296) 2024-11-13 13:27:56 +00:00
David Perez
e804dbd48e PM-14851: Blank names should be considered null (#4292) 2024-11-12 21:30:07 +00:00
Dave Severns
9a5aa217e6 PM-14352 Dismiss Snackbar when user clicks it as a default unless the specific dismiss action is present. (#4291) 2024-11-12 18:39:30 +00:00
Andrew Haisting
c6beaec102 BITAU-200 Log non-fatal authenticator bridge errors (#4228)
Co-authored-by: Patrick Honkonen <phonkonen@bitwarden.com>
2024-11-12 16:15:21 +00:00
renovate[bot]
5a9944f79d [deps]: Update gh minor (#4279)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-12 15:52:20 +00:00
David Perez
89c267aa5d PM-14854: Keep NetworkResult to avoid obfuscation crash in release (#4289) 2024-11-12 15:38:49 +00:00
David Perez
f33296b44f PM-14805: Use network result in all Retrofit API requests (#4286) 2024-11-11 22:17:57 +00:00
David Perez
a8416b073e Improve accessibility autofill performance (#4276) 2024-11-11 21:08:05 +00:00
Dave Severns
fd4a7c5716 PM-14597 remove notification if device login is decleined (#4256) 2024-11-11 21:02:35 +00:00
David Perez
771e719963 PM-14805: Ensure results cannot be double wrapped from 'asSuccess' (#4283) 2024-11-11 20:46:55 +00:00
Patrick Honkonen
c5293715e1 [PM-14526] Add JsonNames annotation to SyncResponseJson (#4269)
Co-authored-by: David Perez <david@livefront.com>
2024-11-11 17:56:27 +00:00
Patrick Honkonen
2c40a7f105 [PM-14589] Prevent SSH key item creation (#4251) 2024-11-11 16:52:53 +00:00
André Bispo
a3ed2bc068 [PM-11303] Add button missing for folders (#4250) 2024-11-11 16:26:04 +00:00
David Perez
16cc70f344 Clean up the generator screen and handlers (#4270) 2024-11-11 16:07:16 +00:00
Dave Severns
6dd783051f PM-13803 Check to see if an existing admin request is pending before … (#4271) 2024-11-11 15:53:11 +00:00
renovate[bot]
1bb85d0fa0 [deps]: Update com.google.devtools.ksp to v2.0.21-1.0.27 (#4278)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-11 15:51:29 +00:00
Dave Severns
dfeb87be10 PM-13988 Hide the action card if the user makes a selection but does not click continue on setup unlock (#4249) 2024-11-11 15:31:21 +00:00
renovate[bot]
dae50a7b88 [deps]: Lock file maintenance (#4280)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-11 15:19:22 +00:00
David Perez
63324fcec1 PM-14458: Fix notifications prompt on first use (#4275) 2024-11-08 23:02:58 +00:00
Patrick Honkonen
49642f5a1d [PM-14656] Add default value to BaseEnumeratedIntSerializer (#4272) 2024-11-08 22:21:16 +00:00
David Perez
016d0f889c PM-14411: Allow accessibility autofill to run when app is already in background (#4255) 2024-11-08 21:30:14 +00:00
Patrick Honkonen
fe84feb184 PM-14433: Null domain data (#4268)
Co-authored-by: Dave Severns <149429124+dseverns-livefront@users.noreply.github.com>
Co-authored-by: David Perez <david@livefront.com>
2024-11-08 20:18:18 +00:00
aj-rosado
54d3b34876 [PM-11753] Listening to vaultUnlock state on mutableCiphers, folders, collections and send state flow (#4214) 2024-11-08 18:33:49 +00:00
Patrick Honkonen
b6dfc3d17b PM-14433 update flow type to nullable so we can handle gracefully and avoid crash (#4263)
Co-authored-by: Dave Severns <149429124+dseverns-livefront@users.noreply.github.com>
2024-11-08 18:23:39 +00:00
github-actions[bot]
96c6b9c214 Autosync Crowdin Translations (#4260)
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-11-08 17:47:22 +00:00
David Perez
27666c193e PM-14644: Segmented control should be conditionally displayed for modal generator screen (#4262) 2024-11-08 17:03:54 +00:00
Dave Severns
b76f7202a4 PM-14621 update the copy for step three instruction and cta button (#4259) 2024-11-08 14:36:54 +00:00
Patrick Honkonen
7ccba88780 [PM-13360] Respect manage permission to assign collections (#4190) 2024-11-07 20:47:22 +00:00
Patrick Honkonen
87d324b063 [PM-12922] Disable delete if user can't manage collection (#4179) 2024-11-06 23:42:06 +00:00
Dave Severns
e397c036e4 PM-14353 : Clean up consumed snackbar on quick resubmission due to state based nav. (#4235) 2024-11-06 19:39:55 +00:00
David Perez
29384596d4 PM-14410: App restart timeout action (#4237) 2024-11-06 17:40:54 +00:00
github-actions[bot]
88a741c93a Autosync Crowdin Translations (#4217)
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-11-06 17:26:48 +00:00
David Perez
db3490f61a PM-14480: Update IntentManager to be able to launch apps (#4233) 2024-11-05 17:36:10 +00:00
David Perez
4930c1032e PM-14458: Update notifications permissions request (#4229) 2024-11-05 17:16:58 +00:00
Dave Severns
202b4de5ca PM-13848 Handle URIs with ports and host matching (#4203) 2024-11-05 15:29:05 +00:00
Andrew Haisting
8f9585e4bc Bump authenticatorbridge sdk version to 1.0.0 (#4221) 2024-11-04 14:24:36 +00:00
André Bispo
e5e0464929 [PM-12406] Introduce new endpoint and replace SSO details response flow (#4177) 2024-11-04 10:53:57 +00:00
David Perez
c2537f329d PM-14036: Add extra slider padding (#4220) 2024-11-01 19:12:55 +00:00
ifernandezdiaz
b7ffa3966d QA-970: Adding testTags for radiobutton and floating options elements (#4188)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Dave Severns <149429124+dseverns-livefront@users.noreply.github.com>
Co-authored-by: David Perez <david@livefront.com>
Co-authored-by: Patrick Honkonen <1883101+SaintPatrck@users.noreply.github.com>
Co-authored-by: Álison Fernandes <vvolkgang@users.noreply.github.com>
2024-11-01 19:03:44 +00:00
David Perez
9240fb82e4 PM-14379: Stop storing 'null' in as the word separator (#4219) 2024-11-01 19:03:03 +00:00
David Perez
2eb41e932b PM-14044: Fix line-breaking logic (#4218) 2024-11-01 16:21:48 +00:00
David Perez
51e299998f Update to AGP 8.7.2 (#4216) 2024-11-01 15:19:03 +00:00
Patrick Honkonen
2f6578fd5a [PM-14273] Add copy functionality for SSH key fields (#4204) 2024-10-31 20:41:13 +00:00
Patrick Honkonen
0844939eca [PM-14271] Disable editing SSH key fields in edit mode (#4201) 2024-10-31 20:40:33 +00:00
Patrick Honkonen
8f2d55c146 [PM-14346] Run alias generation on the IO dispatcher (#4215) 2024-10-31 20:39:11 +00:00
David Perez
3e5e6ce3ab Update compose BOM to 2024.10.01 (#4213) 2024-10-31 20:11:31 +00:00
David Perez
4831750ffd PM-14255: Remove accessibility logic to improve overall performance (#4206) 2024-10-31 18:05:33 +00:00
David Perez
7d7380d622 Update genrator icons on bottom nav (#4211) 2024-10-31 17:29:41 +00:00
David Perez
a0b9e92ae9 Update the camera library (#4210) 2024-10-31 16:27:28 +00:00
Álison Fernandes
ce180f1bbb [PM-14261] Update README and bugs template to remove Beta references (#4198) 2024-10-31 10:43:41 +00:00
David Perez
c99e5ce2de PM-13842: Hide ownership when the user has no organizations (#4199) 2024-10-30 20:13:15 +00:00
Patrick Honkonen
eaa7923d1f [PM-14186] Update SDK to make SSH key properties required (#4200) 2024-10-30 18:42:33 +00:00
Patrick Honkonen
56367cc14e [PM-13900] Update Bitwarden SDK to add canManage to Collection objects (#4169) 2024-10-30 15:53:30 +00:00
Dave Severns
6e0ce3b742 PM-13155 add shortcuts file to beta source set for beta app id (#4196) 2024-10-30 15:53:12 +00:00
Patrick Honkonen
fab018782c [PM-14254] Keep Android verifier for JNI usage (#4197) 2024-10-30 14:48:49 +00:00
aj-rosado
0211729525 [PM-14241] Backport Timber hotfix (#4195) 2024-10-30 12:49:25 +00:00
ifernandezdiaz
540ece5a40 QA-954: Add testtags to Send screen elements (#4162) 2024-10-29 23:21:08 +00:00
Patrick Honkonen
78e7adfbc1 [PM-10405] Add SSH key cipher type (#4158) 2024-10-29 21:40:20 +00:00
David Perez
6f26ae50ea PM-14044: Update generator line breaks to account for padding on both sides (#4187) 2024-10-29 21:20:44 +00:00
Álison Fernandes
a5e57f1836 [PM-14224] Automate Play Store prod variant publishing (#4183) 2024-10-29 18:33:21 +00:00
David Perez
9e5fefa3ee Update copy and generate icons (#4185) 2024-10-29 18:31:12 +00:00
Dave Severns
8b16135955 PM-11188 show snackbar after import success. PM-13943 add relay for snackbar events across screen contexts. (#4152) 2024-10-29 18:23:00 +00:00
David Perez
a1108889cb PM-14200: Update the eyebrows throughout the app (#4181) 2024-10-29 13:57:16 +00:00
David Perez
150c8e0312 PM-14201: Update the default divider thickness (#4182) 2024-10-29 13:56:57 +00:00
Dave Severns
f3916b4ef6 PM-13988 observe changes to unlock status on settings screen (#4180) 2024-10-29 13:27:46 +00:00
ifernandezdiaz
8df4292e08 QA-957: Adding missing testTag for collection list container (#4178) 2024-10-29 12:12:07 +00:00
Dave Severns
05c768610e PM-13908 fixing copy on step2 and step3 and making vault url dynamic (#4154) 2024-10-28 19:11:25 +00:00
Dave Severns
21a5242abe PM-14009 complete fix importlogins card show logic (#4175) 2024-10-28 18:22:30 +00:00
Patrick Honkonen
deb9eb8d9b [PM-13908] Disable ExtraTranslation lint warning (#4176) 2024-10-28 17:47:27 +00:00
David Perez
4a91d87d9d PM-14184: Update the switch thoughout the app (#4170) 2024-10-28 17:09:29 +00:00
Dave Severns
064db9fb6a PM-13698 only dismiss the card if the user dismisses or completes the… (#4165) 2024-10-28 14:38:17 +00:00
ifernandezdiaz
c47f8606cd QA-953: Adding testTag to elements in Add TOTP screen (#4160) 2024-10-28 14:28:07 +00:00
renovate[bot]
3e2f10a5b9 [deps]: Update com.google.devtools.ksp to v2.0.21-1.0.26 (#4172)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-28 14:27:53 +00:00
renovate[bot]
b060b70a6b [deps]: Update gh minor (#4173)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-28 14:26:16 +00:00
renovate[bot]
7ea7d78e66 [deps]: Lock file maintenance (#4174)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-28 14:19:02 +00:00
David Perez
b64175ff6e Update fab design (#4168) 2024-10-25 21:29:16 +00:00
Andrew Haisting
164cc09f19 BITAU-182 BITAU-107 Don't show authetnicator sync toggle below API 31 (#4156) 2024-10-25 21:27:59 +00:00
David Perez
0960f61c37 Simplify usages of turbineScope (#4167) 2024-10-25 19:50:43 +00:00
github-actions[bot]
f8bf864fc9 Autosync Crowdin Translations (#4159)
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-25 19:44:01 +00:00
David Perez
93aece75cf PM-14036: Update the slider UI (#4164) 2024-10-25 18:11:54 +00:00
Andrew Haisting
5159258de5 Make isBuildVersionBelow in authenticatorbridge internal (#4157) 2024-10-25 16:39:53 +00:00
ifernandezdiaz
68a834ac14 QA-955: Adding testTag to About screen rows (#4163) 2024-10-25 16:32:27 +00:00
ifernandezdiaz
33a430419c QA-952: Adding test tags for Toggle items (#4153) 2024-10-25 15:53:48 +00:00
Dave Severns
eb4ffebba0 PM-14009 Refactor storing first time values to the first time action manager (#4161) 2024-10-25 15:40:18 +00:00
David Perez
53d4c4c03e Remove query params from network logging (#4155) 2024-10-24 20:18:20 +00:00
David Perez
e80585f77e PM-13937: Update button padding, remove unused buttons, and rename button (#4151) 2024-10-24 18:21:38 +00:00
ifernandezdiaz
0ff2fe6d6a QA-951: Adding missing IDs for Attachment rows (#4148) 2024-10-24 17:31:56 +00:00
Dave Severns
b0885ff60a PM-13886 show dialog when no logins were imported (#4139) 2024-10-24 17:06:30 +00:00
David Perez
a55fbca16a Update Firebase BOM to 33.5.1 (#4150) 2024-10-24 15:59:51 +00:00
ifernandezdiaz
fcd69e3e6f QA-950: Adding testTag for VaultUnlockedNavBar component (#4146) 2024-10-24 15:42:21 +00:00
David Perez
28e87fe216 PM-13937: Consolidate button UI and logic (#4149) 2024-10-24 15:27:06 +00:00
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
898 changed files with 35973 additions and 10626 deletions

View File

@@ -1,4 +1,4 @@
name: Android Beta Bug Report
name: Android Bug Report
description: File a bug report
labels: [ bug ]
body:
@@ -7,19 +7,7 @@ body:
value: |
Thanks for taking the time to fill out this bug report!
> [!WARNING]
> This is the new native Bitwarden Beta app repository. For the publicly available apps in App Store / Play Store, submit your report in [bitwarden/mobile](https://github.com/bitwarden/mobile)
Please do not submit feature requests. The [Community Forums](https://community.bitwarden.com) has a section for submitting, voting for, and discussing product feature requests.
- type: checkboxes
id: beta
attributes:
label: Bitwarden Beta
options:
- label: "I'm using the new native Bitwarden Beta app and I'm aware that legacy .NET app bugs should be reported in [bitwarden/mobile](https://github.com/bitwarden/mobile)"
validations:
required: true
- type: textarea
id: reproduce
attributes:
@@ -63,6 +51,22 @@ body:
description: What version of our software are you running?
validations:
required: true
- type: dropdown
id: server-region
attributes:
label: What server are you connecting to?
options:
- US
- EU
- Self-host
- N/A
validations:
required: true
- type: input
id: server-version
attributes:
label: Self-host Server Version
description: If self-hosting, what version of Bitwarden Server are you running?
- type: textarea
id: environment-details
attributes:

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@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- 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@6849a6489940f00c2f30c0fb92c6274307ccb58a # v4.1.2
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@6849a6489940f00c2f30c0fb92c6274307ccb58a # v4.1.2
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@8df1039502a15bceb9433410b1a100fbe190c53b # v4.5.0
with:
distribution: "temurin"
java-version: ${{ env.JAVA_VERSION }}
- name: Configure Ruby
uses: ruby/setup-ruby@f321cf5a4d1533575411f8752cf25b86478b0442 # v1.193.0
uses: ruby/setup-ruby@a2bbe5b1b236842c1cb7dd11e8e3b51e0a616acc # v1.202.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@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Configure Ruby
uses: ruby/setup-ruby@f321cf5a4d1533575411f8752cf25b86478b0442 # v1.193.0
uses: ruby/setup-ruby@a2bbe5b1b236842c1cb7dd11e8e3b51e0a616acc # v1.202.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@6849a6489940f00c2f30c0fb92c6274307ccb58a # v4.1.2
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@6849a6489940f00c2f30c0fb92c6274307ccb58a # v4.1.2
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@8df1039502a15bceb9433410b1a100fbe190c53b # v4.5.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: |
@@ -375,19 +382,21 @@ jobs:
- name: Publish Play Store bundle
if: ${{ matrix.variant == 'prod' && matrix.artifact == 'aab' && (inputs.publish-to-play-store || github.ref_name == 'main') }}
run: bundle exec fastlane publishBetaToPlayStore
run: |
bundle exec fastlane publishProdToPlayStore
bundle exec fastlane publishBetaToPlayStore
publish_fdroid:
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@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Configure Ruby
uses: ruby/setup-ruby@f321cf5a4d1533575411f8752cf25b86478b0442 # v1.193.0
uses: ruby/setup-ruby@a2bbe5b1b236842c1cb7dd11e8e3b51e0a616acc # v1.202.0
with:
bundler-cache: true
@@ -427,7 +436,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@6849a6489940f00c2f30c0fb92c6274307ccb58a # v4.1.2
with:
path: |
~/.gradle/caches
@@ -437,7 +446,7 @@ jobs:
${{ runner.os }}-gradle-v2-
- name: Cache build output
uses: actions/cache@0c45773b623bea8c8e75f6c82b208c3cf94ea4f9 # v4.0.2
uses: actions/cache@6849a6489940f00c2f30c0fb92c6274307ccb58a # v4.1.2
with:
path: |
${{ github.workspace }}/build-cache
@@ -446,7 +455,7 @@ jobs:
${{ runner.os }}-build-
- name: Configure JDK
uses: actions/setup-java@2dfa2011c5b2a0f1489bf9e433881c92c1631f88 # v4.3.0
uses: actions/setup-java@8df1039502a15bceb9433410b1a100fbe190c53b # v4.5.0
with:
distribution: "temurin"
java-version: ${{ env.JAVA_VERSION }}
@@ -455,10 +464,17 @@ jobs:
- name: Increment version
run: |
DEFAULT_VERSION_CODE=$((11000+$GITHUB_RUN_NUMBER))
VERSION_CODE="${{ inputs.version-code || '$DEFAULT_VERSION_CODE' }}"
bundle exec fastlane setBuildVersionInfo \
versionCode:${{ inputs.version-code || '$DEFAULT_VERSION_CODE' }} \
versionCode:$VERSION_CODE \
versionName:${{ inputs.version-name || '' }}
regex='versionName = "([^"]+)"'
if [[ "$(cat app/build.gradle.kts)" =~ $regex ]]; then
VERSION_NAME="${BASH_REMATCH[1]}"
fi
echo "Version Name: ${VERSION_NAME}" >> $GITHUB_STEP_SUMMARY
echo "Version Number: $VERSION_CODE" >> $GITHUB_STEP_SUMMARY
- name: Generate F-Droid artifacts
env:
FDROID_STORE_PASSWORD: ${{ secrets.FDROID_KEYSTORE_PASSWORD }}
@@ -481,7 +497,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 +509,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 +528,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@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- 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@2d540f18b0a416b1fbf2ee5be35841bd380fc1da # v2.3.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@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- 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@2d540f18b0a416b1fbf2ee5be35841bd380fc1da # v2.3.0
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
CROWDIN_API_TOKEN: ${{ steps.retrieve-secrets.outputs.crowdin-api-token }}

127
.github/workflows/github-release.yml vendored Normal file
View File

@@ -0,0 +1,127 @@
name: Create GitHub Release
on:
workflow_dispatch:
inputs:
version-name:
description: 'Version Name - E.g. "2024.11.1"'
required: true
type: string
version-number:
description: 'Version Number - E.g. "123456"'
required: true
type: string
artifact_run_id:
description: 'GitHub Action Run ID containing artifacts'
required: true
type: string
draft:
description: 'Create as draft release'
type: boolean
default: true
prerelease:
description: 'Mark as pre-release'
type: boolean
make_latest:
description: 'Set as the latest release'
type: boolean
branch-protection-type:
description: 'Branch protection type'
type: choice
options:
- Branch Name
- GitHub API
default: Branch Name
env:
ARTIFACTS_PATH: artifacts
jobs:
create-release:
runs-on: ubuntu-24.04
permissions:
contents: write
actions: read
steps:
- name: Check out repository
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
with:
fetch-depth: 0
- name: Get branch from workflow run
id: get_release_branch
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
ARTIFACT_RUN_ID: ${{ inputs.artifact_run_id }}
BRANCH_PROTECTION_TYPE: ${{ inputs.branch-protection-type }}
run: |
release_branch=$(gh run view $ARTIFACT_RUN_ID --json headBranch -q .headBranch)
case "$BRANCH_PROTECTION_TYPE" in
"Branch Name")
if [[ "$release_branch" != "main" && ! "$release_branch" =~ ^release/ ]]; then
echo "::error::Branch '$release_branch' is not 'main' or a release branch starting with 'release/'. Releases must be created from protected branches."
exit 1
fi
;;
"GitHub API")
#NOTE requires token with "administration:read" scope
if ! gh api "repos/${{ github.repository }}/branches/$release_branch/protection" | grep -q "required_status_checks"; then
echo "::error::Branch '$release_branch' is not protected. Releases must be created from protected branches. If that's not correct, confirm if the github token user has the 'administration:read' scope."
exit 1
fi
;;
*)
echo "::error::Unsupported branch protection type: $BRANCH_PROTECTION_TYPE"
exit 1
;;
esac
echo "release_branch=$release_branch" >> $GITHUB_OUTPUT
- name: Download artifacts
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
ARTIFACT_RUN_ID: ${{ inputs.artifact_run_id }}
run: |
gh run download $ARTIFACT_RUN_ID -D $ARTIFACTS_PATH
file_count=$(find $ARTIFACTS_PATH -type f | wc -l)
echo "Downloaded $file_count file(s)."
if [ "$file_count" -gt 0 ]; then
echo "Downloaded files:"
find $ARTIFACTS_PATH -type f
fi
- name: Create Release
id: create_release
uses: softprops/action-gh-release@e7a8f85e1c67a31e6ed99a94b41bd0b71bbee6b8 # v2.0.9
with:
tag_name: ${{ inputs.version-name }}
name: "v${{ inputs.version-name }} (${{ inputs.version-number }})"
prerelease: ${{ inputs.prerelease }}
draft: ${{ inputs.draft }}
make_latest: ${{ inputs.make_latest }}
target_commitish: ${{ steps.get_release_branch.outputs.release_branch }}
generate_release_notes: true
files: |
artifacts/**/*
- name: Update Release Description
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
RELEASE_ID: ${{ steps.create_release.outputs.id }}
RELEASE_URL: ${{ steps.create_release.outputs.url }}
ARTIFACT_RUN_ID: ${{ inputs.artifact_run_id }}
run: |
# Get current release body
current_body=$(gh api /repos/${{ github.repository }}/releases/$RELEASE_ID --jq .body)
# Append build source to the end
updated_body="${current_body}
**Builds Source:** https://github.com/${{ github.repository }}/actions/runs/$ARTIFACT_RUN_ID"
# Update release
gh api --method PATCH /repos/${{ github.repository }}/releases/$RELEASE_ID \
-f body="$updated_body"
echo "# :rocket: Release ready at:" >> $GITHUB_STEP_SUMMARY
echo "$RELEASE_URL" >> $GITHUB_STEP_SUMMARY

56
.github/workflows/release-branch.yml vendored Normal file
View File

@@ -0,0 +1,56 @@
name: Cut Release Branch
on:
workflow_dispatch:
inputs:
release_type:
description: 'Release Type'
required: true
type: choice
options:
- RC
- Hotfix
rc_prefix_date:
description: 'RC - Prefix with date. E.g. 2024.11-rc1'
type: boolean
default: true
jobs:
create-release-branch:
runs-on: ubuntu-24.04
permissions:
contents: write
steps:
- name: Check out repository
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
with:
fetch-depth: 0
- name: Create RC Branch
if: inputs.release_type == 'RC'
env:
RC_PREFIX_DATE: ${{ inputs.rc_prefix_date }}
run: |
if [ "$RC_PREFIX_DATE" = "true" ]; then
current_date=$(date +'%Y.%m')
branch_name="release/${current_date}-rc${{ github.run_number }}"
else
branch_name="release/rc${{ github.run_number }}"
fi
git switch main
git switch -c $branch_name
git push origin $branch_name
echo "# :cherry_blossom: RC branch: ${branch_name}" >> $GITHUB_STEP_SUMMARY
- name: Create Hotfix Branch
if: inputs.release_type == 'Hotfix'
run: |
latest_tag=$(git describe --tags --abbrev=0)
if [ -z "$latest_tag" ]; then
echo "::error::No tags found in the repository"
exit 1
fi
branch_name="release/hotfix-${latest_tag}"
git switch -c $branch_name $latest_tag
git push origin $branch_name
echo "# :fire: Hotfix branch: ${branch_name}" >> $GITHUB_STEP_SUMMARY

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@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
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@03a90e7253dadd7e2fff55f5dfbce647b39040a1 # 2.0.37
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@9278e421667d5d90a2839487a482448c4ec7df4d # v3.27.2
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@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
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@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
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@6849a6489940f00c2f30c0fb92c6274307ccb58a # v4.1.2
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@6849a6489940f00c2f30c0fb92c6274307ccb58a # v4.1.2
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@a2bbe5b1b236842c1cb7dd11e8e3b51e0a616acc # v1.202.0
with:
bundler-cache: true
- name: Configure JDK
uses: actions/setup-java@2dfa2011c5b2a0f1489bf9e433881c92c1631f88 # v4.3.0
uses: actions/setup-java@8df1039502a15bceb9433410b1a100fbe190c53b # v4.5.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.1003.0)
aws-sdk-core (3.212.0)
aws-eventstream (~> 1, >= 1.3.0)
aws-partitions (~> 1, >= 1.651.0)
aws-partitions (~> 1, >= 1.992.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.95.0)
aws-sdk-core (~> 3, >= 3.210.0)
aws-sigv4 (~> 1.5)
aws-sdk-s3 (1.162.0)
aws-sdk-core (~> 3, >= 3.205.0)
aws-sdk-s3 (1.170.0)
aws-sdk-core (~> 3, >= 3.210.0)
aws-sdk-kms (~> 1)
aws-sigv4 (~> 1.5)
aws-sigv4 (1.9.1)
aws-sigv4 (1.10.1)
aws-eventstream (~> 1, >= 1.0.2)
babosa (1.0.4)
base64 (0.2.0)
@@ -32,15 +32,15 @@ GEM
colored2 (3.1.2)
commander (4.6.0)
highline (~> 2.0.0)
date (3.3.4)
date (3.4.0)
declarative (0.0.20)
digest-crc (0.6.5)
rake (>= 12.0.0, < 14.0.0)
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.225.0)
CFPropertyList (>= 2.3, < 4.0.0)
addressable (>= 2.8, < 3.0.0)
artifactory (~> 3.0)
@@ -85,6 +85,7 @@ GEM
faraday-cookie_jar (~> 0.0.6)
faraday_middleware (~> 1.0)
fastimage (>= 2.1.0, < 3.0.0)
fastlane-sirp (>= 1.0.0)
gh_inspector (>= 1.1.2, < 2.0.0)
google-apis-androidpublisher_v3 (~> 0.3)
google-apis-playcustomapp_v1 (~> 0.1)
@@ -113,6 +114,8 @@ GEM
fastlane-plugin-firebase_app_distribution (0.9.1)
google-apis-firebaseappdistribution_v1 (~> 0.3.0)
google-apis-firebaseappdistribution_v1alpha (~> 0.2.0)
fastlane-sirp (1.0.0)
sysrandom (~> 1.0)
gh_inspector (1.1.3)
google-apis-androidpublisher_v3 (0.54.0)
google-apis-core (>= 0.11.0, < 2.a)
@@ -159,17 +162,17 @@ GEM
domain_name (~> 0.5)
httpclient (2.8.3)
jmespath (1.6.2)
json (2.7.2)
jwt (2.9.0)
json (2.8.1)
jwt (2.9.3)
base64
mini_magick (4.13.2)
mini_mime (1.1.5)
multi_json (1.15.0)
multipart-post (2.4.1)
nanaimo (0.3.0)
nanaimo (0.4.0)
naturally (2.2.1)
nkf (0.2.0)
optparse (0.5.0)
optparse (0.6.0)
os (1.1.4)
plist (3.7.1)
public_suffix (6.0.1)
@@ -179,7 +182,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.9)
rouge (2.0.7)
ruby2_keywords (0.0.5)
rubyzip (2.3.2)
@@ -192,10 +195,11 @@ GEM
simctl (1.6.10)
CFPropertyList
naturally
sysrandom (1.0.5)
terminal-notifier (2.0.0)
terminal-table (3.0.2)
unicode-display_width (>= 1.1.1, < 3)
time (0.4.0)
time (0.4.1)
date
trailblazer-option (0.1.2)
tty-cursor (0.7.1)
@@ -205,13 +209,13 @@ GEM
uber (0.1.0)
unicode-display_width (2.6.0)
word_wrap (1.0.0)
xcodeproj (1.25.0)
xcodeproj (1.27.0)
CFPropertyList (>= 2.3.3, < 4.0)
atomos (~> 0.1.3)
claide (>= 1.0.2, < 2.0)
colored2 (~> 3.1)
nanaimo (~> 0.3.0)
rexml (>= 3.3.2, < 4.0)
nanaimo (~> 0.4.0)
rexml (>= 3.3.6, < 4.0)
xcpretty (0.3.0)
rouge (~> 2.0.7)
xcpretty-travis-formatter (1.0.1)

View File

@@ -1,7 +1,4 @@
# Bitwarden Android (BETA)
> [!TIP]
> This repo has the new native Android app, currently in [Beta](https://community.bitwarden.com/t/about-the-beta-program/39185). Looking for the legacy .NET MAUI apps? Head on over to [bitwarden/mobile](https://github.com/bitwarden/mobile)
# Bitwarden Android
## Contents
@@ -12,7 +9,7 @@
## Compatibility
- **Minimum SDK**: 29
- **Target SDK**: 34
- **Target SDK**: 35
- **Device Types Supported**: Phone and Tablet
- **Orientations Supported**: Portrait and Landscape
@@ -28,7 +25,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 +168,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")
}
}
@@ -132,7 +135,10 @@ android {
unitTests.isReturnDefaultValues = true
}
lint {
disable.add("MissingTranslation")
disable += listOf(
"MissingTranslation",
"ExtraTranslation",
)
}
}
@@ -156,8 +162,7 @@ dependencies {
add("standardImplementation", dependencyNotation)
}
// TODO: this should use a versioned AAR instead of referencing a local AAR BITAU-94
implementation(files("libs/authenticatorbridge-0.1.0-SNAPSHOT-release.aar"))
implementation(files("libs/authenticatorbridge-1.0.0-release.aar"))
implementation(libs.androidx.activity.compose)
implementation(libs.androidx.appcompat)
@@ -200,6 +205,7 @@ dependencies {
implementation(platform(libs.square.retrofit.bom))
implementation(libs.square.retrofit)
implementation(libs.square.retrofit.kotlinx.serialization)
implementation(libs.timber)
implementation(libs.zxing.zxing.core)
// For now we are restricted to running Compose tests for debug builds only

Binary file not shown.

View File

@@ -6,6 +6,10 @@
# we keep it here.
-keep class com.bitwarden.** { *; }
# The Android Verifier component must be kept because it looks like dead code. Proguard is unable to
# see any JNI usage, so our rules must manually opt into keeping it.
-keep, includedescriptorclasses class org.rustls.platformverifier.** { *; }
################################################################################
# Bitwarden Models
################################################################################

View File

@@ -0,0 +1,256 @@
{
"formatVersion": 1,
"database": {
"version": 4,
"identityHash": "f7906c69e0a2c065d4d3be140fc721b6",
"entities": [
{
"tableName": "ciphers",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `user_id` TEXT NOT NULL, `cipher_type` TEXT NOT NULL, `cipher_json` TEXT NOT NULL, PRIMARY KEY(`id`))",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "userId",
"columnName": "user_id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "cipherType",
"columnName": "cipher_type",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "cipherJson",
"columnName": "cipher_json",
"affinity": "TEXT",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"id"
]
},
"indices": [
{
"name": "index_ciphers_user_id",
"unique": false,
"columnNames": [
"user_id"
],
"orders": [],
"createSql": "CREATE INDEX IF NOT EXISTS `index_ciphers_user_id` ON `${TABLE_NAME}` (`user_id`)"
}
],
"foreignKeys": []
},
{
"tableName": "collections",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `user_id` TEXT NOT NULL, `organization_id` TEXT NOT NULL, `should_hide_passwords` INTEGER NOT NULL, `name` TEXT NOT NULL, `external_id` TEXT, `read_only` INTEGER NOT NULL, `manage` INTEGER NOT NULL, PRIMARY KEY(`id`))",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "userId",
"columnName": "user_id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "organizationId",
"columnName": "organization_id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "shouldHidePasswords",
"columnName": "should_hide_passwords",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "name",
"columnName": "name",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "externalId",
"columnName": "external_id",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "isReadOnly",
"columnName": "read_only",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "canManage",
"columnName": "manage",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"id"
]
},
"indices": [
{
"name": "index_collections_user_id",
"unique": false,
"columnNames": [
"user_id"
],
"orders": [],
"createSql": "CREATE INDEX IF NOT EXISTS `index_collections_user_id` ON `${TABLE_NAME}` (`user_id`)"
}
],
"foreignKeys": []
},
{
"tableName": "domains",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`user_id` TEXT NOT NULL, `domains_json` TEXT NOT NULL, PRIMARY KEY(`user_id`))",
"fields": [
{
"fieldPath": "userId",
"columnName": "user_id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "domainsJson",
"columnName": "domains_json",
"affinity": "TEXT",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"user_id"
]
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "folders",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `user_id` TEXT NOT NULL, `name` TEXT, `revision_date` INTEGER NOT NULL, PRIMARY KEY(`id`))",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "userId",
"columnName": "user_id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "name",
"columnName": "name",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "revisionDate",
"columnName": "revision_date",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"id"
]
},
"indices": [
{
"name": "index_folders_user_id",
"unique": false,
"columnNames": [
"user_id"
],
"orders": [],
"createSql": "CREATE INDEX IF NOT EXISTS `index_folders_user_id` ON `${TABLE_NAME}` (`user_id`)"
}
],
"foreignKeys": []
},
{
"tableName": "sends",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `user_id` TEXT NOT NULL, `send_type` TEXT NOT NULL, `send_json` TEXT NOT NULL, PRIMARY KEY(`id`))",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "userId",
"columnName": "user_id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "sendType",
"columnName": "send_type",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "sendJson",
"columnName": "send_json",
"affinity": "TEXT",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"id"
]
},
"indices": [
{
"name": "index_sends_user_id",
"unique": false,
"columnNames": [
"user_id"
],
"orders": [],
"createSql": "CREATE INDEX IF NOT EXISTS `index_sends_user_id` ON `${TABLE_NAME}` (`user_id`)"
}
],
"foreignKeys": []
}
],
"views": [],
"setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'f7906c69e0a2c065d4d3be140fc721b6')"
]
}
}

View File

@@ -0,0 +1,256 @@
{
"formatVersion": 1,
"database": {
"version": 5,
"identityHash": "ee697e71290c92fe5b607d0b7665481b",
"entities": [
{
"tableName": "ciphers",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `user_id` TEXT NOT NULL, `cipher_type` TEXT NOT NULL, `cipher_json` TEXT NOT NULL, PRIMARY KEY(`id`))",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "userId",
"columnName": "user_id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "cipherType",
"columnName": "cipher_type",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "cipherJson",
"columnName": "cipher_json",
"affinity": "TEXT",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"id"
]
},
"indices": [
{
"name": "index_ciphers_user_id",
"unique": false,
"columnNames": [
"user_id"
],
"orders": [],
"createSql": "CREATE INDEX IF NOT EXISTS `index_ciphers_user_id` ON `${TABLE_NAME}` (`user_id`)"
}
],
"foreignKeys": []
},
{
"tableName": "collections",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `user_id` TEXT NOT NULL, `organization_id` TEXT NOT NULL, `should_hide_passwords` INTEGER NOT NULL, `name` TEXT NOT NULL, `external_id` TEXT, `read_only` INTEGER NOT NULL, `manage` INTEGER NOT NULL, PRIMARY KEY(`id`))",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "userId",
"columnName": "user_id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "organizationId",
"columnName": "organization_id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "shouldHidePasswords",
"columnName": "should_hide_passwords",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "name",
"columnName": "name",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "externalId",
"columnName": "external_id",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "isReadOnly",
"columnName": "read_only",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "canManage",
"columnName": "manage",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"id"
]
},
"indices": [
{
"name": "index_collections_user_id",
"unique": false,
"columnNames": [
"user_id"
],
"orders": [],
"createSql": "CREATE INDEX IF NOT EXISTS `index_collections_user_id` ON `${TABLE_NAME}` (`user_id`)"
}
],
"foreignKeys": []
},
{
"tableName": "domains",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`user_id` TEXT NOT NULL, `domains_json` TEXT, PRIMARY KEY(`user_id`))",
"fields": [
{
"fieldPath": "userId",
"columnName": "user_id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "domainsJson",
"columnName": "domains_json",
"affinity": "TEXT",
"notNull": false
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"user_id"
]
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "folders",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `user_id` TEXT NOT NULL, `name` TEXT, `revision_date` INTEGER NOT NULL, PRIMARY KEY(`id`))",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "userId",
"columnName": "user_id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "name",
"columnName": "name",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "revisionDate",
"columnName": "revision_date",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"id"
]
},
"indices": [
{
"name": "index_folders_user_id",
"unique": false,
"columnNames": [
"user_id"
],
"orders": [],
"createSql": "CREATE INDEX IF NOT EXISTS `index_folders_user_id` ON `${TABLE_NAME}` (`user_id`)"
}
],
"foreignKeys": []
},
{
"tableName": "sends",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `user_id` TEXT NOT NULL, `send_type` TEXT NOT NULL, `send_json` TEXT NOT NULL, PRIMARY KEY(`id`))",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "userId",
"columnName": "user_id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "sendType",
"columnName": "send_type",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "sendJson",
"columnName": "send_json",
"affinity": "TEXT",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"id"
]
},
"indices": [
{
"name": "index_sends_user_id",
"unique": false,
"columnNames": [
"user_id"
],
"orders": [],
"createSql": "CREATE INDEX IF NOT EXISTS `index_sends_user_id` ON `${TABLE_NAME}` (`user_id`)"
}
],
"foreignKeys": []
}
],
"views": [],
"setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'ee697e71290c92fe5b607d0b7665481b')"
]
}
}

View File

@@ -0,0 +1,256 @@
{
"formatVersion": 1,
"database": {
"version": 6,
"identityHash": "ee158c483edfe5102504670f3d9845d4",
"entities": [
{
"tableName": "ciphers",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `user_id` TEXT NOT NULL, `cipher_type` TEXT NOT NULL, `cipher_json` TEXT NOT NULL, PRIMARY KEY(`id`))",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "userId",
"columnName": "user_id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "cipherType",
"columnName": "cipher_type",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "cipherJson",
"columnName": "cipher_json",
"affinity": "TEXT",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"id"
]
},
"indices": [
{
"name": "index_ciphers_user_id",
"unique": false,
"columnNames": [
"user_id"
],
"orders": [],
"createSql": "CREATE INDEX IF NOT EXISTS `index_ciphers_user_id` ON `${TABLE_NAME}` (`user_id`)"
}
],
"foreignKeys": []
},
{
"tableName": "collections",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `user_id` TEXT NOT NULL, `organization_id` TEXT NOT NULL, `should_hide_passwords` INTEGER NOT NULL, `name` TEXT NOT NULL, `external_id` TEXT, `read_only` INTEGER NOT NULL, `manage` INTEGER, PRIMARY KEY(`id`))",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "userId",
"columnName": "user_id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "organizationId",
"columnName": "organization_id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "shouldHidePasswords",
"columnName": "should_hide_passwords",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "name",
"columnName": "name",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "externalId",
"columnName": "external_id",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "isReadOnly",
"columnName": "read_only",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "canManage",
"columnName": "manage",
"affinity": "INTEGER",
"notNull": false
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"id"
]
},
"indices": [
{
"name": "index_collections_user_id",
"unique": false,
"columnNames": [
"user_id"
],
"orders": [],
"createSql": "CREATE INDEX IF NOT EXISTS `index_collections_user_id` ON `${TABLE_NAME}` (`user_id`)"
}
],
"foreignKeys": []
},
{
"tableName": "domains",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`user_id` TEXT NOT NULL, `domains_json` TEXT, PRIMARY KEY(`user_id`))",
"fields": [
{
"fieldPath": "userId",
"columnName": "user_id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "domainsJson",
"columnName": "domains_json",
"affinity": "TEXT",
"notNull": false
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"user_id"
]
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "folders",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `user_id` TEXT NOT NULL, `name` TEXT, `revision_date` INTEGER NOT NULL, PRIMARY KEY(`id`))",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "userId",
"columnName": "user_id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "name",
"columnName": "name",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "revisionDate",
"columnName": "revision_date",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"id"
]
},
"indices": [
{
"name": "index_folders_user_id",
"unique": false,
"columnNames": [
"user_id"
],
"orders": [],
"createSql": "CREATE INDEX IF NOT EXISTS `index_folders_user_id` ON `${TABLE_NAME}` (`user_id`)"
}
],
"foreignKeys": []
},
{
"tableName": "sends",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `user_id` TEXT NOT NULL, `send_type` TEXT NOT NULL, `send_json` TEXT NOT NULL, PRIMARY KEY(`id`))",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "userId",
"columnName": "user_id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "sendType",
"columnName": "send_type",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "sendJson",
"columnName": "send_json",
"affinity": "TEXT",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"id"
]
},
"indices": [
{
"name": "index_sends_user_id",
"unique": false,
"columnNames": [
"user_id"
],
"orders": [],
"createSql": "CREATE INDEX IF NOT EXISTS `index_sends_user_id` ON `${TABLE_NAME}` (`user_id`)"
}
],
"foreignKeys": []
}
],
"views": [],
"setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'ee158c483edfe5102504670f3d9845d4')"
]
}
}

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,27 @@
<?xml version="1.0" encoding="utf-8"?>
<shortcuts xmlns:android="http://schemas.android.com/apk/res/android">
<shortcut
android:enabled="true"
android:icon="@mipmap/ic_generator_shortcut"
android:shortcutId="bitwarden_password_generator"
android:shortcutLongLabel="@string/password_generator"
android:shortcutShortLabel="@string/password_generator">
<intent
android:action="android.intent.action.VIEW"
android:data="bitwarden://password_generator"
android:targetClass="com.x8bit.bitwarden.MainActivity"
android:targetPackage="com.x8bit.bitwarden.beta" />
</shortcut>
<shortcut
android:enabled="true"
android:icon="@mipmap/ic_vault_shortcut"
android:shortcutId="bitwarden_my_vault"
android:shortcutLongLabel="@string/my_vault"
android:shortcutShortLabel="@string/my_vault">
<intent
android:action="android.intent.action.VIEW"
android:data="bitwarden://my_vault"
android:targetClass="com.x8bit.bitwarden.MainActivity"
android:targetPackage="com.x8bit.bitwarden.beta" />
</shortcut>
</shortcuts>

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,
@@ -184,12 +190,14 @@ class MainViewModel @Inject constructor(
private fun handleAccessibilitySelectionReceive(
action: MainAction.Internal.AccessibilitySelectionReceive,
) {
specialCircumstanceManager.specialCircumstance = null
sendEvent(MainEvent.CompleteAccessibilityAutofill(cipherView = action.cipherView))
}
private fun handleAutofillSelectionReceive(
action: MainAction.Internal.AutofillSelectionReceive,
) {
specialCircumstanceManager.specialCircumstance = null
sendEvent(MainEvent.CompleteAutofill(cipherView = action.cipherView))
}
@@ -223,7 +231,7 @@ class MainViewModel @Inject constructor(
)
}
@Suppress("LongMethod")
@Suppress("LongMethod", "CyclomaticComplexMethod")
private fun handleIntent(
intent: Intent,
isFirstIntent: Boolean,
@@ -232,14 +240,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 +303,11 @@ class MainViewModel @Inject constructor(
)
}
totpData != null -> {
specialCircumstanceManager.specialCircumstance =
SpecialCircumstance.AddTotpLoginItem(data = totpData)
}
shareData != null -> {
specialCircumstanceManager.specialCircumstance =
SpecialCircumstance.ShareNewSend(
@@ -320,6 +358,11 @@ class MainViewModel @Inject constructor(
hasVaultShortcut -> {
specialCircumstanceManager.specialCircumstance = SpecialCircumstance.VaultShortcut
}
hasAccountSecurityShortcut -> {
specialCircumstanceManager.specialCircumstance =
SpecialCircumstance.AccountSecurityShortcut
}
}
}

View File

@@ -181,6 +181,11 @@ interface AuthDiskSource {
*/
fun storeUserBiometricUnlockKey(userId: String, biometricsKey: String?)
/**
* Gets the flow for the biometrics key for the given [userId].
*/
fun getUserBiometicUnlockKeyFlow(userId: String): Flow<String?>
/**
* Retrieves a pin-protected user key for the given [userId].
*/
@@ -198,6 +203,11 @@ interface AuthDiskSource {
inMemoryOnly: Boolean = false,
)
/**
* Retrieves a flow for the pin-protected user key for the given [userId].
*/
fun getPinProtectedUserKeyFlow(userId: String): Flow<String?>
/**
* Gets a two-factor auth token using a user's [email].
*/
@@ -306,4 +316,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,11 @@ class AuthDiskSourceImpl(
mutableMapOf<String, MutableSharedFlow<AccountTokensJson?>>()
private val mutableOnboardingStatusFlowMap =
mutableMapOf<String, MutableSharedFlow<OnboardingStatus?>>()
private val mutableShowImportLoginsFlowMap = mutableMapOf<String, MutableSharedFlow<Boolean?>>()
private val mutableBiometricUnlockKeyFlowMap =
mutableMapOf<String, MutableSharedFlow<String?>>()
private val mutablePinProtectedUserKeyFlowMap =
mutableMapOf<String, MutableSharedFlow<String?>>()
private val mutableUserStateFlow = bufferedMutableSharedFlow<UserStateJson?>(replay = 1)
override var userState: UserStateJson?
@@ -143,9 +149,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? =
@@ -280,8 +288,13 @@ class AuthDiskSourceImpl(
key = BIOMETRICS_UNLOCK_KEY.appendIdentifier(userId),
value = biometricsKey,
)
getMutableBiometricUnlockKeyFlow(userId).tryEmit(biometricsKey)
}
override fun getUserBiometicUnlockKeyFlow(userId: String): Flow<String?> =
getMutableBiometricUnlockKeyFlow(userId)
.onSubscription { emit(getUserBiometricUnlockKey(userId = userId)) }
override fun getPinProtectedUserKey(userId: String): String? =
inMemoryPinProtectedUserKeys[userId]
?: getString(key = PIN_PROTECTED_USER_KEY_KEY.appendIdentifier(userId))
@@ -297,8 +310,13 @@ class AuthDiskSourceImpl(
key = PIN_PROTECTED_USER_KEY_KEY.appendIdentifier(userId),
value = pinProtectedUserKey,
)
getMutablePinProtectedUserKeyFlow(userId).tryEmit(pinProtectedUserKey)
}
override fun getPinProtectedUserKeyFlow(userId: String): Flow<String?> =
getMutablePinProtectedUserKeyFlow(userId)
.onSubscription { emit(getPinProtectedUserKey(userId = userId)) }
override fun getTwoFactorToken(email: String): String? =
getString(key = TWO_FACTOR_TOKEN_KEY.appendIdentifier(email))
@@ -437,6 +455,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 +514,24 @@ class AuthDiskSourceImpl(
bufferedMutableSharedFlow(replay = 1)
}
private fun getMutableShowImportLoginsFlow(
userId: String,
): MutableSharedFlow<Boolean?> = mutableShowImportLoginsFlowMap.getOrPut(userId) {
bufferedMutableSharedFlow(replay = 1)
}
private fun getMutableBiometricUnlockKeyFlow(
userId: String,
): MutableSharedFlow<String?> = mutableBiometricUnlockKeyFlowMap.getOrPut(userId) {
bufferedMutableSharedFlow(replay = 1)
}
private fun getMutablePinProtectedUserKeyFlow(
userId: String,
): MutableSharedFlow<String?> = mutablePinProtectedUserKeyFlowMap.getOrPut(userId) {
bufferedMutableSharedFlow(replay = 1)
}
private fun migrateAccountTokens() {
userState
?.accounts

View File

@@ -2,8 +2,10 @@ package com.x8bit.bitwarden.data.auth.datasource.disk.model
import com.x8bit.bitwarden.data.auth.datasource.network.model.KdfTypeJson
import com.x8bit.bitwarden.data.auth.datasource.network.model.UserDecryptionOptionsJson
import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.JsonNames
/**
* Represents the current account information for a given user.
@@ -45,6 +47,7 @@ data class AccountJson(
* @property kdfParallelism The number of threads to use when calculating a password hash.
* @property userDecryptionOptions The options available to a user for decryption.
*/
@OptIn(ExperimentalSerializationApi::class)
@Serializable
data class Profile(
@SerialName("userId")
@@ -86,7 +89,8 @@ data class AccountJson(
@SerialName("kdfParallelism")
val kdfParallelism: Int?,
@SerialName("accountDecryptionOptions")
@SerialName("userDecryptionOptions")
@JsonNames("accountDecryptionOptions")
val userDecryptionOptions: UserDecryptionOptionsJson?,
)

View File

@@ -7,13 +7,21 @@ import kotlinx.serialization.Serializable
* Container for the user's API tokens.
*
* @property requestId The ID of the pending Auth Request.
* @property requestPrivateKey The private of the pending Auth Request.
* @property requestPrivateKey The private key of the pending Auth Request.
* @property requestAccessCode The access code of the pending Auth Request.
* @property requestFingerprint The fingerprint of the pending Auth Request.
*/
@Serializable
data class PendingAuthRequestJson(
@SerialName("Id")
@SerialName("id")
val requestId: String,
@SerialName("PrivateKey")
@SerialName("privateKey")
val requestPrivateKey: String,
@SerialName("accessCode")
val requestAccessCode: String,
@SerialName("fingerprint")
val requestFingerprint: String,
)

View File

@@ -5,6 +5,7 @@ import com.x8bit.bitwarden.data.auth.datasource.network.model.DeleteAccountReque
import com.x8bit.bitwarden.data.auth.datasource.network.model.ResetPasswordRequestJson
import com.x8bit.bitwarden.data.auth.datasource.network.model.SetPasswordRequestJson
import com.x8bit.bitwarden.data.auth.datasource.network.model.VerifyOtpRequestJson
import com.x8bit.bitwarden.data.platform.datasource.network.model.NetworkResult
import retrofit2.http.Body
import retrofit2.http.HTTP
import retrofit2.http.POST
@@ -18,43 +19,43 @@ interface AuthenticatedAccountsApi {
* Converts the currently active account to a key-connector account.
*/
@POST("/accounts/convert-to-key-connector")
suspend fun convertToKeyConnector(): Result<Unit>
suspend fun convertToKeyConnector(): NetworkResult<Unit>
/**
* Creates the keys for the current account.
*/
@POST("/accounts/keys")
suspend fun createAccountKeys(@Body body: CreateAccountKeysRequest): Result<Unit>
suspend fun createAccountKeys(@Body body: CreateAccountKeysRequest): NetworkResult<Unit>
/**
* Deletes the current account.
*/
@HTTP(method = "DELETE", path = "/accounts", hasBody = true)
suspend fun deleteAccount(@Body body: DeleteAccountRequestJson): Result<Unit>
suspend fun deleteAccount(@Body body: DeleteAccountRequestJson): NetworkResult<Unit>
@POST("/accounts/request-otp")
suspend fun requestOtp(): Result<Unit>
suspend fun requestOtp(): NetworkResult<Unit>
@POST("/accounts/verify-otp")
suspend fun verifyOtp(
@Body body: VerifyOtpRequestJson,
): Result<Unit>
): NetworkResult<Unit>
/**
* Resets the temporary password.
*/
@HTTP(method = "PUT", path = "/accounts/update-temp-password", hasBody = true)
suspend fun resetTempPassword(@Body body: ResetPasswordRequestJson): Result<Unit>
suspend fun resetTempPassword(@Body body: ResetPasswordRequestJson): NetworkResult<Unit>
/**
* Resets the password.
*/
@HTTP(method = "POST", path = "/accounts/password", hasBody = true)
suspend fun resetPassword(@Body body: ResetPasswordRequestJson): Result<Unit>
suspend fun resetPassword(@Body body: ResetPasswordRequestJson): NetworkResult<Unit>
/**
* Sets the password.
*/
@POST("/accounts/set-password")
suspend fun setPassword(@Body body: SetPasswordRequestJson): Result<Unit>
suspend fun setPassword(@Body body: SetPasswordRequestJson): NetworkResult<Unit>
}

View File

@@ -3,6 +3,7 @@ package com.x8bit.bitwarden.data.auth.datasource.network.api
import com.x8bit.bitwarden.data.auth.datasource.network.model.AuthRequestRequestJson
import com.x8bit.bitwarden.data.auth.datasource.network.model.AuthRequestUpdateRequestJson
import com.x8bit.bitwarden.data.auth.datasource.network.model.AuthRequestsResponseJson
import com.x8bit.bitwarden.data.platform.datasource.network.model.NetworkResult
import retrofit2.http.Body
import retrofit2.http.GET
import retrofit2.http.Header
@@ -22,7 +23,7 @@ interface AuthenticatedAuthRequestsApi {
suspend fun createAdminAuthRequest(
@Header("Device-Identifier") deviceIdentifier: String,
@Body body: AuthRequestRequestJson,
): Result<AuthRequestsResponseJson.AuthRequest>
): NetworkResult<AuthRequestsResponseJson.AuthRequest>
/**
* Updates an authentication request.
@@ -31,13 +32,13 @@ interface AuthenticatedAuthRequestsApi {
suspend fun updateAuthRequest(
@Path("id") userId: String,
@Body body: AuthRequestUpdateRequestJson,
): Result<AuthRequestsResponseJson.AuthRequest>
): NetworkResult<AuthRequestsResponseJson.AuthRequest>
/**
* Gets a list of auth requests for this device.
*/
@GET("/auth-requests")
suspend fun getAuthRequests(): Result<AuthRequestsResponseJson>
suspend fun getAuthRequests(): NetworkResult<AuthRequestsResponseJson>
/**
* Retrieves an existing authentication request by ID.
@@ -45,5 +46,5 @@ interface AuthenticatedAuthRequestsApi {
@GET("/auth-requests/{requestId}")
suspend fun getAuthRequest(
@Path("requestId") requestId: String,
): Result<AuthRequestsResponseJson.AuthRequest>
): NetworkResult<AuthRequestsResponseJson.AuthRequest>
}

View File

@@ -3,6 +3,7 @@ package com.x8bit.bitwarden.data.auth.datasource.network.api
import androidx.annotation.Keep
import com.x8bit.bitwarden.data.auth.datasource.network.model.TrustedDeviceKeysRequestJson
import com.x8bit.bitwarden.data.auth.datasource.network.model.TrustedDeviceKeysResponseJson
import com.x8bit.bitwarden.data.platform.datasource.network.model.NetworkResult
import retrofit2.http.Body
import retrofit2.http.PUT
import retrofit2.http.Path
@@ -16,5 +17,5 @@ interface AuthenticatedDevicesApi {
suspend fun updateTrustedDeviceKeys(
@Path(value = "appId") appId: String,
@Body request: TrustedDeviceKeysRequestJson,
): Result<TrustedDeviceKeysResponseJson>
): NetworkResult<TrustedDeviceKeysResponseJson>
}

View File

@@ -2,6 +2,7 @@ package com.x8bit.bitwarden.data.auth.datasource.network.api
import androidx.annotation.Keep
import com.x8bit.bitwarden.data.auth.datasource.network.model.KeyConnectorMasterKeyRequestJson
import com.x8bit.bitwarden.data.platform.datasource.network.model.NetworkResult
import retrofit2.http.Body
import retrofit2.http.POST
import retrofit2.http.Url
@@ -15,5 +16,5 @@ interface AuthenticatedKeyConnectorApi {
suspend fun storeMasterKeyToKeyConnector(
@Url url: String,
@Body body: KeyConnectorMasterKeyRequestJson,
): Result<Unit>
): NetworkResult<Unit>
}

View File

@@ -3,6 +3,7 @@ package com.x8bit.bitwarden.data.auth.datasource.network.api
import com.x8bit.bitwarden.data.auth.datasource.network.model.OrganizationAutoEnrollStatusResponseJson
import com.x8bit.bitwarden.data.auth.datasource.network.model.OrganizationKeysResponseJson
import com.x8bit.bitwarden.data.auth.datasource.network.model.OrganizationResetPasswordEnrollRequestJson
import com.x8bit.bitwarden.data.platform.datasource.network.model.NetworkResult
import retrofit2.http.Body
import retrofit2.http.GET
import retrofit2.http.PUT
@@ -20,7 +21,7 @@ interface AuthenticatedOrganizationApi {
@Path("orgId") organizationId: String,
@Path("userId") userId: String,
@Body body: OrganizationResetPasswordEnrollRequestJson,
): Result<Unit>
): NetworkResult<Unit>
/**
* Checks whether this organization auto enrolls users in password reset.
@@ -28,7 +29,7 @@ interface AuthenticatedOrganizationApi {
@GET("/organizations/{identifier}/auto-enroll-status")
suspend fun getOrganizationAutoEnrollResponse(
@Path("identifier") organizationIdentifier: String,
): Result<OrganizationAutoEnrollStatusResponseJson>
): NetworkResult<OrganizationAutoEnrollStatusResponseJson>
/**
* Gets the public and private keys for this organization.
@@ -36,5 +37,5 @@ interface AuthenticatedOrganizationApi {
@GET("/organizations/{id}/keys")
suspend fun getOrganizationKeys(
@Path("id") organizationId: String,
): Result<OrganizationKeysResponseJson>
): NetworkResult<OrganizationKeysResponseJson>
}

View File

@@ -1,5 +1,6 @@
package com.x8bit.bitwarden.data.auth.datasource.network.api
import com.x8bit.bitwarden.data.platform.datasource.network.model.NetworkResult
import okhttp3.ResponseBody
import retrofit2.http.GET
import retrofit2.http.Path
@@ -14,5 +15,5 @@ interface HaveIBeenPwnedApi {
suspend fun fetchBreachedPasswords(
@Path("hashPrefix")
hashPrefix: String,
): Result<ResponseBody>
): NetworkResult<ResponseBody>
}

View File

@@ -3,6 +3,7 @@ package com.x8bit.bitwarden.data.auth.datasource.network.api
import com.x8bit.bitwarden.data.auth.datasource.network.model.KeyConnectorKeyRequestJson
import com.x8bit.bitwarden.data.auth.datasource.network.model.PasswordHintRequestJson
import com.x8bit.bitwarden.data.auth.datasource.network.model.ResendEmailRequestJson
import com.x8bit.bitwarden.data.platform.datasource.network.model.NetworkResult
import com.x8bit.bitwarden.data.platform.datasource.network.util.HEADER_KEY_AUTHORIZATION
import retrofit2.http.Body
import retrofit2.http.Header
@@ -15,16 +16,16 @@ interface UnauthenticatedAccountsApi {
@POST("/accounts/password-hint")
suspend fun passwordHintRequest(
@Body body: PasswordHintRequestJson,
): Result<Unit>
): NetworkResult<Unit>
@POST("/two-factor/send-email-login")
suspend fun resendVerificationCodeEmail(
@Body body: ResendEmailRequestJson,
): Result<Unit>
): NetworkResult<Unit>
@POST("/accounts/set-key-connector-key")
suspend fun setKeyConnectorKey(
@Body body: KeyConnectorKeyRequestJson,
@Header(HEADER_KEY_AUTHORIZATION) bearerToken: String,
): Result<Unit>
): NetworkResult<Unit>
}

View File

@@ -2,6 +2,7 @@ package com.x8bit.bitwarden.data.auth.datasource.network.api
import com.x8bit.bitwarden.data.auth.datasource.network.model.AuthRequestRequestJson
import com.x8bit.bitwarden.data.auth.datasource.network.model.AuthRequestsResponseJson
import com.x8bit.bitwarden.data.platform.datasource.network.model.NetworkResult
import retrofit2.http.Body
import retrofit2.http.GET
import retrofit2.http.Header
@@ -21,7 +22,7 @@ interface UnauthenticatedAuthRequestsApi {
suspend fun createAuthRequest(
@Header("Device-Identifier") deviceIdentifier: String,
@Body body: AuthRequestRequestJson,
): Result<AuthRequestsResponseJson.AuthRequest>
): NetworkResult<AuthRequestsResponseJson.AuthRequest>
/**
* Queries for updates to a given auth request.
@@ -30,5 +31,5 @@ interface UnauthenticatedAuthRequestsApi {
suspend fun getAuthRequestUpdate(
@Path("requestId") requestId: String,
@Query("code") accessCode: String,
): Result<AuthRequestsResponseJson.AuthRequest>
): NetworkResult<AuthRequestsResponseJson.AuthRequest>
}

View File

@@ -1,5 +1,6 @@
package com.x8bit.bitwarden.data.auth.datasource.network.api
import com.x8bit.bitwarden.data.platform.datasource.network.model.NetworkResult
import retrofit2.http.GET
import retrofit2.http.Header
@@ -11,5 +12,5 @@ interface UnauthenticatedDevicesApi {
suspend fun getIsKnownDevice(
@Header(value = "X-Request-Email") emailAddress: String,
@Header(value = "X-Device-Identifier") deviceId: String,
): Result<Boolean>
): NetworkResult<Boolean>
}

View File

@@ -10,6 +10,7 @@ import com.x8bit.bitwarden.data.auth.datasource.network.model.RegisterRequestJso
import com.x8bit.bitwarden.data.auth.datasource.network.model.RegisterResponseJson
import com.x8bit.bitwarden.data.auth.datasource.network.model.SendVerificationEmailRequestJson
import com.x8bit.bitwarden.data.auth.datasource.network.model.VerifyEmailTokenRequestJson
import com.x8bit.bitwarden.data.platform.datasource.network.model.NetworkResult
import kotlinx.serialization.json.JsonPrimitive
import retrofit2.Call
import retrofit2.http.Body
@@ -46,12 +47,12 @@ interface UnauthenticatedIdentityApi {
@Field(value = "twoFactorProvider") twoFactorMethod: String?,
@Field(value = "twoFactorRemember") twoFactorRemember: String?,
@Field(value = "authRequest") authRequestId: String?,
): Result<GetTokenResponseJson.Success>
): NetworkResult<GetTokenResponseJson.Success>
@GET("/sso/prevalidate")
suspend fun prevalidateSso(
@Query("domainHint") organizationIdentifier: String,
): Result<PrevalidateSsoResponseJson>
): NetworkResult<PrevalidateSsoResponseJson>
/**
* This call needs to be synchronous so we need it to return a [Call] directly. The identity
@@ -66,23 +67,25 @@ interface UnauthenticatedIdentityApi {
): Call<RefreshTokenResponseJson>
@POST("/accounts/prelogin")
suspend fun preLogin(@Body body: PreLoginRequestJson): Result<PreLoginResponseJson>
suspend fun preLogin(@Body body: PreLoginRequestJson): NetworkResult<PreLoginResponseJson>
@POST("/accounts/register")
suspend fun register(@Body body: RegisterRequestJson): Result<RegisterResponseJson.Success>
suspend fun register(
@Body body: RegisterRequestJson,
): NetworkResult<RegisterResponseJson.Success>
@POST("/accounts/register/finish")
suspend fun registerFinish(
@Body body: RegisterFinishRequestJson,
): Result<RegisterResponseJson.Success>
): NetworkResult<RegisterResponseJson.Success>
@POST("/accounts/register/send-verification-email")
suspend fun sendVerificationEmail(
@Body body: SendVerificationEmailRequestJson,
): Result<JsonPrimitive?>
): NetworkResult<JsonPrimitive?>
@POST("/accounts/register/verification-email-clicked")
suspend fun verifyEmailToken(
@Body body: VerifyEmailTokenRequestJson,
): Result<Unit>
): NetworkResult<Unit>
}

View File

@@ -3,6 +3,7 @@ package com.x8bit.bitwarden.data.auth.datasource.network.api
import androidx.annotation.Keep
import com.x8bit.bitwarden.data.auth.datasource.network.model.KeyConnectorMasterKeyRequestJson
import com.x8bit.bitwarden.data.auth.datasource.network.model.KeyConnectorMasterKeyResponseJson
import com.x8bit.bitwarden.data.platform.datasource.network.model.NetworkResult
import com.x8bit.bitwarden.data.platform.datasource.network.util.HEADER_KEY_AUTHORIZATION
import retrofit2.http.Body
import retrofit2.http.GET
@@ -20,11 +21,11 @@ interface UnauthenticatedKeyConnectorApi {
@Url url: String,
@Header(HEADER_KEY_AUTHORIZATION) bearerToken: String,
@Body body: KeyConnectorMasterKeyRequestJson,
): Result<Unit>
): NetworkResult<Unit>
@GET
suspend fun getMasterKeyFromKeyConnector(
@Url url: String,
@Header(HEADER_KEY_AUTHORIZATION) bearerToken: String,
): Result<KeyConnectorMasterKeyResponseJson>
): NetworkResult<KeyConnectorMasterKeyResponseJson>
}

View File

@@ -2,6 +2,9 @@ package com.x8bit.bitwarden.data.auth.datasource.network.api
import com.x8bit.bitwarden.data.auth.datasource.network.model.OrganizationDomainSsoDetailsRequestJson
import com.x8bit.bitwarden.data.auth.datasource.network.model.OrganizationDomainSsoDetailsResponseJson
import com.x8bit.bitwarden.data.auth.datasource.network.model.VerifiedOrganizationDomainSsoDetailsRequest
import com.x8bit.bitwarden.data.auth.datasource.network.model.VerifiedOrganizationDomainSsoDetailsResponse
import com.x8bit.bitwarden.data.platform.datasource.network.model.NetworkResult
import retrofit2.http.Body
import retrofit2.http.POST
@@ -15,5 +18,13 @@ interface UnauthenticatedOrganizationApi {
@POST("/organizations/domain/sso/details")
suspend fun getClaimedDomainOrganizationDetails(
@Body body: OrganizationDomainSsoDetailsRequestJson,
): Result<OrganizationDomainSsoDetailsResponseJson>
): NetworkResult<OrganizationDomainSsoDetailsResponseJson>
/**
* Checks for the verfied organization domains of an email for SSO purposes.
*/
@POST("/organizations/domain/sso/verified")
suspend fun getVerifiedOrganizationDomainsByEmail(
@Body body: VerifiedOrganizationDomainSsoDetailsRequest,
): NetworkResult<VerifiedOrganizationDomainSsoDetailsResponse>
}

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

@@ -1,15 +1,19 @@
package com.x8bit.bitwarden.data.auth.datasource.network.model
import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.JsonNames
/**
* Decryption options related to a user's key connector.
*
* @property keyConnectorUrl URL to the user's key connector.
*/
@OptIn(ExperimentalSerializationApi::class)
@Serializable
data class KeyConnectorUserDecryptionOptionsJson(
@SerialName("KeyConnectorUrl")
@SerialName("keyConnectorUrl")
@JsonNames("KeyConnectorUrl")
val keyConnectorUrl: String,
)

View File

@@ -1,16 +1,26 @@
package com.x8bit.bitwarden.data.auth.datasource.network.model
import kotlinx.serialization.Contextual
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import java.time.ZonedDateTime
/**
* Response object returned when requesting organization domain SSO details.
*
* @property isSsoAvailable Whether or not SSO is available for this domain.
* @property organizationIdentifier The organization's identifier.
* @property verifiedDate The date the domain was verified.
*/
@Serializable
data class OrganizationDomainSsoDetailsResponseJson(
@SerialName("ssoAvailable") val isSsoAvailable: Boolean,
@SerialName("organizationIdentifier") val organizationIdentifier: String,
@SerialName("ssoAvailable")
val isSsoAvailable: Boolean,
@SerialName("organizationIdentifier")
val organizationIdentifier: String,
@SerialName("verifiedDate")
@Contextual
val verifiedDate: ZonedDateTime?,
)

View File

@@ -1,7 +1,9 @@
package com.x8bit.bitwarden.data.auth.datasource.network.model
import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.JsonNames
/**
* Decryption options related to a user's trusted device.
@@ -13,20 +15,26 @@ import kotlinx.serialization.Serializable
* @property hasManageResetPasswordPermission Whether or not the user has manage reset password
* permission.
*/
@OptIn(ExperimentalSerializationApi::class)
@Serializable
data class TrustedDeviceUserDecryptionOptionsJson(
@SerialName("EncryptedPrivateKey")
@SerialName("encryptedPrivateKey")
@JsonNames("EncryptedPrivateKey")
val encryptedPrivateKey: String?,
@SerialName("EncryptedUserKey")
@SerialName("encryptedUserKey")
@JsonNames("EncryptedUserKey")
val encryptedUserKey: String?,
@SerialName("HasAdminApproval")
@SerialName("hasAdminApproval")
@JsonNames("HasAdminApproval")
val hasAdminApproval: Boolean,
@SerialName("HasLoginApprovingDevice")
@SerialName("hasLoginApprovingDevice")
@JsonNames("HasLoginApprovingDevice")
val hasLoginApprovingDevice: Boolean,
@SerialName("HasManageResetPasswordPermission")
@SerialName("hasManageResetPasswordPermission")
@JsonNames("HasManageResetPasswordPermission")
val hasManageResetPasswordPermission: Boolean,
)

View File

@@ -1,7 +1,9 @@
package com.x8bit.bitwarden.data.auth.datasource.network.model
import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.JsonNames
/**
* The options available to a user for decryption.
@@ -12,14 +14,18 @@ import kotlinx.serialization.Serializable
* device.
* @property keyConnectorUserDecryptionOptions Decryption options related to a user's key connector.
*/
@OptIn(ExperimentalSerializationApi::class)
@Serializable
data class UserDecryptionOptionsJson(
@SerialName("HasMasterPassword")
@SerialName("hasMasterPassword")
@JsonNames("HasMasterPassword")
val hasMasterPassword: Boolean,
@SerialName("TrustedDeviceOption")
@SerialName("trustedDeviceOption")
@JsonNames("TrustedDeviceOption")
val trustedDeviceUserDecryptionOptions: TrustedDeviceUserDecryptionOptionsJson?,
@SerialName("KeyConnectorOption")
@SerialName("keyConnectorOption")
@JsonNames("KeyConnectorOption")
val keyConnectorUserDecryptionOptions: KeyConnectorUserDecryptionOptionsJson?,
)

View File

@@ -0,0 +1,14 @@
package com.x8bit.bitwarden.data.auth.datasource.network.model
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
/**
* Request body object when retrieving organization verified domain SSO info.
*
* @param email The email address to check against.
*/
@Serializable
data class VerifiedOrganizationDomainSsoDetailsRequest(
@SerialName("email") val email: String,
)

View File

@@ -0,0 +1,35 @@
package com.x8bit.bitwarden.data.auth.datasource.network.model
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
/**
* Response object returned when requesting organization verified domain SSO details.
*
* @property verifiedOrganizationDomainSsoDetails The list of verified organization domain SSO
* details.
*/
@Serializable
data class VerifiedOrganizationDomainSsoDetailsResponse(
@SerialName("data")
val verifiedOrganizationDomainSsoDetails: List<VerifiedOrganizationDomainSsoDetail>,
) {
/**
* Response body for an organization verified domain SSO details.
*
* @property organizationName The name of the organization.
* @property organizationIdentifier The identifier of the organization.
* @property domainName The name of the domain.
*/
@Serializable
data class VerifiedOrganizationDomainSsoDetail(
@SerialName("organizationName")
val organizationName: String,
@SerialName("organizationIdentifier")
val organizationIdentifier: String,
@SerialName("domainName")
val domainName: String,
)
}

View File

@@ -19,6 +19,7 @@ import com.x8bit.bitwarden.data.auth.datasource.network.model.VerifyOtpRequestJs
import com.x8bit.bitwarden.data.platform.datasource.network.model.toBitwardenError
import com.x8bit.bitwarden.data.platform.datasource.network.util.HEADER_VALUE_BEARER_PREFIX
import com.x8bit.bitwarden.data.platform.datasource.network.util.parseErrorBodyOrNull
import com.x8bit.bitwarden.data.platform.datasource.network.util.toResult
import kotlinx.serialization.json.Json
/**
@@ -37,18 +38,22 @@ class AccountsServiceImpl(
* Converts the currently active account to a key-connector account.
*/
override suspend fun convertToKeyConnector(): Result<Unit> =
authenticatedAccountsApi.convertToKeyConnector()
authenticatedAccountsApi
.convertToKeyConnector()
.toResult()
override suspend fun createAccountKeys(
publicKey: String,
encryptedPrivateKey: String,
): Result<Unit> =
authenticatedAccountsApi.createAccountKeys(
body = CreateAccountKeysRequest(
publicKey = publicKey,
encryptedPrivateKey = encryptedPrivateKey,
),
)
authenticatedAccountsApi
.createAccountKeys(
body = CreateAccountKeysRequest(
publicKey = publicKey,
encryptedPrivateKey = encryptedPrivateKey,
),
)
.toResult()
override suspend fun deleteAccount(
masterPasswordHash: String?,
@@ -61,9 +66,8 @@ class AccountsServiceImpl(
oneTimePassword = oneTimePassword,
),
)
.map {
DeleteAccountResponseJson.Success
}
.toResult()
.map { DeleteAccountResponseJson.Success }
.recoverCatching { throwable ->
throwable
.toBitwardenError()
@@ -75,20 +79,25 @@ class AccountsServiceImpl(
}
override suspend fun requestOneTimePasscode(): Result<Unit> =
authenticatedAccountsApi.requestOtp()
authenticatedAccountsApi
.requestOtp()
.toResult()
override suspend fun verifyOneTimePasscode(passcode: String): Result<Unit> =
authenticatedAccountsApi.verifyOtp(
VerifyOtpRequestJson(
oneTimePasscode = passcode,
),
)
authenticatedAccountsApi
.verifyOtp(
VerifyOtpRequestJson(
oneTimePasscode = passcode,
),
)
.toResult()
override suspend fun requestPasswordHint(
email: String,
): Result<PasswordHintResponseJson> =
unauthenticatedAccountsApi
.passwordHintRequest(PasswordHintRequestJson(email))
.toResult()
.map { PasswordHintResponseJson.Success }
.recoverCatching { throwable ->
throwable
@@ -101,54 +110,70 @@ class AccountsServiceImpl(
}
override suspend fun resendVerificationCodeEmail(body: ResendEmailRequestJson): Result<Unit> =
unauthenticatedAccountsApi.resendVerificationCodeEmail(body = body)
unauthenticatedAccountsApi
.resendVerificationCodeEmail(body = body)
.toResult()
override suspend fun resetPassword(body: ResetPasswordRequestJson): Result<Unit> {
return if (body.currentPasswordHash == null) {
authenticatedAccountsApi.resetTempPassword(body = body)
override suspend fun resetPassword(body: ResetPasswordRequestJson): Result<Unit> =
if (body.currentPasswordHash == null) {
authenticatedAccountsApi
.resetTempPassword(body = body)
.toResult()
} else {
authenticatedAccountsApi.resetPassword(body = body)
authenticatedAccountsApi
.resetPassword(body = body)
.toResult()
}
}
override suspend fun setKeyConnectorKey(
accessToken: String,
body: KeyConnectorKeyRequestJson,
): Result<Unit> = unauthenticatedAccountsApi.setKeyConnectorKey(
body = body,
bearerToken = "$HEADER_VALUE_BEARER_PREFIX$accessToken",
)
): Result<Unit> =
unauthenticatedAccountsApi
.setKeyConnectorKey(
body = body,
bearerToken = "$HEADER_VALUE_BEARER_PREFIX$accessToken",
)
.toResult()
override suspend fun setPassword(
body: SetPasswordRequestJson,
): Result<Unit> = authenticatedAccountsApi.setPassword(body)
): Result<Unit> = authenticatedAccountsApi
.setPassword(body)
.toResult()
override suspend fun getMasterKeyFromKeyConnector(
url: String,
accessToken: String,
): Result<KeyConnectorMasterKeyResponseJson> =
unauthenticatedKeyConnectorApi.getMasterKeyFromKeyConnector(
url = "$url/user-keys",
bearerToken = "$HEADER_VALUE_BEARER_PREFIX$accessToken",
)
unauthenticatedKeyConnectorApi
.getMasterKeyFromKeyConnector(
url = "$url/user-keys",
bearerToken = "$HEADER_VALUE_BEARER_PREFIX$accessToken",
)
.toResult()
override suspend fun storeMasterKeyToKeyConnector(
url: String,
masterKey: String,
): Result<Unit> =
authenticatedKeyConnectorApi.storeMasterKeyToKeyConnector(
url = "$url/user-keys",
body = KeyConnectorMasterKeyRequestJson(masterKey = masterKey),
)
authenticatedKeyConnectorApi
.storeMasterKeyToKeyConnector(
url = "$url/user-keys",
body = KeyConnectorMasterKeyRequestJson(masterKey = masterKey),
)
.toResult()
override suspend fun storeMasterKeyToKeyConnector(
url: String,
accessToken: String,
masterKey: String,
): Result<Unit> =
unauthenticatedKeyConnectorApi.storeMasterKeyToKeyConnector(
url = "$url/user-keys",
bearerToken = "$HEADER_VALUE_BEARER_PREFIX$accessToken",
body = KeyConnectorMasterKeyRequestJson(masterKey = masterKey),
)
unauthenticatedKeyConnectorApi
.storeMasterKeyToKeyConnector(
url = "$url/user-keys",
bearerToken = "$HEADER_VALUE_BEARER_PREFIX$accessToken",
body = KeyConnectorMasterKeyRequestJson(masterKey = masterKey),
)
.toResult()
}

View File

@@ -3,17 +3,22 @@ package com.x8bit.bitwarden.data.auth.datasource.network.service
import com.x8bit.bitwarden.data.auth.datasource.network.api.AuthenticatedAuthRequestsApi
import com.x8bit.bitwarden.data.auth.datasource.network.model.AuthRequestUpdateRequestJson
import com.x8bit.bitwarden.data.auth.datasource.network.model.AuthRequestsResponseJson
import com.x8bit.bitwarden.data.platform.datasource.network.util.toResult
class AuthRequestsServiceImpl(
private val authenticatedAuthRequestsApi: AuthenticatedAuthRequestsApi,
) : AuthRequestsService {
override suspend fun getAuthRequests(): Result<AuthRequestsResponseJson> =
authenticatedAuthRequestsApi.getAuthRequests()
authenticatedAuthRequestsApi
.getAuthRequests()
.toResult()
override suspend fun getAuthRequest(
requestId: String,
): Result<AuthRequestsResponseJson.AuthRequest> =
authenticatedAuthRequestsApi.getAuthRequest(requestId = requestId)
authenticatedAuthRequestsApi
.getAuthRequest(requestId = requestId)
.toResult()
override suspend fun updateAuthRequest(
requestId: String,
@@ -22,13 +27,15 @@ class AuthRequestsServiceImpl(
deviceId: String,
isApproved: Boolean,
): Result<AuthRequestsResponseJson.AuthRequest> =
authenticatedAuthRequestsApi.updateAuthRequest(
userId = requestId,
body = AuthRequestUpdateRequestJson(
key = key,
masterPasswordHash = masterPasswordHash,
deviceId = deviceId,
isApproved = isApproved,
),
)
authenticatedAuthRequestsApi
.updateAuthRequest(
userId = requestId,
body = AuthRequestUpdateRequestJson(
key = key,
masterPasswordHash = masterPasswordHash,
deviceId = deviceId,
isApproved = isApproved,
),
)
.toResult()
}

View File

@@ -5,6 +5,7 @@ import com.x8bit.bitwarden.data.auth.datasource.network.api.UnauthenticatedDevic
import com.x8bit.bitwarden.data.auth.datasource.network.model.TrustedDeviceKeysRequestJson
import com.x8bit.bitwarden.data.auth.datasource.network.model.TrustedDeviceKeysResponseJson
import com.x8bit.bitwarden.data.platform.datasource.network.util.base64UrlEncode
import com.x8bit.bitwarden.data.platform.datasource.network.util.toResult
class DevicesServiceImpl(
private val authenticatedDevicesApi: AuthenticatedDevicesApi,
@@ -13,22 +14,26 @@ class DevicesServiceImpl(
override suspend fun getIsKnownDevice(
emailAddress: String,
deviceId: String,
): Result<Boolean> = unauthenticatedDevicesApi.getIsKnownDevice(
emailAddress = emailAddress.base64UrlEncode(),
deviceId = deviceId,
)
): Result<Boolean> = unauthenticatedDevicesApi
.getIsKnownDevice(
emailAddress = emailAddress.base64UrlEncode(),
deviceId = deviceId,
)
.toResult()
override suspend fun trustDevice(
appId: String,
encryptedUserKey: String,
encryptedDevicePublicKey: String,
encryptedDevicePrivateKey: String,
): Result<TrustedDeviceKeysResponseJson> = authenticatedDevicesApi.updateTrustedDeviceKeys(
appId = appId,
request = TrustedDeviceKeysRequestJson(
encryptedUserKey = encryptedUserKey,
encryptedDevicePublicKey = encryptedDevicePublicKey,
encryptedDevicePrivateKey = encryptedDevicePrivateKey,
),
)
): Result<TrustedDeviceKeysResponseJson> = authenticatedDevicesApi
.updateTrustedDeviceKeys(
appId = appId,
request = TrustedDeviceKeysRequestJson(
encryptedUserKey = encryptedUserKey,
encryptedDevicePublicKey = encryptedDevicePublicKey,
encryptedDevicePrivateKey = encryptedDevicePrivateKey,
),
)
.toResult()
}

View File

@@ -1,6 +1,7 @@
package com.x8bit.bitwarden.data.auth.datasource.network.service
import com.x8bit.bitwarden.data.auth.datasource.network.api.HaveIBeenPwnedApi
import com.x8bit.bitwarden.data.platform.datasource.network.util.toResult
import java.security.MessageDigest
class HaveIBeenPwnedServiceImpl(private val api: HaveIBeenPwnedApi) : HaveIBeenPwnedService {
@@ -17,6 +18,7 @@ class HaveIBeenPwnedServiceImpl(private val api: HaveIBeenPwnedApi) : HaveIBeenP
return api
.fetchBreachedPasswords(hashPrefix = hashPrefix)
.toResult()
.mapCatching { responseBody ->
responseBody.string()
// First split the response by newline: each hashed password is on a new line.

View File

@@ -16,8 +16,9 @@ import com.x8bit.bitwarden.data.auth.datasource.network.model.VerifyEmailTokenRe
import com.x8bit.bitwarden.data.auth.datasource.network.model.VerifyEmailTokenResponseJson
import com.x8bit.bitwarden.data.platform.datasource.network.model.toBitwardenError
import com.x8bit.bitwarden.data.platform.datasource.network.util.base64UrlEncode
import com.x8bit.bitwarden.data.platform.datasource.network.util.executeForResult
import com.x8bit.bitwarden.data.platform.datasource.network.util.executeForNetworkResult
import com.x8bit.bitwarden.data.platform.datasource.network.util.parseErrorBodyOrNull
import com.x8bit.bitwarden.data.platform.datasource.network.util.toResult
import com.x8bit.bitwarden.data.platform.util.DeviceModelProvider
import kotlinx.serialization.json.Json
@@ -28,12 +29,15 @@ class IdentityServiceImpl(
) : IdentityService {
override suspend fun preLogin(email: String): Result<PreLoginResponseJson> =
unauthenticatedIdentityApi.preLogin(PreLoginRequestJson(email = email))
unauthenticatedIdentityApi
.preLogin(PreLoginRequestJson(email = email))
.toResult()
@Suppress("MagicNumber")
override suspend fun register(body: RegisterRequestJson): Result<RegisterResponseJson> =
unauthenticatedIdentityApi
.register(body)
.toResult()
.recoverCatching { throwable ->
val bitwardenError = throwable.toBitwardenError()
bitwardenError
@@ -75,6 +79,7 @@ class IdentityServiceImpl(
captchaResponse = captchaToken,
authRequestId = authModel.authRequestId,
)
.toResult()
.recoverCatching { throwable ->
val bitwardenError = throwable.toBitwardenError()
bitwardenError.parseErrorBodyOrNull<GetTokenResponseJson.CaptchaRequired>(
@@ -95,6 +100,7 @@ class IdentityServiceImpl(
.prevalidateSso(
organizationIdentifier = organizationIdentifier,
)
.toResult()
override fun refreshTokenSynchronously(
refreshToken: String,
@@ -104,7 +110,8 @@ class IdentityServiceImpl(
grantType = "refresh_token",
refreshToken = refreshToken,
)
.executeForResult()
.executeForNetworkResult()
.toResult()
@Suppress("MagicNumber")
override suspend fun registerFinish(
@@ -112,6 +119,7 @@ class IdentityServiceImpl(
): Result<RegisterResponseJson> =
unauthenticatedIdentityApi
.registerFinish(body)
.toResult()
.recoverCatching { throwable ->
val bitwardenError = throwable.toBitwardenError()
bitwardenError
@@ -127,6 +135,7 @@ class IdentityServiceImpl(
): Result<String?> {
return unauthenticatedIdentityApi
.sendVerificationEmail(body = body)
.toResult()
.map { it?.content }
}
@@ -136,9 +145,8 @@ class IdentityServiceImpl(
.verifyEmailToken(
body = body,
)
.map {
VerifyEmailTokenResponseJson.Valid
}
.toResult()
.map { VerifyEmailTokenResponseJson.Valid }
.recoverCatching { throwable ->
val bitwardenError = throwable.toBitwardenError()
bitwardenError

View File

@@ -5,6 +5,7 @@ import com.x8bit.bitwarden.data.auth.datasource.network.api.UnauthenticatedAuthR
import com.x8bit.bitwarden.data.auth.datasource.network.model.AuthRequestRequestJson
import com.x8bit.bitwarden.data.auth.datasource.network.model.AuthRequestTypeJson
import com.x8bit.bitwarden.data.auth.datasource.network.model.AuthRequestsResponseJson
import com.x8bit.bitwarden.data.platform.datasource.network.util.toResult
import com.x8bit.bitwarden.data.platform.util.asFailure
/**
@@ -24,17 +25,19 @@ class NewAuthRequestServiceImpl(
): Result<AuthRequestsResponseJson.AuthRequest> =
when (authRequestType) {
AuthRequestTypeJson.LOGIN_WITH_DEVICE -> {
unauthenticatedAuthRequestsApi.createAuthRequest(
deviceIdentifier = deviceId,
body = AuthRequestRequestJson(
email = email,
publicKey = publicKey,
deviceId = deviceId,
accessCode = accessCode,
fingerprint = fingerprint,
type = authRequestType,
),
)
unauthenticatedAuthRequestsApi
.createAuthRequest(
deviceIdentifier = deviceId,
body = AuthRequestRequestJson(
email = email,
publicKey = publicKey,
deviceId = deviceId,
accessCode = accessCode,
fingerprint = fingerprint,
type = authRequestType,
),
)
.toResult()
}
AuthRequestTypeJson.UNLOCK -> {
@@ -43,17 +46,19 @@ class NewAuthRequestServiceImpl(
}
AuthRequestTypeJson.ADMIN_APPROVAL -> {
authenticatedAuthRequestsApi.createAdminAuthRequest(
deviceIdentifier = deviceId,
body = AuthRequestRequestJson(
email = email,
publicKey = publicKey,
deviceId = deviceId,
accessCode = accessCode,
fingerprint = fingerprint,
type = authRequestType,
),
)
authenticatedAuthRequestsApi
.createAdminAuthRequest(
deviceIdentifier = deviceId,
body = AuthRequestRequestJson(
email = email,
publicKey = publicKey,
deviceId = deviceId,
accessCode = accessCode,
fingerprint = fingerprint,
type = authRequestType,
),
)
.toResult()
}
}
@@ -63,11 +68,15 @@ class NewAuthRequestServiceImpl(
isSso: Boolean,
): Result<AuthRequestsResponseJson.AuthRequest> =
if (isSso) {
authenticatedAuthRequestsApi.getAuthRequest(requestId)
authenticatedAuthRequestsApi
.getAuthRequest(requestId = requestId)
.toResult()
} else {
unauthenticatedAuthRequestsApi.getAuthRequestUpdate(
requestId = requestId,
accessCode = accessCode,
)
unauthenticatedAuthRequestsApi
.getAuthRequestUpdate(
requestId = requestId,
accessCode = accessCode,
)
.toResult()
}
}

View File

@@ -3,6 +3,7 @@ package com.x8bit.bitwarden.data.auth.datasource.network.service
import com.x8bit.bitwarden.data.auth.datasource.network.model.OrganizationAutoEnrollStatusResponseJson
import com.x8bit.bitwarden.data.auth.datasource.network.model.OrganizationDomainSsoDetailsResponseJson
import com.x8bit.bitwarden.data.auth.datasource.network.model.OrganizationKeysResponseJson
import com.x8bit.bitwarden.data.auth.datasource.network.model.VerifiedOrganizationDomainSsoDetailsResponse
/**
* Provides an API for querying organization endpoints.
@@ -38,4 +39,12 @@ interface OrganizationService {
suspend fun getOrganizationKeys(
organizationId: String,
): Result<OrganizationKeysResponseJson>
/**
* Request organization verified domain details for an [email] needed for SSO
* requests.
*/
suspend fun getVerifiedOrganizationDomainSsoDetails(
email: String,
): Result<VerifiedOrganizationDomainSsoDetailsResponse>
}

View File

@@ -7,6 +7,9 @@ import com.x8bit.bitwarden.data.auth.datasource.network.model.OrganizationDomain
import com.x8bit.bitwarden.data.auth.datasource.network.model.OrganizationDomainSsoDetailsResponseJson
import com.x8bit.bitwarden.data.auth.datasource.network.model.OrganizationKeysResponseJson
import com.x8bit.bitwarden.data.auth.datasource.network.model.OrganizationResetPasswordEnrollRequestJson
import com.x8bit.bitwarden.data.auth.datasource.network.model.VerifiedOrganizationDomainSsoDetailsRequest
import com.x8bit.bitwarden.data.auth.datasource.network.model.VerifiedOrganizationDomainSsoDetailsResponse
import com.x8bit.bitwarden.data.platform.datasource.network.util.toResult
/**
* Default implementation of [OrganizationService].
@@ -29,6 +32,7 @@ class OrganizationServiceImpl(
resetPasswordKey = resetPasswordKey,
),
)
.toResult()
override suspend fun getOrganizationDomainSsoDetails(
email: String,
@@ -38,6 +42,7 @@ class OrganizationServiceImpl(
email = email,
),
)
.toResult()
override suspend fun getOrganizationAutoEnrollStatus(
organizationIdentifier: String,
@@ -45,6 +50,7 @@ class OrganizationServiceImpl(
.getOrganizationAutoEnrollResponse(
organizationIdentifier = organizationIdentifier,
)
.toResult()
override suspend fun getOrganizationKeys(
organizationId: String,
@@ -52,4 +58,15 @@ class OrganizationServiceImpl(
.getOrganizationKeys(
organizationId = organizationId,
)
.toResult()
override suspend fun getVerifiedOrganizationDomainSsoDetails(
email: String,
): Result<VerifiedOrganizationDomainSsoDetailsResponse> = unauthenticatedOrganizationApi
.getVerifiedOrganizationDomainsByEmail(
body = VerifiedOrganizationDomainSsoDetailsRequest(
email = email,
),
)
.toResult()
}

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

@@ -17,6 +17,7 @@ import com.x8bit.bitwarden.data.auth.manager.model.CreateAuthRequestResult
import com.x8bit.bitwarden.data.auth.manager.util.isSso
import com.x8bit.bitwarden.data.auth.manager.util.toAuthRequestTypeJson
import com.x8bit.bitwarden.data.platform.util.asFailure
import com.x8bit.bitwarden.data.platform.util.asSuccess
import com.x8bit.bitwarden.data.platform.util.flatMap
import com.x8bit.bitwarden.data.vault.datasource.sdk.VaultSdkSource
import kotlinx.coroutines.currentCoroutineContext
@@ -65,7 +66,7 @@ class AuthRequestManagerImpl(
email: String,
authRequestType: AuthRequestType,
): Flow<CreateAuthRequestResult> = flow {
val initialResult = createNewAuthRequest(
val initialResult = createNewAuthRequestIfNecessary(
email = email,
authRequestType = authRequestType.toAuthRequestTypeJson(),
)
@@ -74,7 +75,6 @@ class AuthRequestManagerImpl(
emit(CreateAuthRequestResult.Error)
return@flow
}
val authRequestResponse = initialResult.authRequestResponse
var authRequest = initialResult.authRequest
emit(CreateAuthRequestResult.Update(authRequest))
@@ -84,7 +84,7 @@ class AuthRequestManagerImpl(
newAuthRequestService
.getAuthRequestUpdate(
requestId = authRequest.id,
accessCode = authRequestResponse.accessCode,
accessCode = initialResult.accessCode,
isSso = authRequestType.isSso,
)
.map { request ->
@@ -112,7 +112,8 @@ class AuthRequestManagerImpl(
emit(
CreateAuthRequestResult.Success(
authRequest = updateAuthRequest,
authRequestResponse = authRequestResponse,
privateKey = initialResult.privateKey,
accessCode = initialResult.accessCode,
),
)
}
@@ -354,6 +355,52 @@ class AuthRequestManagerImpl(
)
}
/**
* Creates a new auth request for the given email and returns a [NewAuthRequestData].
* If the auth request type is [AuthRequestTypeJson.ADMIN_APPROVAL], check for a
* pending auth request and return it if it exists we should return that request.
*/
private suspend fun createNewAuthRequestIfNecessary(
email: String,
authRequestType: AuthRequestTypeJson,
): Result<NewAuthRequestData> {
return if (authRequestType == AuthRequestTypeJson.ADMIN_APPROVAL) {
authDiskSource
.getPendingAuthRequest(requireNotNull(activeUserId))
?.let { pendingAuthRequest ->
authRequestsService
.getAuthRequest(pendingAuthRequest.requestId)
.map {
NewAuthRequestData(
authRequest = AuthRequest(
id = it.id,
publicKey = it.publicKey,
platform = it.platform,
ipAddress = it.ipAddress,
key = it.key,
masterPasswordHash = it.masterPasswordHash,
creationDate = it.creationDate,
responseDate = it.responseDate,
requestApproved = it.requestApproved ?: false,
originUrl = it.originUrl,
fingerprint = pendingAuthRequest.requestFingerprint,
),
privateKey = pendingAuthRequest.requestPrivateKey,
accessCode = pendingAuthRequest.requestAccessCode,
)
.asSuccess()
}
.getOrNull()
}
?: createNewAuthRequest(email = email, authRequestType = authRequestType)
} else {
createNewAuthRequest(
email = email,
authRequestType = authRequestType,
)
}
}
/**
* Attempts to create a new auth request for the given email and returns a [NewAuthRequestData]
* with the [AuthRequest] and [AuthRequestResponse].
@@ -381,6 +428,8 @@ class AuthRequestManagerImpl(
pendingAuthRequest = PendingAuthRequestJson(
requestId = it.id,
requestPrivateKey = authRequestResponse.privateKey,
requestAccessCode = authRequestResponse.accessCode,
requestFingerprint = authRequestResponse.fingerprint,
),
)
}
@@ -400,7 +449,13 @@ class AuthRequestManagerImpl(
fingerprint = authRequestResponse.fingerprint,
)
}
.map { NewAuthRequestData(it, authRequestResponse) }
.map {
NewAuthRequestData(
authRequest = it,
privateKey = authRequestResponse.privateKey,
accessCode = authRequestResponse.accessCode,
)
}
}
private suspend fun getFingerprintPhrase(
@@ -420,5 +475,6 @@ class AuthRequestManagerImpl(
*/
private data class NewAuthRequestData(
val authRequest: AuthRequest,
val authRequestResponse: AuthRequestResponse,
val privateKey: String,
val accessCode: String,
)

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

@@ -1,7 +1,5 @@
package com.x8bit.bitwarden.data.auth.manager.model
import com.bitwarden.core.AuthRequestResponse
/**
* Models result of creating a new login approval request.
*/
@@ -18,7 +16,8 @@ sealed class CreateAuthRequestResult {
*/
data class Success(
val authRequest: AuthRequest,
val authRequestResponse: AuthRequestResponse,
val privateKey: String,
val accessCode: String,
) : CreateAuthRequestResult()
/**

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

@@ -28,6 +28,7 @@ import com.x8bit.bitwarden.data.auth.repository.model.SwitchAccountResult
import com.x8bit.bitwarden.data.auth.repository.model.UserState
import com.x8bit.bitwarden.data.auth.repository.model.ValidatePasswordResult
import com.x8bit.bitwarden.data.auth.repository.model.ValidatePinResult
import com.x8bit.bitwarden.data.auth.repository.model.VerifiedOrganizationDomainSsoDetailsResult
import com.x8bit.bitwarden.data.auth.repository.model.VerifyOtpResult
import com.x8bit.bitwarden.data.auth.repository.util.CaptchaCallbackTokenResult
import com.x8bit.bitwarden.data.auth.repository.util.DuoCallbackTokenResult
@@ -212,6 +213,7 @@ interface AuthRepository : AuthenticatorProvider, AuthRequestManager {
password: String?,
twoFactorData: TwoFactorDataModel,
captchaToken: String?,
orgIdentifier: String?,
): LoginResult
/**
@@ -328,6 +330,13 @@ interface AuthRepository : AuthenticatorProvider, AuthRequestManager {
email: String,
): OrganizationDomainSsoDetailsResult
/**
* Get the verified organization domain SSO details for the given [email].
*/
suspend fun getVerifiedOrganizationDomainSsoDetails(
email: String,
): VerifiedOrganizationDomainSsoDetailsResult
/**
* Prevalidates the organization identifier used in an SSO request.
*/

View File

@@ -68,6 +68,7 @@ import com.x8bit.bitwarden.data.auth.repository.model.UserState
import com.x8bit.bitwarden.data.auth.repository.model.ValidatePasswordResult
import com.x8bit.bitwarden.data.auth.repository.model.ValidatePinResult
import com.x8bit.bitwarden.data.auth.repository.model.VaultUnlockType
import com.x8bit.bitwarden.data.auth.repository.model.VerifiedOrganizationDomainSsoDetailsResult
import com.x8bit.bitwarden.data.auth.repository.model.VerifyOtpResult
import com.x8bit.bitwarden.data.auth.repository.model.toLoginErrorResult
import com.x8bit.bitwarden.data.auth.repository.util.CaptchaCallbackTokenResult
@@ -92,15 +93,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 +157,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 +167,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 +263,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 +277,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 +290,7 @@ class AuthRepositoryImpl(
isBiometricsEnabledProvider = ::isBiometricsEnabled,
vaultUnlockTypeProvider = ::getVaultUnlockType,
isDeviceTrustedProvider = ::isDeviceTrusted,
firstTimeState = firstTimeState,
)
}
.filterNot { mutableHasPendingAccountDeletionStateFlow.value }
@@ -298,6 +310,7 @@ class AuthRepositoryImpl(
isBiometricsEnabledProvider = ::isBiometricsEnabled,
vaultUnlockTypeProvider = ::getVaultUnlockType,
isDeviceTrustedProvider = ::isDeviceTrusted,
firstTimeState = firstTimeActionManager.currentOrDefaultUserFirstTimeState,
),
)
@@ -354,6 +367,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 +659,7 @@ class AuthRepositoryImpl(
password: String?,
twoFactorData: TwoFactorDataModel,
captchaToken: String?,
orgIdentifier: String?,
): LoginResult = identityTokenAuthModel
?.let {
loginCommon(
@@ -637,6 +669,7 @@ class AuthRepositoryImpl(
twoFactorData = twoFactorData,
captchaToken = captchaToken ?: twoFactorResponse?.captchaToken,
deviceData = twoFactorDeviceData,
orgIdentifier = orgIdentifier,
)
}
?: LoginResult.Error(errorMessage = null)
@@ -998,7 +1031,7 @@ class AuthRepositoryImpl(
ForcePasswordResetReason.ADMIN_FORCE_PASSWORD_RESET,
ForcePasswordResetReason.WEAK_MASTER_PASSWORD_ON_LOGIN,
null,
-> {
-> {
authSdkSource
.makeRegisterKeys(
email = activeAccount.profile.email,
@@ -1048,7 +1081,7 @@ class AuthRepositoryImpl(
is VaultUnlockResult.AuthenticationError,
VaultUnlockResult.InvalidStateError,
VaultUnlockResult.GenericError,
-> {
-> {
IllegalStateException("Failed to unlock vault").asFailure()
}
}
@@ -1091,11 +1124,27 @@ class AuthRepositoryImpl(
OrganizationDomainSsoDetailsResult.Success(
isSsoAvailable = it.isSsoAvailable,
organizationIdentifier = it.organizationIdentifier,
verifiedDate = it.verifiedDate,
)
},
onFailure = { OrganizationDomainSsoDetailsResult.Failure },
)
override suspend fun getVerifiedOrganizationDomainSsoDetails(
email: String,
): VerifiedOrganizationDomainSsoDetailsResult = organizationService
.getVerifiedOrganizationDomainSsoDetails(
email = email,
)
.fold(
onSuccess = {
VerifiedOrganizationDomainSsoDetailsResult.Success(
verifiedOrganizationDomainSsoDetails = it.verifiedOrganizationDomainSsoDetails,
)
},
onFailure = { VerifiedOrganizationDomainSsoDetailsResult.Failure },
)
override suspend fun prevalidateSso(
organizationIdentifier: String,
): PrevalidateSsoResult = identityService
@@ -1253,7 +1302,7 @@ class AuthRepositoryImpl(
.sendVerificationEmail(
SendVerificationEmailRequestJson(
email = email,
name = name,
name = name.takeUnless { it.isBlank() },
receiveMarketingEmails = receiveMarketingEmails,
),
)
@@ -1459,7 +1508,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 +1536,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,5 +1,7 @@
package com.x8bit.bitwarden.data.auth.repository.model
import java.time.ZonedDateTime
/**
* Response types when checking for an email's claimed domain organization.
*/
@@ -9,10 +11,12 @@ sealed class OrganizationDomainSsoDetailsResult {
*
* @property isSsoAvailable Indicates if SSO is available for the email address.
* @property organizationIdentifier The claimed organization identifier for the email address.
* @property verifiedDate The date and time when the domain was verified.
*/
data class Success(
val isSsoAvailable: Boolean,
val organizationIdentifier: String,
val verifiedDate: ZonedDateTime?,
) : OrganizationDomainSsoDetailsResult()
/**

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

@@ -0,0 +1,22 @@
package com.x8bit.bitwarden.data.auth.repository.model
import com.x8bit.bitwarden.data.auth.datasource.network.model.VerifiedOrganizationDomainSsoDetailsResponse.VerifiedOrganizationDomainSsoDetail
/**
* Response types when checking for an email's claimed domain organization.
*/
sealed class VerifiedOrganizationDomainSsoDetailsResult {
/**
* The request was successful.
*
* @property verifiedOrganizationDomainSsoDetails The verified organization domain SSO details.
*/
data class Success(
val verifiedOrganizationDomainSsoDetails: List<VerifiedOrganizationDomainSsoDetail>,
) : VerifiedOrganizationDomainSsoDetailsResult()
/**
* The request failed.
*/
data object Failure : VerifiedOrganizationDomainSsoDetailsResult()
}

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

@@ -22,8 +22,7 @@ class BitwardenAccessibilityService : AccessibilityService() {
lateinit var processor: BitwardenAccessibilityProcessor
override fun onAccessibilityEvent(event: AccessibilityEvent) {
if (rootInActiveWindow?.packageName != event.packageName) return
processor.processAccessibilityEvent(rootAccessibilityNodeInfo = rootInActiveWindow)
processor.processAccessibilityEvent(event = event) { rootInActiveWindow }
}
override fun onInterrupt() = Unit

View File

@@ -5,7 +5,7 @@ import androidx.lifecycle.LifecycleCoroutineScope
import com.x8bit.bitwarden.data.autofill.accessibility.manager.AccessibilityActivityManager
import com.x8bit.bitwarden.data.autofill.accessibility.manager.AccessibilityActivityManagerImpl
import com.x8bit.bitwarden.data.autofill.accessibility.manager.AccessibilityEnabledManager
import com.x8bit.bitwarden.data.platform.manager.AppForegroundManager
import com.x8bit.bitwarden.data.platform.manager.AppStateManager
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
@@ -24,13 +24,13 @@ object ActivityAccessibilityModule {
fun providesAccessibilityActivityManager(
@ApplicationContext context: Context,
accessibilityEnabledManager: AccessibilityEnabledManager,
appForegroundManager: AppForegroundManager,
appStateManager: AppStateManager,
lifecycleScope: LifecycleCoroutineScope,
): AccessibilityActivityManager =
AccessibilityActivityManagerImpl(
context = context,
accessibilityEnabledManager = accessibilityEnabledManager,
appForegroundManager = appForegroundManager,
appStateManager = appStateManager,
lifecycleScope = lifecycleScope,
)
}

View File

@@ -3,7 +3,7 @@ package com.x8bit.bitwarden.data.autofill.accessibility.manager
import android.content.Context
import androidx.lifecycle.LifecycleCoroutineScope
import com.x8bit.bitwarden.data.autofill.accessibility.util.isAccessibilityServiceEnabled
import com.x8bit.bitwarden.data.platform.manager.AppForegroundManager
import com.x8bit.bitwarden.data.platform.manager.AppStateManager
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
@@ -13,11 +13,11 @@ import kotlinx.coroutines.flow.onEach
class AccessibilityActivityManagerImpl(
private val context: Context,
private val accessibilityEnabledManager: AccessibilityEnabledManager,
appForegroundManager: AppForegroundManager,
appStateManager: AppStateManager,
lifecycleScope: LifecycleCoroutineScope,
) : AccessibilityActivityManager {
init {
appForegroundManager
appStateManager
.appForegroundStateFlow
.onEach {
accessibilityEnabledManager.isAccessibilityEnabled =

View File

@@ -26,18 +26,18 @@ class AccessibilityCompletionManagerImpl(
.intent
?.getAutofillSelectionDataOrNull()
?: run {
activity.finish()
activity.finishAndRemoveTask()
return
}
if (autofillSelectionData.framework != AutofillSelectionData.Framework.ACCESSIBILITY) {
activity.finish()
activity.finishAndRemoveTask()
return
}
val uri = autofillSelectionData
.uri
?.toUriOrNull()
?: run {
activity.finish()
activity.finishAndRemoveTask()
return
}
@@ -47,7 +47,7 @@ class AccessibilityCompletionManagerImpl(
)
mainScope.launch {
totpManager.tryCopyTotpToClipboard(cipherView = cipherView)
activity.finish()
}
activity.finishAndRemoveTask()
}
}

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

@@ -8,4 +8,6 @@ import android.view.accessibility.AccessibilityNodeInfo
data class FillableFields(
val usernameField: AccessibilityNodeInfo?,
val passwordFields: List<AccessibilityNodeInfo>,
)
) {
val hasFields: Boolean = usernameField != null && passwordFields.isNotEmpty()
}

View File

@@ -1,5 +1,6 @@
package com.x8bit.bitwarden.data.autofill.accessibility.processor
import android.view.accessibility.AccessibilityEvent
import android.view.accessibility.AccessibilityNodeInfo
/**
@@ -7,7 +8,12 @@ import android.view.accessibility.AccessibilityNodeInfo
*/
interface BitwardenAccessibilityProcessor {
/**
* Processes the [AccessibilityNodeInfo] for autofill options.
* Processes the [AccessibilityEvent] for autofill options and grant access to the current
* [AccessibilityNodeInfo] via the [rootAccessibilityNodeInfoProvider] (note that calling the
* `rootAccessibilityNodeInfoProvider` is expensive).
*/
fun processAccessibilityEvent(rootAccessibilityNodeInfo: AccessibilityNodeInfo?)
fun processAccessibilityEvent(
event: AccessibilityEvent,
rootAccessibilityNodeInfoProvider: () -> AccessibilityNodeInfo?,
)
}

View File

@@ -2,6 +2,7 @@ package com.x8bit.bitwarden.data.autofill.accessibility.processor
import android.content.Context
import android.os.PowerManager
import android.view.accessibility.AccessibilityEvent
import android.view.accessibility.AccessibilityNodeInfo
import android.widget.Toast
import com.x8bit.bitwarden.R
@@ -25,37 +26,48 @@ class BitwardenAccessibilityProcessorImpl(
private val launcherPackageNameManager: LauncherPackageNameManager,
private val powerManager: PowerManager,
) : BitwardenAccessibilityProcessor {
override fun processAccessibilityEvent(rootAccessibilityNodeInfo: AccessibilityNodeInfo?) {
val rootNode = rootAccessibilityNodeInfo ?: return
override fun processAccessibilityEvent(
event: AccessibilityEvent,
rootAccessibilityNodeInfoProvider: () -> AccessibilityNodeInfo?,
) {
val eventNode = event.source ?: return
// Ignore the event when the phone is inactive
if (!powerManager.isInteractive) return
// We skip if the system package
if (rootNode.isSystemPackage) return
// We skip any package that is a launcher or unsupported
if (rootNode.shouldSkipPackage ||
launcherPackageNameManager.launcherPackages.any { it == rootNode.packageName }
) {
// Clear the action since this event needs to be ignored completely
accessibilityAutofillManager.accessibilityAction = null
if (eventNode.isSystemPackage) return
// We skip any package that is unsupported
if (eventNode.shouldSkipPackage) return
// We skip any package that is a launcher
if (launcherPackageNameManager.launcherPackages.any { it == eventNode.packageName }) {
return
}
// Only process the event if the tile was clicked
val accessibilityAction = accessibilityAutofillManager.accessibilityAction ?: return
// We only call for the root node once after all other checks
// have passed because it is significant performance hit
if (rootAccessibilityNodeInfoProvider()?.packageName != event.packageName) return
// Clear the action since we are now acting on it
accessibilityAutofillManager.accessibilityAction = null
when (accessibilityAction) {
is AccessibilityAction.AttemptFill -> {
handleAttemptFill(rootNode = rootNode, attemptFill = accessibilityAction)
handleAttemptFill(rootNode = eventNode, attemptFill = accessibilityAction)
}
AccessibilityAction.AttemptParseUri -> handleAttemptParseUri(rootNode = rootNode)
AccessibilityAction.AttemptParseUri -> handleAttemptParseUri(rootNode = eventNode)
}
}
private fun handleAttemptParseUri(rootNode: AccessibilityNodeInfo) {
accessibilityParser
.parseForUriOrPackageName(rootNode = rootNode)
?.takeIf {
accessibilityParser
.parseForFillableFields(rootNode = rootNode, uri = it)
.hasFields
}
?.let { uri ->
context.startActivity(
createAutofillSelectionIntent(

View File

@@ -8,7 +8,7 @@ import androidx.lifecycle.lifecycleScope
import com.x8bit.bitwarden.data.autofill.manager.AutofillActivityManager
import com.x8bit.bitwarden.data.autofill.manager.AutofillActivityManagerImpl
import com.x8bit.bitwarden.data.autofill.manager.AutofillEnabledManager
import com.x8bit.bitwarden.data.platform.manager.AppForegroundManager
import com.x8bit.bitwarden.data.platform.manager.AppStateManager
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
@@ -27,13 +27,13 @@ object ActivityAutofillModule {
@Provides
fun provideAutofillActivityManager(
@ActivityScopedManager autofillManager: AutofillManager,
appForegroundManager: AppForegroundManager,
appStateManager: AppStateManager,
autofillEnabledManager: AutofillEnabledManager,
lifecycleScope: LifecycleCoroutineScope,
): AutofillActivityManager =
AutofillActivityManagerImpl(
autofillManager = autofillManager,
appForegroundManager = appForegroundManager,
appStateManager = appStateManager,
autofillEnabledManager = autofillEnabledManager,
lifecycleScope = lifecycleScope,
)

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

@@ -1,6 +1,7 @@
package com.x8bit.bitwarden.data.autofill.fido2.datasource.network.api
import com.x8bit.bitwarden.data.autofill.fido2.datasource.network.model.DigitalAssetLinkResponseJson
import com.x8bit.bitwarden.data.platform.datasource.network.model.NetworkResult
import retrofit2.http.GET
import retrofit2.http.Url
@@ -15,5 +16,5 @@ interface DigitalAssetLinkApi {
@GET
suspend fun getDigitalAssetLinks(
@Url url: String,
): Result<List<DigitalAssetLinkResponseJson>>
): NetworkResult<List<DigitalAssetLinkResponseJson>>
}

View File

@@ -2,6 +2,7 @@ package com.x8bit.bitwarden.data.autofill.fido2.datasource.network.service
import com.x8bit.bitwarden.data.autofill.fido2.datasource.network.api.DigitalAssetLinkApi
import com.x8bit.bitwarden.data.autofill.fido2.datasource.network.model.DigitalAssetLinkResponseJson
import com.x8bit.bitwarden.data.platform.datasource.network.util.toResult
/**
* Primary implementation of [DigitalAssetLinkService].
@@ -18,4 +19,5 @@ class DigitalAssetLinkServiceImpl(
.getDigitalAssetLinks(
url = "$scheme$relyingParty/.well-known/assetlinks.json",
)
.toResult()
}

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

@@ -2,7 +2,7 @@ package com.x8bit.bitwarden.data.autofill.manager
import android.view.autofill.AutofillManager
import androidx.lifecycle.LifecycleCoroutineScope
import com.x8bit.bitwarden.data.platform.manager.AppForegroundManager
import com.x8bit.bitwarden.data.platform.manager.AppStateManager
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
@@ -12,7 +12,7 @@ import kotlinx.coroutines.flow.onEach
class AutofillActivityManagerImpl(
private val autofillManager: AutofillManager,
private val autofillEnabledManager: AutofillEnabledManager,
appForegroundManager: AppForegroundManager,
appStateManager: AppStateManager,
lifecycleScope: LifecycleCoroutineScope,
) : AutofillActivityManager {
private val isAutofillEnabledAndSupported: Boolean
@@ -21,7 +21,7 @@ class AutofillActivityManagerImpl(
autofillManager.isAutofillSupported
init {
appForegroundManager
appStateManager
.appForegroundStateFlow
.onEach { autofillEnabledManager.isAutofillEnabled = isAutofillEnabledAndSupported }
.launchIn(lifecycleScope)

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

@@ -38,7 +38,7 @@ fun createAutofillSelectionIntent(
.apply {
// This helps prevent a crash when using the accessibility framework
if (framework == AutofillSelectionData.Framework.ACCESSIBILITY) {
setFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
setFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_REORDER_TO_FRONT)
}
putExtra(
AUTOFILL_BUNDLE_KEY,

View File

@@ -68,6 +68,17 @@ 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?
/**
* Emits updates that track [lastDatabaseSchemeChangeInstant].
*/
val lastDatabaseSchemeChangeInstantFlow: Flow<Instant?>
/**
* Clears all the settings data for the given user.
*/
@@ -292,4 +303,20 @@ interface SettingsDiskSource {
* Emits updates that track [getShowUnlockSettingBadge] for the given [userId].
*/
fun getShowUnlockSettingBadgeFlow(userId: String): Flow<Boolean?>
/**
* Gets whether or not the given [userId] has signalled they want to import logins later.
*/
fun getShowImportLoginsSettingBadge(userId: String): Boolean?
/**
* Stores the given value for whether or not the given [userId] has signalled they want to
* set import logins later, during first time usage.
*/
fun storeShowImportLoginsSettingBadge(userId: String, showBadge: Boolean?)
/**
* Emits updates that track [getShowImportLoginsSettingBadge] for the given [userId].
*/
fun getShowImportLoginsSettingBadgeFlow(userId: String): Flow<Boolean?>
}

View File

@@ -35,6 +35,8 @@ 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 SHOW_IMPORT_LOGINS_SETTING_BADGE = "showImportLoginsSettingBadge"
private const val LAST_SCHEME_CHANGE_INSTANT = "lastDatabaseSchemeChangeInstant"
/**
* Primary implementation of [SettingsDiskSource].
@@ -64,12 +66,17 @@ class SettingsDiskSourceImpl(
private val mutableShowUnlockSettingBadgeFlowMap =
mutableMapOf<String, MutableSharedFlow<Boolean?>>()
private val mutableShowImportLoginsSettingBadgeFlowMap =
mutableMapOf<String, MutableSharedFlow<Boolean?>>()
private val mutableIsIconLoadingDisabledFlow = bufferedMutableSharedFlow<Boolean?>()
private val mutableIsCrashLoggingEnabledFlow = bufferedMutableSharedFlow<Boolean?>()
private val mutableHasUserLoggedInOrCreatedAccountFlow = bufferedMutableSharedFlow<Boolean?>()
private val mutableLastDatabaseSchemeChangeInstantFlow = bufferedMutableSharedFlow<Instant?>()
private val mutableScreenCaptureAllowedFlowMap =
mutableMapOf<String, MutableSharedFlow<Boolean?>>()
@@ -151,6 +158,17 @@ 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())
mutableLastDatabaseSchemeChangeInstantFlow.tryEmit(value)
}
override val lastDatabaseSchemeChangeInstantFlow: Flow<Instant?>
get() = mutableLastDatabaseSchemeChangeInstantFlow
.onSubscription { emit(lastDatabaseSchemeChangeInstant) }
override fun clearData(userId: String) {
storeVaultTimeoutInMinutes(userId = userId, vaultTimeoutInMinutes = null)
storeVaultTimeoutAction(userId = userId, vaultTimeoutAction = null)
@@ -167,6 +185,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(
@@ -405,6 +425,24 @@ class SettingsDiskSourceImpl(
getMutableShowUnlockSettingBadgeFlow(userId = userId)
.onSubscription { emit(getShowUnlockSettingBadge(userId)) }
override fun getShowImportLoginsSettingBadge(userId: String): Boolean? {
return getBoolean(
key = SHOW_IMPORT_LOGINS_SETTING_BADGE.appendIdentifier(userId),
)
}
override fun storeShowImportLoginsSettingBadge(userId: String, showBadge: Boolean?) {
putBoolean(
key = SHOW_IMPORT_LOGINS_SETTING_BADGE.appendIdentifier(userId),
showBadge,
)
getMutableShowImportLoginsSettingBadgeFlow(userId).tryEmit(showBadge)
}
override fun getShowImportLoginsSettingBadgeFlow(userId: String): Flow<Boolean?> =
getMutableShowImportLoginsSettingBadgeFlow(userId)
.onSubscription { emit(getShowImportLoginsSettingBadge(userId)) }
private fun getMutableLastSyncFlow(
userId: String,
): MutableSharedFlow<Instant?> =
@@ -448,4 +486,11 @@ class SettingsDiskSourceImpl(
mutableShowUnlockSettingBadgeFlowMap.getOrPut(userId) {
bufferedMutableSharedFlow(replay = 1)
}
private fun getMutableShowImportLoginsSettingBadgeFlow(
userId: String,
): MutableSharedFlow<Boolean?> =
mutableShowImportLoginsSettingBadgeFlowMap.getOrPut(userId) {
bufferedMutableSharedFlow(replay = 1)
}
}

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
@@ -68,7 +70,10 @@ object PlatformDiskModule {
@Provides
@Singleton
fun provideEventDatabase(app: Application): PlatformDatabase =
fun provideEventDatabase(
app: Application,
databaseSchemeManager: DatabaseSchemeManager,
): PlatformDatabase =
Room
.databaseBuilder(
context = app,
@@ -77,6 +82,7 @@ object PlatformDiskModule {
)
.fallbackToDestructiveMigration()
.addTypeConverter(ZonedDateTimeTypeConverter())
.addCallback(DatabaseSchemeCallback(databaseSchemeManager = databaseSchemeManager))
.build()
@Provides

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