Compare commits

..

226 Commits

Author SHA1 Message Date
aj-rosado
bee09de972 [PM-18936] Show key connector domain (#5034) 2025-04-17 19:20:50 +00:00
David Perez
33da0d8138 PM-20375: Events to use their own scope (#5072) 2025-04-17 17:52:20 +00:00
Patrick Honkonen
d5d8da2410 [PM-20373] Migrate Digital Asset Link API to network module (#5071) 2025-04-17 16:55:01 +00:00
Patrick Honkonen
3722a45359 [PM-20305] Migrate BaseUrlInterceptor and BaseUrlInterceptors to network module (#5068) 2025-04-17 16:28:05 +00:00
Patrick Honkonen
f23079b5ac [PM-20196] Migrate SyncService to the network module (#5056) 2025-04-17 15:59:33 +00:00
David Perez
524ddb6d0c Update Androidx libraries (#5070) 2025-04-17 14:38:38 +00:00
Patrick Honkonen
0d40d1e569 [PM-20195] Migrate SendsService to network module (#5055) 2025-04-17 13:54:30 +00:00
David Perez
4f65044179 PM-19593: Update expiration string to be 'Expires on <date>' (#5069) 2025-04-16 18:56:34 +00:00
David Perez
d67e74e48b PM-19620: Auto-delete flight recorder logs after 30 days (#5062) 2025-04-16 17:09:31 +00:00
Patrick Honkonen
b760b58669 Update user-agent header in Authenticator app (#5067) 2025-04-16 17:03:08 +00:00
Patrick Honkonen
36e6fbc14c [PM-20193] Migrate DownloadService to network module (#5053) 2025-04-16 16:46:30 +00:00
Patrick Honkonen
0be26c1eda [PM-20306] Migrate Auth Token Interceptor (#5065) 2025-04-16 16:41:43 +00:00
Patrick Honkonen
3311086dfc [PM-20309] Migrate Environment and EnvironmentUrlDataJson to data module (#5063) 2025-04-16 16:37:29 +00:00
David Perez
c912a3f12a PM-19622: Add ability to share flight recorder logs (#5060) 2025-04-16 15:43:14 +00:00
David Perez
e67790438e Update to latest Mockk (#5066) 2025-04-16 15:37:58 +00:00
Patrick Honkonen
ff72efe0ed [PM-20127] Only prompt passkey user verification once (#5026) 2025-04-16 15:35:25 +00:00
Patrick Honkonen
9dd71eaea2 [PM-20304] Migrate HeadersInterceptor to network module (#5061) 2025-04-16 15:09:00 +00:00
Patrick Honkonen
2d416eade5 [PM-20192] Migrate CiphersService to network module (#5052) 2025-04-16 15:08:17 +00:00
Patrick Honkonen
83de8b888d [PM-20191] Migrate OrganizationService to network module (#5049) 2025-04-15 19:42:21 +00:00
Patrick Honkonen
e35be360d7 [PM-20190] Migrate NewAuthRequestService to network module (#5048) 2025-04-15 18:20:25 +00:00
Patrick Honkonen
899689ba7b [PM-20189] Migrate IdentityService and related components to network module (#5047) 2025-04-15 16:35:47 +00:00
Patrick Honkonen
71237cb3a7 Make MobileErrorReporting flag remotely configured (#5015) 2025-04-15 16:07:49 +00:00
Álison Fernandes
18c7333cf3 Revert "[PM-19821] Consolidate scan.yml and scan-ci.yml" (#5058) 2025-04-15 14:42:01 +00:00
Patrick Honkonen
b6d9bee266 Update fastlane (#5025) 2025-04-15 14:23:57 +00:00
Patrick Honkonen
868f8091d2 [PM-20188] Migrate HaveIBeenPwnedService to network module (#5046) 2025-04-15 14:01:21 +00:00
Patrick Honkonen
dcd2d26d6c [PM-20187] Migrate DevicesService to the network module (#5040) 2025-04-14 21:28:34 +00:00
Patrick Honkonen
548c9ee092 [PM-20194] Migrate FolderService and related models to network module (#5054) 2025-04-14 20:55:50 +00:00
David Perez
186741e052 Use explicit timezone in tests (#5057) 2025-04-14 20:52:25 +00:00
André Bispo
d989c61bc2 [PM-19613] Save attachment error message update (#5013) 2025-04-14 20:50:59 +00:00
David Perez
8f311861e3 PM-19593: Display all flight recorder logs (#5050) 2025-04-14 20:00:20 +00:00
David Perez
942b95927f Limit network logs in production (#5051) 2025-04-14 18:29:56 +00:00
Patrick Honkonen
f087b6aac7 [PM-20186] Migrate AuthRequestsService to the network module (#5039) 2025-04-14 18:28:55 +00:00
Patrick Honkonen
87ef0e2961 [PM-20066] Migrate EventService and EventServiceImpl to network module (#5036) 2025-04-14 14:31:17 +00:00
David Perez
b60f30d8a1 PM-19592: Enabled and disable flight recorder (#5035) 2025-04-14 14:17:55 +00:00
github-actions[bot]
c553d2caec Update Google privileged browsers list (#5045)
Co-authored-by: GitHub Actions Bot <actions@github.com>
Co-authored-by: Patrick Honkonen <phonkonen@bitwarden.com>
2025-04-14 13:47:21 +00:00
Patrick Honkonen
d1ba730012 [PM-20185] Migrate AccountService to network module (#5038) 2025-04-11 21:33:05 +00:00
Patrick Honkonen
944eb64562 [PM-20067] Migrate PushService to the network module (#5037) 2025-04-11 21:09:14 +00:00
David Perez
12ce1e5229 PM-20133: Add initial logging logic (#5029) 2025-04-11 17:33:06 +00:00
David Perez
b72da1ba69 PM-20172: Display errors from WebAuthN results (#5032) 2025-04-11 15:53:40 +00:00
Patrick Honkonen
ed97695228 [PM-20073] Migrate SyncApi to the network module (#5028) 2025-04-11 14:18:09 +00:00
bw-ghapp[bot]
05b1266b1b Autosync Crowdin Translations (#5030)
Co-authored-by: bitwarden-devops-bot <106330231+bitwarden-devops-bot@users.noreply.github.com>
2025-04-11 13:08:49 +00:00
Patrick Honkonen
8153b25d89 [PM-20072] Migrate SendsApi to network module (#5027) 2025-04-11 13:05:11 +00:00
Patrick Honkonen
2fd5ab6f8d [PM-20071] Migrate FoldersApi to network module (#5023) 2025-04-10 22:30:25 +00:00
Patrick Honkonen
392695cb54 [PM-20069] Migrate CiphersApi to network module (#5021) 2025-04-10 21:09:21 +00:00
David Perez
d7f771990e PM-20123: Add flight recorder metadata (#5024) 2025-04-10 20:57:24 +00:00
Patrick Honkonen
dd0f08d759 [PM-19864] Migrate UnauthenticatedOrganizationApi to network module (#5019) 2025-04-10 20:42:26 +00:00
Patrick Honkonen
dd9ca853a7 [PM-20070] Migrate DownloadApi to network module (#5022) 2025-04-10 20:09:44 +00:00
Patrick Honkonen
2ef0ca3620 [PM-20068] Migrate AzureApi to network module (#5020) 2025-04-10 20:03:47 +00:00
Patrick Honkonen
97ef0ec004 [PM-19863] Migrate UnauthenticatedKeyConnectorApi to network module (#5018) 2025-04-10 19:16:04 +00:00
Patrick Honkonen
9d7df2bc10 [PM-19862] Migrate UnauthenticatedIdentityApi to the network module (#5017) 2025-04-10 15:19:46 +00:00
Patrick Honkonen
cb05787256 [PM-19861] Migrate UnauthenticatedDevicesApi to network module (#5016) 2025-04-10 15:15:50 +00:00
Patrick Honkonen
8767f2dd24 Update androidx.credentials (#4919) 2025-04-09 15:35:06 +00:00
David Perez
bf1ed690a4 PM-20005: Make the BitwardenOverflowActionItem more generic (#5011) 2025-04-09 15:04:53 +00:00
Patrick Honkonen
a12f325815 [PM-19958] Migrate Text to the UI module (#5010) 2025-04-08 20:33:45 +00:00
Auj625197595
c2872b6b9b [PM-18559] add kitobrowser series support (#4770)
Co-authored-by: Patrick Honkonen <1883101+SaintPatrck@users.noreply.github.com>
2025-04-08 19:53:12 +00:00
Patrick Honkonen
622d68e40c [PM-19952] Provide SharedPreference from the data module (#5006) 2025-04-08 19:46:16 +00:00
David Perez
6ebece1b1e PM-19978: Build out flight recorder UI (#5009) 2025-04-08 19:11:05 +00:00
Patrick Honkonen
c540d3ef47 [PM-19951] Move EncryptedPreferences to the correct di package (#5004) 2025-04-08 17:00:04 +00:00
Patrick Honkonen
385f5efac5 [PM-19959] Introduce a new UI module (#5008) 2025-04-08 16:12:12 +00:00
Patrick Honkonen
6668af58d2 [PM-19949] Provide ConfigDiskSource from data module (#5003) 2025-04-08 16:03:04 +00:00
Patrick Honkonen
c5e216783e [PM-19948] Migrate ServerConfigRepository to data module (#5002) 2025-04-07 21:58:39 +00:00
Álison Fernandes
acf222d151 [PM-19821] Consolidate scan.yml and scan-ci.yml (#4969)
Co-authored-by: Matt Andreko <mandreko@bitwarden.com>
2025-04-07 20:19:29 +00:00
Patrick Honkonen
1df96fdb62 [PM-19947] Provide system clock in the core module (#5000) 2025-04-07 20:03:53 +00:00
David Perez
942f6e2475 PM-19938: Add empty and loading state to the recorded logs screen (#5001) 2025-04-07 18:33:15 +00:00
Patrick Honkonen
2176b61cd3 [PM-19905] Migrate DispatcherManager to data module (#4999) 2025-04-07 18:16:04 +00:00
David Perez
4a63a709b8 PM-19937: Make navigation rail scrollable (#5005) 2025-04-07 18:15:23 +00:00
Patrick Honkonen
62cfcbbd72 Remove unused FeatureFlagRepository and FeatureFlagDiskSource (#4998) 2025-04-07 13:07:13 +00:00
Patrick Honkonen
538f1feb2e [PM-19872] Migrate UnencryptedPreferences to data module (#4994) 2025-04-04 20:09:20 +00:00
David Perez
70e42a27db Bump internal version name (#4997) 2025-04-04 19:45:45 +00:00
David Perez
4d9a19f43c PM-19645: Remove the new device UI email access flow (#4996) 2025-04-04 19:41:34 +00:00
Patrick Honkonen
dda8237ce5 [PM-19870] Migrate ServerConfig and ConfigDiskSource to the data module (#4992) 2025-04-04 18:33:54 +00:00
Patrick Honkonen
c4f54ee93c [PM-19860] Migrate UnauthenticateAuthRequestsApi and models to network module (#4990) 2025-04-04 17:33:55 +00:00
Patrick Honkonen
cac22346dd [PM-19859] Migrate UnauthenticatedAccountsApi and models to network module (#4989) 2025-04-04 16:45:20 +00:00
Patrick Honkonen
3f35ace6e9 [PM-19871] Migrate EncryptedPreferences to data module (#4993) 2025-04-04 14:38:50 +00:00
Patrick Honkonen
d1e4078c5a [PM-19851] Migrate network AuthenticatedAccountsApi to network module (#4982) 2025-04-04 14:17:37 +00:00
bw-ghapp[bot]
241a89fe13 Autosync Crowdin Translations (#4995)
Co-authored-by: bitwarden-devops-bot <106330231+bitwarden-devops-bot@users.noreply.github.com>
2025-04-04 13:55:25 +00:00
David Perez
4df1b245e8 PM-19873: Migrate the screen capture feature from user-scoped to app-scoped (#4972) 2025-04-03 22:02:53 +00:00
Patrick Honkonen
853069ee1c [PM-19866] Migrate BaseEncryptedDiskSource to data module (#4991) 2025-04-03 21:52:54 +00:00
Patrick Honkonen
1149e91dd5 [PM-19858] Migrate HaveIBeenPwnedApi to network module (#4988) 2025-04-03 21:34:39 +00:00
Patrick Honkonen
d5de173431 [PM-19857] Migrate AuthenticatedOrganizationApi and models to network module (#4987) 2025-04-03 21:34:35 +00:00
Patrick Honkonen
f6d5302a73 [PM-19856] Migrate KeyConnectorApi and models to network module (#4986) 2025-04-03 21:34:30 +00:00
Patrick Honkonen
0ea7d00e93 [PM-19855] Migrate AuthenticatedDevicesApi and models to network module (#4985) 2025-04-03 21:34:10 +00:00
Patrick Honkonen
ebdf5f816a [PM-19854] Migrate AuthenticatedAuthRequestsApi to network module (#4984) 2025-04-03 21:33:32 +00:00
Patrick Honkonen
85109a2e4b [PM-19849] Move PushApi and PushTokenRequest to network module (#4979) 2025-04-03 21:18:12 +00:00
Patrick Honkonen
5017e935d7 [PM-19845] Migrate BaseDiskSource to data module (#4978) 2025-04-03 19:34:52 +00:00
David Perez
ce0aa7adda Update Firbase BOM to the latest version (33.12.0) (#4981) 2025-04-03 18:44:10 +00:00
David Perez
111d141fac PM-19850: Update dialog scrim (#4980) 2025-04-03 18:32:43 +00:00
Patrick Honkonen
4676f4bf8c [PM-19841] Migrate Event API and models to network module (#4976) 2025-04-03 18:22:53 +00:00
Patrick Honkonen
321a764f20 [PM-19831] Migrate ConfigService to network module (#4971) 2025-04-03 18:15:51 +00:00
David Perez
5d4df86bc9 PM-19812: Navigation Rail design feedback (#4977) 2025-04-03 17:37:03 +00:00
Patrick Honkonen
291c568583 [PM-19840] Migrate BaseEnumeratedIntSerializer to core module (#4975) 2025-04-03 16:06:36 +00:00
Patrick Honkonen
7938c8c2bb [PM-19832] Create data module (#4973) 2025-04-03 14:42:12 +00:00
David Perez
1fecd4af5f PM-19591: Initial flight recorder UI (#4970) 2025-04-03 13:42:13 +00:00
David Perez
1e6f896328 PM-19830: Updating padding for last sync time label (#4974) 2025-04-03 13:41:38 +00:00
Patrick Honkonen
e67a143474 [PM-19813] Migrate BaseServiceTest to Network module and enable test fixtures (#4967) 2025-04-02 21:12:36 +00:00
David Perez
a6862bb791 PM-19812: Add navigation rail (#4966) 2025-04-02 21:04:29 +00:00
Patrick Honkonen
d70e658c8b [PM-19783] Migrate ConfigApi and ConfigResponseJson to network module (#4964) 2025-04-02 20:07:41 +00:00
Álison Fernandes
f43702cb83 Set SARIF upload branch to the merge commit ref/sha retrieved from GH CLI (#4958) 2025-04-02 19:32:59 +00:00
Patrick Honkonen
20bda929b3 [PM-19820] Replace ResultCallAdapterFactory in authenticator module (#4968) 2025-04-02 18:48:45 +00:00
Patrick Honkonen
ce139623d6 [PM-19793] Migrate ZonedDateTimeSerializer to core module (#4960) 2025-04-02 17:42:51 +00:00
David Perez
8e7de92609 PM-19807: Remove IconResource class (#4963) 2025-04-02 15:21:58 +00:00
Patrick Honkonen
b82b1ad570 Update CODEOWNERS (#4965) 2025-04-02 15:19:29 +00:00
Patrick Honkonen
56cce8ffdd [PM-19782] Migrate network error handling to network module (#4957) 2025-04-02 15:04:59 +00:00
Patrick Honkonen
7abae5e86a Update network module minSdk version (#4961) 2025-04-02 14:18:35 +00:00
Patrick Honkonen
862384db3a [PM-19738] Migrate NetworkResultCallAdapter to network module (#4949) 2025-04-02 13:40:43 +00:00
David Perez
01c4a3db03 PM-19705: Log reason for logout (#4959) 2025-04-02 13:00:17 +00:00
David Perez
e5ddeb44fd PM-19645: Remove new device feature flags (#4950) 2025-04-02 12:59:48 +00:00
David Perez
a476436bff Update the Hilt and Androidx dependencies (#4954) 2025-04-01 21:40:22 +00:00
Álison Fernandes
d3e14e8f52 [PM-19772] Remove scan-authenticator.yml (#4952) 2025-04-01 17:20:19 +00:00
Álison Fernandes
36fa907d87 Remove scan-authenticator.yml 2025-04-01 17:50:35 +01:00
Phil Cappelli
a4ee8017ae PM-19131 - Custom hidden fields not working properly if TOTP also configured (#4916) 2025-04-01 16:38:18 +00:00
Patrick Honkonen
aa35e2f93c [PM-19625] Migrate DataStateExtensions to core module (#4943) 2025-04-01 16:09:20 +00:00
Philip Cappelli
40179429bf Merge branch 'main' into phil/PM-19131-Custom-hidden-fields-not-working-properly-if-TOTP-also-configured 2025-04-01 11:59:15 -04:00
Philip Cappelli
bdf50fd1a6 PR comments 2025-04-01 11:59:03 -04:00
Patrick Honkonen
445d6ec3b8 [PM-19738] Create network module (#4948) 2025-04-01 14:32:54 +00:00
Patrick Honkonen
c304a4306f [PM-19627] Migrate JsonExtensions to core module (#4932) 2025-04-01 14:14:40 +00:00
Philip Cappelli
f82bda5bce lint fixes 2025-03-31 17:21:28 -04:00
Patrick Honkonen
a2f4e4f3b5 [PM-19738] Create network module
Create a library module responsible for communicating with the Bitwarden network API.

Additionally, the following changes were made to checks and workflows:
- Updated `build.gradle.kts` to include Kover for the `network` module.
- Updated `Fastfile` to include lint, test and kover report generation tasks for the `network` module.
2025-03-31 17:19:51 -04:00
Philip Cappelli
d4c079140d remove space 2025-03-31 17:09:30 -04:00
Philip Cappelli
f1c82eb027 Merge branch 'main' into phil/PM-19131-Custom-hidden-fields-not-working-properly-if-TOTP-also-configured 2025-03-31 17:08:35 -04:00
Philip Cappelli
932eced64e clean up 2025-03-31 17:07:15 -04:00
Patrick Honkonen
1eced037a4 [PM-19627] Move Json extension functions to core module
Move JsonExtensions and related tests to `core` module.

- Moved `decodeFromStringOrNull` and related testing from `app` and `authenticator` modules to `core` module.
- Updated all usages of `decodeFromStringOrNull` in `app` and `authenticator` to the new location in `core`.
- Deleted unused `JsonExtensionsTest` and `JsonExtensions.kt` in `app` and `authenticator`.
- Updated dependencies for core.
2025-03-31 17:06:06 -04:00
Philip Cappelli
a4fb50d3d8 revert previous changes and go back to hash solution 2025-03-31 17:05:19 -04:00
Patrick Honkonen
9a50399116 Migrate DataStateExtensions to core module
Move `DataStateExtensions` to the `core` module.

- Deleted `DataStateExtensions.kt` from `authenticator` and `app` modules.
- Updated all imports.
- Added `junit5` and `kotlinx.coroutines.test` test implementation to `core` module.
2025-03-31 16:55:09 -04:00
Patrick Honkonen
5d5bc25a45 [PM-19652] Consolidate check configurations (#4937) 2025-03-31 21:46:19 +01:00
Patrick Honkonen
af528fdd82 Add matching fallback for the beta build type to release (#4946) 2025-03-31 19:56:26 +00:00
David Perez
7d119fb552 Update to AGP 8.9.1 (#4947) 2025-03-31 19:56:18 +00:00
David Perez
81b43d13b0 PM-19404: Improve email validation and validation error parsing (#4945) 2025-03-31 19:36:01 +00:00
Patrick Honkonen
cd86413ff6 Fix detekt issues in authenticator tests (#4944) 2025-03-31 19:20:10 +00:00
Dave Severns
05094cf6e7 PM-19723 Group "no folder" items when there is a collection present. (#4941) 2025-03-31 19:19:25 +00:00
Patrick Honkonen
75af4868e2 [PM-19640] Migrate SpecialCharWithPrecedenceComparator to core module (#4942) 2025-03-31 19:19:03 +00:00
Patrick Honkonen
b7948948f0 [PM-19628] Migrate ResultExtensions to core module (#4934) 2025-03-31 17:38:45 +00:00
David Perez
efec5cb4ca PM-19653: Add tooltip and subtext tupport for the switch (#4936) 2025-03-31 15:15:58 +00:00
Patrick Honkonen
6369b20f18 Run detekt on authenticatorbridge module (#4940) 2025-03-31 14:22:18 +00:00
Patrick Honkonen
2e11c81f45 [PM-19626] Migrate SharedFlowExtensions to core module (#4931) 2025-03-31 12:52:18 +00:00
Philip Cappelli
3926fbca7f Revert "WIP"
This reverts commit f698d09b43.
2025-03-28 14:27:40 -04:00
Philip Cappelli
29838b19e0 Revert "WIP"
This reverts commit 66dae199c8.
2025-03-28 14:27:30 -04:00
Philip Cappelli
66dae199c8 WIP 2025-03-28 13:52:16 -04:00
Philip Cappelli
f698d09b43 WIP 2025-03-28 11:44:42 -04:00
bw-ghapp[bot]
9486f4c4e2 Autosync Crowdin Translations (#4935)
Co-authored-by: bitwarden-devops-bot <106330231+bitwarden-devops-bot@users.noreply.github.com>
2025-03-28 13:50:14 +00:00
David Perez
6664ccc53f PM-19466: Fix snackbar ime padding (#4933) 2025-03-28 13:40:54 +00:00
Patrick Honkonen
d62e3164dc [PM-19624] Migrate DataState to core module (#4930) 2025-03-28 13:29:39 +00:00
Philip Cappelli
b0729e8cd2 WIP 2025-03-27 17:32:59 -04:00
Philip Cappelli
c51f61c585 WIP 2025-03-27 17:32:53 -04:00
Patrick Honkonen
6340c2dd04 [PM-19616] Move OmitFromCoverage annotation to core module (#4928) 2025-03-27 21:11:53 +00:00
David Perez
42df9733c8 PM-19543: Add flight recorder feature flag (#4929) 2025-03-27 20:51:29 +00:00
Patrick Honkonen
cfa753cb12 [PM-19013] Create core module (#4836) 2025-03-27 19:53:29 +00:00
David Perez
71250a28fa Update the README dependencies section and clean up toml file (#4926) 2025-03-27 19:51:24 +00:00
David Perez
5279e6d18c PM-19547: Delay the delete account success dialog to avoid flicker (#4927) 2025-03-27 18:52:38 +00:00
aj-rosado
8dd5a9df9f [PM-8331] Using default send url when on US cloud (#4925) 2025-03-27 18:30:49 +00:00
Patrick Honkonen
346961856f Update passkey privileged app community list (#4923) 2025-03-27 13:53:24 +00:00
Dave Severns
4cae0823f2 PM-19559 Remove "Give feedback" from Settings > About (#4924) 2025-03-26 20:08:55 +00:00
Dave Severns
98b6f68821 PM-19550: Accept app language settings "System default" (#4922) 2025-03-26 19:37:19 +00:00
github-actions[bot]
a6d1f210c8 Update Google privileged browsers list (#4910)
Co-authored-by: GitHub Actions Bot <actions@github.com>
Co-authored-by: Patrick Honkonen <1883101+SaintPatrck@users.noreply.github.com>
2025-03-26 19:05:33 +00:00
Philip Cappelli
c0707ae08c Revert "PM-19131 - moved isVisible logic for hidden custom fields to be managed on view side vs model side"
This reverts commit bc76e46185.
2025-03-26 14:10:15 -04:00
David Perez
3e15b60178 PM-19498: Update cipher migration logic (#4920) 2025-03-26 17:29:28 +00:00
David Perez
6081b7e932 Replace landscape and portrait logic with sceen width logic (#4915) 2025-03-26 16:09:41 +00:00
Philip Cappelli
bc76e46185 PM-19131 - moved isVisible logic for hidden custom fields to be managed on view side vs model side 2025-03-25 17:21:29 -04:00
David Perez
b4b4f753ca PM-8953: Require 4 digits for pin entry (#4914) 2025-03-25 21:20:15 +00:00
David Perez
22376bfe4b PM-19403: Add better error messages for username generation (#4911) 2025-03-24 18:32:24 +00:00
David Perez
ab270cd243 PM-19466: Handle the IME padding internal to the BitwardenScaffold (#4912) 2025-03-24 17:13:35 +00:00
David Perez
8ed9b97805 Update protobuf lib to 4.30.1 (#4909) 2025-03-21 21:18:01 +00:00
David Perez
27e4c6a2b4 Update Compose BOM to 2025.03.00 (#4908) 2025-03-21 21:17:41 +00:00
David Perez
a6ed702a95 BWA-154: Fix privacy policy padding (#4907) 2025-03-21 20:25:06 +00:00
David Perez
0792f44b6b Update to Firebase 33.11.0 (#4906) 2025-03-21 18:36:54 +00:00
David Perez
94a91702cc PM-19399: Do not show 'Share error details' button when user enter incorrect password (#4905) 2025-03-21 16:29:38 +00:00
David Perez
21af60f4de Update to Junit 5.12.1 (#4903) 2025-03-21 16:11:12 +00:00
Matt Andreko
1add57d56c Fix SARIF upload branch ref/sha (#4899) 2025-03-21 13:13:00 +00:00
bw-ghapp[bot]
b0421c774b Autosync Crowdin Translations (#4902)
Co-authored-by: bitwarden-devops-bot <106330231+bitwarden-devops-bot@users.noreply.github.com>
2025-03-21 13:06:20 +00:00
David Perez
7d846ae383 PM-18862: Update IME handling for BitwardenScaffold and VaultUnlockedNavBarScreen (#4901) 2025-03-20 20:44:46 +00:00
David Perez
29371bdcb5 PM-19389: Handle encoding error when migrating biometric key (#4900) 2025-03-20 19:15:44 +00:00
Álison Fernandes
3eed1c1abe [PM-19049] Add workflow to regularly fetch updates to fido2_privileged_google.json (#4858)
Co-authored-by: Patrick Honkonen <1883101+SaintPatrck@users.noreply.github.com>
2025-03-19 21:13:40 +00:00
aj-rosado
ad6bc883b8 [PM-13257] Checking if user is navigating from vault before showing prompt for biometrics (#4846) 2025-03-19 10:23:50 +00:00
David Perez
f4f669683e PM-19334: Propagate errors to the UI (#4893) 2025-03-18 17:27:41 +00:00
Dave Severns
475a82e0fb PM-19335 add throwable val to generator error results (#4894) 2025-03-18 17:08:11 +00:00
Phil Cappelli
f22156389b BWA-119 - Unable to Access Manual Code Entry After Denying Camera Permissions on (#4891) 2025-03-18 15:50:51 +00:00
Phil Cappelli
4f09f5dae4 PM-18872 - When a Folder name is long, the View/Edit Item > Folder selection screen doesn't adjust well (#4892) 2025-03-18 15:50:39 +00:00
David Perez
3934bc9ae2 PM-19314: Propagate remaining auth errors to the UI (#4888) 2025-03-18 15:24:17 +00:00
David Perez
72c9149d27 PM-19295: Propagate password errors to the UI (#4884) 2025-03-18 14:05:36 +00:00
David Perez
a040a38ce8 PM-19296: Propagate login errors to the UI (#4885) 2025-03-18 14:05:07 +00:00
Dave Severns
ef3b7730d0 PM-19289 propagating remaining vault result errors. (#4881) 2025-03-18 13:51:30 +00:00
David Perez
ad8d8d271a PM-19294: Propagate the Register errors to the UI (#4883) 2025-03-17 19:54:43 +00:00
David Perez
4954e57007 Update gem dependencies (#4882) 2025-03-17 19:29:22 +00:00
Dave Severns
6f50fffd17 PM-19275 propagate the errors for the vault unlock error result types (#4878) 2025-03-17 19:24:13 +00:00
David Perez
44c5755301 PM-19284: Propagate SSO flow errors to the UI (#4880) 2025-03-17 19:14:17 +00:00
David Perez
b20eece3aa PM-19283: Propagate error from email token and known device flows (#4879) 2025-03-17 17:47:07 +00:00
renovate[bot]
869a3b00a5 [deps]: Lock file maintenance (#4875)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-03-17 15:47:03 +00:00
David Perez
7f8e848c46 PM-19245: Propagate error from password validation to UI (#4877) 2025-03-17 15:36:01 +00:00
Dave Severns
6c784d28eb PM-19272 propagate errors from cipher results (#4876) 2025-03-17 15:18:05 +00:00
Dave Severns
dfcdc72499 PM-19241 folder result errors propagated to UI (#4870) 2025-03-17 12:20:44 +00:00
Dave Severns
db287ddce5 PM-19243 send result errors propagated to UI (#4872) 2025-03-14 22:01:42 +00:00
David Perez
9ea85917b1 PM-19239: Propagate delete account errors to the UI (#4871) 2025-03-14 21:27:56 +00:00
Dave Severns
6db4165c4c PM-19234 propagates attachment result errors to UI (#4869) 2025-03-14 20:47:25 +00:00
David Perez
18ce45e7e5 PM-19233: Propagate auth request errors to the UI (#4868) 2025-03-14 18:33:39 +00:00
Dave Severns
6fe9eba620 PM-11356 Adjust autofocus delay to be greater than screen refresh delay. (#4866) 2025-03-14 16:48:34 +00:00
David Perez
b084987758 PM-19226: Propagate error from create auth request flow to UI (#4867) 2025-03-14 15:47:03 +00:00
bw-ghapp[bot]
5d0593026f Autosync Crowdin Translations (#4865)
Co-authored-by: bitwarden-devops-bot <106330231+bitwarden-devops-bot@users.noreply.github.com>
2025-03-14 14:39:07 +00:00
David Perez
47abeb7843 PM-14435: Improve accessibility service detection (#4864) 2025-03-14 14:16:25 +00:00
Phil Cappelli
e2779e4edb PM-18681 - Update Showing Coach Mark Tour Logic To Only Consider User's Personal Vault (#4863) 2025-03-13 20:46:14 +00:00
Álison Fernandes
90cc9f77c5 [PM-19207] Add Passkey / FIDO2 Bug Report template (#4859) 2025-03-13 20:31:30 +00:00
David Perez
40760f270a Use immutable map in debug menu (#4861) 2025-03-13 19:28:27 +00:00
David Perez
8a773141a4 Simplify RootNavScreenTest (#4860) 2025-03-13 19:20:14 +00:00
David Perez
0c149abdd9 PM-18844: Update BitwardenBasicDialog to allow it to share error logs (#4855) 2025-03-13 18:16:22 +00:00
David Perez
f540f86b19 PM-19199: hoist debug menu up to top level of the app (#4857) 2025-03-13 17:51:22 +00:00
Dave Severns
ca64ce2176 PM-18877 Respect system app specific language selection on Android 13 and up. (#4849) 2025-03-13 13:14:41 +00:00
André Bispo
da63c9e36b [PM-17995] Adjust custom fields section (#4835) 2025-03-13 11:33:39 +00:00
André Bispo
e16ad44d5e [PM-17242] While on autofill search on all item types. (#4824) 2025-03-13 11:33:12 +00:00
Phil Cappelli
d26a2ee52a PM-18681 - Update Showing Coach Mark Tour Logic To Account for Org Only Policy (#4854) 2025-03-12 20:20:44 +00:00
Dave Severns
1eb741ab58 PM-11356 prevent extra soft-keyboard showing. (#4845) 2025-03-11 20:10:46 +00:00
David Perez
e10ca9a6ec PM-19099: Centralize app metadata (#4847) 2025-03-11 19:47:07 +00:00
David Perez
3fca61ad3e Remove the language change dialog (#4658) 2025-03-11 14:33:22 +00:00
David Perez
409529b9ca Update AndroidX Activity to 1.10.1 (#4844) 2025-03-11 13:51:39 +00:00
David Perez
4568dd53d4 Update Firebase BOM to 33.10.0 (#4843) 2025-03-10 21:20:56 +00:00
David Perez
b9b90165bf PM-10725: Always show share sheet after creating send regardless of how it was made (#4841) 2025-03-10 20:43:49 +00:00
David Perez
778a630012 Update to AGP 8.9.0 (#4840) 2025-03-10 15:46:30 +00:00
Dave Severns
4809066ad7 PM-17087 update notification payloads to support camelCase JSON keys. (#4823) 2025-03-10 14:54:58 +00:00
Patrick Honkonen
d03c6c243d [PM-18873] Refactor ItemHeader.kt to improve location display (#4814) 2025-03-07 17:02:15 +00:00
bw-ghapp[bot]
d19ab498ff Autosync Crowdin Translations (#4832)
Co-authored-by: bitwarden-devops-bot <106330231+bitwarden-devops-bot@users.noreply.github.com>
2025-03-07 16:48:31 +00:00
Phil Cappelli
efc3d21fde PM-18681 - Update Showing Coach Mark Tour Logic To Only Consider User's Personal Vault (#4821) 2025-03-05 16:01:41 +00:00
Phil Cappelli
35e585a60e PM-18570 Update Owner Selection Field to Bottom Sheet Selector (#4810) 2025-03-04 22:24:31 +00:00
Patrick Honkonen
39787f9bf0 [deps] Update mockk to 1.13.17 (#4818) 2025-03-04 20:17:42 +00:00
Patrick Honkonen
3940997ef9 [deps] Update junit5 to 5.11.4 (#4819) 2025-03-04 20:17:23 +00:00
Patrick Honkonen
ce482e744d [deps] Update testng to 7.11.0 (#4820) 2025-03-04 19:36:18 +00:00
Patrick Honkonen
b0157d10e2 [deps] Update detekt to 1.23.8 (#4817) 2025-03-04 19:36:00 +00:00
renovate[bot]
cf3c2fb56d [deps]: migrate renovate config (#4815)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-03-04 17:56:09 +00:00
Dave Severns
a88a173e00 PM-18773 update the keyName for the ChromeAutofill flag (#4812) 2025-03-03 15:31:02 +00:00
1288 changed files with 20329 additions and 16294 deletions

2
.github/CODEOWNERS vendored
View File

@@ -5,7 +5,7 @@
# https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/about-code-owners
# Default file owners.
* @bitwarden/team-android @brian-livefront @david-livefront @dseverns-livefront @ahaisting-livefront @phil-livefront
* @bitwarden/team-android @brian-livefront @david-livefront
# Actions and workflow changes.
.github/ @bitwarden/dept-development-mobile

64
.github/ISSUE_TEMPLATE/bug-passkey.yml vendored Normal file
View File

@@ -0,0 +1,64 @@
name: Passkey Bug Report
description: File a Passkey / FIDO2 related bug report
labels: [ "app:password-manager", "bug-passkey" ]
body:
- type: markdown
attributes:
value: |
Thanks for taking the time to fill out this Passkey-related bug report!
Please provide as much detail as possible to help us investigate the issue.
- type: dropdown
id: origin
attributes:
label: Origin
description: Are you using a web browser or a native application?
options:
- Web (Browser)
- Native Application (non-browser app)
validations:
required: true
- type: input
id: rp-id
attributes:
label: Web URL or App name
description: The website domain or app name you were trying to use the Passkey with
placeholder: "e.g. example.com or ExampleApp"
validations:
required: true
- type: checkboxes
id: operation-type
attributes:
label: Passkey Action
description: What passkey related action(s) were you trying to perform?
options:
- label: Creating new passkey (Registration)
- label: Signing in (Authentication)
validations:
required: true
- type: textarea
id: build-info
attributes:
label: Build Information
description: Please retrieve the build information from the About screen by tapping the Version number field
validations:
required: true
- type: textarea
id: additional-info
attributes:
label: Additional Information
description: Any additional context, steps to reproduce, error messages, or relevant information about the issue
- type: checkboxes
id: issue-tracking-info
attributes:
label: Issue Tracking Info
description: |
Issue tracking information
options:
- label: I understand that work is tracked outside of Github. A PR will be linked to this issue should one be opened to address it, but Bitwarden doesn't use fields like "assigned", "milestone", or "project" to track progress.

51
.github/renovate.json vendored
View File

@@ -1,33 +1,56 @@
{
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
"extends": ["github>bitwarden/renovate-config"],
"enabledManagers": ["github-actions", "gradle", "bundler"],
"extends": [
"github>bitwarden/renovate-config"
],
"enabledManagers": [
"github-actions",
"gradle",
"bundler"
],
"packageRules": [
{
"groupName": "gh minor",
"matchManagers": ["github-actions"],
"matchUpdateTypes": ["minor", "patch"]
"matchManagers": [
"github-actions"
],
"matchUpdateTypes": [
"minor",
"patch"
]
},
{
"groupName": "gradle minor",
"matchUpdateTypes": ["minor", "patch"],
"matchManagers": ["gradle"]
"matchUpdateTypes": [
"minor",
"patch"
],
"matchManagers": [
"gradle"
]
},
{
"groupName": "kotlin",
"description": "Kotlin and Compose dependencies that must be updated together to maintain compatibility.",
"matchPackagePatterns": [
"androidx.compose:compose-bom",
"androidx.lifecycle:*",
"org.jetbrains.kotlin.*",
"com.google.devtools.ksp"
"matchManagers": [
"gradle"
],
"matchManagers": ["gradle"]
"matchPackageNames": [
"/androidx.compose:compose-bom/",
"/androidx.lifecycle:*/",
"/org.jetbrains.kotlin.*/",
"/com.google.devtools.ksp/"
]
},
{
"groupName": "bundler minor",
"matchUpdateTypes": ["minor", "patch"],
"matchManagers": ["bundler"]
"matchUpdateTypes": [
"minor",
"patch"
],
"matchManagers": [
"bundler"
]
}
]
}

View File

@@ -1,64 +0,0 @@
import sys
import subprocess
from typing import List
def create_linked_issue_comment(repo_owner: str, repo_name: str, release_name: str, release_link: str, pr_numbers: List[int]) -> str:
if len(pr_numbers) == 0:
return ""
pr_links = [f"* https://github.com/{repo_owner}/{repo_name}/pull/{pr_number}" for pr_number in pr_numbers]
return f":shipit: Pull Request(s) linked to this issue released in [{release_name}]({release_link}):\n\n"+ "\n".join(pr_links)
def comment_linked_issues_in_pr(owner: str, repo: str, pr_number: int) -> None:
"""Use GitHub CLI to comment all issues linked to a PR.
"""
linked_issues = get_linked_issues(owner, repo, pr_number)
for issue_number in linked_issues:
comment_github_issue(owner, repo, issue_number, comment)
def comment_github_issue(owner: str, repo: str, issue_number: int, comment: str) -> None:
"""Use GitHub CLI to comment on an issue.
"""
subprocess.run([
'gh', 'issue', 'comment', str(issue_number), '--body', comment, '--repo', f'{owner}/{repo}'
], check=True)
def get_linked_issues(owner: str, repo: str, pr_number: int) -> List[int]:
"""Use GitHub CLI to retrieve linked issue numbers for a PR.
"""
query = """
query ($owner: String!, $repo: String!, $pr: Int!) {
repository(owner: $owner, name: $repo) {
pullRequest(number: $pr) {
closingIssuesReferences(first: 100) {
nodes {
number
}
}
}
}
}
"""
try:
result = subprocess.run([
'gh', 'api', 'graphql',
'-F', f'owner={owner}',
'-F', f'repo={repo}',
'-F', f'pr={pr_number}',
'-f', f'query={query}',
'--jq', '.data.repository.pullRequest.closingIssuesReferences.nodes[].number'
], capture_output=True, text=True, check=True)
# Split output into lines and convert to integers
if result.stdout.strip():
return [int(num) for num in result.stdout.strip().split('\n')]
return []
except subprocess.CalledProcessError:
print(f"Error fetching linked issues for PR #{pr_number}")
return []

View File

@@ -1,112 +0,0 @@
import re
import sys
import subprocess
import json
from typing import List, Tuple
def extract_jira_tickets(line: str) -> List[str]:
# Find all Jira tickets in format ABC-123 (with any prefix/suffix)
return re.findall(r'[A-Z]+-\d+', line)
def extract_pr_numbers(line: str) -> List[str]:
# Match PR numbers from GitHub format (#123)
return re.findall(r'#(\d+)', line)
def process_line(line: str) -> str:
"""Process a single line from release notes by removing Jira tickets, conventional commit prefixes and other common patterns.
Args:
line: A single line from release notes
Returns:
Processed line with tickets and prefixes removed
Example:
>>> process_line("[ABC-123] feat(ui): Add new button")
"Add new button"
"""
original = line
# Remove Jira ticket patterns:
# line = re.sub(r'\[[A-Z]+-\d+\]', '', line) # [ABC-123] -> ""
# line = re.sub(r'[A-Z]+-\d+:\s', '', line) # ABC-123: -> ""
# line = re.sub(r'[A-Z]+-\d+\s-\s', '', line) # ABC-123 - -> ""
# Remove keywords and their variations
patterns = [
r'BACKPORT', # BACKPORT -> ""
r'[deps]:', # [deps]: -> ""
r'feat(?:\([^)]*\))?:', # feat: or feat(ui): -> ""
r'bug(?:\([^)]*\))?:', # bug: or bug(core): -> ""
r'ci(?:\([^)]*\))?:' # ci: or ci(workflow): -> ""
]
for pattern in patterns:
line = re.sub(pattern, '', line)
cleaned = line.strip()
if cleaned != original.strip():
print(f"Processed: {original.strip()} -> {cleaned}")
return cleaned
def process_file(input_file: str) -> Tuple[List[str], List[str], List[str]]:
jira_tickets: List[str] = []
pr_numbers: List[str] = []
processed_lines: List[str] = []
print("Processing file: ", input_file)
with open(input_file, 'r') as f:
for line in f:
line = line.strip()
should_process = line and not line.endswith(':')
if should_process:
tickets = extract_jira_tickets(line)
jira_tickets.extend(tickets)
prs = extract_pr_numbers(line)
pr_numbers.extend(prs)
processed_lines.append(process_line(line))
else:
processed_lines.append(line)
# Remove duplicates while preserving order
jira_tickets = list(dict.fromkeys(jira_tickets))
pr_numbers = list(dict.fromkeys(pr_numbers))
print("Jira tickets:", ",".join(jira_tickets))
print("PR numbers:", ",".join(pr_numbers))
print("Finished processing file: ", input_file)
return jira_tickets, pr_numbers, processed_lines
def save_results(jira_tickets: List[str], pr_numbers: List[str], processed_lines: List[str],
jira_file: str = 'jira_tickets.txt',
pr_file: str = 'pr_numbers.txt',
processed_file: str = 'processed_notes.txt') -> None:
with open(jira_file, 'w') as f:
f.write('\n'.join(jira_tickets))
with open(pr_file, 'w') as f:
f.write('\n'.join(pr_numbers))
with open(processed_file, 'w') as f:
f.write('\n'.join(processed_lines))
if __name__ == '__main__':
input_file = 'release_notes.txt'
jira_file = 'jira_tickets.txt'
pr_file = 'pr_numbers.txt'
processed_file = 'processed_notes.txt'
if len(sys.argv) >= 2:
input_file = sys.argv[1]
if len(sys.argv) >= 3:
jira_file = sys.argv[2]
if len(sys.argv) >= 4:
pr_file = sys.argv[3]
if len(sys.argv) >= 5:
processed_file = sys.argv[4]
jira_tickets, pr_numbers, processed_lines = process_file(input_file)
save_results(jira_tickets, pr_numbers, processed_lines, jira_file, pr_file, processed_file)

View File

@@ -1,4 +0,0 @@
[project]
name = "release-notes-processor"
description = "Process GitHub release notes to clean up formatting and extract relevant IDs."
requires-python = ">=3.13"

View File

@@ -1,30 +0,0 @@
import unittest
from linked_issues import get_linked_issues, create_linked_issue_comment
class TestLinkedIssues(unittest.TestCase):
def test_create_linked_issue_comment(self):
test_cases = [
("bitwarden", "android", "v2025.1.0", "https://github.com/bitwarden/android/releases/tag/v2025.1.0", [4696]),
("bitwarden", "android", "v2025.2.0", "https://github.com/bitwarden/android/releases/tag/v2025.2.0", [4809, 1, 2, 3]),
("bitwarden", "android", "v2025.3.0", "https://github.com/bitwarden/android/releases/tag/v2025.3.0", []),
]
for owner, repo, release_name, release_link, pr_numbers in test_cases:
with self.subTest(msg=f"Creating comment for issue in release {release_name}"):
comment = create_linked_issue_comment(owner, repo, release_name, release_link, pr_numbers)
print(comment + "\n")
def test_get_linked_issues(self):
test_cases = [
("bitwarden", "android", 4696, [4659]),
("bitwarden", "android", 4809, [])
]
for owner, repo, pr_id, expected_linked_issues in test_cases:
with self.subTest(msg=f"Testing PR #{pr_id} for {owner}/{repo}"):
result = get_linked_issues(owner, repo, pr_id)
self.assertEqual(sorted(result), sorted(expected_linked_issues))
if __name__ == '__main__':
unittest.main()

View File

@@ -1,95 +0,0 @@
import unittest
import tempfile
import os
from process_release_notes import extract_jira_tickets, extract_pr_numbers, process_line, process_file, get_linked_issues
class TestProcessReleaseNotes(unittest.TestCase):
def setUp(self):
self.test_file = tempfile.NamedTemporaryFile(delete=False)
def tearDown(self):
os.unlink(self.test_file.name)
def test_extract_jira_tickets(self):
test_cases = [
("[ABC-123] Some text", ["ABC-123"]),
("DEF-456: Some text", ["DEF-456"]),
("GHI-789 - Some text", ["GHI-789"]),
("Multiple [ABC-123] and DEF-456: tickets", ["ABC-123", "DEF-456"]),
("No tickets here", []),
("Mixed formats ABC-123 [DEF-456] GHI-789:", ["ABC-123", "DEF-456", "GHI-789"])
]
for input_text, expected in test_cases:
with self.subTest(input_text=input_text):
result = extract_jira_tickets(input_text)
self.assertEqual(result, expected)
def test_extract_pr_numbers(self):
test_cases = [
("PR #123 text", ["123"]),
("Multiple PRs #456 and #789", ["456", "789"]),
("No PR numbers", [])
]
for input_text, expected in test_cases:
with self.subTest(input_text=input_text):
result = extract_pr_numbers(input_text)
self.assertEqual(result, expected)
def test_process_line(self):
test_cases = [
("[ABC-123] BACKPORT Some text", "Some text"),
("DEF-456: feat(component): Some text", "Some text"),
("GHI-789 - bug(fix): Some text", "Some text"),
("ci: Some text", "Some text"),
("ci(workflow): Some text", "Some text"),
("feat: Direct feature", "Direct feature"),
("bug: Simple bugfix", "Simple bugfix"),
("Normal text", "Normal text")
]
for input_text, expected in test_cases:
with self.subTest(input_text=input_text):
result = process_line(input_text)
self.assertEqual(result, expected)
def test_process_file(self):
content = """
### Features:
[ABC-123] feat(comp): Feature 1 #123
DEF-456: bug(fix): Bug fix #456
GHI-789 - BACKPORT Some text #789
### Bug Fixes:
Another line without changes
"""
with open(self.test_file.name, 'w') as f:
f.write(content)
jira_tickets, pr_numbers, processed_lines = process_file(self.test_file.name)
self.assertEqual(jira_tickets, ["ABC-123", "DEF-456", "GHI-789"])
self.assertEqual(pr_numbers, ["123", "456", "789"])
self.assertEqual(processed_lines, [
'',
'### Features:',
'Feature 1 #123',
'Bug fix #456',
'Some text #789',
'',
'### Bug Fixes:',
'Another line without changes'
])
def test_get_linked_issues(self):
test_cases = [
("bitwarden", "android", 4696, [4659]),
("bitwarden", "android", 4809, [])
]
for owner, repo, pr_id, expected_linked_issues in test_cases:
with self.subTest(msg=f"Testing PR #{pr_id} for {owner}/{repo}"):
result = get_linked_issues(owner, repo, pr_id)
self.assertEqual(sorted(result), sorted(expected_linked_issues))
if __name__ == '__main__':
unittest.main()

View File

@@ -0,0 +1 @@
3.13

39
.github/scripts/validate-json/README.md vendored Normal file
View File

@@ -0,0 +1,39 @@
# JSON Validation Scripts
Utility scripts for validating JSON files and checking for duplicate package names between Google and Community privileged browser lists.
## Usage
### Validate a JSON file
```bash
python validate_json.py validate <json_file>
```
### Check for duplicates between two JSON files
```bash
python validate_json.py duplicates <json_file1> <json_file2> [output_file]
```
If `output_file` is not specified, duplicates will be saved to `duplicates.txt`.
## Running Tests
```bash
# Run all tests
python -m unittest test_validate_json.py
# Run the invalid JSON test individually
python -m unittest test_validate_json.TestValidateJson.test_validate_json_invalid
```
## Examples
```bash
# Validate Google privileged browsers list
python validate_json.py validate ../../app/src/main/assets/fido2_privileged_google.json
# Check for duplicates between Google and Community lists
python validate_json.py duplicates ../../app/src/main/assets/fido2_privileged_google.json ../../app/src/main/assets/fido2_privileged_community.json duplicates.txt
```

View File

@@ -0,0 +1,20 @@
{
"apps": [
"type": "android",
"info": {
"package_name": "com.android.chrome",
"signatures": [
{
"build": "release",
"cert_fingerprint_sha256": "F0:FD:6C:5B:41:0F:25:CB:25:C3:B5:33:46:C8:97:2F:AE:30:F8:EE:74:11:DF:91:04:80:AD:6B:2D:60:DB:83"
},
{
"build": "userdebug",
"cert_fingerprint_sha256": "19:75:B2:F1:71:77:BC:89:A5:DF:F3:1F:9E:64:A6:CA:E2:81:A5:3D:C1:D1:D5:9B:1D:14:7F:E1:C8:2A:FA:00"
}
]
}
}
]
}

View File

@@ -0,0 +1,48 @@
{
"apps": [
{
"type": "android",
"info": {
"package_name": "com.android.chrome",
"signatures": [
{
"build": "release",
"cert_fingerprint_sha256": "F0:FD:6C:5B:41:0F:25:CB:25:C3:B5:33:46:C8:97:2F:AE:30:F8:EE:74:11:DF:91:04:80:AD:6B:2D:60:DB:83"
},
{
"build": "userdebug",
"cert_fingerprint_sha256": "19:75:B2:F1:71:77:BC:89:A5:DF:F3:1F:9E:64:A6:CA:E2:81:A5:3D:C1:D1:D5:9B:1D:14:7F:E1:C8:2A:FA:00"
}
]
}
},
{
"type": "android",
"info": {
"package_name": "com.chrome.dev",
"signatures": [
{
"build": "release",
"cert_fingerprint_sha256": "90:44:EE:5F:EE:4B:BC:5E:21:DD:44:66:54:31:C4:EB:1F:1F:71:A3:27:16:A0:BC:92:7B:CB:B3:92:33:CA:BF"
},
{
"build": "release",
"cert_fingerprint_sha256": "3D:7A:12:23:01:9A:A3:9D:9E:A0:E3:43:6A:B7:C0:89:6B:FB:4F:B6:79:F4:DE:5F:E7:C2:3F:32:6C:8F:99:4A"
}
]
}
},
{
"type": "android",
"info": {
"package_name": "com.chrome.canary",
"signatures": [
{
"build": "release",
"cert_fingerprint_sha256": "DF:A1:FB:23:EF:BF:70:C5:BC:D1:44:3C:5B:EA:B0:4F:3F:2F:F4:36:6E:9A:C1:E3:45:76:39:A2:4C:FC"
}
]
}
}
]
}

View File

@@ -0,0 +1,20 @@
{
"apps": [
{
"type": "android",
"info": {
"package_name": "org.chromium.chrome",
"signatures": [
{
"build": "release",
"cert_fingerprint_sha256": "C6:AD:B8:B8:3C:6D:4C:17:D2:92:AF:DE:56:FD:48:8A:51:D3:16:FF:8F:2C:11:C5:41:02:23:BF:F8:A7:DB:B3"
},
{
"build": "userdebug",
"cert_fingerprint_sha256": "19:75:B2:F1:71:77:BC:89:A5:DF:F3:1F:9E:64:A6:CA:E2:81:A5:3D:C1:D1:D5:9B:1D:14:7F:E1:C8:2A:FA:00"
}
]
}
}
]
}

View File

@@ -0,0 +1,48 @@
#!/usr/bin/env python3
import unittest
import os
import json
from validate_json import validate_json, find_duplicates, get_package_names
from unittest.mock import patch
import io
class TestValidateJson(unittest.TestCase):
def setUp(self):
self.valid_file = os.path.join(os.path.dirname(__file__), "fixtures/sample-valid1.json")
self.valid_file2 = os.path.join(os.path.dirname(__file__), "fixtures/sample-valid2.json")
self.invalid_file = os.path.join(os.path.dirname(__file__), "fixtures/sample-invalid.json")
# Suppress stdout
self.stdout_patcher = patch('sys.stdout', new=io.StringIO())
self.stdout_patcher.start()
def tearDown(self):
self.stdout_patcher.stop()
def test_validate_json_valid(self):
"""Test validation of valid JSON file"""
self.assertTrue(validate_json(self.valid_file))
def test_validate_json_invalid(self):
"""Test validation of invalid JSON file"""
self.assertFalse(validate_json(self.invalid_file))
def test_find_duplicates(self):
"""Test when using the same file (should find duplicates)"""
expected_package_names = get_package_names(self.valid_file)
duplicates = find_duplicates(self.valid_file, self.valid_file)
self.assertEqual(len(duplicates), len(expected_package_names))
for package_name in expected_package_names:
self.assertIn(package_name, duplicates)
def test_find_duplicates_returns_empty_list_when_no_duplicates(self):
"""Test when using different files (should not find duplicates)"""
duplicates = find_duplicates(self.valid_file, self.valid_file2)
self.assertEqual(len(duplicates), 0)
if __name__ == "__main__":
unittest.main()

View File

@@ -0,0 +1,145 @@
#!/usr/bin/env python3
import json
import sys
import os
from typing import List, Dict, Any, Set
def get_package_names(file_path: str) -> Set[str]:
"""
Extracts package names from a JSON file.
Args:
file_path: Path to the JSON file
Returns:
Set of package names
"""
with open(file_path, 'r') as f:
data = json.load(f)
package_names = set()
for app in data["apps"]:
package_names.add(app["info"]["package_name"])
return package_names
def validate_json(file_path: str) -> bool:
"""
Validates if a JSON file is correctly formatted by attempting to deserialize it.
Args:
file_path: Path to the JSON file to validate
Returns:
True if valid, False otherwise
"""
try:
if not os.path.exists(file_path):
print(f"Error: File {file_path} does not exist")
return False
with open(file_path, 'r') as f:
json.load(f)
print(f"✅ JSON file {file_path} is valid")
return True
except json.JSONDecodeError as e:
print(f"❌ Invalid JSON in {file_path}: {str(e)}")
return False
except Exception as e:
print(f"❌ Error validating {file_path}: {str(e)}")
return False
def find_duplicates(file1_path: str, file2_path: str) -> List[str]:
"""
Checks for duplicate package_name entries between two JSON files.
Args:
file1_path: Path to the first JSON file
file2_path: Path to the second JSON file
Returns:
List of duplicate package names, empty list if none found
"""
try:
# Get package names from both files
packages1 = get_package_names(file1_path)
packages2 = get_package_names(file2_path)
# Find duplicates
duplicates = list(packages1.intersection(packages2))
if duplicates:
print(f"❌ Found {len(duplicates)} duplicate package names between {file1_path} and {file2_path}:")
for dup in duplicates:
print(f" - {dup}")
return duplicates
else:
print(f"✅ No duplicate package names found between {file1_path} and {file2_path}")
return []
except Exception as e:
print(f"❌ Error checking duplicates: {str(e)}")
return []
def save_duplicates_to_file(duplicates: List[str], output_file: str) -> None:
"""
Saves the list of duplicates to a file.
Args:
duplicates: List of duplicate package names
output_file: Path to save the list of duplicates
"""
try:
with open(output_file, 'w') as f:
for dup in duplicates:
f.write(f"{dup}\n")
print(f"Duplicates saved to {output_file}")
except Exception as e:
print(f"❌ Error saving duplicates to file: {str(e)}")
def main():
if len(sys.argv) < 2:
print("Usage:")
print(" Validate JSON: python validate_json.py validate <json_file>")
print(" Check duplicates: python validate_json.py duplicates <json_file1> <json_file2> [output_file]")
sys.exit(1)
command = sys.argv[1]
match command:
case "validate":
if len(sys.argv) < 3:
print("Error: Missing JSON file path")
sys.exit(1)
file_path = sys.argv[2]
success = validate_json(file_path)
sys.exit(0 if success else 1)
case "duplicates":
if len(sys.argv) < 4:
print("Error: Missing JSON file paths")
sys.exit(1)
file1_path = sys.argv[2]
file2_path = sys.argv[3]
output_file = sys.argv[4] if len(sys.argv) > 4 else "duplicates.txt"
duplicates = find_duplicates(file1_path, file2_path)
if duplicates:
save_duplicates_to_file(duplicates, output_file)
sys.exit(0)
case _:
print(f"Unknown command: {command}")
sys.exit(1)
if __name__ == "__main__":
main()

View File

@@ -78,7 +78,7 @@ jobs:
bundle install --jobs 4 --retry 3
- name: Check Authenticator
run: bundle exec fastlane checkAuthenticator
run: bundle exec fastlane check
- name: Build Authenticator
run: bundle exec fastlane buildAuthenticatorDebug

View File

@@ -0,0 +1,98 @@
name: Cron / Sync Google Privileged Browsers List
on:
schedule:
# Run weekly on Monday at 00:00 UTC
- cron: '0 0 * * 1'
workflow_dispatch:
env:
SOURCE_URL: https://www.gstatic.com/gpm-passkeys-privileged-apps/apps.json
GOOGLE_FILE: app/src/main/assets/fido2_privileged_google.json
COMMUNITY_FILE: app/src/main/assets/fido2_privileged_community.json
jobs:
sync-privileged-browsers:
name: Sync Google Privileged Browsers List
runs-on: ubuntu-24.04
permissions:
contents: write
pull-requests: write
steps:
- name: Checkout
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # 4.2.2
- name: Download Google Privileged Browsers List
run: curl -s $SOURCE_URL -o $GOOGLE_FILE
- name: Check for changes
id: check-changes
run: |
if git diff --quiet -- $GOOGLE_FILE; then
echo "👀 No changes detected, skipping..."
echo "has_changes=false" >> $GITHUB_OUTPUT
exit 0
fi
echo "has_changes=true" >> $GITHUB_OUTPUT
echo "👀 Changes detected, validating fido2_privileged_google.json..."
python .github/scripts/validate-json/validate_json.py validate $GOOGLE_FILE
if [ $? -ne 0 ]; then
echo "::error::JSON validation failed for $GOOGLE_FILE"
exit 1
fi
echo "👀 fido2_privileged_google.json is valid, checking for duplicates..."
# Check for duplicates between Google and Community files
python .github/scripts/validate-json/validate_json.py duplicates $GOOGLE_FILE $COMMUNITY_FILE duplicates.txt
if [ -f duplicates.txt ]; then
echo "::warning::Duplicate package names found between Google and Community files."
echo "duplicates_found=true" >> $GITHUB_OUTPUT
else
echo "✅ No duplicate package names found between Google and Community files"
echo "duplicates_found=false" >> $GITHUB_OUTPUT
fi
- name: Create branch and commit
if: steps.check-changes.outputs.has_changes == 'true'
run: |
echo "👀 Committing fido2_privileged_google.json..."
BRANCH_NAME="cron-sync-privileged-browsers/$GITHUB_RUN_NUMBER-sync"
git config user.name "GitHub Actions Bot"
git config user.email "actions@github.com"
git checkout -b $BRANCH_NAME
git add $GOOGLE_FILE
git commit -m "Update Google privileged browsers list"
git push origin $BRANCH_NAME
echo "BRANCH_NAME=$BRANCH_NAME" >> $GITHUB_ENV
echo "🌱 Branch created: $BRANCH_NAME"
- name: Create Pull Request
if: steps.check-changes.outputs.has_changes == 'true'
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
DUPLICATES_FOUND: ${{ steps.check-changes.outputs.duplicates_found }}
BASE_PR_URL: ${{ github.server_url }}/${{ github.repository }}/pull/
run: |
PR_BODY="Updates the Google privileged browsers list with the latest data from $SOURCE_URL"
if [ "$DUPLICATES_FOUND" = "true" ]; then
PR_BODY="$PR_BODY\n\n> [!WARNING]\n> :suspect: The following package(s) appear in both Google and Community files:"
while IFS= read -r line; do
PR_BODY="$PR_BODY\n> - $line"
done < duplicates.txt
fi
# Use echo -e to interpret escape sequences and pipe to gh pr create
PR_URL=$(echo -e "$PR_BODY" | gh pr create \
--title "Update Google privileged browsers list" \
--body-file - \
--base main \
--head $BRANCH_NAME \
--label "automated-pr" \
--label "t:ci")

View File

@@ -1,76 +0,0 @@
name: Scan Authenticator
on:
workflow_dispatch:
push:
branches:
- "main"
- "rc"
- "hotfix-rc"
pull_request_target:
types: [opened, synchronize]
jobs:
check-run:
name: Check PR run
uses: bitwarden/gh-actions/.github/workflows/check-run.yml@main
sast:
name: SAST scan
runs-on: ubuntu-24.04
needs: check-run
permissions:
contents: read
pull-requests: write
security-events: write
steps:
- name: Check out repo
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
ref: ${{ github.event.pull_request.head.sha }}
- name: Scan with Checkmarx
uses: checkmarx/ast-github-action@184bf2f64f55d1c93fd6636d539edf274703e434 # 2.0.41
env:
INCREMENTAL: "${{ contains(github.event_name, 'pull_request') && '--sast-incremental' || '' }}"
with:
project_name: ${{ github.repository }}
cx_tenant: ${{ secrets.CHECKMARX_TENANT }}
base_uri: https://ast.checkmarx.net/
cx_client_id: ${{ secrets.CHECKMARX_CLIENT_ID }}
cx_client_secret: ${{ secrets.CHECKMARX_SECRET }}
additional_params: |
--report-format sarif \
--filter "state=TO_VERIFY;PROPOSED_NOT_EXPLOITABLE;CONFIRMED;URGENT" \
--output-path . ${{ env.INCREMENTAL }}
- name: Upload Checkmarx results to GitHub
uses: github/codeql-action/upload-sarif@d68b2d4edb4189fd2a5366ac14e72027bd4b37dd # v3.28.2
with:
sarif_file: cx_result.sarif
quality:
name: Quality scan
runs-on: ubuntu-24.04
needs: check-run
permissions:
contents: read
pull-requests: write
steps:
- name: Check out repo
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
fetch-depth: 0
ref: ${{ github.event.pull_request.head.sha }}
- name: Scan with SonarCloud
uses: sonarsource/sonarqube-scan-action@bfd4e558cda28cda6b5defafb9232d191be8c203 # v4.2.1
env:
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
with:
args: >
-Dsonar.organization=${{ github.repository_owner }}
-Dsonar.projectKey=${{ github.repository_owner }}_${{ github.event.repository.name }}
-Dsonar.pullrequest.key=${{ github.event.pull_request.number }}

View File

@@ -58,4 +58,3 @@ jobs:
args: >
-Dsonar.organization=${{ github.repository_owner }}
-Dsonar.projectKey=${{ github.repository_owner }}_${{ github.event.repository.name }}
-Dsonar.pullrequest.key=${{ github.event.pull_request.number }}

View File

@@ -44,6 +44,8 @@ jobs:
uses: github/codeql-action/upload-sarif@b6a472f63d85b9c78a3ac5e89422239fc15e9b3c # v3.28.1
with:
sarif_file: cx_result.sarif
sha: ${{ contains(github.event_name, 'pull_request') && github.event.pull_request.head.sha || github.sha }}
ref: ${{ contains(github.event_name, 'pull_request') && format('refs/pull/{0}/head', github.event.pull_request.number) || github.ref }}
quality:
name: Quality scan

View File

@@ -1,82 +0,0 @@
name: Test Authenticator
on:
push:
branches:
- "main"
- "rc"
- "hotfix-rc"
pull_request_target:
types: [opened, synchronize]
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
JAVA_VERSION: 17
jobs:
check-run:
name: Check PR run
uses: bitwarden/gh-actions/.github/workflows/check-run.yml@main
test:
name: Test
runs-on: ubuntu-24.04
needs: check-run
permissions:
contents: read
packages: read
pull-requests: write
steps:
- name: Check out repo
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
ref: ${{ github.event.pull_request.head.sha }}
- name: Validate Gradle wrapper
uses: gradle/actions/wrapper-validation@0bdd871935719febd78681f197cd39af5b6e16a6 # v4.2.2
- name: Cache Gradle files
uses: actions/cache@1bd1e32a3bdc45362d1e726936510720a7c30a57 # v4.2.0
with:
path: |
~/.gradle/caches
~/.gradle/wrapper
key: ${{ runner.os }}-gradle-v2-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties', '**/libs.versions.toml') }}
restore-keys: |
${{ runner.os }}-gradle-v2-
- name: Cache build output
uses: actions/cache@1bd1e32a3bdc45362d1e726936510720a7c30a57 # v4.2.0
with:
path: |
${{ github.workspace }}/build-cache
key: ${{ runner.os }}-build-cache-${{ github.sha }}
restore-keys: |
${{ runner.os }}-build-
- name: Configure Ruby
uses: ruby/setup-ruby@28c4deda893d5a96a6b2d958c5b47fc18d65c9d3 # v1.213.0
with:
bundler-cache: true
- name: Configure JDK
uses: actions/setup-java@7a6d8a8234af8eb26422e24e3006232cccaa061b # v4.6.0
with:
distribution: "temurin"
java-version: ${{ env.JAVA_VERSION }}
- name: Install Fastlane
run: |
gem install bundler:2.2.27
bundle config path vendor/bundle
bundle install --jobs 4 --retry 3
- name: Build and test Authenticator
run: |
bundle exec fastlane checkAuthenticator
- name: Upload to codecov.io
uses: codecov/codecov-action@1e68e06f1dbfde0e4cefc87efeba9e4643565303 # v5.1.2
with:
files: authenticator/build/reports/kover/reportDebug.xml

View File

@@ -23,6 +23,7 @@ jobs:
runs-on: ubuntu-24.04
permissions:
packages: read
pull-requests: write
steps:
- name: Check out repo
@@ -79,25 +80,14 @@ jobs:
with:
name: test-reports
path: |
build/reports/kover/reportMergedCoverage.xml
app/build/reports/tests/
app/build/reports/kover/reportStandardDebug.xml
report:
name: Process Test Reports
needs: test
runs-on: ubuntu-24.04
permissions:
contents: read
issues: write
pull-requests: write
if: success()
steps:
- name: Download test artifacts
uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8
if: github.event_name == 'push' || github.event_name == 'pull_request'
with:
name: test-reports
authenticator/build/reports/tests/
authenticatorbridge/build/reports/tests/
core/build/reports/tests/
data/build/reports/tests/
network/build/reports/tests/
ui/build/reports/tests/
- name: Upload to codecov.io
id: upload-to-codecov
@@ -106,8 +96,9 @@ jobs:
continue-on-error: true
with:
os: linux
files: kover/reportStandardDebug.xml
files: build/reports/kover/reportMergedCoverage.xml
fail_ci_if_error: true
disable_search: true
- name: Comment PR if tests failed
if: steps.upload-to-codecov.outcome == 'failure' && (github.event_name == 'push' || github.event_name == 'pull_request')

9
.gitignore vendored
View File

@@ -29,8 +29,9 @@ user.properties
/app/src/standardRelease/google-services.json
/authenticator/src/google-services.json
# python
# Python
.python-version
__pycache__/
*.py[cod]
*$py.class
*.so
# Generated by .github/scripts/validate-json/validate-json.py
duplicates.txt

View File

@@ -9,17 +9,19 @@ GEM
public_suffix (>= 2.0.2, < 7.0)
artifactory (3.0.17)
atomos (0.1.3)
aws-eventstream (1.3.0)
aws-partitions (1.1040.0)
aws-sdk-core (3.216.0)
aws-eventstream (1.3.2)
aws-partitions (1.1084.0)
aws-sdk-core (3.222.1)
aws-eventstream (~> 1, >= 1.3.0)
aws-partitions (~> 1, >= 1.992.0)
aws-sigv4 (~> 1.9)
base64
jmespath (~> 1, >= 1.6.1)
aws-sdk-kms (1.97.0)
logger
aws-sdk-kms (1.99.0)
aws-sdk-core (~> 3, >= 3.216.0)
aws-sigv4 (~> 1.5)
aws-sdk-s3 (1.178.0)
aws-sdk-s3 (1.183.0)
aws-sdk-core (~> 3, >= 3.216.0)
aws-sdk-kms (~> 1)
aws-sigv4 (~> 1.5)
@@ -34,7 +36,7 @@ GEM
highline (~> 2.0.0)
date (3.4.1)
declarative (0.0.20)
digest-crc (0.6.5)
digest-crc (0.7.0)
rake (>= 12.0.0, < 14.0.0)
domain_name (0.6.20240107)
dotenv (2.8.1)
@@ -69,7 +71,7 @@ GEM
faraday_middleware (1.2.1)
faraday (~> 1.0)
fastimage (2.4.0)
fastlane (2.226.0)
fastlane (2.227.1)
CFPropertyList (>= 2.3, < 4.0.0)
addressable (>= 2.8, < 3.0.0)
artifactory (~> 3.0)
@@ -109,7 +111,7 @@ GEM
tty-spinner (>= 0.8.0, < 1.0.0)
word_wrap (~> 1.0.0)
xcodeproj (>= 1.13.0, < 2.0.0)
xcpretty (~> 0.4.0)
xcpretty (~> 0.4.1)
xcpretty-travis-formatter (>= 0.0.3, < 2.0.0)
fastlane-plugin-firebase_app_distribution (0.10.0)
google-apis-firebaseappdistribution_v1 (~> 0.3.0)
@@ -137,12 +139,12 @@ GEM
google-apis-core (>= 0.11.0, < 2.a)
google-apis-storage_v1 (0.31.0)
google-apis-core (>= 0.11.0, < 2.a)
google-cloud-core (1.7.1)
google-cloud-core (1.8.0)
google-cloud-env (>= 1.0, < 3.a)
google-cloud-errors (~> 1.0)
google-cloud-env (1.6.0)
faraday (>= 0.17.3, < 3.0)
google-cloud-errors (1.4.0)
google-cloud-errors (1.5.0)
google-cloud-storage (1.47.0)
addressable (~> 2.8)
digest-crc (~> 0.4)
@@ -160,15 +162,18 @@ GEM
highline (2.0.3)
http-cookie (1.0.8)
domain_name (~> 0.5)
httpclient (2.8.3)
httpclient (2.9.0)
mutex_m
jmespath (1.6.2)
json (2.9.1)
json (2.10.2)
jwt (2.10.1)
base64
logger (1.7.0)
mini_magick (4.13.2)
mini_mime (1.1.5)
multi_json (1.15.0)
multipart-post (2.4.1)
mutex_m (0.3.0)
nanaimo (0.4.0)
naturally (2.2.1)
nkf (0.2.0)
@@ -182,7 +187,7 @@ GEM
trailblazer-option (>= 0.1.1, < 0.2.0)
uber (< 0.2.0)
retriable (3.1.2)
rexml (3.4.0)
rexml (3.4.1)
rouge (3.28.0)
ruby2_keywords (0.0.5)
rubyzip (2.4.1)
@@ -216,7 +221,7 @@ GEM
colored2 (~> 3.1)
nanaimo (~> 0.4.0)
rexml (>= 3.3.6, < 4.0)
xcpretty (0.4.0)
xcpretty (0.4.1)
rouge (~> 3.28.0)
xcpretty-travis-formatter (1.0.1)
xcpretty (~> 0.2, >= 0.0.7)
@@ -233,4 +238,4 @@ RUBY VERSION
ruby 3.3.1p55
BUNDLED WITH
2.5.9
2.6.6

View File

@@ -58,6 +58,11 @@
The following is a list of all third-party dependencies included as part of the application beyond the standard Android SDK.
- **AndroidX Activity**
- https://developer.android.com/jetpack/androidx/releases/activity
- Purpose: Allows access composable APIs built on top of Activity.
- License: Apache 2.0
- **AndroidX Appcompat**
- https://developer.android.com/jetpack/androidx/releases/appcompat
- Purpose: Allows access to new APIs on older API versions.
@@ -78,7 +83,7 @@ The following is a list of all third-party dependencies included as part of the
- Purpose: Displays webpages with the user's default browser.
- License: Apache 2.0
- **AndroidX CameraX Camera2**
- **AndroidX Camera**
- https://developer.android.com/jetpack/androidx/releases/camera
- Purpose: Display and capture images for barcode scanning.
- License: Apache 2.0
@@ -88,9 +93,9 @@ The following is a list of all third-party dependencies included as part of the
- Purpose: A Kotlin-based declarative UI framework.
- License: Apache 2.0
- **AndroidX Core SplashScreen**
- **AndroidX Core**
- https://developer.android.com/jetpack/androidx/releases/core
- Purpose: Backwards compatible SplashScreen API implementation.
- Purpose: Backwards compatible platform features and APIs.
- License: Apache 2.0
- **AndroidX Credentials**
@@ -103,6 +108,11 @@ The following is a list of all third-party dependencies included as part of the
- Purpose: Lifecycle aware components and tooling.
- License: Apache 2.0
- **AndroidX Navigation**
- https://developer.android.com/jetpack/androidx/releases/navigation
- Purpose: Provides a consistent API for navigating between Android components.
- License: Apache 2.0
- **AndroidX Room**
- https://developer.android.com/jetpack/androidx/releases/room
- Purpose: A convenient SQLite-based persistence layer for Android.
@@ -123,21 +133,6 @@ The following is a list of all third-party dependencies included as part of the
- Purpose: Dependency injection framework.
- License: Apache 2.0
- **Firebase Cloud Messaging**
- https://github.com/firebase/firebase-android-sdk
- Purpose: Allows for push notification support. (**NOTE:** This dependency is not included in builds distributed via F-Droid.)
- License: Apache 2.0
- **Firebase Crashlytics**
- https://github.com/firebase/firebase-android-sdk
- Purpose: SDK for crash and non-fatal error reporting. (**NOTE:** This dependency is not included in builds distributed via F-Droid.)
- License: Apache 2.0
- **Google Play Reviews**
- https://developer.android.com/reference/com/google/android/play/core/release-notes
- Purpose: On standard builds provide an interface to add a review for the password manager application in Google Play.
- License: Apache 2.0
- **Glide**
- https://github.com/bumptech/glide
- Purpose: Image loading and caching.
@@ -158,11 +153,6 @@ The following is a list of all third-party dependencies included as part of the
- Purpose: JSON serialization library for Kotlin.
- License: Apache 2.0
- **kotlinx.serialization converter**
- https://github.com/square/retrofit/tree/trunk/retrofit-converters/kotlinx-serialization
- Purpose: Converter for Retrofit 2 and kotlinx.serialization.
- License: Apache 2.0
- **OkHttp 3**
- https://github.com/square/okhttp
- Purpose: An HTTP client used by the library to intercept and log traffic.
@@ -178,16 +168,28 @@ The following is a list of all third-party dependencies included as part of the
- Purpose: Extensible logging library for Android.
- License: Apache 2.0
- **zxcvbn4j**
- https://github.com/nulab/zxcvbn4j
- Purpose: Password strength estimation.
- License: MIT
- **ZXing**
- https://github.com/zxing/zxing
- Purpose: Barcode scanning and generation.
- License: Apache 2.0
The following is an additional list of third-party dependencies that are only included in the non-F-Droid build variants of the application.
- **Firebase Cloud Messaging**
- https://github.com/firebase/firebase-android-sdk
- Purpose: Allows for push notification support.
- License: Apache 2.0
- **Firebase Crashlytics**
- https://github.com/firebase/firebase-android-sdk
- Purpose: SDK for crash and non-fatal error reporting.
- License: Apache 2.0
- **Google Play Reviews**
- https://developer.android.com/reference/com/google/android/play/core/release-notes
- Purpose: On standard builds provide an interface to add a review for the password manager application in Google Play.
- License: Apache 2.0
### Development Environment Dependencies
The following is a list of additional third-party dependencies used as part of the local development environment. This includes test-related artifacts as well as tools related to code quality and linting. These are not present in the final packaged application.

View File

@@ -13,16 +13,13 @@ plugins {
// Crashlytics is enabled for all builds initially but removed for FDroid builds in gradle and
// standardDebug builds in the merged manifest.
alias(libs.plugins.crashlytics)
alias(libs.plugins.detekt)
alias(libs.plugins.hilt)
alias(libs.plugins.kotlin.android)
alias(libs.plugins.kotlin.compose.compiler)
alias(libs.plugins.kotlin.parcelize)
alias(libs.plugins.kotlin.serialization)
alias(libs.plugins.kotlinx.kover)
alias(libs.plugins.ksp)
alias(libs.plugins.google.services)
alias(libs.plugins.sonarqube)
}
/**
@@ -54,7 +51,7 @@ android {
minSdk = libs.versions.minSdk.get().toInt()
targetSdk = libs.versions.targetSdk.get().toInt()
versionCode = 1
versionName = "2024.9.0"
versionName = "2025.4.0"
setProperty("archivesBaseName", "com.x8bit.bitwarden")
@@ -68,7 +65,7 @@ android {
buildConfigField(
type = "String",
name = "CI_INFO",
value = "${ciProperties.getOrDefault("ci.info", "\"local\"")}"
value = "${ciProperties.getOrDefault("ci.info", "\"local\"")}",
)
}
@@ -102,9 +99,10 @@ android {
applicationIdSuffix = ".beta"
isDebuggable = false
isMinifyEnabled = true
matchingFallbacks += listOf("release")
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
"proguard-rules.pro",
)
buildConfigField(type = "boolean", name = "HAS_DEBUG_MENU", value = "false")
@@ -115,7 +113,7 @@ android {
isMinifyEnabled = true
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
"proguard-rules.pro",
)
buildConfigField(type = "boolean", name = "HAS_DEBUG_MENU", value = "false")
@@ -180,7 +178,6 @@ android {
excludes += "/META-INF/{AL2.0,LGPL2.1}"
}
}
@Suppress("UnstableApiUsage")
testOptions {
// Required for Robolectric
unitTests.isIncludeAndroidResources = true
@@ -216,6 +213,11 @@ dependencies {
implementation(files("libs/authenticatorbridge-1.0.0-release.aar"))
implementation(project(":core"))
implementation(project(":data"))
implementation(project(":network"))
implementation(project(":ui"))
implementation(libs.androidx.activity.compose)
implementation(libs.androidx.appcompat)
implementation(libs.androidx.autofill)
@@ -227,6 +229,7 @@ dependencies {
implementation(platform(libs.androidx.compose.bom))
implementation(libs.androidx.compose.animation)
implementation(libs.androidx.compose.material3)
implementation(libs.androidx.compose.material3.adaptive)
implementation(libs.androidx.compose.runtime)
implementation(libs.androidx.compose.ui)
implementation(libs.androidx.compose.ui.graphics)
@@ -251,7 +254,6 @@ dependencies {
implementation(libs.kotlinx.collections.immutable)
implementation(libs.kotlinx.coroutines.android)
implementation(libs.kotlinx.serialization)
implementation(libs.nulab.zxcvbn4j)
implementation(libs.square.okhttp)
implementation(libs.square.okhttp.logging)
implementation(platform(libs.square.retrofit.bom))
@@ -270,8 +272,14 @@ dependencies {
standardImplementation(libs.google.firebase.crashlytics)
standardImplementation(libs.google.play.review)
// Pull in test fixtures from other modules
testImplementation(testFixtures(project(":data")))
testImplementation(testFixtures(project(":network")))
testImplementation(libs.androidx.compose.ui.test)
testImplementation(libs.google.hilt.android.testing)
testImplementation(platform(libs.junit.bom))
testRuntimeOnly(libs.junit.platform.launcher)
testImplementation(libs.junit.junit5)
testImplementation(libs.junit.vintage)
testImplementation(libs.kotlinx.coroutines.test)
@@ -279,90 +287,9 @@ dependencies {
testImplementation(libs.robolectric.robolectric)
testImplementation(libs.square.okhttp.mockwebserver)
testImplementation(libs.square.turbine)
detektPlugins(libs.detekt.detekt.formatting)
detektPlugins(libs.detekt.detekt.rules)
}
detekt {
autoCorrect = true
config.from(files("$rootDir/detekt-config.yml"))
}
kover {
currentProject {
sources {
excludeJava = true
}
}
reports {
filters {
excludes {
androidGeneratedClasses()
annotatedBy(
// Compose previews
"androidx.compose.ui.tooling.preview.Preview",
"androidx.compose.ui.tooling.preview.PreviewScreenSizes",
// Manually excluded classes/files/etc.
"com.x8bit.bitwarden.data.platform.annotation.OmitFromCoverage",
)
classes(
// Navigation helpers
"*.*NavigationKt*",
// Composable singletons
"*.*ComposableSingletons*",
// Generated classes related to interfaces with default values
"*.*DefaultImpls*",
// Databases
"*.database.*Database*",
"*.dao.*Dao*",
// Dagger Hilt
"dagger.hilt.*",
"hilt_aggregated_deps.*",
"*_Factory",
"*_Factory\$*",
"*_*Factory",
"*_*Factory\$*",
"*.Hilt_*",
"*_HiltModules",
"*_HiltModules*",
"*_HiltModules\$*",
"*_Impl",
"*_Impl\$*",
"*_MembersInjector",
)
packages(
// Dependency injection
"*.di",
// Models
"*.model",
// Custom UI components
"com.x8bit.bitwarden.ui.platform.components",
// Theme-related code
"com.x8bit.bitwarden.ui.platform.theme",
)
}
}
}
}
tasks {
getByName("check") {
// Add detekt with type resolution to check
dependsOn("detekt")
}
getByName("sonar") {
dependsOn("check")
}
withType<io.gitlab.arturbosch.detekt.Detekt>().configureEach {
jvmTarget = libs.versions.jvmTarget.get()
}
withType<io.gitlab.arturbosch.detekt.DetektCreateBaselineTask>().configureEach {
jvmTarget = libs.versions.jvmTarget.get()
}
withType<Test> {
useJUnitPlatform()
maxHeapSize = "2g"
@@ -382,18 +309,6 @@ afterEvaluate {
.forEach { it.enabled = false }
}
sonar {
properties {
property("sonar.projectKey", "bitwarden_android")
property("sonar.organization", "bitwarden")
property("sonar.host.url", "https://sonarcloud.io")
property("sonar.sources", "app/src/")
property("sonar.tests", "app/src/")
property("sonar.test.inclusions", "app/src/test/")
property("sonar.exclusions", "app/src/test/")
}
}
private fun renameFile(path: String, newName: String) {
val originalFile = File(path)
if (!originalFile.exists()) {

View File

@@ -1,9 +1,9 @@
package com.x8bit.bitwarden.data.platform.manager
import com.bitwarden.data.repository.model.Environment
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
/**

View File

@@ -12,20 +12,6 @@
]
}
},
{
"type": "android",
"info": {
"package_name": "net.quetta.browser",
"signatures": [
{
"build": "release",
"cert_fingerprint_sha256": "BE:FE:E7:31:12:6A:A5:6E:7E:FD:AE:AF:5E:F3:FA:EA:44:1C:19:CC:E0:CA:EC:42:6B:65:BB:F8:2C:59:46:80"
}
]
}
},
{
"type": "android",
"info": {
@@ -62,18 +48,6 @@
]
}
},
{
"type": "android",
"info": {
"package_name": "org.mozilla.fenix",
"signatures": [
{
"build": "release",
"cert_fingerprint_sha256": "50:04:77:90:88:E7:F9:88:D5:BC:5C:C5:F8:79:8F:EB:F4:F8:CD:08:4A:1B:2A:46:EF:D4:C8:EE:4A:EA:F2:11"
}
]
}
},
{
"type": "android",
"info": {

View File

@@ -160,6 +160,78 @@
]
}
},
{
"type": "android",
"info": {
"package_name": "org.mozilla.fenix",
"signatures": [
{
"build": "release",
"cert_fingerprint_sha256": "50:04:77:90:88:E7:F9:88:D5:BC:5C:C5:F8:79:8F:EB:F4:F8:CD:08:4A:1B:2A:46:EF:D4:C8:EE:4A:EA:F2:11"
}
]
}
},
{
"type": "android",
"info": {
"package_name": "org.mozilla.fenix.debug",
"signatures": [
{
"build": "userdebug",
"cert_fingerprint_sha256": "BD:AE:82:02:80:D2:AF:B7:74:94:EF:22:58:AA:78:A9:AE:A1:36:41:7E:8B:C2:3D:C9:87:75:2E:6F:48:E8:48"
}
]
}
},
{
"type": "android",
"info": {
"package_name": "org.mozilla.focus.beta",
"signatures": [
{
"build": "release",
"cert_fingerprint_sha256": "62:03:A4:73:BE:36:D6:4E:E3:7F:87:FA:50:0E:DB:C7:9E:AB:93:06:10:AB:9B:9F:A4:CA:7D:5C:1F:1B:4F:FC"
}
]
}
},
{
"type": "android",
"info": {
"package_name": "org.mozilla.focus.nightly",
"signatures": [
{
"build": "release",
"cert_fingerprint_sha256": "62:03:A4:73:BE:36:D6:4E:E3:7F:87:FA:50:0E:DB:C7:9E:AB:93:06:10:AB:9B:9F:A4:CA:7D:5C:1F:1B:4F:FC"
}
]
}
},
{
"type": "android",
"info": {
"package_name": "org.mozilla.klar",
"signatures": [
{
"build": "release",
"cert_fingerprint_sha256": "62:03:A4:73:BE:36:D6:4E:E3:7F:87:FA:50:0E:DB:C7:9E:AB:93:06:10:AB:9B:9F:A4:CA:7D:5C:1F:1B:4F:FC"
}
]
}
},
{
"type": "android",
"info": {
"package_name": "org.mozilla.reference.browser",
"signatures": [
{
"build": "release",
"cert_fingerprint_sha256": "B0:09:90:E3:0F:9D:81:5D:2E:BC:7B:9B:B2:21:CE:47:E5:C9:D5:17:AA:C7:0E:7F:D5:95:B1:E5:3E:9A:4B:14"
}
]
}
},
{
"type": "android",
"info": {
@@ -571,6 +643,142 @@
}
]
}
},
{
"type": "android",
"info": {
"package_name": "io.island.Island",
"signatures": [
{
"build": "release",
"cert_fingerprint_sha256": "D9:C3:39:AC:9C:3A:EE:E1:75:1D:85:8C:35:D9:BA:C5:CC:87:B3:CE:76:30:93:F0:F5:10:64:F5:A2:F6:9B:04"
},
{
"build": "userdebug",
"cert_fingerprint_sha256": "6C:65:BD:B0:33:F5:CE:B1:74:09:EF:F9:99:48:D5:58:9F:55:63:9A:63:78:D5:A5:00:EB:95:FC:01:BC:6D:44"
}
]
}
},
{
"type": "android",
"info": {
"package_name": "io.island.IslandCanary",
"signatures": [
{
"build": "release",
"cert_fingerprint_sha256": "90:17:13:23:45:6E:6F:39:CB:FD:CF:B2:56:BE:1D:CF:F3:BC:1C:59:8A:15:93:30:E4:97:73:D0:4C:B9:C9:05"
},
{
"build": "userdebug",
"cert_fingerprint_sha256": "6C:65:BD:B0:33:F5:CE:B1:74:09:EF:F9:99:48:D5:58:9F:55:63:9A:63:78:D5:A5:00:EB:95:FC:01:BC:6D:44"
}
]
}
},
{
"type": "android",
"info": {
"package_name": "io.island.IslandBeta",
"signatures": [
{
"build": "release",
"cert_fingerprint_sha256": "35:31:83:1A:9E:2B:21:1D:E6:AA:C3:69:4B:45:83:6E:56:09:B9:D7:D0:04:C3:1B:21:87:40:FB:77:17:38:D1"
},
{
"build": "userdebug",
"cert_fingerprint_sha256": "6C:65:BD:B0:33:F5:CE:B1:74:09:EF:F9:99:48:D5:58:9F:55:63:9A:63:78:D5:A5:00:EB:95:FC:01:BC:6D:44"
}
]
}
},
{
"type": "android",
"info": {
"package_name": "io.island.IslandDev",
"signatures": [
{
"build": "userdebug",
"cert_fingerprint_sha256": "6C:65:BD:B0:33:F5:CE:B1:74:09:EF:F9:99:48:D5:58:9F:55:63:9A:63:78:D5:A5:00:EB:95:FC:01:BC:6D:44"
}
]
}
},
{
"type": "android",
"info": {
"package_name": "io.island.island.intune",
"signatures": [
{
"build": "release",
"cert_fingerprint_sha256": "C2:38:24:15:41:20:A0:8F:C3:95:42:AC:D8:2A:E9:24:94:78:80:1E:47:FD:6C:66:2B:18:1C:28:CA:7E:59:4E"
},
{
"build": "userdebug",
"cert_fingerprint_sha256": "6C:65:BD:B0:33:F5:CE:B1:74:09:EF:F9:99:48:D5:58:9F:55:63:9A:63:78:D5:A5:00:EB:95:FC:01:BC:6D:44"
}
]
}
},
{
"type": "android",
"info": {
"package_name": "io.island.island.canary.intune",
"signatures": [
{
"build": "release",
"cert_fingerprint_sha256": "1E:16:74:BB:79:EA:09:FB:37:CF:9F:1B:07:1B:1D:51:8D:46:03:0E:D3:EE:F2:C1:4E:AD:93:9E:C6:EE:3A:4C"
},
{
"build": "userdebug",
"cert_fingerprint_sha256": "6C:65:BD:B0:33:F5:CE:B1:74:09:EF:F9:99:48:D5:58:9F:55:63:9A:63:78:D5:A5:00:EB:95:FC:01:BC:6D:44"
}
]
}
},
{
"type": "android",
"info": {
"package_name": "io.island.island.beta.intune",
"signatures": [
{
"build": "release",
"cert_fingerprint_sha256": "D2:5E:AD:F6:1C:E6:36:6C:A4:23:A4:7F:C4:DB:9B:8C:9C:8A:35:B4:B0:19:E8:D9:82:FB:D0:8A:D9:DB:49:5A"
},
{
"build": "userdebug",
"cert_fingerprint_sha256": "6C:65:BD:B0:33:F5:CE:B1:74:09:EF:F9:99:48:D5:58:9F:55:63:9A:63:78:D5:A5:00:EB:95:FC:01:BC:6D:44"
}
]
}
},
{
"type": "android",
"info": {
"package_name": "io.island.island.dev.intune",
"signatures": [
{
"build": "userdebug",
"cert_fingerprint_sha256": "6C:65:BD:B0:33:F5:CE:B1:74:09:EF:F9:99:48:D5:58:9F:55:63:9A:63:78:D5:A5:00:EB:95:FC:01:BC:6D:44"
}
]
}
},
{
"type": "android",
"info": {
"package_name": "net.quetta.browser",
"signatures": [
{
"build": "release",
"cert_fingerprint_sha256": "BE:FE:E7:31:12:6A:A5:6E:7E:FD:AE:AF:5E:F3:FA:EA:44:1C:19:CC:E0:CA:EC:42:6B:65:BB:F8:2C:59:46:80"
},
{
"build": "userdebug",
"cert_fingerprint_sha256": "F1:38:00:4F:38:04:51:D4:8A:05:2B:B3:A3:EF:17:24:23:D4:B0:D0:C8:A3:AA:DD:FB:DB:66:30:31:48:EC:A4"
}
]
}
}
]
}
}

View File

@@ -2,7 +2,7 @@ package com.x8bit.bitwarden
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import com.x8bit.bitwarden.data.platform.annotation.OmitFromCoverage
import com.bitwarden.core.annotation.OmitFromCoverage
/**
* An activity to be launched and then immediately closed so that the OS Shade can be collapsed

View File

@@ -4,7 +4,7 @@ import android.content.Intent
import android.os.Bundle
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
import com.x8bit.bitwarden.data.platform.annotation.OmitFromCoverage
import com.bitwarden.core.annotation.OmitFromCoverage
import dagger.hilt.android.AndroidEntryPoint
/**

View File

@@ -4,8 +4,8 @@ import android.os.Bundle
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.lifecycleScope
import com.bitwarden.core.annotation.OmitFromCoverage
import com.x8bit.bitwarden.data.autofill.manager.AutofillCompletionManager
import com.x8bit.bitwarden.data.platform.annotation.OmitFromCoverage
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach

View File

@@ -5,10 +5,10 @@ import android.content.Intent
import android.os.Build
import androidx.annotation.Keep
import androidx.core.app.AppComponentFactory
import com.bitwarden.core.annotation.OmitFromCoverage
import com.x8bit.bitwarden.data.autofill.BitwardenAutofillService
import com.x8bit.bitwarden.data.autofill.accessibility.BitwardenAccessibilityService
import com.x8bit.bitwarden.data.autofill.fido2.BitwardenFido2ProviderService
import com.x8bit.bitwarden.data.platform.annotation.OmitFromCoverage
import com.x8bit.bitwarden.data.tiles.BitwardenAutofillTileService
import com.x8bit.bitwarden.data.tiles.BitwardenGeneratorTileService
import com.x8bit.bitwarden.data.tiles.BitwardenVaultTileService

View File

@@ -1,8 +1,8 @@
package com.x8bit.bitwarden
import android.app.Application
import com.bitwarden.core.annotation.OmitFromCoverage
import com.x8bit.bitwarden.data.auth.manager.AuthRequestNotificationManager
import com.x8bit.bitwarden.data.platform.annotation.OmitFromCoverage
import com.x8bit.bitwarden.data.platform.manager.LogsManager
import com.x8bit.bitwarden.data.platform.manager.event.OrganizationEventManager
import com.x8bit.bitwarden.data.platform.manager.network.NetworkConfigManager

View File

@@ -6,6 +6,11 @@ package com.x8bit.bitwarden
const val LEGACY_ACCESSIBILITY_SERVICE_NAME: String =
"com.x8bit.bitwarden.Accessibility.AccessibilityService"
/**
* The short form legacy name for the accessibility service.
*/
const val LEGACY_SHORT_ACCESSIBILITY_SERVICE_NAME: String = ".Accessibility.AccessibilityService"
/**
* The legacy name for the autofill service.
*/

View File

@@ -1,6 +1,7 @@
package com.x8bit.bitwarden
import android.content.Intent
import android.os.Build
import android.os.Bundle
import android.view.KeyEvent
import android.view.MotionEvent
@@ -15,25 +16,31 @@ import androidx.compose.runtime.remember
import androidx.core.os.LocaleListCompat
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.rememberNavController
import com.bitwarden.core.annotation.OmitFromCoverage
import com.x8bit.bitwarden.data.autofill.accessibility.manager.AccessibilityCompletionManager
import com.x8bit.bitwarden.data.autofill.manager.AutofillActivityManager
import com.x8bit.bitwarden.data.autofill.manager.AutofillCompletionManager
import com.x8bit.bitwarden.data.platform.annotation.OmitFromCoverage
import com.x8bit.bitwarden.data.platform.manager.util.ObserveScreenDataEffect
import com.x8bit.bitwarden.data.platform.repository.SettingsRepository
import com.x8bit.bitwarden.ui.platform.base.util.EventsEffect
import com.x8bit.bitwarden.ui.platform.composition.LocalManagerProvider
import com.x8bit.bitwarden.ui.platform.feature.debugmenu.debugMenuDestination
import com.x8bit.bitwarden.ui.platform.feature.debugmenu.manager.DebugMenuLaunchManager
import com.x8bit.bitwarden.ui.platform.feature.debugmenu.navigateToDebugMenuScreen
import com.x8bit.bitwarden.ui.platform.feature.rootnav.RootNavScreen
import com.x8bit.bitwarden.ui.platform.feature.rootnav.ROOT_ROUTE
import com.x8bit.bitwarden.ui.platform.feature.rootnav.rootNavDestination
import com.x8bit.bitwarden.ui.platform.feature.settings.appearance.model.AppLanguage
import com.x8bit.bitwarden.ui.platform.theme.BitwardenTheme
import com.x8bit.bitwarden.ui.platform.util.appLanguage
import dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject
/**
* Primary entry point for the application.
*/
@Suppress("TooManyFunctions")
@OmitFromCoverage
@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
@@ -69,13 +76,9 @@ class MainActivity : AppCompatActivity() {
)
}
// Within the app the language and theme will change dynamically and will be managed by the
// Within the app the theme will change dynamically and will be managed by the
// OS, but we need to ensure we properly set the values when upgrading from older versions
// that handle this differently or when the activity restarts.
settingsRepository.appLanguage.localeName?.let { localeName ->
val localeList = LocaleListCompat.forLanguageTags(localeName)
AppCompatDelegate.setApplicationLocales(localeList)
}
AppCompatDelegate.setDefaultNightMode(settingsRepository.appTheme.osValue)
setContent {
val state by mainViewModel.stateFlow.collectAsStateWithLifecycle()
@@ -111,7 +114,7 @@ class MainActivity : AppCompatActivity() {
}
}
updateScreenCapture(isScreenCaptureAllowed = state.isScreenCaptureAllowed)
LocalManagerProvider {
LocalManagerProvider(featureFlagsState = state.featureFlagsState) {
ObserveScreenDataEffect(
onDataUpdate = remember(mainViewModel) {
{
@@ -122,10 +125,19 @@ class MainActivity : AppCompatActivity() {
},
)
BitwardenTheme(theme = state.theme) {
RootNavScreen(
onSplashScreenRemoved = { shouldShowSplashScreen = false },
NavHost(
navController = navController,
)
startDestination = ROOT_ROUTE,
) {
// Nothing else should end up at this top level, we just want the ability
// to have the debug menu appear on top of the rest of the app without
// interacting with the state-based navigation used by the RootNavScreen.
rootNavDestination { shouldShowSplashScreen = false }
debugMenuDestination(
onNavigateBack = { navController.popBackStack() },
onSplashScreenRemoved = { shouldShowSplashScreen = false },
)
}
}
}
}
@@ -140,6 +152,33 @@ class MainActivity : AppCompatActivity() {
)
}
override fun onResume() {
super.onResume()
// When the app resumes check for any app specific language which may have been
// set via the device settings. Similar to the theme setting in onCreate this
// ensures we properly set the values when upgrading from older versions
// that handle this differently or when the activity restarts.
val appSpecificLanguage = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
val locales: LocaleListCompat = AppCompatDelegate.getApplicationLocales()
if (locales.isEmpty) {
// App is using the system language
null
} else {
// App has specific language settings
locales.get(0)?.appLanguage
}
} else {
// For older versions, use what ever language is available from the repository.
settingsRepository.appLanguage
}
mainViewModel.trySendAction(
action = MainAction.AppSpecificLanguageUpdate(
appLanguage = appSpecificLanguage ?: AppLanguage.DEFAULT,
),
)
}
override fun onStop() {
super.onStop()
// In some scenarios on an emulator the Activity can leak when recreated

View File

@@ -4,6 +4,8 @@ import android.content.Intent
import android.os.Parcelable
import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.viewModelScope
import com.bitwarden.ui.util.Text
import com.bitwarden.ui.util.asText
import com.bitwarden.vault.CipherView
import com.x8bit.bitwarden.data.auth.manager.AddTotpItemFromAuthenticatorManager
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
@@ -19,10 +21,12 @@ import com.x8bit.bitwarden.data.autofill.manager.AutofillSelectionManager
import com.x8bit.bitwarden.data.autofill.util.getAutofillSaveItemOrNull
import com.x8bit.bitwarden.data.autofill.util.getAutofillSelectionDataOrNull
import com.x8bit.bitwarden.data.platform.manager.AppResumeManager
import com.x8bit.bitwarden.data.platform.manager.FeatureFlagManager
import com.x8bit.bitwarden.data.platform.manager.SpecialCircumstanceManager
import com.x8bit.bitwarden.data.platform.manager.garbage.GarbageCollectionManager
import com.x8bit.bitwarden.data.platform.manager.model.AppResumeScreenData
import com.x8bit.bitwarden.data.platform.manager.model.CompleteRegistrationData
import com.x8bit.bitwarden.data.platform.manager.model.FlagKey
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
@@ -30,10 +34,10 @@ import com.x8bit.bitwarden.data.platform.util.isAddTotpLoginItemFromAuthenticato
import com.x8bit.bitwarden.data.vault.manager.model.VaultStateEvent
import com.x8bit.bitwarden.data.vault.repository.VaultRepository
import com.x8bit.bitwarden.ui.platform.base.BaseViewModel
import com.x8bit.bitwarden.ui.platform.base.util.Text
import com.x8bit.bitwarden.ui.platform.base.util.asText
import com.x8bit.bitwarden.ui.platform.feature.settings.appearance.model.AppLanguage
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.model.FeatureFlagsState
import com.x8bit.bitwarden.ui.platform.util.isAccountSecurityShortcut
import com.x8bit.bitwarden.ui.platform.util.isMyVaultShortcut
import com.x8bit.bitwarden.ui.platform.util.isPasswordGeneratorShortcut
@@ -54,6 +58,7 @@ import java.time.Clock
import javax.inject.Inject
private const val SPECIAL_CIRCUMSTANCE_KEY = "special-circumstance"
private const val ANIMATION_REFRESH_DELAY = 500L
/**
* A view model that helps launch actions for the [MainActivity].
@@ -63,12 +68,13 @@ private const val SPECIAL_CIRCUMSTANCE_KEY = "special-circumstance"
class MainViewModel @Inject constructor(
accessibilitySelectionManager: AccessibilitySelectionManager,
autofillSelectionManager: AutofillSelectionManager,
featureFlagManager: FeatureFlagManager,
private val addTotpItemFromAuthenticatorManager: AddTotpItemFromAuthenticatorManager,
private val specialCircumstanceManager: SpecialCircumstanceManager,
private val garbageCollectionManager: GarbageCollectionManager,
private val fido2CredentialManager: Fido2CredentialManager,
private val intentManager: IntentManager,
settingsRepository: SettingsRepository,
private val settingsRepository: SettingsRepository,
private val vaultRepository: VaultRepository,
private val authRepository: AuthRepository,
private val environmentRepository: EnvironmentRepository,
@@ -79,6 +85,9 @@ class MainViewModel @Inject constructor(
initialState = MainState(
theme = settingsRepository.appTheme,
isScreenCaptureAllowed = settingsRepository.isScreenCaptureAllowed,
isErrorReportingDialogEnabled = featureFlagManager.getFeatureFlag(
key = FlagKey.MobileErrorReporting,
),
),
) {
private var specialCircumstance: SpecialCircumstance?
@@ -96,6 +105,12 @@ class MainViewModel @Inject constructor(
.onEach { specialCircumstance = it }
.launchIn(viewModelScope)
featureFlagManager
.getFeatureFlagFlow(key = FlagKey.MobileErrorReporting)
.map { MainAction.Internal.OnMobileErrorReportingReceive(it) }
.onEach(::sendAction)
.launchIn(viewModelScope)
accessibilitySelectionManager
.accessibilitySelectionFlow
.map { MainAction.Internal.AccessibilitySelectionReceive(it) }
@@ -134,8 +149,7 @@ class MainViewModel @Inject constructor(
// Switching between account states often involves some kind of animation (ex:
// account switcher) that we might want to give time to finish before triggering
// a refresh.
@Suppress("MagicNumber")
delay(500)
delay(ANIMATION_REFRESH_DELAY)
trySendAction(MainAction.Internal.CurrentUserStateChange)
}
.launchIn(viewModelScope)
@@ -147,8 +161,7 @@ class MainViewModel @Inject constructor(
is VaultStateEvent.Locked -> {
// Similar to account switching, triggering this action too soon can
// interfere with animations or navigation logic, so we will delay slightly.
@Suppress("MagicNumber")
delay(500)
delay(ANIMATION_REFRESH_DELAY)
trySendAction(MainAction.Internal.VaultUnlockStateChange)
}
@@ -172,6 +185,17 @@ class MainViewModel @Inject constructor(
}
override fun handleAction(action: MainAction) {
when (action) {
is MainAction.ReceiveFirstIntent -> handleFirstIntentReceived(action)
is MainAction.ReceiveNewIntent -> handleNewIntentReceived(action)
MainAction.OpenDebugMenu -> handleOpenDebugMenu()
is MainAction.ResumeScreenDataReceived -> handleAppResumeDataUpdated(action)
is MainAction.AppSpecificLanguageUpdate -> handleAppSpecificLanguageUpdate(action)
is MainAction.Internal -> handleInternalAction(action)
}
}
private fun handleInternalAction(action: MainAction.Internal) {
when (action) {
is MainAction.Internal.AccessibilitySelectionReceive -> {
handleAccessibilitySelectionReceive(action)
@@ -185,13 +209,24 @@ class MainViewModel @Inject constructor(
is MainAction.Internal.ScreenCaptureUpdate -> handleScreenCaptureUpdate(action)
is MainAction.Internal.ThemeUpdate -> handleAppThemeUpdated(action)
is MainAction.Internal.VaultUnlockStateChange -> handleVaultUnlockStateChange()
is MainAction.ReceiveFirstIntent -> handleFirstIntentReceived(action)
is MainAction.ReceiveNewIntent -> handleNewIntentReceived(action)
MainAction.OpenDebugMenu -> handleOpenDebugMenu()
is MainAction.ResumeScreenDataReceived -> handleAppResumeDataUpdated(action)
is MainAction.Internal.OnMobileErrorReportingReceive -> {
handleOnMobileErrorReportingReceive(action)
}
}
}
private fun handleOnMobileErrorReportingReceive(
action: MainAction.Internal.OnMobileErrorReportingReceive,
) {
mutableStateFlow.update {
it.copy(isErrorReportingDialogEnabled = action.isErrorReportingEnabled)
}
}
private fun handleAppSpecificLanguageUpdate(action: MainAction.AppSpecificLanguageUpdate) {
settingsRepository.appLanguage = action.appLanguage
}
private fun handleAppResumeDataUpdated(action: MainAction.ResumeScreenDataReceived) {
when (val data = action.screenResumeData) {
null -> appResumeManager.clearResumeScreen()
@@ -274,10 +309,10 @@ class MainViewModel @Inject constructor(
val hasGeneratorShortcut = intent.isPasswordGeneratorShortcut
val hasVaultShortcut = intent.isMyVaultShortcut
val hasAccountSecurityShortcut = intent.isAccountSecurityShortcut
val fido2CreateCredentialRequestData = intent.getFido2CreateCredentialRequestOrNull()
val completeRegistrationData = intent.getCompleteRegistrationDataIntentOrNull()
val fido2CredentialAssertionRequest = intent.getFido2AssertionRequestOrNull()
val fido2CreateCredentialRequest = intent.getFido2CreateCredentialRequestOrNull()
val fido2GetCredentialsRequest = intent.getFido2GetCredentialsRequestOrNull()
val fido2AssertCredentialRequest = intent.getFido2AssertionRequestOrNull()
when {
passwordlessRequestData != null -> {
authRepository.activeUserId?.let {
@@ -335,34 +370,36 @@ class MainViewModel @Inject constructor(
)
}
fido2CreateCredentialRequestData != null -> {
fido2CreateCredentialRequest != null -> {
// Set the user's verification status when a new FIDO 2 request is received to force
// explicit verification if the user's vault is unlocked when the request is
// received.
fido2CreateCredentialRequestData.isUserVerified
?.let { isVerified -> fido2CredentialManager.isUserVerified = isVerified }
fido2CredentialManager.isUserVerified =
fido2CreateCredentialRequest.isUserPreVerified
specialCircumstanceManager.specialCircumstance =
SpecialCircumstance.Fido2Save(
fido2CreateCredentialRequest = fido2CreateCredentialRequestData,
fido2CreateCredentialRequest = fido2CreateCredentialRequest,
)
// Switch accounts if the selected user is not the active user.
if (authRepository.activeUserId != null &&
authRepository.activeUserId != fido2CreateCredentialRequestData.userId
authRepository.activeUserId != fido2CreateCredentialRequest.userId
) {
authRepository.switchAccount(fido2CreateCredentialRequestData.userId)
authRepository.switchAccount(fido2CreateCredentialRequest.userId)
}
}
fido2CredentialAssertionRequest != null -> {
// If device biometric verification was performed as part of single-tap
// authentication, set the user's verification state to the device result.
// Otherwise, retain the verification state as-is.
fido2CredentialAssertionRequest.isUserVerified
?.let { isVerified -> fido2CredentialManager.isUserVerified = isVerified }
fido2AssertCredentialRequest != null -> {
// Set the user's verification status when a new FIDO 2 request is received to force
// explicit verification if the user's vault is unlocked when the request is
// received.
fido2CredentialManager.isUserVerified =
fido2AssertCredentialRequest.isUserPreVerified
specialCircumstanceManager.specialCircumstance =
SpecialCircumstance.Fido2Assertion(
fido2AssertionRequest = fido2CredentialAssertionRequest,
fido2AssertionRequest = fido2AssertCredentialRequest,
)
}
@@ -445,7 +482,16 @@ class MainViewModel @Inject constructor(
data class MainState(
val theme: AppTheme,
val isScreenCaptureAllowed: Boolean,
) : Parcelable
private val isErrorReportingDialogEnabled: Boolean,
) : Parcelable {
/**
* Contains all feature flags that are available to the UI.
*/
val featureFlagsState: FeatureFlagsState
get() = FeatureFlagsState(
isErrorReportingDialogEnabled = isErrorReportingDialogEnabled,
)
}
/**
* Models actions for the [MainActivity].
@@ -471,6 +517,12 @@ sealed class MainAction {
*/
data class ResumeScreenDataReceived(val screenResumeData: AppResumeScreenData?) : MainAction()
/**
* Receive if there is an app specific locale selection made by user
* in the device's settings.
*/
data class AppSpecificLanguageUpdate(val appLanguage: AppLanguage) : MainAction()
/**
* Actions for internal use by the ViewModel.
*/
@@ -483,6 +535,13 @@ sealed class MainAction {
val cipherView: CipherView,
) : Internal()
/**
* Indicates the Mobile Error Reporting feature flag has been updated.
*/
data class OnMobileErrorReportingReceive(
val isErrorReportingEnabled: Boolean,
) : Internal()
/**
* Indicates the user has manually selected the given [cipherView] for autofill.
*/

View File

@@ -1,11 +1,10 @@
package com.x8bit.bitwarden.data.auth.datasource.disk
import com.bitwarden.network.model.SyncResponseJson
import com.x8bit.bitwarden.data.auth.datasource.disk.model.AccountTokensJson
import com.x8bit.bitwarden.data.auth.datasource.disk.model.NewDeviceNoticeState
import com.x8bit.bitwarden.data.auth.datasource.disk.model.OnboardingStatus
import com.x8bit.bitwarden.data.auth.datasource.disk.model.PendingAuthRequestJson
import com.x8bit.bitwarden.data.auth.datasource.disk.model.UserStateJson
import com.x8bit.bitwarden.data.vault.datasource.network.model.SyncResponseJson
import kotlinx.coroutines.flow.Flow
import java.time.Instant
@@ -344,16 +343,6 @@ interface AuthDiskSource {
*/
fun getShowImportLoginsFlow(userId: String): Flow<Boolean?>
/**
* Gets the new device notice state for the given [userId].
*/
fun getNewDeviceNoticeState(userId: String): NewDeviceNoticeState
/**
* Stores the new device notice state for the given [userId].
*/
fun storeNewDeviceNoticeState(userId: String, newState: NewDeviceNoticeState?)
/**
* Gets the last lock timestamp for the given [userId].
*/

View File

@@ -1,17 +1,15 @@
package com.x8bit.bitwarden.data.auth.datasource.disk
import android.content.SharedPreferences
import com.bitwarden.core.data.repository.util.bufferedMutableSharedFlow
import com.bitwarden.core.data.util.decodeFromStringOrNull
import com.bitwarden.data.datasource.disk.BaseEncryptedDiskSource
import com.bitwarden.network.model.SyncResponseJson
import com.x8bit.bitwarden.data.auth.datasource.disk.model.AccountTokensJson
import com.x8bit.bitwarden.data.auth.datasource.disk.model.NewDeviceNoticeDisplayStatus
import com.x8bit.bitwarden.data.auth.datasource.disk.model.NewDeviceNoticeState
import com.x8bit.bitwarden.data.auth.datasource.disk.model.OnboardingStatus
import com.x8bit.bitwarden.data.auth.datasource.disk.model.PendingAuthRequestJson
import com.x8bit.bitwarden.data.auth.datasource.disk.model.UserStateJson
import com.x8bit.bitwarden.data.platform.datasource.disk.BaseEncryptedDiskSource
import com.x8bit.bitwarden.data.platform.datasource.disk.legacy.LegacySecureStorageMigrator
import com.x8bit.bitwarden.data.platform.repository.util.bufferedMutableSharedFlow
import com.x8bit.bitwarden.data.platform.util.decodeFromStringOrNull
import com.x8bit.bitwarden.data.vault.datasource.network.model.SyncResponseJson
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.onSubscription
@@ -49,7 +47,6 @@ 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"
private const val NEW_DEVICE_NOTICE_STATE = "newDeviceNoticeState"
private const val LAST_LOCK_TIMESTAMP = "lastLockTimestamp"
/**
@@ -489,22 +486,6 @@ class AuthDiskSourceImpl(
getMutableShowImportLoginsFlow(userId)
.onSubscription { emit(getShowImportLogins(userId)) }
override fun getNewDeviceNoticeState(userId: String): NewDeviceNoticeState {
return getString(key = NEW_DEVICE_NOTICE_STATE.appendIdentifier(userId))?.let {
json.decodeFromStringOrNull(it)
} ?: NewDeviceNoticeState(
displayStatus = NewDeviceNoticeDisplayStatus.HAS_NOT_SEEN,
lastSeenDate = null,
)
}
override fun storeNewDeviceNoticeState(userId: String, newState: NewDeviceNoticeState?) {
putString(
key = NEW_DEVICE_NOTICE_STATE.appendIdentifier(userId),
value = newState?.let { json.encodeToString(it) },
)
}
override fun getLastLockTimestamp(userId: String): Instant? {
return getLong(key = LAST_LOCK_TIMESTAMP.appendIdentifier(userId))?.let {
Instant.ofEpochMilli(it)

View File

@@ -1,10 +1,10 @@
package com.x8bit.bitwarden.data.auth.datasource.disk.di
import android.content.SharedPreferences
import com.bitwarden.data.datasource.disk.di.EncryptedPreferences
import com.bitwarden.data.datasource.disk.di.UnencryptedPreferences
import com.x8bit.bitwarden.data.auth.datasource.disk.AuthDiskSource
import com.x8bit.bitwarden.data.auth.datasource.disk.AuthDiskSourceImpl
import com.x8bit.bitwarden.data.platform.datasource.di.EncryptedPreferences
import com.x8bit.bitwarden.data.platform.datasource.di.UnencryptedPreferences
import com.x8bit.bitwarden.data.platform.datasource.disk.legacy.LegacySecureStorageMigrator
import dagger.Module
import dagger.Provides

View File

@@ -1,7 +1,8 @@
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 com.bitwarden.data.datasource.disk.model.EnvironmentUrlDataJson
import com.bitwarden.network.model.KdfTypeJson
import com.bitwarden.network.model.UserDecryptionOptionsJson
import kotlinx.serialization.Contextual
import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.SerialName

View File

@@ -1,60 +0,0 @@
package com.x8bit.bitwarden.data.auth.datasource.disk.model
import kotlinx.serialization.Contextual
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import java.time.ZonedDateTime
/**
* Describes the current display status of the new device notice screen.
*/
@Serializable
enum class NewDeviceNoticeDisplayStatus {
/**
* The user has seen the screen and indicated they can access their email.
*/
@SerialName("canAccessEmail")
CAN_ACCESS_EMAIL,
/**
* The user has indicated they can access their email
* as specified by the Permanent mode of the notice.
*/
@SerialName("canAccessEmailPermanent")
CAN_ACCESS_EMAIL_PERMANENT,
/**
* The user has not seen the screen.
*/
@SerialName("hasNotSeen")
HAS_NOT_SEEN,
/**
* The user has seen the screen and selected "remind me later".
*/
@SerialName("hasSeen")
HAS_SEEN,
}
/**
* The state of the new device notice screen.
*/
@Suppress("MagicNumber")
@Serializable
data class NewDeviceNoticeState(
@SerialName("displayStatus")
val displayStatus: NewDeviceNoticeDisplayStatus,
@SerialName("lastSeenDate")
@Contextual
val lastSeenDate: ZonedDateTime?,
) {
/**
* Whether the [lastSeenDate] is at least 7 days old.
*/
val shouldDisplayNoticeIfSeen = lastSeenDate
?.isBefore(
ZonedDateTime.now().minusDays(7),
)
?: false
}

View File

@@ -1,19 +1,19 @@
package com.x8bit.bitwarden.data.auth.datasource.network.di
import com.x8bit.bitwarden.data.auth.datasource.network.service.AccountsService
import com.x8bit.bitwarden.data.auth.datasource.network.service.AccountsServiceImpl
import com.x8bit.bitwarden.data.auth.datasource.network.service.AuthRequestsService
import com.x8bit.bitwarden.data.auth.datasource.network.service.AuthRequestsServiceImpl
import com.x8bit.bitwarden.data.auth.datasource.network.service.DevicesService
import com.x8bit.bitwarden.data.auth.datasource.network.service.DevicesServiceImpl
import com.x8bit.bitwarden.data.auth.datasource.network.service.HaveIBeenPwnedService
import com.x8bit.bitwarden.data.auth.datasource.network.service.HaveIBeenPwnedServiceImpl
import com.x8bit.bitwarden.data.auth.datasource.network.service.IdentityService
import com.x8bit.bitwarden.data.auth.datasource.network.service.IdentityServiceImpl
import com.x8bit.bitwarden.data.auth.datasource.network.service.NewAuthRequestService
import com.x8bit.bitwarden.data.auth.datasource.network.service.NewAuthRequestServiceImpl
import com.x8bit.bitwarden.data.auth.datasource.network.service.OrganizationService
import com.x8bit.bitwarden.data.auth.datasource.network.service.OrganizationServiceImpl
import com.bitwarden.network.service.AccountsService
import com.bitwarden.network.service.AccountsServiceImpl
import com.bitwarden.network.service.AuthRequestsService
import com.bitwarden.network.service.AuthRequestsServiceImpl
import com.bitwarden.network.service.DevicesService
import com.bitwarden.network.service.DevicesServiceImpl
import com.bitwarden.network.service.HaveIBeenPwnedService
import com.bitwarden.network.service.HaveIBeenPwnedServiceImpl
import com.bitwarden.network.service.IdentityService
import com.bitwarden.network.service.IdentityServiceImpl
import com.bitwarden.network.service.NewAuthRequestService
import com.bitwarden.network.service.NewAuthRequestServiceImpl
import com.bitwarden.network.service.OrganizationService
import com.bitwarden.network.service.OrganizationServiceImpl
import com.x8bit.bitwarden.data.platform.datasource.network.retrofit.Retrofits
import dagger.Module
import dagger.Provides

View File

@@ -1,9 +1,9 @@
package com.x8bit.bitwarden.data.auth.datasource.sdk.util
import com.bitwarden.crypto.Kdf
import com.x8bit.bitwarden.data.auth.datasource.network.model.KdfTypeJson
import com.x8bit.bitwarden.data.auth.datasource.network.model.KdfTypeJson.ARGON2_ID
import com.x8bit.bitwarden.data.auth.datasource.network.model.KdfTypeJson.PBKDF2_SHA256
import com.bitwarden.network.model.KdfTypeJson
import com.bitwarden.network.model.KdfTypeJson.ARGON2_ID
import com.bitwarden.network.model.KdfTypeJson.PBKDF2_SHA256
/**
* Convert a [Kdf] to a [KdfTypeJson].

View File

@@ -1,11 +1,14 @@
package com.x8bit.bitwarden.data.auth.manager
import com.bitwarden.core.AuthRequestResponse
import com.bitwarden.core.data.util.asFailure
import com.bitwarden.core.data.util.asSuccess
import com.bitwarden.core.data.util.flatMap
import com.bitwarden.network.model.AuthRequestTypeJson
import com.bitwarden.network.service.AuthRequestsService
import com.bitwarden.network.service.NewAuthRequestService
import com.x8bit.bitwarden.data.auth.datasource.disk.AuthDiskSource
import com.x8bit.bitwarden.data.auth.datasource.disk.model.PendingAuthRequestJson
import com.x8bit.bitwarden.data.auth.datasource.network.model.AuthRequestTypeJson
import com.x8bit.bitwarden.data.auth.datasource.network.service.AuthRequestsService
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.model.AuthRequest
import com.x8bit.bitwarden.data.auth.manager.model.AuthRequestResult
@@ -16,9 +19,7 @@ import com.x8bit.bitwarden.data.auth.manager.model.AuthRequestsUpdatesResult
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.platform.error.NoActiveUserException
import com.x8bit.bitwarden.data.vault.datasource.sdk.VaultSdkSource
import kotlinx.coroutines.currentCoroutineContext
import kotlinx.coroutines.delay
@@ -51,7 +52,9 @@ class AuthRequestManagerImpl(
override fun getAuthRequestsWithUpdates(): Flow<AuthRequestsUpdatesResult> = flow {
while (currentCoroutineContext().isActive) {
when (val result = getAuthRequests()) {
AuthRequestsResult.Error -> emit(AuthRequestsUpdatesResult.Error)
is AuthRequestsResult.Error -> {
emit(AuthRequestsUpdatesResult.Error(error = result.error))
}
is AuthRequestsResult.Success -> {
emit(AuthRequestsUpdatesResult.Update(authRequests = result.authRequests))
@@ -70,9 +73,8 @@ class AuthRequestManagerImpl(
email = email,
authRequestType = authRequestType.toAuthRequestTypeJson(),
)
.getOrNull()
?: run {
emit(CreateAuthRequestResult.Error)
.getOrElse {
emit(CreateAuthRequestResult.Error(error = it))
return@flow
}
var authRequest = initialResult.authRequest
@@ -103,7 +105,7 @@ class AuthRequestManagerImpl(
)
}
.fold(
onFailure = { emit(CreateAuthRequestResult.Error) },
onFailure = { emit(CreateAuthRequestResult.Error(error = it)) },
onSuccess = { updateAuthRequest ->
when {
updateAuthRequest.requestApproved -> {
@@ -182,7 +184,7 @@ class AuthRequestManagerImpl(
)
}
.fold(
onFailure = { emit(AuthRequestUpdatesResult.Error) },
onFailure = { emit(AuthRequestUpdatesResult.Error(error = it)) },
onSuccess = { updateAuthRequest ->
when {
updateAuthRequest.requestApproved -> {
@@ -218,13 +220,18 @@ class AuthRequestManagerImpl(
fingerprint: String,
): Flow<AuthRequestUpdatesResult> = getAuthRequest {
when (val authRequestsResult = getAuthRequests()) {
AuthRequestsResult.Error -> AuthRequestUpdatesResult.Error
is AuthRequestsResult.Error -> {
AuthRequestUpdatesResult.Error(error = authRequestsResult.error)
}
is AuthRequestsResult.Success -> {
authRequestsResult
.authRequests
.firstOrNull { it.fingerprint == fingerprint }
?.let { AuthRequestUpdatesResult.Update(it) }
?: AuthRequestUpdatesResult.Error
?: AuthRequestUpdatesResult.Error(
error = IllegalStateException("Could not find the auth request."),
)
}
}
}
@@ -234,30 +241,28 @@ class AuthRequestManagerImpl(
): Flow<AuthRequestUpdatesResult> = getAuthRequest {
authRequestsService
.getAuthRequest(requestId)
.map { response ->
getFingerprintPhrase(response.publicKey).getOrNull()?.let { fingerprint ->
AuthRequest(
id = response.id,
publicKey = response.publicKey,
platform = response.platform,
ipAddress = response.ipAddress,
key = response.key,
masterPasswordHash = response.masterPasswordHash,
creationDate = response.creationDate,
responseDate = response.responseDate,
requestApproved = response.requestApproved ?: false,
originUrl = response.originUrl,
fingerprint = fingerprint,
)
}
.mapCatching { response ->
getFingerprintPhrase(response.publicKey)
.getOrThrow()
.let { fingerprint ->
AuthRequest(
id = response.id,
publicKey = response.publicKey,
platform = response.platform,
ipAddress = response.ipAddress,
key = response.key,
masterPasswordHash = response.masterPasswordHash,
creationDate = response.creationDate,
responseDate = response.responseDate,
requestApproved = response.requestApproved ?: false,
originUrl = response.originUrl,
fingerprint = fingerprint,
)
}
}
.fold(
onFailure = { AuthRequestUpdatesResult.Error },
onSuccess = { authRequest ->
authRequest
?.let { AuthRequestUpdatesResult.Update(it) }
?: AuthRequestUpdatesResult.Error
},
onFailure = { AuthRequestUpdatesResult.Error(error = it) },
onSuccess = { AuthRequestUpdatesResult.Update(authRequest = it) },
)
}
@@ -309,7 +314,7 @@ class AuthRequestManagerImpl(
}
}
.fold(
onFailure = { AuthRequestsResult.Error },
onFailure = { AuthRequestsResult.Error(error = it) },
onSuccess = { AuthRequestsResult.Success(authRequests = it) },
)
@@ -319,7 +324,7 @@ class AuthRequestManagerImpl(
publicKey: String,
isApproved: Boolean,
): AuthRequestResult {
val userId = activeUserId ?: return AuthRequestResult.Error
val userId = activeUserId ?: return AuthRequestResult.Error(error = NoActiveUserException())
return vaultSdkSource
.getAuthRequestKey(
publicKey = publicKey,
@@ -350,7 +355,7 @@ class AuthRequestManagerImpl(
)
}
.fold(
onFailure = { AuthRequestResult.Error },
onFailure = { AuthRequestResult.Error(error = it) },
onSuccess = { AuthRequestResult.Success(authRequest = it) },
)
}
@@ -462,7 +467,7 @@ class AuthRequestManagerImpl(
publicKey: String,
): Result<String> {
val profile = authDiskSource.userState?.activeAccount?.profile
?: return IllegalStateException("No active account").asFailure()
?: return NoActiveUserException().asFailure()
return authSdkSource.getUserFingerprint(
email = profile.email,
publicKey = publicKey,

View File

@@ -7,13 +7,13 @@ import androidx.compose.ui.graphics.Color
import androidx.core.app.NotificationChannelCompat
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
import com.bitwarden.core.annotation.OmitFromCoverage
import com.bitwarden.data.manager.DispatcherManager
import com.x8bit.bitwarden.R
import com.x8bit.bitwarden.data.auth.datasource.disk.AuthDiskSource
import com.x8bit.bitwarden.data.auth.util.createPasswordlessRequestDataIntent
import com.x8bit.bitwarden.data.autofill.util.toPendingIntentMutabilityFlag
import com.x8bit.bitwarden.data.platform.annotation.OmitFromCoverage
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.PasswordlessRequestData
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.launchIn

View File

@@ -0,0 +1,8 @@
package com.x8bit.bitwarden.data.auth.manager
import com.bitwarden.network.interceptor.AuthTokenProvider
/**
* A manager class for handling authentication tokens.
*/
interface AuthTokenManager : AuthTokenProvider

View File

@@ -0,0 +1,17 @@
package com.x8bit.bitwarden.data.auth.manager
import com.x8bit.bitwarden.data.auth.datasource.disk.AuthDiskSource
/**
* Default implementation of [AuthTokenManager].
*/
class AuthTokenManagerImpl(
private val authDiskSource: AuthDiskSource,
) : AuthTokenManager {
override fun getActiveAccessTokenOrNull(): String? = authDiskSource
.userState
?.activeUserId
?.let { authDiskSource.getAccountTokens(it) }
?.accessToken
}

View File

@@ -2,8 +2,8 @@ package com.x8bit.bitwarden.data.auth.manager
import com.bitwarden.core.KeyConnectorResponse
import com.bitwarden.crypto.Kdf
import com.x8bit.bitwarden.data.auth.datasource.network.model.KdfTypeJson
import com.x8bit.bitwarden.data.auth.datasource.network.model.KeyConnectorMasterKeyResponseJson
import com.bitwarden.network.model.KdfTypeJson
import com.bitwarden.network.model.KeyConnectorMasterKeyResponseJson
/**
* Manager used to interface with a key connector.

View File

@@ -1,13 +1,13 @@
package com.x8bit.bitwarden.data.auth.manager
import com.bitwarden.core.KeyConnectorResponse
import com.bitwarden.core.data.util.flatMap
import com.bitwarden.crypto.Kdf
import com.x8bit.bitwarden.data.auth.datasource.network.model.KdfTypeJson
import com.x8bit.bitwarden.data.auth.datasource.network.model.KeyConnectorKeyRequestJson
import com.x8bit.bitwarden.data.auth.datasource.network.model.KeyConnectorMasterKeyResponseJson
import com.x8bit.bitwarden.data.auth.datasource.network.service.AccountsService
import com.bitwarden.network.model.KdfTypeJson
import com.bitwarden.network.model.KeyConnectorKeyRequestJson
import com.bitwarden.network.model.KeyConnectorMasterKeyResponseJson
import com.bitwarden.network.service.AccountsService
import com.x8bit.bitwarden.data.auth.datasource.sdk.AuthSdkSource
import com.x8bit.bitwarden.data.platform.util.flatMap
import com.x8bit.bitwarden.data.vault.datasource.sdk.VaultSdkSource
/**

View File

@@ -1,11 +1,11 @@
package com.x8bit.bitwarden.data.auth.manager
import com.bitwarden.core.data.util.asSuccess
import com.bitwarden.core.data.util.flatMap
import com.bitwarden.crypto.TrustDeviceResponse
import com.bitwarden.network.service.DevicesService
import com.x8bit.bitwarden.data.auth.datasource.disk.AuthDiskSource
import com.x8bit.bitwarden.data.auth.datasource.network.service.DevicesService
import com.x8bit.bitwarden.data.auth.manager.util.toUserStateJson
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
/**

View File

@@ -1,6 +1,7 @@
package com.x8bit.bitwarden.data.auth.manager
import com.x8bit.bitwarden.data.auth.manager.model.LogoutEvent
import com.x8bit.bitwarden.data.auth.repository.model.LogoutReason
import kotlinx.coroutines.flow.SharedFlow
/**
@@ -14,15 +15,14 @@ interface UserLogoutManager {
val logoutEventFlow: SharedFlow<LogoutEvent>
/**
* Completely logs out the given [userId], removing all data. If [isExpired] is true, a toast
* will be displayed letting the user know the session has expired.
* Completely logs out the given [userId], removing all data. The [reason] indicates why the
* user is being logged out.
*/
fun logout(userId: String, isExpired: Boolean = false)
fun logout(userId: String, reason: LogoutReason)
/**
* Partially logs out the given [userId]. All data for the given [userId] will be removed with
* the exception of basic account data. If [isExpired] is true, a toast will be displayed
* letting the user know the session has expired.
* the exception of basic account data. The [reason] indicates why the user is being logged out.
*/
fun softLogout(userId: String, isExpired: Boolean = false)
fun softLogout(userId: String, reason: LogoutReason)
}

View File

@@ -3,13 +3,14 @@ package com.x8bit.bitwarden.data.auth.manager
import android.content.Context
import android.widget.Toast
import androidx.annotation.StringRes
import com.bitwarden.core.data.repository.util.bufferedMutableSharedFlow
import com.bitwarden.data.manager.DispatcherManager
import com.x8bit.bitwarden.R
import com.x8bit.bitwarden.data.auth.datasource.disk.AuthDiskSource
import com.x8bit.bitwarden.data.auth.manager.model.LogoutEvent
import com.x8bit.bitwarden.data.auth.repository.model.LogoutReason
import com.x8bit.bitwarden.data.platform.datasource.disk.PushDiskSource
import com.x8bit.bitwarden.data.platform.datasource.disk.SettingsDiskSource
import com.x8bit.bitwarden.data.platform.manager.dispatcher.DispatcherManager
import com.x8bit.bitwarden.data.platform.repository.util.bufferedMutableSharedFlow
import com.x8bit.bitwarden.data.tools.generator.datasource.disk.GeneratorDiskSource
import com.x8bit.bitwarden.data.tools.generator.datasource.disk.PasswordHistoryDiskSource
import com.x8bit.bitwarden.data.vault.datasource.disk.VaultDiskSource
@@ -19,6 +20,7 @@ import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.flow.asSharedFlow
import kotlinx.coroutines.launch
import timber.log.Timber
/**
* Primary implementation of [UserLogoutManager].
@@ -42,9 +44,10 @@ class UserLogoutManagerImpl(
bufferedMutableSharedFlow()
override val logoutEventFlow: SharedFlow<LogoutEvent> = mutableLogoutEventFlow.asSharedFlow()
override fun logout(userId: String, isExpired: Boolean) {
override fun logout(userId: String, reason: LogoutReason) {
authDiskSource.userState ?: return
Timber.i("logout reason=$reason")
val isExpired = reason == LogoutReason.SecurityStamp
if (isExpired) {
showToast(message = R.string.login_expired)
}
@@ -64,7 +67,9 @@ class UserLogoutManagerImpl(
mutableLogoutEventFlow.tryEmit(LogoutEvent(loggedOutUserId = userId))
}
override fun softLogout(userId: String, isExpired: Boolean) {
override fun softLogout(userId: String, reason: LogoutReason) {
Timber.i("softLogout reason=$reason")
val isExpired = reason == LogoutReason.SecurityStamp
if (isExpired) {
showToast(message = R.string.login_expired)
}

View File

@@ -1,11 +1,12 @@
package com.x8bit.bitwarden.data.auth.manager.di
import android.content.Context
import com.bitwarden.data.manager.DispatcherManager
import com.bitwarden.network.service.AccountsService
import com.bitwarden.network.service.AuthRequestsService
import com.bitwarden.network.service.DevicesService
import com.bitwarden.network.service.NewAuthRequestService
import com.x8bit.bitwarden.data.auth.datasource.disk.AuthDiskSource
import com.x8bit.bitwarden.data.auth.datasource.network.service.AccountsService
import com.x8bit.bitwarden.data.auth.datasource.network.service.AuthRequestsService
import com.x8bit.bitwarden.data.auth.datasource.network.service.DevicesService
import com.x8bit.bitwarden.data.auth.datasource.network.service.NewAuthRequestService
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
@@ -13,6 +14,8 @@ import com.x8bit.bitwarden.data.auth.manager.AuthRequestManager
import com.x8bit.bitwarden.data.auth.manager.AuthRequestManagerImpl
import com.x8bit.bitwarden.data.auth.manager.AuthRequestNotificationManager
import com.x8bit.bitwarden.data.auth.manager.AuthRequestNotificationManagerImpl
import com.x8bit.bitwarden.data.auth.manager.AuthTokenManager
import com.x8bit.bitwarden.data.auth.manager.AuthTokenManagerImpl
import com.x8bit.bitwarden.data.auth.manager.KeyConnectorManager
import com.x8bit.bitwarden.data.auth.manager.KeyConnectorManagerImpl
import com.x8bit.bitwarden.data.auth.manager.TrustedDeviceManager
@@ -22,7 +25,6 @@ import com.x8bit.bitwarden.data.auth.manager.UserLogoutManagerImpl
import com.x8bit.bitwarden.data.platform.datasource.disk.PushDiskSource
import com.x8bit.bitwarden.data.platform.datasource.disk.SettingsDiskSource
import com.x8bit.bitwarden.data.platform.manager.PushManager
import com.x8bit.bitwarden.data.platform.manager.dispatcher.DispatcherManager
import com.x8bit.bitwarden.data.tools.generator.datasource.disk.GeneratorDiskSource
import com.x8bit.bitwarden.data.tools.generator.datasource.disk.PasswordHistoryDiskSource
import com.x8bit.bitwarden.data.vault.datasource.disk.VaultDiskSource
@@ -131,4 +133,10 @@ object AuthManagerModule {
@Singleton
fun providesAddTotpItemFromAuthenticatorManager(): AddTotpItemFromAuthenticatorManager =
AddTotpItemFromAuthenticatorManagerImpl()
@Provides
@Singleton
fun providesAuthTokenManager(
authDiskSource: AuthDiskSource,
): AuthTokenManager = AuthTokenManagerImpl(authDiskSource = authDiskSource)
}

View File

@@ -14,5 +14,7 @@ sealed class AuthRequestResult {
/**
* There was an error getting the user's auth requests.
*/
data object Error : AuthRequestResult()
data class Error(
val error: Throwable,
) : AuthRequestResult()
}

View File

@@ -19,7 +19,9 @@ sealed class AuthRequestUpdatesResult {
/**
* There was an error getting the user's auth requests.
*/
data object Error : AuthRequestUpdatesResult()
data class Error(
val error: Throwable,
) : AuthRequestUpdatesResult()
/**
* The auth request has been declined.

View File

@@ -14,5 +14,7 @@ sealed class AuthRequestsResult {
/**
* There was an error getting the user's auth requests.
*/
data object Error : AuthRequestsResult()
data class Error(
val error: Throwable,
) : AuthRequestsResult()
}

View File

@@ -14,5 +14,7 @@ sealed class AuthRequestsUpdatesResult {
/**
* There was an error getting the user's auth requests.
*/
data object Error : AuthRequestsUpdatesResult()
data class Error(
val error: Throwable,
) : AuthRequestsUpdatesResult()
}

View File

@@ -23,7 +23,9 @@ sealed class CreateAuthRequestResult {
/**
* There was a generic error getting the user's auth requests.
*/
data object Error : CreateAuthRequestResult()
data class Error(
val error: Throwable,
) : CreateAuthRequestResult()
/**
* The auth request has been declined.

View File

@@ -1,6 +1,6 @@
package com.x8bit.bitwarden.data.auth.manager.util
import com.x8bit.bitwarden.data.auth.datasource.network.model.AuthRequestTypeJson
import com.bitwarden.network.model.AuthRequestTypeJson
import com.x8bit.bitwarden.data.auth.manager.model.AuthRequestType
/**

View File

@@ -1,9 +1,9 @@
package com.x8bit.bitwarden.data.auth.manager.util
import com.bitwarden.crypto.TrustDeviceResponse
import com.bitwarden.network.model.TrustedDeviceUserDecryptionOptionsJson
import com.bitwarden.network.model.UserDecryptionOptionsJson
import com.x8bit.bitwarden.data.auth.datasource.disk.model.UserStateJson
import com.x8bit.bitwarden.data.auth.datasource.network.model.TrustedDeviceUserDecryptionOptionsJson
import com.x8bit.bitwarden.data.auth.datasource.network.model.UserDecryptionOptionsJson
/**
* Converts the given [TrustDeviceResponse] to an updated [UserStateJson], given the following

View File

@@ -1,17 +1,19 @@
package com.x8bit.bitwarden.data.auth.repository
import com.bitwarden.network.model.GetTokenResponseJson
import com.bitwarden.network.model.SyncResponseJson
import com.bitwarden.network.model.TwoFactorDataModel
import com.x8bit.bitwarden.data.auth.datasource.disk.model.ForcePasswordResetReason
import com.x8bit.bitwarden.data.auth.datasource.disk.model.NewDeviceNoticeState
import com.x8bit.bitwarden.data.auth.datasource.disk.model.OnboardingStatus
import com.x8bit.bitwarden.data.auth.datasource.network.model.GetTokenResponseJson
import com.x8bit.bitwarden.data.auth.datasource.network.model.TwoFactorDataModel
import com.x8bit.bitwarden.data.auth.manager.AuthRequestManager
import com.x8bit.bitwarden.data.auth.repository.model.AuthState
import com.x8bit.bitwarden.data.auth.repository.model.BreachCountResult
import com.x8bit.bitwarden.data.auth.repository.model.DeleteAccountResult
import com.x8bit.bitwarden.data.auth.repository.model.EmailTokenResult
import com.x8bit.bitwarden.data.auth.repository.model.KnownDeviceResult
import com.x8bit.bitwarden.data.auth.repository.model.LeaveOrganizationResult
import com.x8bit.bitwarden.data.auth.repository.model.LoginResult
import com.x8bit.bitwarden.data.auth.repository.model.LogoutReason
import com.x8bit.bitwarden.data.auth.repository.model.NewSsoUserResult
import com.x8bit.bitwarden.data.auth.repository.model.OrganizationDomainSsoDetailsResult
import com.x8bit.bitwarden.data.auth.repository.model.PasswordHintResult
@@ -37,7 +39,6 @@ import com.x8bit.bitwarden.data.auth.repository.util.SsoCallbackResult
import com.x8bit.bitwarden.data.auth.repository.util.WebAuthResult
import com.x8bit.bitwarden.data.auth.util.YubiKeyResult
import com.x8bit.bitwarden.data.platform.datasource.network.authenticator.AuthenticatorProvider
import com.x8bit.bitwarden.data.vault.datasource.network.model.SyncResponseJson
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.StateFlow
@@ -243,10 +244,20 @@ interface AuthRepository : AuthenticatorProvider, AuthRequestManager {
orgIdentifier: String?,
): LoginResult
/**
* Continue the previously halted login attempt.
*/
suspend fun continueKeyConnectorLogin(): LoginResult
/**
* Cancel the previously halted login attempt.
*/
fun cancelKeyConnectorLogin()
/**
* Log out the current user.
*/
fun logout()
fun logout(reason: LogoutReason)
/**
* Requests that a one-time passcode be sent to the user's email.
@@ -424,17 +435,9 @@ interface AuthRepository : AuthenticatorProvider, AuthRequestManager {
fun setOnboardingStatus(status: OnboardingStatus)
/**
* Checks if a new device notice should be displayed.
* Leaves the organization that matches the given [organizationId]
*/
fun checkUserNeedsNewDeviceTwoFactorNotice(): Boolean
/**
* Gets the new device notice state of active user.
*/
fun getNewDeviceNoticeState(): NewDeviceNoticeState?
/**
* Stores the new device notice state for active user.
*/
fun setNewDeviceNoticeState(newState: NewDeviceNoticeState?)
suspend fun leaveOrganization(
organizationId: String,
): LeaveOrganizationResult
}

View File

@@ -2,42 +2,51 @@ package com.x8bit.bitwarden.data.auth.repository
import com.bitwarden.core.AuthRequestMethod
import com.bitwarden.core.InitUserCryptoMethod
import com.bitwarden.core.data.repository.util.bufferedMutableSharedFlow
import com.bitwarden.core.data.util.asFailure
import com.bitwarden.core.data.util.asSuccess
import com.bitwarden.core.data.util.flatMap
import com.bitwarden.crypto.HashPurpose
import com.bitwarden.crypto.Kdf
import com.bitwarden.data.datasource.disk.ConfigDiskSource
import com.bitwarden.data.manager.DispatcherManager
import com.bitwarden.data.repository.util.toEnvironmentUrls
import com.bitwarden.network.model.DeleteAccountResponseJson
import com.bitwarden.network.model.GetTokenResponseJson
import com.bitwarden.network.model.IdentityTokenAuthModel
import com.bitwarden.network.model.OrganizationType
import com.bitwarden.network.model.PasswordHintResponseJson
import com.bitwarden.network.model.PolicyTypeJson
import com.bitwarden.network.model.PrevalidateSsoResponseJson
import com.bitwarden.network.model.RefreshTokenResponseJson
import com.bitwarden.network.model.RegisterFinishRequestJson
import com.bitwarden.network.model.RegisterRequestJson
import com.bitwarden.network.model.RegisterResponseJson
import com.bitwarden.network.model.ResendEmailRequestJson
import com.bitwarden.network.model.ResendNewDeviceOtpRequestJson
import com.bitwarden.network.model.ResetPasswordRequestJson
import com.bitwarden.network.model.SendVerificationEmailRequestJson
import com.bitwarden.network.model.SendVerificationEmailResponseJson
import com.bitwarden.network.model.SetPasswordRequestJson
import com.bitwarden.network.model.SyncResponseJson
import com.bitwarden.network.model.TrustedDeviceUserDecryptionOptionsJson
import com.bitwarden.network.model.TwoFactorAuthMethod
import com.bitwarden.network.model.TwoFactorDataModel
import com.bitwarden.network.model.VerifyEmailTokenRequestJson
import com.bitwarden.network.model.VerifyEmailTokenResponseJson
import com.bitwarden.network.service.AccountsService
import com.bitwarden.network.service.DevicesService
import com.bitwarden.network.service.HaveIBeenPwnedService
import com.bitwarden.network.service.IdentityService
import com.bitwarden.network.service.OrganizationService
import com.bitwarden.network.util.isSslHandShakeError
import com.x8bit.bitwarden.data.auth.datasource.disk.AuthDiskSource
import com.x8bit.bitwarden.data.auth.datasource.disk.model.AccountJson
import com.x8bit.bitwarden.data.auth.datasource.disk.model.AccountTokensJson
import com.x8bit.bitwarden.data.auth.datasource.disk.model.ForcePasswordResetReason
import com.x8bit.bitwarden.data.auth.datasource.disk.model.NewDeviceNoticeDisplayStatus
import com.x8bit.bitwarden.data.auth.datasource.disk.model.NewDeviceNoticeState
import com.x8bit.bitwarden.data.auth.datasource.disk.model.OnboardingStatus
import com.x8bit.bitwarden.data.auth.datasource.disk.model.UserStateJson
import com.x8bit.bitwarden.data.auth.datasource.network.model.DeleteAccountResponseJson
import com.x8bit.bitwarden.data.auth.datasource.network.model.DeviceDataModel
import com.x8bit.bitwarden.data.auth.datasource.network.model.GetTokenResponseJson
import com.x8bit.bitwarden.data.auth.datasource.network.model.IdentityTokenAuthModel
import com.x8bit.bitwarden.data.auth.datasource.network.model.PasswordHintResponseJson
import com.x8bit.bitwarden.data.auth.datasource.network.model.PrevalidateSsoResponseJson
import com.x8bit.bitwarden.data.auth.datasource.network.model.RefreshTokenResponseJson
import com.x8bit.bitwarden.data.auth.datasource.network.model.RegisterFinishRequestJson
import com.x8bit.bitwarden.data.auth.datasource.network.model.RegisterRequestJson
import com.x8bit.bitwarden.data.auth.datasource.network.model.RegisterResponseJson
import com.x8bit.bitwarden.data.auth.datasource.network.model.ResendEmailRequestJson
import com.x8bit.bitwarden.data.auth.datasource.network.model.ResendNewDeviceOtpRequestJson
import com.x8bit.bitwarden.data.auth.datasource.network.model.ResetPasswordRequestJson
import com.x8bit.bitwarden.data.auth.datasource.network.model.SendVerificationEmailRequestJson
import com.x8bit.bitwarden.data.auth.datasource.network.model.SendVerificationEmailResponseJson
import com.x8bit.bitwarden.data.auth.datasource.network.model.SetPasswordRequestJson
import com.x8bit.bitwarden.data.auth.datasource.network.model.TrustedDeviceUserDecryptionOptionsJson
import com.x8bit.bitwarden.data.auth.datasource.network.model.TwoFactorAuthMethod
import com.x8bit.bitwarden.data.auth.datasource.network.model.TwoFactorDataModel
import com.x8bit.bitwarden.data.auth.datasource.network.model.VerifyEmailTokenRequestJson
import com.x8bit.bitwarden.data.auth.datasource.network.model.VerifyEmailTokenResponseJson
import com.x8bit.bitwarden.data.auth.datasource.network.service.AccountsService
import com.x8bit.bitwarden.data.auth.datasource.network.service.DevicesService
import com.x8bit.bitwarden.data.auth.datasource.network.service.HaveIBeenPwnedService
import com.x8bit.bitwarden.data.auth.datasource.network.service.IdentityService
import com.x8bit.bitwarden.data.auth.datasource.network.service.OrganizationService
import com.x8bit.bitwarden.data.auth.datasource.sdk.AuthSdkSource
import com.x8bit.bitwarden.data.auth.datasource.sdk.util.toInt
import com.x8bit.bitwarden.data.auth.datasource.sdk.util.toKdfTypeJson
@@ -50,7 +59,9 @@ import com.x8bit.bitwarden.data.auth.repository.model.BreachCountResult
import com.x8bit.bitwarden.data.auth.repository.model.DeleteAccountResult
import com.x8bit.bitwarden.data.auth.repository.model.EmailTokenResult
import com.x8bit.bitwarden.data.auth.repository.model.KnownDeviceResult
import com.x8bit.bitwarden.data.auth.repository.model.LeaveOrganizationResult
import com.x8bit.bitwarden.data.auth.repository.model.LoginResult
import com.x8bit.bitwarden.data.auth.repository.model.LogoutReason
import com.x8bit.bitwarden.data.auth.repository.model.NewSsoUserResult
import com.x8bit.bitwarden.data.auth.repository.model.OrganizationDomainSsoDetailsResult
import com.x8bit.bitwarden.data.auth.repository.model.PasswordHintResult
@@ -97,28 +108,18 @@ 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.datasource.network.util.isSslHandShakeError
import com.x8bit.bitwarden.data.platform.error.MissingPropertyException
import com.x8bit.bitwarden.data.platform.error.NoActiveUserException
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.model.Environment
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
import com.x8bit.bitwarden.data.vault.datasource.network.model.OrganizationType
import com.x8bit.bitwarden.data.vault.datasource.network.model.PolicyTypeJson
import com.x8bit.bitwarden.data.vault.datasource.network.model.SyncResponseJson
import com.x8bit.bitwarden.data.vault.datasource.sdk.VaultSdkSource
import com.x8bit.bitwarden.data.vault.repository.VaultRepository
import com.x8bit.bitwarden.data.vault.repository.model.VaultUnlockData
@@ -146,7 +147,6 @@ import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.receiveAsFlow
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.flow.update
import java.time.ZonedDateTime
import javax.inject.Singleton
/**
@@ -239,6 +239,8 @@ class AuthRepositoryImpl(
*/
private var passwordsToCheckMap = mutableMapOf<String, String>()
private var keyConnectorResponse: GetTokenResponseJson.Success? = null
override var twoFactorResponse: GetTokenResponseJson.TwoFactorRequired? = null
override val ssoOrganizationIdentifier: String? get() = organizationIdentifier
@@ -409,7 +411,7 @@ class AuthRepositoryImpl(
pushManager
.logoutFlow
.onEach { logout(userId = it.userId) }
.onEach { logout(userId = it.userId, reason = LogoutReason.Notification) }
.launchIn(unconfinedScope)
// When the policies for the user have been set, complete the login process.
@@ -467,7 +469,7 @@ class AuthRepositoryImpl(
masterPassword: String,
): DeleteAccountResult {
val profile = authDiskSource.userState?.activeAccount?.profile
?: return DeleteAccountResult.Error(message = null)
?: return DeleteAccountResult.Error(message = null, error = NoActiveUserException())
mutableHasPendingAccountDeletionStateFlow.value = true
return authSdkSource
.hashPassword(
@@ -501,22 +503,17 @@ class AuthRepositoryImpl(
fold(
onFailure = {
clearPendingAccountDeletion()
DeleteAccountResult.Error(message = null)
DeleteAccountResult.Error(error = it, message = null)
},
onSuccess = { response ->
when (response) {
is DeleteAccountResponseJson.Invalid -> {
clearPendingAccountDeletion()
DeleteAccountResult.Error(
message = response.validationErrors
?.values
?.firstOrNull()
?.firstOrNull(),
)
DeleteAccountResult.Error(message = response.message, error = null)
}
DeleteAccountResponseJson.Success -> {
logout()
logout(reason = LogoutReason.AccountDelete)
DeleteAccountResult.Success
}
}
@@ -524,8 +521,10 @@ class AuthRepositoryImpl(
)
override suspend fun createNewSsoUser(): NewSsoUserResult {
val account = authDiskSource.userState?.activeAccount ?: return NewSsoUserResult.Failure
val orgIdentifier = rememberedOrgIdentifier ?: return NewSsoUserResult.Failure
val account = authDiskSource.userState?.activeAccount
?: return NewSsoUserResult.Failure(error = NoActiveUserException())
val orgIdentifier = rememberedOrgIdentifier
?: return NewSsoUserResult.Failure(error = MissingPropertyException("OrgIdentifier"))
val userId = account.profile.userId
return organizationService
.getOrganizationAutoEnrollStatus(orgIdentifier)
@@ -577,7 +576,7 @@ class AuthRepositoryImpl(
}
.fold(
onSuccess = { NewSsoUserResult.Success },
onFailure = { NewSsoUserResult.Failure },
onFailure = { NewSsoUserResult.Failure(error = it) },
)
}
@@ -586,10 +585,13 @@ class AuthRepositoryImpl(
asymmetricalKey: String,
): LoginResult {
val profile = authDiskSource.userState?.activeAccount?.profile
?: return LoginResult.Error(errorMessage = null)
?: return LoginResult.Error(errorMessage = null, error = NoActiveUserException())
val userId = profile.userId
val privateKey = authDiskSource.getPrivateKey(userId = userId)
?: return LoginResult.Error(errorMessage = null)
?: return LoginResult.Error(
errorMessage = null,
error = MissingPropertyException("Private Key"),
)
checkForVaultUnlockError(
onVaultUnlockError = { error ->
@@ -639,7 +641,7 @@ class AuthRepositoryImpl(
onFailure = { throwable ->
when {
throwable.isSslHandShakeError() -> LoginResult.CertificateError
else -> LoginResult.Error(errorMessage = null)
else -> LoginResult.Error(errorMessage = null, error = throwable)
}
},
onSuccess = { it },
@@ -688,7 +690,10 @@ class AuthRepositoryImpl(
orgIdentifier = orgIdentifier,
)
}
?: LoginResult.Error(errorMessage = null)
?: LoginResult.Error(
errorMessage = null,
error = MissingPropertyException("Identity Token Auth Model"),
)
override suspend fun login(
email: String,
@@ -708,7 +713,29 @@ class AuthRepositoryImpl(
orgIdentifier = orgIdentifier,
)
}
?: LoginResult.Error(errorMessage = null)
?: LoginResult.Error(
errorMessage = null,
error = MissingPropertyException("Identity Token Auth Model"),
)
override suspend fun continueKeyConnectorLogin(): LoginResult {
val response = keyConnectorResponse ?: return LoginResult.Error(
errorMessage = null,
error = MissingPropertyException("Key Connector Response"),
)
return handleLoginCommonSuccess(
loginResponse = response,
email = rememberedEmailAddress.orEmpty(),
orgIdentifier = rememberedOrgIdentifier,
password = null,
deviceData = null,
userConfirmedKeyConnector = true,
)
}
override fun cancelKeyConnectorLogin() {
keyConnectorResponse = null
}
override suspend fun login(
email: String,
@@ -756,18 +783,18 @@ class AuthRepositoryImpl(
}
}
override fun logout() {
activeUserId?.let { userId -> logout(userId) }
override fun logout(reason: LogoutReason) {
activeUserId?.let { userId -> logout(userId = userId, reason = reason) }
}
override fun logout(userId: String) {
userLogoutManager.logout(userId = userId)
override fun logout(userId: String, reason: LogoutReason) {
userLogoutManager.logout(userId = userId, reason = reason)
}
override suspend fun requestOneTimePasscode(): RequestOtpResult =
accountsService.requestOneTimePasscode()
.fold(
onFailure = { RequestOtpResult.Error(it.message) },
onFailure = { RequestOtpResult.Error(message = it.message, error = it) },
onSuccess = { RequestOtpResult.Success },
)
@@ -777,7 +804,7 @@ class AuthRepositoryImpl(
passcode = oneTimePasscode,
)
.fold(
onFailure = { VerifyOtpResult.NotVerified(it.message) },
onFailure = { VerifyOtpResult.NotVerified(errorMessage = it.message, error = it) },
onSuccess = { VerifyOtpResult.Verified },
)
@@ -785,21 +812,27 @@ class AuthRepositoryImpl(
resendEmailRequestJson
?.let { jsonRequest ->
accountsService.resendVerificationCodeEmail(body = jsonRequest).fold(
onFailure = { ResendEmailResult.Error(message = it.message) },
onFailure = { ResendEmailResult.Error(message = it.message, error = it) },
onSuccess = { ResendEmailResult.Success },
)
}
?: ResendEmailResult.Error(message = null)
?: ResendEmailResult.Error(
message = null,
error = MissingPropertyException("Resend Email Request"),
)
override suspend fun resendNewDeviceOtp(): ResendEmailResult =
resendNewDeviceOtpRequestJson
?.let { jsonRequest ->
accountsService.resendNewDeviceOtp(body = jsonRequest).fold(
onFailure = { ResendEmailResult.Error(message = it.message) },
onFailure = { ResendEmailResult.Error(message = it.message, error = it) },
onSuccess = { ResendEmailResult.Success },
)
}
?: ResendEmailResult.Error(message = null)
?: ResendEmailResult.Error(
message = null,
error = MissingPropertyException("Resend New Device OTP Request"),
)
override fun switchAccount(userId: String): SwitchAccountResult {
val currentUserState = authDiskSource.userState ?: return SwitchAccountResult.NoChange
@@ -901,7 +934,10 @@ class AuthRepositoryImpl(
is RegisterResponseJson.CaptchaRequired -> {
it.validationErrors.captchaKeys.firstOrNull()
?.let { key -> RegisterResult.CaptchaRequired(captchaId = key) }
?: RegisterResult.Error(errorMessage = null)
?: RegisterResult.Error(
errorMessage = null,
error = MissingPropertyException("Captcha ID"),
)
}
is RegisterResponseJson.Success -> {
@@ -910,11 +946,11 @@ class AuthRepositoryImpl(
}
is RegisterResponseJson.Invalid -> {
RegisterResult.Error(errorMessage = it.message)
RegisterResult.Error(errorMessage = it.message, error = null)
}
}
},
onFailure = { RegisterResult.Error(errorMessage = null) },
onFailure = { RegisterResult.Error(errorMessage = null, error = it) },
)
}
@@ -922,11 +958,15 @@ class AuthRepositoryImpl(
return accountsService.requestPasswordHint(email).fold(
onSuccess = {
when (it) {
is PasswordHintResponseJson.Error -> PasswordHintResult.Error(it.errorMessage)
is PasswordHintResponseJson.Error -> PasswordHintResult.Error(
message = it.errorMessage,
error = null,
)
PasswordHintResponseJson.Success -> PasswordHintResult.Success
}
},
onFailure = { PasswordHintResult.Error(null) },
onFailure = { PasswordHintResult.Error(message = null, error = it) },
)
}
@@ -934,12 +974,12 @@ class AuthRepositoryImpl(
val activeAccount = authDiskSource
.userState
?.activeAccount
?: return RemovePasswordResult.Error
?: return RemovePasswordResult.Error(error = NoActiveUserException())
val profile = activeAccount.profile
val userId = profile.userId
val userKey = authDiskSource
.getUserKey(userId = userId)
?: return RemovePasswordResult.Error
?: return RemovePasswordResult.Error(error = MissingPropertyException("User Key"))
val keyConnectorUrl = organizations
.find {
it.shouldUseKeyConnector &&
@@ -947,7 +987,9 @@ class AuthRepositoryImpl(
it.type != OrganizationType.ADMIN
}
?.keyConnectorUrl
?: return RemovePasswordResult.Error
?: return RemovePasswordResult.Error(
error = MissingPropertyException("Key Connector URL"),
)
return keyConnectorManager
.migrateExistingUserToKeyConnector(
userId = userId,
@@ -965,7 +1007,7 @@ class AuthRepositoryImpl(
settingsRepository.setDefaultsIfNecessary(userId = userId)
}
.fold(
onFailure = { RemovePasswordResult.Error },
onFailure = { RemovePasswordResult.Error(error = it) },
onSuccess = { RemovePasswordResult.Success },
)
}
@@ -978,7 +1020,7 @@ class AuthRepositoryImpl(
val activeAccount = authDiskSource
.userState
?.activeAccount
?: return ResetPasswordResult.Error
?: return ResetPasswordResult.Error(error = NoActiveUserException())
val currentPasswordHash = currentPassword?.let { password ->
authSdkSource
.hashPassword(
@@ -988,7 +1030,7 @@ class AuthRepositoryImpl(
purpose = HashPurpose.SERVER_AUTHORIZATION,
)
.fold(
onFailure = { return ResetPasswordResult.Error },
onFailure = { return ResetPasswordResult.Error(error = it) },
onSuccess = { it },
)
}
@@ -1034,7 +1076,7 @@ class AuthRepositoryImpl(
// Return the success.
ResetPasswordResult.Success
},
onFailure = { ResetPasswordResult.Error },
onFailure = { ResetPasswordResult.Error(error = it) },
)
}
@@ -1047,7 +1089,7 @@ class AuthRepositoryImpl(
val activeAccount = authDiskSource
.userState
?.activeAccount
?: return SetPasswordResult.Error
?: return SetPasswordResult.Error(error = NoActiveUserException())
val userId = activeAccount.profile.userId
// Update the saved master password hash.
@@ -1058,7 +1100,7 @@ class AuthRepositoryImpl(
kdf = activeAccount.profile.toSdkParams(),
purpose = HashPurpose.SERVER_AUTHORIZATION,
)
.getOrElse { return@setPassword SetPasswordResult.Error }
.getOrElse { return@setPassword SetPasswordResult.Error(error = it) }
return when (activeAccount.profile.forcePasswordResetReason) {
ForcePasswordResetReason.TDE_USER_WITHOUT_PASSWORD_HAS_PASSWORD_RESET_PERMISSION -> {
@@ -1108,7 +1150,7 @@ class AuthRepositoryImpl(
}
}
.flatMap {
when (vaultRepository.unlockVaultWithMasterPassword(password)) {
when (val result = vaultRepository.unlockVaultWithMasterPassword(password)) {
is VaultUnlockResult.Success -> {
enrollUserInPasswordReset(
userId = userId,
@@ -1117,12 +1159,9 @@ class AuthRepositoryImpl(
)
}
is VaultUnlockResult.AuthenticationError,
VaultUnlockResult.BiometricDecodingError,
VaultUnlockResult.InvalidStateError,
VaultUnlockResult.GenericError,
-> {
IllegalStateException("Failed to unlock vault").asFailure()
is VaultUnlockError -> {
(result.error ?: IllegalStateException("Failed to unlock vault"))
.asFailure()
}
}
}
@@ -1132,7 +1171,7 @@ class AuthRepositoryImpl(
this.organizationIdentifier = null
}
.fold(
onFailure = { SetPasswordResult.Error },
onFailure = { SetPasswordResult.Error(error = it) },
onSuccess = { SetPasswordResult.Success },
)
}
@@ -1167,7 +1206,7 @@ class AuthRepositoryImpl(
verifiedDate = it.verifiedDate,
)
},
onFailure = { OrganizationDomainSsoDetailsResult.Failure },
onFailure = { OrganizationDomainSsoDetailsResult.Failure(error = it) },
)
override suspend fun getVerifiedOrganizationDomainSsoDetails(
@@ -1182,7 +1221,7 @@ class AuthRepositoryImpl(
verifiedOrganizationDomainSsoDetails = it.verifiedOrganizationDomainSsoDetails,
)
},
onFailure = { VerifiedOrganizationDomainSsoDetailsResult.Failure },
onFailure = { VerifiedOrganizationDomainSsoDetailsResult.Failure(error = it) },
)
override suspend fun prevalidateSso(
@@ -1192,22 +1231,23 @@ class AuthRepositoryImpl(
organizationIdentifier = organizationIdentifier,
)
.fold(
onSuccess = {
when (it) {
onSuccess = { response ->
when (response) {
is PrevalidateSsoResponseJson.Error -> {
PrevalidateSsoResult.Failure(message = it.message)
PrevalidateSsoResult.Failure(message = response.message, error = null)
}
is PrevalidateSsoResponseJson.Success -> {
if (it.token.isNullOrBlank()) {
PrevalidateSsoResult.Failure()
} else {
PrevalidateSsoResult.Success(token = it.token)
}
response.token
?.takeUnless { it.isBlank() }
?.let { PrevalidateSsoResult.Success(token = it) }
?: PrevalidateSsoResult.Failure(
error = MissingPropertyException("Token"),
)
}
}
},
onFailure = { PrevalidateSsoResult.Failure() },
onFailure = { PrevalidateSsoResult.Failure(error = it) },
)
override fun setSsoCallbackResult(result: SsoCallbackResult) {
@@ -1221,15 +1261,15 @@ class AuthRepositoryImpl(
deviceId = authDiskSource.uniqueAppId,
)
.fold(
onFailure = { KnownDeviceResult.Error },
onSuccess = { KnownDeviceResult.Success(it) },
onFailure = { KnownDeviceResult.Error(error = it) },
onSuccess = { KnownDeviceResult.Success(isKnownDevice = it) },
)
override suspend fun getPasswordBreachCount(password: String): BreachCountResult =
haveIBeenPwnedService
.getPasswordBreachCount(password)
.fold(
onFailure = { BreachCountResult.Error },
onFailure = { BreachCountResult.Error(error = it) },
onSuccess = { BreachCountResult.Success(it) },
)
@@ -1249,11 +1289,11 @@ class AuthRepositoryImpl(
)
.fold(
onSuccess = { PasswordStrengthResult.Success(passwordStrength = it) },
onFailure = { PasswordStrengthResult.Error },
onFailure = { PasswordStrengthResult.Error(error = it) },
)
override suspend fun validatePassword(password: String): ValidatePasswordResult {
val userId = activeUserId ?: return ValidatePasswordResult.Error
val userId = activeUserId ?: return ValidatePasswordResult.Error(NoActiveUserException())
return authDiskSource
.getMasterPasswordHash(userId = userId)
?.let { masterPasswordHash ->
@@ -1265,13 +1305,13 @@ class AuthRepositoryImpl(
)
.fold(
onSuccess = { ValidatePasswordResult.Success(isValid = it) },
onFailure = { ValidatePasswordResult.Error },
onFailure = { ValidatePasswordResult.Error(error = it) },
)
}
?: run {
val encryptedKey = authDiskSource
.getUserKey(userId)
?: return ValidatePasswordResult.Error
?: return ValidatePasswordResult.Error(MissingPropertyException("UserKey"))
vaultSdkSource
.validatePasswordUserKey(
userId = userId,
@@ -1301,10 +1341,12 @@ class AuthRepositoryImpl(
.userState
?.activeAccount
?.profile
?: return ValidatePinResult.Error
?: return ValidatePinResult.Error(error = NoActiveUserException())
val pinProtectedUserKey = authDiskSource
.getPinProtectedUserKey(userId = activeAccount.userId)
?: return ValidatePinResult.Error
?: return ValidatePinResult.Error(
error = MissingPropertyException("Pin Protected User Key"),
)
return vaultSdkSource
.validatePin(
userId = activeAccount.userId,
@@ -1313,7 +1355,7 @@ class AuthRepositoryImpl(
)
.fold(
onSuccess = { ValidatePinResult.Success(isValid = it) },
onFailure = { ValidatePinResult.Error },
onFailure = { ValidatePinResult.Error(error = it) },
)
}
@@ -1339,7 +1381,10 @@ class AuthRepositoryImpl(
onSuccess = {
when (it) {
is SendVerificationEmailResponseJson.Invalid -> {
SendVerificationEmailResult.Error(it.message)
SendVerificationEmailResult.Error(
errorMessage = it.message,
error = null,
)
}
is SendVerificationEmailResponseJson.Success -> {
@@ -1347,9 +1392,7 @@ class AuthRepositoryImpl(
}
}
},
onFailure = {
SendVerificationEmailResult.Error(null)
},
onFailure = { SendVerificationEmailResult.Error(errorMessage = null, error = it) },
)
override suspend fun validateEmailToken(email: String, token: String): EmailTokenResult {
@@ -1365,15 +1408,13 @@ class AuthRepositoryImpl(
when (val json = it) {
VerifyEmailTokenResponseJson.Valid -> EmailTokenResult.Success
is VerifyEmailTokenResponseJson.Invalid -> {
EmailTokenResult.Error(json.message)
EmailTokenResult.Error(message = json.message, error = null)
}
VerifyEmailTokenResponseJson.TokenExpired -> EmailTokenResult.Expired
}
},
onFailure = {
EmailTokenResult.Error(message = null)
},
onFailure = { EmailTokenResult.Error(message = null, error = it) },
)
}
@@ -1386,91 +1427,11 @@ class AuthRepositoryImpl(
}
}
override fun getNewDeviceNoticeState(): NewDeviceNoticeState? {
return activeUserId?.let { userId ->
authDiskSource.getNewDeviceNoticeState(userId = userId)
}
}
override fun setNewDeviceNoticeState(newState: NewDeviceNoticeState?) {
activeUserId?.let { userId ->
authDiskSource.storeNewDeviceNoticeState(userId = userId, newState = newState)
}
}
override fun checkUserNeedsNewDeviceTwoFactorNotice(): Boolean {
return activeUserId?.let { userId ->
val temporaryFlag = featureFlagManager.getFeatureFlag(FlagKey.NewDeviceTemporaryDismiss)
val permanentFlag = featureFlagManager.getFeatureFlag(FlagKey.NewDevicePermanentDismiss)
// check if feature flags are disabled
if (!temporaryFlag && !permanentFlag) {
return false
}
if (!newDeviceNoticePreConditionsValid()) {
return false
}
val newDeviceNoticeState = authDiskSource.getNewDeviceNoticeState(userId = userId)
return when (newDeviceNoticeState.displayStatus) {
// if the user has already attested email access but permanent flag is enabled,
// the notice needs to appear again
NewDeviceNoticeDisplayStatus.CAN_ACCESS_EMAIL -> permanentFlag
// if the user has already seen but 7 days have already passed,
// the notice needs to appear again
NewDeviceNoticeDisplayStatus.HAS_SEEN ->
newDeviceNoticeState.shouldDisplayNoticeIfSeen
NewDeviceNoticeDisplayStatus.HAS_NOT_SEEN -> true
// the user never needs to see the notice again
NewDeviceNoticeDisplayStatus.CAN_ACCESS_EMAIL_PERMANENT -> false
}
}
?: false
}
/**
* Checks if the preconditions are met for a user to see a new device notice:
* - Must be a Bitwarden cloud user.
* - The account must be at least one week old.
* - Cannot have an active policy requiring SSO to be enabled.
* - Cannot have two-factor authentication enabled.
*/
private fun newDeviceNoticePreConditionsValid(): Boolean {
val checkEnvironment = !featureFlagManager.getFeatureFlag(FlagKey.IgnoreEnvironmentCheck)
val isSelfHosted = environmentRepository.environment.type == Environment.Type.SELF_HOSTED
if (checkEnvironment && isSelfHosted) {
return false
}
val userProfile = authDiskSource.userState?.activeAccount?.profile
val isProfileAtLeastWeekOld = userProfile
?.let {
it.creationDate
?.plusWeeks(1)
?.isBefore(
ZonedDateTime.now(),
)
}
?: false
if (!isProfileAtLeastWeekOld) {
return false
}
val hasTwoFactorEnabled = userProfile
?.isTwoFactorEnabled
?: false
if (hasTwoFactorEnabled) {
return false
}
val hasSSOPolicy =
policyManager.getActivePolicies(type = PolicyTypeJson.REQUIRE_SSO)
.any { p -> p.isEnabled }
return !hasSSOPolicy
}
override suspend fun leaveOrganization(organizationId: String): LeaveOrganizationResult =
organizationService.leaveOrganization(organizationId).fold(
onSuccess = { LeaveOrganizationResult.Success },
onFailure = { LeaveOrganizationResult.Error(error = it) },
)
@Suppress("CyclomaticComplexMethod")
private suspend fun validatePasswordAgainstPolicy(
@@ -1619,6 +1580,7 @@ class AuthRepositoryImpl(
* A helper function to extract the common logic of logging in through
* any of the available methods.
*/
@Suppress("LongMethod")
private suspend fun loginCommon(
email: String,
password: String? = null,
@@ -1645,7 +1607,10 @@ class AuthRepositoryImpl(
LoginResult.UnofficialServerError
}
else -> LoginResult.Error(errorMessage = null)
else -> LoginResult.Error(
errorMessage = null,
error = throwable,
)
}
},
onSuccess = { loginResponse ->
@@ -1667,6 +1632,7 @@ class AuthRepositoryImpl(
password = password,
deviceData = deviceData,
orgIdentifier = orgIdentifier,
userConfirmedKeyConnector = false,
)
is GetTokenResponseJson.Invalid -> {
@@ -1681,6 +1647,7 @@ class AuthRepositoryImpl(
is GetTokenResponseJson.Invalid.InvalidType.GenericInvalid -> {
LoginResult.Error(
errorMessage = loginResponse.errorMessage,
error = null,
)
}
}
@@ -1699,6 +1666,7 @@ class AuthRepositoryImpl(
password: String?,
deviceData: DeviceDataModel?,
orgIdentifier: String?,
userConfirmedKeyConnector: Boolean,
): LoginResult = userStateTransaction {
val userStateJson = loginResponse.toUserState(
previousUserState = authDiskSource.userState,
@@ -1728,6 +1696,21 @@ class AuthRepositoryImpl(
deviceData = deviceData,
)
} else if (keyConnectorUrl != null && orgIdentifier != null) {
val isNewKeyConnectorUser =
loginResponse.userDecryptionOptions?.hasMasterPassword == false &&
loginResponse.key == null &&
loginResponse.privateKey == null
val isNotConfirmed = !userConfirmedKeyConnector
// If a new KeyConnector user is logging in for the first time,
// we should ask him to confirm the domain
if (isNewKeyConnectorUser && isNotConfirmed) {
keyConnectorResponse = loginResponse
return LoginResult.ConfirmKeyConnectorDomain(
domain = keyConnectorUrl,
)
}
unlockVaultWithKeyConnectorOnLoginSuccess(
profile = profile,
keyConnectorUrl = keyConnectorUrl,
@@ -1801,6 +1784,7 @@ class AuthRepositoryImpl(
resendEmailRequestJson = null
twoFactorDeviceData = null
resendNewDeviceOtpRequestJson = null
keyConnectorResponse = null
settingsRepository.setDefaultsIfNecessary(userId = userId)
settingsRepository.storeUserHasLoggedInValue(userId)
vaultRepository.syncIfNecessary()
@@ -1854,17 +1838,20 @@ class AuthRepositoryImpl(
/**
* Attempt to unlock the current user's vault with key connector data.
*/
@Suppress("LongMethod")
private suspend fun unlockVaultWithKeyConnectorOnLoginSuccess(
profile: AccountJson.Profile,
keyConnectorUrl: String,
orgIdentifier: String,
loginResponse: GetTokenResponseJson.Success,
): VaultUnlockResult? =
if (loginResponse.userDecryptionOptions?.hasMasterPassword != false) {
): VaultUnlockResult? {
val key = loginResponse.key
val privateKey = loginResponse.privateKey
return if (loginResponse.userDecryptionOptions?.hasMasterPassword != false) {
// This user has a master password, so we skip the key-connector logic as it is not
// setup yet. The user can still unlock the vault with their master password.
null
} else if (loginResponse.key != null && loginResponse.privateKey != null) {
} else if (key != null && privateKey != null) {
// This is a returning user who should already have the key connector setup
keyConnectorManager
.getMasterKeyFromKeyConnector(
@@ -1874,16 +1861,16 @@ class AuthRepositoryImpl(
.map {
unlockVault(
accountProfile = profile,
privateKey = loginResponse.privateKey,
privateKey = privateKey,
initUserCryptoMethod = InitUserCryptoMethod.KeyConnector(
masterKey = it.masterKey,
userKey = loginResponse.key,
userKey = key,
),
)
}
.fold(
// If the request failed, we want to abort the login process
onFailure = { VaultUnlockResult.GenericError },
onFailure = { VaultUnlockResult.GenericError(error = it) },
onSuccess = { it },
)
} else {
@@ -1923,10 +1910,11 @@ class AuthRepositoryImpl(
}
.fold(
// If the request failed, we want to abort the login process
onFailure = { VaultUnlockResult.GenericError },
onFailure = { VaultUnlockResult.GenericError(error = it) },
onSuccess = { it },
)
}
}
/**
* Attempt to unlock the current user's vault with password data.
@@ -1960,11 +1948,13 @@ class AuthRepositoryImpl(
): VaultUnlockResult? {
// Attempt to unlock the vault with auth request if possible.
// These values will only be null during the Just-in-Time provisioning flow.
if (loginResponse.privateKey != null && loginResponse.key != null) {
val privateKey = loginResponse.privateKey
val key = loginResponse.key
if (privateKey != null && key != null) {
deviceData?.let { model ->
return unlockVault(
accountProfile = profile,
privateKey = loginResponse.privateKey,
privateKey = privateKey,
initUserCryptoMethod = InitUserCryptoMethod.AuthRequest(
requestPrivateKey = model.privateKey,
method = model
@@ -1972,7 +1962,7 @@ class AuthRepositoryImpl(
?.let {
AuthRequestMethod.MasterKey(
protectedMasterKey = model.asymmetricalKey,
authRequestKey = loginResponse.key,
authRequestKey = key,
)
}
?: AuthRequestMethod.UserKey(protectedUserKey = model.asymmetricalKey),

View File

@@ -1,11 +1,13 @@
package com.x8bit.bitwarden.data.auth.repository.di
import com.bitwarden.data.datasource.disk.ConfigDiskSource
import com.bitwarden.data.manager.DispatcherManager
import com.bitwarden.network.service.AccountsService
import com.bitwarden.network.service.DevicesService
import com.bitwarden.network.service.HaveIBeenPwnedService
import com.bitwarden.network.service.IdentityService
import com.bitwarden.network.service.OrganizationService
import com.x8bit.bitwarden.data.auth.datasource.disk.AuthDiskSource
import com.x8bit.bitwarden.data.auth.datasource.network.service.AccountsService
import com.x8bit.bitwarden.data.auth.datasource.network.service.DevicesService
import com.x8bit.bitwarden.data.auth.datasource.network.service.HaveIBeenPwnedService
import com.x8bit.bitwarden.data.auth.datasource.network.service.IdentityService
import com.x8bit.bitwarden.data.auth.datasource.network.service.OrganizationService
import com.x8bit.bitwarden.data.auth.datasource.sdk.AuthSdkSource
import com.x8bit.bitwarden.data.auth.manager.AuthRequestManager
import com.x8bit.bitwarden.data.auth.manager.KeyConnectorManager
@@ -13,13 +15,11 @@ 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
import com.x8bit.bitwarden.data.platform.repository.EnvironmentRepository
import com.x8bit.bitwarden.data.platform.repository.SettingsRepository
import com.x8bit.bitwarden.data.vault.datasource.sdk.VaultSdkSource

View File

@@ -12,5 +12,8 @@ sealed class BreachCountResult {
/**
* There was an error determining if the password has been breached.
*/
data object Error : BreachCountResult()
data class Error(
val error: Throwable,
val message: String? = null,
) : BreachCountResult()
}

View File

@@ -12,5 +12,8 @@ sealed class DeleteAccountResult {
/**
* There was an error deleting the account.
*/
data class Error(val message: String?) : DeleteAccountResult()
data class Error(
val message: String?,
val error: Throwable?,
) : DeleteAccountResult()
}

View File

@@ -18,5 +18,8 @@ sealed class EmailTokenResult {
/**
* There was an error validating the token.
*/
data class Error(val message: String?) : EmailTokenResult()
data class Error(
val message: String?,
val error: Throwable?,
) : EmailTokenResult()
}

View File

@@ -12,5 +12,7 @@ sealed class KnownDeviceResult {
/**
* There was an error determining if this is a known device.
*/
data object Error : KnownDeviceResult()
data class Error(
val error: Throwable,
) : KnownDeviceResult()
}

View File

@@ -0,0 +1,18 @@
package com.x8bit.bitwarden.data.auth.repository.model
/**
* Models result of deleting an account.
*/
sealed class LeaveOrganizationResult {
/**
* Leave organization succeeded.
*/
data object Success : LeaveOrganizationResult()
/**
* There was an error leaving the organization.
*/
data class Error(
val error: Throwable?,
) : LeaveOrganizationResult()
}

View File

@@ -19,10 +19,20 @@ sealed class LoginResult {
*/
data object TwoFactorRequired : LoginResult()
/**
* User should confirm KeyConnector domain
*/
data class ConfirmKeyConnectorDomain(
val domain: String,
) : LoginResult()
/**
* There was an error logging in.
*/
data class Error(val errorMessage: String?) : LoginResult()
data class Error(
val errorMessage: String?,
val error: Throwable?,
) : LoginResult()
/**
* There was an error while logging into an unofficial Bitwarden server.

View File

@@ -8,9 +8,12 @@ import com.x8bit.bitwarden.data.vault.repository.model.VaultUnlockResult
* the necessary `message` if applicable.
*/
fun VaultUnlockError.toLoginErrorResult(): LoginResult.Error = when (this) {
is VaultUnlockResult.AuthenticationError -> LoginResult.Error(this.message)
VaultUnlockResult.BiometricDecodingError,
VaultUnlockResult.GenericError,
VaultUnlockResult.InvalidStateError,
-> LoginResult.Error(errorMessage = null)
is VaultUnlockResult.AuthenticationError -> {
LoginResult.Error(errorMessage = this.message, error = this.error)
}
is VaultUnlockResult.BiometricDecodingError,
is VaultUnlockResult.GenericError,
is VaultUnlockResult.InvalidStateError,
-> LoginResult.Error(errorMessage = null, error = this.error)
}

View File

@@ -0,0 +1,76 @@
package com.x8bit.bitwarden.data.auth.repository.model
/**
* Indicates the reason that the user is being logged out.
*/
sealed class LogoutReason {
/**
* An optional additional tag for an event.
*/
open val source: String? = null
/**
* Indicates that the logout is happening because the account was deleted.
*/
data object AccountDelete : LogoutReason()
/**
* Indicates that the logout is related to biometrics.
*/
sealed class Biometrics : LogoutReason() {
/**
* Indicates that the logout is caused by a biometrics lockout.
*/
data object Lockout : Biometrics()
/**
* Indicates that the logout is happening because biometrics is no longer supported.
*/
data object NoLongerSupported : Biometrics()
}
/**
* Indicates that the logout is happening because of an invalid state.
*/
data class InvalidState(
override val source: String,
) : LogoutReason()
/**
* Indicates that the logout is happening because the user opted to logout via a button.
*/
data class Click(
override val source: String,
) : LogoutReason()
/**
* Indicates that the logout is happening because the a logout notification was received.
*/
data object Notification : LogoutReason()
/**
* Indicates that the logout is happening because the sync security stamp was invalidated.
*/
data object SecurityStamp : LogoutReason()
/**
* Indicates that the logout is happening because of a timeout action.
*/
data object Timeout : LogoutReason()
/**
* Indicates that the logout is happening because the access token could not be refreshed.
*/
data object TokenRefreshFail : LogoutReason()
/**
* Indicates that the logout is happening because the user tried to unlock the vault
* unsuccessfully too many times.
*/
data object TooManyUnlockAttempts : LogoutReason()
/**
* Indicates that the logout is happening because the left the organization.
*/
data object LeftOrganization : LogoutReason()
}

View File

@@ -12,5 +12,7 @@ sealed class NewSsoUserResult {
/**
* There was an error while truing to create the new user.
*/
data object Failure : NewSsoUserResult()
data class Failure(
val error: Throwable,
) : NewSsoUserResult()
}

View File

@@ -1,6 +1,6 @@
package com.x8bit.bitwarden.data.auth.repository.model
import com.x8bit.bitwarden.data.vault.datasource.network.model.OrganizationType
import com.bitwarden.network.model.OrganizationType
/**
* Represents an organization a user may be a member of.
@@ -11,6 +11,7 @@ import com.x8bit.bitwarden.data.vault.datasource.network.model.OrganizationType
* own password.
* @property shouldUseKeyConnector Indicates that the organization uses a key connector.
* @property role The user's role in the organization.
* @property keyConnectorUrl The key connector domain (if applicable).
*/
data class Organization(
val id: String,
@@ -18,4 +19,5 @@ data class Organization(
val shouldManageResetPassword: Boolean,
val shouldUseKeyConnector: Boolean,
val role: OrganizationType,
val keyConnectorUrl: String?,
)

View File

@@ -22,5 +22,7 @@ sealed class OrganizationDomainSsoDetailsResult {
/**
* The request failed.
*/
data object Failure : OrganizationDomainSsoDetailsResult()
data class Failure(
val error: Throwable,
) : OrganizationDomainSsoDetailsResult()
}

View File

@@ -13,5 +13,8 @@ sealed class PasswordHintResult {
/**
* There was an error.
*/
data class Error(val message: String?) : PasswordHintResult()
data class Error(
val message: String?,
val error: Throwable?,
) : PasswordHintResult()
}

View File

@@ -16,5 +16,7 @@ sealed class PasswordStrengthResult {
/**
* There was an error determining the password strength.
*/
data object Error : PasswordStrengthResult()
data class Error(
val error: Throwable,
) : PasswordStrengthResult()
}

View File

@@ -16,5 +16,6 @@ sealed class PrevalidateSsoResult {
*/
data class Failure(
val message: String? = null,
val error: Throwable?,
) : PrevalidateSsoResult()
}

View File

@@ -23,7 +23,10 @@ sealed class RegisterResult {
*
* @param errorMessage a message describing the error.
*/
data class Error(val errorMessage: String?) : RegisterResult()
data class Error(
val errorMessage: String?,
val error: Throwable?,
) : RegisterResult()
/**
* Password hash was found in a data breach.

View File

@@ -12,5 +12,7 @@ sealed class RemovePasswordResult {
/**
* There was an error removing the password.
*/
data object Error : RemovePasswordResult()
data class Error(
val error: Throwable,
) : RemovePasswordResult()
}

View File

@@ -13,5 +13,8 @@ sealed class RequestOtpResult {
/**
* Represents a failure to send the one-time passcode.
*/
data class Error(val message: String?) : RequestOtpResult()
data class Error(
val message: String?,
val error: Throwable,
) : RequestOtpResult()
}

View File

@@ -13,5 +13,8 @@ sealed class ResendEmailResult {
/**
* There was an error.
*/
data class Error(val message: String?) : ResendEmailResult()
data class Error(
val message: String?,
val error: Throwable,
) : ResendEmailResult()
}

View File

@@ -12,5 +12,7 @@ sealed class ResetPasswordResult {
/**
* There was an error resetting the password.
*/
data object Error : ResetPasswordResult()
data class Error(
val error: Throwable,
) : ResetPasswordResult()
}

View File

@@ -18,5 +18,8 @@ sealed class SendVerificationEmailResult {
*
* @param errorMessage a message describing the error.
*/
data class Error(val errorMessage: String?) : SendVerificationEmailResult()
data class Error(
val errorMessage: String?,
val error: Throwable?,
) : SendVerificationEmailResult()
}

View File

@@ -12,5 +12,7 @@ sealed class SetPasswordResult {
/**
* There was an error setting the password.
*/
data object Error : SetPasswordResult()
data class Error(
val error: Throwable,
) : SetPasswordResult()
}

View File

@@ -14,5 +14,7 @@ sealed class UserFingerprintResult {
/**
* There was an error getting the user fingerprint.
*/
data object Error : UserFingerprintResult()
data class Error(
val error: Throwable,
) : UserFingerprintResult()
}

View File

@@ -1,9 +1,8 @@
package com.x8bit.bitwarden.data.auth.repository.model
import com.bitwarden.data.repository.model.Environment
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
/**
* Represents the overall "user state" of the current active user as well as any users that may be

View File

@@ -15,5 +15,7 @@ sealed class ValidatePasswordResult {
/**
* There was an error determining if the validity of the password.
*/
data object Error : ValidatePasswordResult()
data class Error(
val error: Throwable,
) : ValidatePasswordResult()
}

View File

@@ -14,5 +14,7 @@ sealed class ValidatePinResult {
/**
* There was an error determining if the validity of the PIN.
*/
data object Error : ValidatePinResult()
data class Error(
val error: Throwable,
) : ValidatePinResult()
}

View File

@@ -1,6 +1,6 @@
package com.x8bit.bitwarden.data.auth.repository.model
import com.x8bit.bitwarden.data.auth.datasource.network.model.VerifiedOrganizationDomainSsoDetailsResponse.VerifiedOrganizationDomainSsoDetail
import com.bitwarden.network.model.VerifiedOrganizationDomainSsoDetailsResponse.VerifiedOrganizationDomainSsoDetail
/**
* Response types when checking for an email's claimed domain organization.
@@ -18,5 +18,7 @@ sealed class VerifiedOrganizationDomainSsoDetailsResult {
/**
* The request failed.
*/
data object Failure : VerifiedOrganizationDomainSsoDetailsResult()
data class Failure(
val error: Throwable,
) : VerifiedOrganizationDomainSsoDetailsResult()
}

View File

@@ -13,5 +13,8 @@ sealed class VerifyOtpResult {
/**
* Represents a failure to verify the one-time passcode.
*/
data class NotVerified(val errorMessage: String?) : VerifyOtpResult()
data class NotVerified(
val errorMessage: String?,
val error: Throwable,
) : VerifyOtpResult()
}

View File

@@ -1,8 +1,8 @@
package com.x8bit.bitwarden.data.auth.repository.util
import com.bitwarden.crypto.Kdf
import com.bitwarden.network.model.KdfTypeJson
import com.x8bit.bitwarden.data.auth.datasource.disk.model.AccountJson
import com.x8bit.bitwarden.data.auth.datasource.network.model.KdfTypeJson
import com.x8bit.bitwarden.data.auth.util.KdfParamsConstants
/**

View File

@@ -1,10 +1,10 @@
package com.x8bit.bitwarden.data.auth.repository.util
import com.bitwarden.data.datasource.disk.model.EnvironmentUrlDataJson
import com.bitwarden.network.model.GetTokenResponseJson
import com.x8bit.bitwarden.data.auth.datasource.disk.model.AccountJson
import com.x8bit.bitwarden.data.auth.datasource.disk.model.EnvironmentUrlDataJson
import com.x8bit.bitwarden.data.auth.datasource.disk.model.ForcePasswordResetReason
import com.x8bit.bitwarden.data.auth.datasource.disk.model.UserStateJson
import com.x8bit.bitwarden.data.auth.datasource.network.model.GetTokenResponseJson
/**
* Converts the given [GetTokenResponseJson.Success] to a [UserStateJson], given the following
@@ -67,13 +67,16 @@ fun GetTokenResponseJson.Success.toUserState(
private fun GetTokenResponseJson.Success.toForcePasswordResetReason(): ForcePasswordResetReason? =
this
.userDecryptionOptions
?.trustedDeviceUserDecryptionOptions
?.let { options ->
ForcePasswordResetReason.TDE_USER_WITHOUT_PASSWORD_HAS_PASSWORD_RESET_PERMISSION
.takeIf {
!this.userDecryptionOptions.hasMasterPassword &&
options.hasManageResetPasswordPermission
?.let { decryptionOptionsJson ->
decryptionOptionsJson
.trustedDeviceUserDecryptionOptions
?.let { options ->
ForcePasswordResetReason.TDE_USER_WITHOUT_PASSWORD_HAS_PASSWORD_RESET_PERMISSION
.takeIf {
!decryptionOptionsJson.hasMasterPassword &&
options.hasManageResetPasswordPermission
}
}
?: ForcePasswordResetReason.ADMIN_FORCE_PASSWORD_RESET
.takeIf { this.shouldForcePasswordReset }
}
?: ForcePasswordResetReason.ADMIN_FORCE_PASSWORD_RESET
.takeIf { this.shouldForcePasswordReset }

View File

@@ -1,7 +1,7 @@
package com.x8bit.bitwarden.data.auth.repository.util
import com.bitwarden.network.util.base64UrlDecodeOrNull
import com.x8bit.bitwarden.data.auth.repository.model.JwtTokenDataJson
import com.x8bit.bitwarden.data.platform.datasource.network.util.base64UrlDecodeOrNull
import kotlinx.serialization.json.Json
import timber.log.Timber

View File

@@ -1,10 +1,10 @@
package com.x8bit.bitwarden.data.auth.repository.util
import com.bitwarden.core.data.util.decodeFromStringOrNull
import com.bitwarden.network.model.PolicyTypeJson
import com.bitwarden.network.model.SyncResponseJson
import com.x8bit.bitwarden.data.auth.repository.model.Organization
import com.x8bit.bitwarden.data.auth.repository.model.PolicyInformation
import com.x8bit.bitwarden.data.platform.util.decodeFromStringOrNull
import com.x8bit.bitwarden.data.vault.datasource.network.model.PolicyTypeJson
import com.x8bit.bitwarden.data.vault.datasource.network.model.SyncResponseJson
import kotlinx.serialization.json.Json
private val JSON = Json {
@@ -22,6 +22,7 @@ fun SyncResponseJson.Profile.Organization.toOrganization(): Organization =
shouldUseKeyConnector = this.shouldUseKeyConnector,
role = this.type,
shouldManageResetPassword = this.permissions.shouldManageResetPassword,
keyConnectorUrl = this.keyConnectorUrl,
)
/**

View File

@@ -1,17 +1,17 @@
package com.x8bit.bitwarden.data.auth.repository.util
import com.bitwarden.data.repository.util.toEnvironmentUrlsOrDefault
import com.bitwarden.network.model.OrganizationType
import com.bitwarden.network.model.SyncResponseJson
import com.bitwarden.network.model.UserDecryptionOptionsJson
import com.x8bit.bitwarden.data.auth.datasource.disk.model.OnboardingStatus
import com.x8bit.bitwarden.data.auth.datasource.disk.model.UserStateJson
import com.x8bit.bitwarden.data.auth.datasource.network.model.UserDecryptionOptionsJson
import com.x8bit.bitwarden.data.auth.repository.model.UserAccountTokens
import com.x8bit.bitwarden.data.auth.repository.model.UserKeyConnectorState
import com.x8bit.bitwarden.data.auth.repository.model.UserOrganizations
import com.x8bit.bitwarden.data.auth.repository.model.UserState
import com.x8bit.bitwarden.data.auth.repository.model.VaultUnlockType
import com.x8bit.bitwarden.data.platform.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
import com.x8bit.bitwarden.data.vault.repository.model.VaultUnlockData
import com.x8bit.bitwarden.data.vault.repository.util.statusFor
import com.x8bit.bitwarden.ui.platform.base.util.toHexColorRepresentation

View File

@@ -27,7 +27,7 @@ fun Intent.getWebAuthResultOrNull(): WebAuthResult? {
localData
.getQueryParameter("data")
?.let { WebAuthResult.Success(token = it) }
?: WebAuthResult.Failure
?: WebAuthResult.Failure(message = localData.getQueryParameter("error"))
} else {
null
}
@@ -74,5 +74,5 @@ sealed class WebAuthResult {
/**
* Represents a failure in the web auth callback.
*/
data object Failure : WebAuthResult()
data class Failure(val message: String?) : WebAuthResult()
}

View File

@@ -1,7 +1,7 @@
package com.x8bit.bitwarden.data.auth.util
import com.bitwarden.core.annotation.OmitFromCoverage
import com.bitwarden.crypto.Kdf
import com.x8bit.bitwarden.data.platform.annotation.OmitFromCoverage
/**
* Constants relating to [Kdf] initialization defaults.

View File

@@ -1,7 +1,7 @@
package com.x8bit.bitwarden.data.auth.util
import com.bitwarden.crypto.Kdf
import com.x8bit.bitwarden.data.auth.datasource.network.model.PreLoginResponseJson
import com.bitwarden.network.model.PreLoginResponseJson
/**
* Convert [PreLoginResponseJson.KdfParams] to [Kdf] params for use with Bitwarden SDK.

View File

@@ -8,9 +8,9 @@ import android.service.autofill.FillRequest
import android.service.autofill.SaveCallback
import android.service.autofill.SaveRequest
import androidx.annotation.Keep
import com.bitwarden.core.annotation.OmitFromCoverage
import com.x8bit.bitwarden.data.autofill.model.AutofillAppInfo
import com.x8bit.bitwarden.data.autofill.processor.AutofillProcessor
import com.x8bit.bitwarden.data.platform.annotation.OmitFromCoverage
import dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject

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