Compare commits

...

417 Commits

Author SHA1 Message Date
bw-ghapp[bot]
6f6aacabfb Update SDK to 1.0.0-3101-0eba924a (#5893)
Co-authored-by: bw-ghapp[bot] <178206702+bw-ghapp[bot]@users.noreply.github.com>
Co-authored-by: Carlos Gonçalves <cgoncalves@bitwarden.com>
2025-09-19 16:21:39 +00:00
David Perez
b0e0b44671 Pm 25258 browser autofill dialog (#5907) 2025-09-19 16:20:16 +00:00
David Perez
d53f3f313c Refactor Folder logic into FolderManager (#5904) 2025-09-19 15:37:31 +00:00
David Perez
4f244c52fa PM-25908: Process 400 responses from verification code APIs (#5900) 2025-09-19 15:29:28 +00:00
Patrick Honkonen
b4a31764c4 [PM-25824] Add "Import items" screen (#5906) 2025-09-19 13:59:26 +00:00
bw-ghapp[bot]
f4569cef2b Crowdin Pull (#5908)
Co-authored-by: bitwarden-devops-bot <106330231+bitwarden-devops-bot@users.noreply.github.com>
2025-09-19 13:54:27 +00:00
Patrick Honkonen
b4926b72d9 Update registerExport to return RegisterExportResponse (#5903) 2025-09-18 14:37:14 +00:00
Patrick Honkonen
0f899df83c [PM-25826] Update folderRelationships type for cipher import (#5885) 2025-09-17 21:49:16 +00:00
Patrick Honkonen
ff03f49f43 [PM-25912] Remove ImportCredentialsRequest (#5901) 2025-09-17 20:42:42 +00:00
David Perez
2756bd9fde Refactor cipher logic into CipherManager (#5898) 2025-09-17 19:51:44 +00:00
Patrick Honkonen
a39f83349f Move NativeLibraryManager to data module (#5899) 2025-09-17 19:21:37 +00:00
Patrick Honkonen
7d3ed2af88 [PM-25822] Add ImportItemsViewModel and related strings (#5882) 2025-09-17 17:58:22 +00:00
David Perez
8de465381e Refactor Send logic into SendManager (#5892) 2025-09-17 14:37:14 +00:00
Patrick Honkonen
f22f4399be [PM-25664] Add CredentialExchangeImportManager for CXF payload import (#5872) 2025-09-16 21:30:24 +00:00
David Perez
766e6b1bb9 Update resources to use LocalResources (#5894) 2025-09-16 21:01:45 +00:00
David Perez
0fb364128e Update Androidx libraries to latest versions (#5890) 2025-09-16 21:01:28 +00:00
bw-ghapp[bot]
0cbce39499 Update SDK to 1.0.0-3005-5a722fd2 (#5860)
Co-authored-by: bw-ghapp[bot] <178206702+bw-ghapp[bot]@users.noreply.github.com>
Co-authored-by: Carlos Gonçalves <cgoncalves@bitwarden.com>
2025-09-16 17:13:26 +00:00
Patrick Honkonen
f954b0b941 Refactor Vault Sync Logic into VaultSyncManager (#5871) 2025-09-16 16:44:52 +00:00
David Perez
cfd0a5b8a5 Update the Protobuf library (#5891) 2025-09-16 16:40:12 +00:00
renovate[bot]
d61e1cb6f1 [deps]: Update actions/setup-java action to v5 (#5880)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-16 14:48:08 +00:00
renovate[bot]
b31983da8b [deps]: Update actions/checkout action to v5 (#5879)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-16 14:47:36 +00:00
David Perez
e22d309423 Update navigation libs to latest version (#5889) 2025-09-15 23:32:36 +00:00
Patrick Honkonen
9b53095b5e [PM-15051] Add CredentialExchangeRegistry (#5869) 2025-09-15 21:48:56 +00:00
David Perez
c6814c8870 Update to Kotlin v2.2.20 (#5888) 2025-09-15 21:12:11 +00:00
David Perez
7710ad8a73 Update to AGP v8.13.0 (#5887) 2025-09-15 19:55:17 +00:00
Patrick Honkonen
80b3a7e675 [PM-25663] Introduce CredentialExchangeImporter (#5868) 2025-09-15 19:44:03 +00:00
David Perez
8235045dad PM-24234: Add missing plurals (#5886) 2025-09-15 19:02:34 +00:00
Patrick Honkonen
481a8c8fbc [PM-25662] Add CredentialExchangeCompletionManager (#5867) 2025-09-15 18:36:48 +00:00
renovate[bot]
1dc6ea2227 [deps]: Lock file maintenance (#5881)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-15 16:17:39 +00:00
renovate[bot]
6554234898 [deps]: Update gh minor (#5877)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-15 16:16:55 +00:00
David Perez
e990397b29 Update Robolectric to v4.16 (#5833) 2025-09-15 15:33:36 +00:00
bw-ghapp[bot]
417835ef3f Crowdin Pull (#5874)
Co-authored-by: bitwarden-devops-bot <106330231+bitwarden-devops-bot@users.noreply.github.com>
Co-authored-by: Patrick Honkonen <1883101+SaintPatrck@users.noreply.github.com>
2025-09-15 13:48:03 +00:00
aj-rosado
39a6dd1c4b [PM-22320] Default to SHA1 on 2fas importer if algorithm is missing (#5875) 2025-09-13 08:57:13 +00:00
Patrick Honkonen
4093e61b09 [PM-25665] Add BitwardenImportCredentialsRequest and helper (#5870) 2025-09-11 17:51:57 +00:00
Patrick Honkonen
c4adf3ad42 [PM-25661] Add placeholder ProviderEvents API for credential import/export (#5866) 2025-09-11 14:06:57 +00:00
André Bispo
417a1494e3 [PM-25640] Dialog flickers when switching accounts (#5865) 2025-09-11 13:41:34 +00:00
André Bispo
ef39ea6d5d [PM-25624] Hide decryption errors from autofill list view (#5855) 2025-09-11 13:41:21 +00:00
Patrick Honkonen
f6c20e08d1 [PM-25637] Add CXF module for Credential Exchange support (#5858) 2025-09-11 12:49:06 +00:00
Álison Fernandes
987e065dd7 Fix sdk-update Test by using Java 21 in setup-android action (#5861) 2025-09-10 18:31:37 +00:00
Patrick Honkonen
ba7ee04281 [PM-15056] Add exportVaultDataToCxf function to VaultRepository (#5847) 2025-09-10 14:40:05 +00:00
Konrad
808d57edc5 Update untranslatable strings (#5854) 2025-09-10 13:43:50 +00:00
David Perez
3356925c7a Update to Java 21 (#5835) 2025-09-10 13:41:41 +00:00
bw-ghapp[bot]
0487d95122 Update SDK to 1.0.0-2944-8447df0c (#5830)
Co-authored-by: bw-ghapp[bot] <178206702+bw-ghapp[bot]@users.noreply.github.com>
Co-authored-by: Carlos Gonçalves <cgoncalves@bitwarden.com>
2025-09-10 09:03:31 +00:00
bw-ghapp[bot]
0834a7a883 Crowdin Pull (#5853)
Co-authored-by: bitwarden-devops-bot <106330231+bitwarden-devops-bot@users.noreply.github.com>
2025-09-08 16:17:54 +00:00
David Perez
2b0e8f9941 Update appVersionName to 2025.9.1 (#5848) 2025-09-05 21:52:04 +00:00
Patrick Honkonen
0702078b04 [PM-25523] Add importCxfPayload to VaultRepository (#5846) 2025-09-05 19:57:43 +00:00
David Perez
46c7e79039 Cleanup minor lint warnings in string resources (#5843) 2025-09-05 19:56:46 +00:00
David Perez
1d6e733c08 Update protobuff library to v4.32.0 (#5845) 2025-09-05 18:56:12 +00:00
Patrick Honkonen
a298b85374 [PM-25522] Add importCxf function to VaultSdkSource (#5841) 2025-09-05 18:56:01 +00:00
David Perez
fe79ea4822 PM-25162: Fix a navigation bug in bottom navigation (#5842) 2025-09-05 16:38:11 +00:00
Patrick Honkonen
4c50f873e2 [PM-15055] Add SDK support for exporting vault data to CXF (#5840) 2025-09-05 16:29:32 +00:00
David Perez
2bd4834b14 PM-25478: Update sends and folders while vault is locked (#5837) 2025-09-05 14:32:49 +00:00
David Perez
393931a5c6 PM-25474: Allow SYNC_CIPHER_DELETE notification to delete Cipher for inactive user (#5836) 2025-09-05 14:32:34 +00:00
bw-ghapp[bot]
fe6346013b Crowdin Pull (#5838)
Co-authored-by: bitwarden-devops-bot <106330231+bitwarden-devops-bot@users.noreply.github.com>
2025-09-05 13:38:33 +00:00
Konrad
41e499fdf5 [PM-25133] Plural forms (#5773)
Co-authored-by: Patrick Honkonen <1883101+SaintPatrck@users.noreply.github.com>
Co-authored-by: Patrick Honkonen <phonkonen@bitwarden.com>
2025-09-04 18:13:58 +00:00
David Perez
aa39e6c6be PM-25462: Allow SYNC_FOLDER_DELETE notification to delete Folders for inactive user (#5832) 2025-09-04 17:31:00 +00:00
David Perez
eec4233486 PM-25431: Allow SYNC_SEND_DELETE notification to delete sends for inactive user (#5827) 2025-09-04 15:29:35 +00:00
David Perez
58db64da1a Update Kotlin to the latest version v2.2.10 (#5828) 2025-09-03 22:40:07 +00:00
David Perez
a7d0d6844d Update Hilt to v2.57.1 (#5826) 2025-09-03 20:09:57 +00:00
David Perez
249e1d3a5c Update Firebase BOM (#5823) 2025-09-03 18:00:03 +00:00
bw-ghapp[bot]
d8f3e7af92 Update SDK to 1.0.0-2887-7b5d9db2 (#5815)
Co-authored-by: bw-ghapp[bot] <178206702+bw-ghapp[bot]@users.noreply.github.com>
2025-09-03 14:08:58 +00:00
Patrick Honkonen
1c4e4dcaf4 [PM-25394] Sort default user collections by Organization name (#5819) 2025-09-03 14:04:45 +00:00
David Perez
9adc25471e Update the Compose BOM and Androidx Lifecycle libraries (#5820) 2025-09-03 13:44:51 +00:00
Patrick Honkonen
ec6562336c [PM-23665] Refactor FIDO2 credential discovery (#5817) 2025-09-03 13:15:32 +00:00
Álison Fernandes
f402391ed8 [PM-25396] Publish store builds when release branches are updated (#5821) 2025-09-03 12:45:05 +00:00
David Perez
9b074f2106 PM-25393: Allow push notifications to update a cipher while vault is locked (#5818) 2025-09-02 20:27:30 +00:00
David Perez
3fa33faa35 Update AGP to v8.12.2 (#5816) 2025-09-02 18:29:25 +00:00
Patrick Honkonen
e1434dfe21 [PM-25327] Display default user collections first (#5810) 2025-09-02 18:25:55 +00:00
Álison Fernandes
659bbc5169 [PM-24930] Fix updating open SDK PRs and set token permissions (#5804) 2025-09-01 13:59:33 +00:00
bw-ghapp[bot]
dfa1f24c30 Crowdin Pull (#5807)
Co-authored-by: bitwarden-devops-bot <106330231+bitwarden-devops-bot@users.noreply.github.com>
Co-authored-by: Patrick Honkonen <1883101+SaintPatrck@users.noreply.github.com>
2025-08-29 16:30:03 +00:00
bw-ghapp[bot]
4f65c3f7d3 Update SDK to 1.0.0-2825-e05ba6eb (#5809)
Co-authored-by: bw-ghapp[bot] <178206702+bw-ghapp[bot]@users.noreply.github.com>
2025-08-29 13:34:03 +00:00
Patrick Honkonen
0f74c3dded Fix plurals string for decryption error (#5796) 2025-08-28 15:48:05 +00:00
Patrick Honkonen
f7139b8b91 [PM-25239] Remove unnecessary vault sync from Fido2CredentialStoreImpl (#5794) 2025-08-28 15:47:44 +00:00
David Perez
2b35ac0d3a PM-25143: Retain intent data on recreate (#5787) 2025-08-27 19:45:16 +00:00
David Perez
4a79d7e6c8 PM-25238: Remove debug toast (#5792) 2025-08-27 16:43:03 +00:00
bw-ghapp[bot]
b9a496aa57 Update SDK to 1.0.0-2807-bc66e3d0 (#5785)
Co-authored-by: bw-ghapp[bot] <178206702+bw-ghapp[bot]@users.noreply.github.com>
2025-08-27 15:23:01 +00:00
André Bispo
0a398839c4 [PM-18210] Cipher key encryption error handling (#5611)
Co-authored-by: Patrick Honkonen <1883101+SaintPatrck@users.noreply.github.com>
Co-authored-by: Patrick Honkonen <phonkonen@bitwarden.com>
2025-08-27 13:00:00 +00:00
Patrick Honkonen
aab8198457 [PM-25057] Refactor card restriction logic in AutofillCipherProvider (#5788) 2025-08-26 18:47:18 +00:00
David Perez
d2d89b5a0f PM-25193: Clear last sync time on push notification for inactive user (#5784) 2025-08-26 18:28:51 +00:00
David Perez
ddadd0135f PM-25194: Fix CollectionTypeJson data type in database (#5786) 2025-08-25 21:40:59 +00:00
David Perez
dc198eaf72 PM-25125: Refactor user state managment into UserStateManager (#5774) 2025-08-25 18:45:43 +00:00
David Perez
ff23dc3ab2 PM-25069: Update VaultAddEditViewModel toasts to snackbars (#5769) 2025-08-25 18:45:12 +00:00
Patrick Honkonen
191ff4c652 Update ARCHITECTURE.md (#5765) 2025-08-25 18:18:38 +00:00
bw-ghapp[bot]
99ab2245f6 Update SDK to 1.0.0-2681-1a956d45 (#5756)
Co-authored-by: bw-ghapp[bot] <178206702+bw-ghapp[bot]@users.noreply.github.com>
Co-authored-by: Carlos Gonçalves <cgoncalves@bitwarden.com>
2025-08-22 18:57:39 +00:00
bw-ghapp[bot]
bc7e682941 Crowdin Pull (#5772)
Co-authored-by: bitwarden-devops-bot <106330231+bitwarden-devops-bot@users.noreply.github.com>
2025-08-22 16:50:45 +00:00
David Perez
517829e7b0 Remove the RemoveCardPolicy feature flag (#5770) 2025-08-22 16:33:08 +00:00
Patrick Honkonen
a1c6276092 [PM-25057] Filter Card Autofill Ciphers by Policy (#5768) 2025-08-21 13:57:19 +00:00
Patrick Honkonen
bc67bf3dff Suppress Gradle lint warnings (#5767) 2025-08-20 21:54:37 +00:00
renovate[bot]
bc5788556c [deps]: Update gh minor (#5766)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-20 20:24:25 +00:00
David Perez
45e20d8c9e PM-17755: Fix comparator inconsistency based on Locale (#5762) 2025-08-20 20:20:03 +00:00
David Perez
a972a40a49 Update AGP to v8.12.1 (#5763) 2025-08-20 20:19:44 +00:00
aj-rosado
717d5665e0 [PM-24697] Allow cleartext traffic on OCSP and CRL servers (#5761) 2025-08-20 20:10:03 +00:00
David Perez
bc0a18f250 Standardize ui model packages (#5760) 2025-08-20 16:32:00 +00:00
David Perez
2f72553454 PM-22465: Identity state is not pre-populated on edit screen (#5759) 2025-08-20 16:15:16 +00:00
David Perez
e5a1546291 PM-25028: Migrate coachmarks and tooltips to UI module (#5757) 2025-08-20 16:04:19 +00:00
Patrick Honkonen
d8e319948c [PM-25027] Rename "Ask to add login" to "Ask to add item" (#5758) 2025-08-20 16:03:36 +00:00
David Perez
b3528249e9 PM-24544: Update Segmented Control to handle large font better (#5748) 2025-08-20 14:59:40 +00:00
David Perez
5f42c9bb39 PM-25006: Migrate row components to the UI module (#5753) 2025-08-19 22:07:26 +00:00
Patrick Honkonen
b010c9a29d [PM-24226] Reorder SSH key fields (#5754) 2025-08-19 22:00:41 +00:00
Patrick Honkonen
3e55f561c9 [PM-24940] Add Card Brand to Autofill (#5750) 2025-08-19 21:38:24 +00:00
David Perez
277e4d8d6f PM-20198: Update generator modal 'Save' button to 'Apply' (#5745) 2025-08-19 21:27:17 +00:00
David Perez
32e8fb7d8e PM-25004: Migrate the MultiSelectButton to the UI module (#5752) 2025-08-19 21:03:06 +00:00
David Perez
4a18e57cca PM-25003: Migrate bottom sheet to the UI module (#5751) 2025-08-19 20:58:03 +00:00
David Perez
070ef45087 PM-24993: Move account components to UI module (#5749) 2025-08-19 19:47:59 +00:00
Patrick Honkonen
a658cf890a Refactor AccountKeysJson property names (#5747) 2025-08-19 17:10:16 +00:00
David Perez
d3dea3c9cb PM-24283: Migrate the common dialogs to the UI module (#5746) 2025-08-19 16:33:25 +00:00
Patrick Honkonen
5ab0517bf3 [PM-24577] Provision SDK with AccountKeys (#5682) 2025-08-19 16:00:34 +00:00
Álison Fernandes
e8b01c2d44 [PM-24930] New workflow to update the SDK and test ongoing work (#5742) 2025-08-19 15:19:57 +00:00
Patrick Honkonen
b34d873471 [PM-24411] Migrate IntentManager to ui module (#5634) 2025-08-19 15:13:40 +00:00
David Perez
3c3d8710c9 PM-24944: Migrate scaffold to ui module (#5738) 2025-08-19 13:53:00 +00:00
Igorro
20dea9b5ff Fix autofill overwriting user data with empty field values (#5649) 2025-08-19 13:47:31 +00:00
Patrick Honkonen
44410efe56 [PM-24938] Improve Autofill Card Expiration Month and Year Parsing (#5717) 2025-08-18 21:27:39 +00:00
bitwarden-charlie
a999592fb6 chore/SRE-583 Deprecate usage of Auth-Email Header (#5097)
Co-authored-by: sneakernuts <671942+sneakernuts@users.noreply.github.com>
2025-08-18 21:03:34 +00:00
David Perez
25a78f60ab PM-24942: Move Segmented control to UI module (#5727) 2025-08-18 20:51:31 +00:00
renovate[bot]
a8546bb4eb [deps]: Update gh minor (#5722)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-18 20:46:44 +00:00
David Perez
6c6d4f2d91 PM-24950: Migrate the image package to the ui module (#5731) 2025-08-18 20:45:24 +00:00
David Perez
7347d91fdd PM-24949: Move headers package to the ui module (#5730) 2025-08-18 20:40:13 +00:00
David Perez
0a99359978 PM-24943: Move the scrim package to the UI module (#5728) 2025-08-18 20:32:28 +00:00
David Perez
aca4b05b59 PM-24798: Move text components to UI module (#5718) 2025-08-18 18:08:14 +00:00
Patrick Honkonen
b0c7995cb7 Support both camel and pascal case for AccountKeysJson (#5724) 2025-08-18 17:31:59 +00:00
Patrick Honkonen
af322b5d1f [PM-24599] Add cardholderName to AutofillSaveItem.Card (#5716) 2025-08-18 16:29:26 +00:00
Álison Fernandes
9fcfcc9e41 [PM-24930] Add placeholder workflow for sdlc-sdk-update.yml (#5723) 2025-08-18 15:52:19 +00:00
aj-rosado
ff6b7b675d [PM-24347] Tracking UserClientExportedVault event when user exports the vault (#5710) 2025-08-18 15:10:52 +00:00
David Perez
3164c29184 PM-24786: Move radio button to UI module (#5708) 2025-08-15 21:14:14 +00:00
David Perez
c5663431af Update the app dependencies (#5715) 2025-08-15 21:04:04 +00:00
Patrick Honkonen
4fb96cb782 [PM-24598] Map AutofillSaveItem to VaultItemCipherType (#5714) 2025-08-15 20:12:01 +00:00
David Perez
36e06cdac7 PM-24770: Move snackbars to the UI module (#5712) 2025-08-15 18:46:25 +00:00
David Perez
3cf325becf Rename the AutofillTotpCopyActivity (#5713) 2025-08-15 18:24:05 +00:00
Patrick Honkonen
584bdb6277 [PM-24700] Update email validation in LandingViewModel (#5711) 2025-08-15 17:34:42 +00:00
David Perez
b2a9f4b455 Remove context param from IntentManager extensions (#5706) 2025-08-15 17:31:26 +00:00
Patrick Honkonen
b0b4379307 [PM-24411] Extract Authenticator functions from IntentManager (#5702) 2025-08-15 16:09:21 +00:00
Patrick Honkonen
b9cc664efa Refactor Detekt task to use staged files (#5705) 2025-08-15 16:07:56 +00:00
aj-rosado
e30e0ffbb4 [PM-23723] Fix close and cancel text on Match detection dialogs (#5707) 2025-08-15 16:05:37 +00:00
Patrick Honkonen
2ffd71c69a Fix Autofill settings deeplink (#5704) 2025-08-15 15:59:30 +00:00
David Perez
3488ad6217 PM-24771: Move the slider to the UI module (#5698) 2025-08-15 15:26:20 +00:00
Patrick Honkonen
58005d908a [PM-24740] Make VaultAddEditUriItem a multiline URI field (#5700) 2025-08-15 14:06:38 +00:00
David Perez
a320e6ea61 PM-24769: Move the stepper to the UI module (#5699) 2025-08-15 14:04:12 +00:00
bw-ghapp[bot]
5a23ceabc1 Crowdin Pull (#5701)
Co-authored-by: bitwarden-devops-bot <106330231+bitwarden-devops-bot@users.noreply.github.com>
2025-08-15 02:06:13 +00:00
David Perez
f4102bcd30 Update Autofill logging (#5697) 2025-08-14 22:09:49 +00:00
David Perez
6d25c12271 PM-24768: Move text fields to the UI module (#5696) 2025-08-14 21:00:21 +00:00
Patrick Honkonen
ef03cdb2db [PM-24652] Remove AEAD enrollment on key rotation feature flag (#5695) 2025-08-14 20:39:08 +00:00
David Perez
474ec4907f PM-24726: Update MDM functionality (#5694) 2025-08-14 18:21:24 +00:00
Patrick Honkonen
a68fd8b44f [PM-24721] Refactor AccountKeys to top-level common model (#5693) 2025-08-14 18:12:03 +00:00
David Perez
3282992221 PM-24727: Update VaultUnlockScreen to use user specific environment (#5690) 2025-08-14 14:06:19 +00:00
Patrick Honkonen
26252ebcdb [PM-24411] Generalize IntentManager activity handling (#5689) 2025-08-13 22:03:09 +00:00
aj-rosado
a688693f43 [PM-23723] URI Matching detection layout updates on advanced options (#5574) 2025-08-13 16:09:29 +00:00
David Perez
3ed63ef5eb PM-24688: Use the realtime elapse time to determine vault lock timeouts (#5684) 2025-08-13 15:04:19 +00:00
David Perez
1e2bc4aa70 PM-24690: Use ToastManager in MainViewModel (#5685) 2025-08-13 15:04:02 +00:00
aj-rosado
694865c213 [PM-24642] Remove captcha connector code (#5677) 2025-08-12 20:56:18 +00:00
David Perez
29243c8f44 Remove unused ClearClipboardWorker from Authenticator (#5683) 2025-08-12 18:02:41 +00:00
Andy Pixley
4e1dfcaeec [BRE-1074] Adding debug info for failing to find release (#5673) 2025-08-12 17:11:13 +00:00
Patrick Honkonen
75f3065085 [PM-24569] Save accountKeys to AuthDiskSource (#5679) 2025-08-12 16:54:53 +00:00
Álison Fernandes
402e399fd4 [PM-24675] Fix renovate update warning (#5680) 2025-08-12 15:09:05 +00:00
Álison Fernandes
810cbc8da5 [PM-24590] Add support to hotfix specific apps in Cut Release Branch workflow (#5671) 2025-08-12 14:37:04 +00:00
Patrick Honkonen
9bfbe0c087 [PM-24568] Add accountKeys to SyncResponseJson.Profile (#5678) 2025-08-11 19:37:30 +00:00
Patrick Honkonen
d06c87beb3 [PM-24411] Use BuildInfoManager for build-related information (#5663) 2025-08-11 18:34:47 +00:00
Matt Andreko
9b120701eb Fix reusable scan in CI build (#5668) 2025-08-08 20:58:04 +00:00
David Perez
e8f1242744 Add header and custom supportContent functionality to BitwardenMultiSelectButton (#5669) 2025-08-08 18:24:43 +00:00
Patrick Honkonen
1c525b9dfc [PM-24575] Add feature flag for AEAD enrollment on key rotation (#5665) 2025-08-08 14:43:34 +00:00
Álison Fernandes
c613c2df86 [PM-24564] Address GitHub Release creation workflow feedback (#5666) 2025-08-08 12:38:34 +00:00
Álison Fernandes
9a9125321e [PM-24589] Trigger CI builds for release branches (#5667) 2025-08-08 12:35:19 +00:00
bw-ghapp[bot]
2902b89402 Crowdin Pull (#5664)
Co-authored-by: bitwarden-devops-bot <106330231+bitwarden-devops-bot@users.noreply.github.com>
2025-08-08 01:56:34 +00:00
Patrick Honkonen
93edbb61bf [PM-24411] Add MIME type parameter to file chooser intent (#5661) 2025-08-07 17:55:50 +00:00
David Perez
85bc76d0a6 PM-24565: Syncronize token refreshes to avoid duplicate requests (#5662) 2025-08-07 17:54:22 +00:00
Patrick Honkonen
db18e8012a [PM-24411] Use shareErrorReport in BitwardenBasicDialog (#5656) 2025-08-07 16:13:59 +00:00
Patrick Honkonen
db03c7d703 Refactor Autofill Hint Logic and Add Card Autofill Support (#5640) 2025-08-07 14:47:53 +00:00
Matt Andreko
6ee7f9b80f Update scan workflow to use centralized reusable component (#5592) 2025-08-07 14:26:49 +00:00
David Perez
fc88ca1ba8 PM-24539: Prevent token refresh from looping (#5658) 2025-08-07 13:53:56 +00:00
David Perez
3c033d4aa2 PM-24481: Logout when token refresh API returns 401 or 403 (#5651) 2025-08-06 20:38:01 +00:00
Patrick Honkonen
59c2261e7c [PM-24411] Add shareErrorReport to IntentManager (#5655) 2025-08-06 20:25:40 +00:00
Patrick Honkonen
b6aa0952b1 Set base.archivesName for app and authenticator modules (#5657) 2025-08-06 20:25:26 +00:00
Patrick Honkonen
905e3248f2 [PM-24411] Introduce BuildInfoManager for build-related information (#5654) 2025-08-06 18:53:03 +00:00
David Perez
72250dce90 [PM-24481] Update AuthTokenInterceptor to refresh token on expiration (#5647) 2025-08-06 18:05:07 +00:00
Carlos Gonçalves
60ee129e0b [PM-24456] Update bitwarden sdk to 1.0.0-2450-9fe3aeda (#5652) 2025-08-06 15:50:27 +00:00
André Bispo
911bb40be8 [PM-24473] Remove exemption from restrict item types policy (#5646) 2025-08-05 22:15:40 +00:00
David Perez
308a8a564c Update to Gradle v9.0.0 (#5642) 2025-08-05 21:31:44 +00:00
David Perez
337e751c05 Move FileData to 'ui' module (#5644) 2025-08-05 18:01:36 +00:00
Patrick Honkonen
f4c4e06dcc [PM-24411] Extract pending intent management for Credential Manager requests (#5636) 2025-08-05 14:04:12 +00:00
David Perez
38b92133ff PM-24440: Log user out for 'invalid_grant' (#5641) 2025-08-04 19:13:44 +00:00
renovate[bot]
e381d72d5c [deps]: Update gh minor (#5631)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-04 18:39:11 +00:00
David Perez
87a61bbbbd Update to AGP 8.12.0 (#5639) 2025-08-04 15:23:40 +00:00
David Perez
7cc3c1c755 Handle tile intents without IntentManager (#5635) 2025-08-01 20:31:25 +00:00
David Perez
f614d6039f Commonize version name and bump it (#5559) 2025-08-01 16:58:55 +00:00
renovate[bot]
a6d622c3b9 [deps]: Lock file maintenance (#5632)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-01 16:39:48 +00:00
David Perez
b781acb1fa Update Androidx dependencies to the latest versions (#5630) 2025-08-01 16:19:27 +00:00
mKoonrad
45f0ddc60f [PM-24292] Correct redundant string interpolation (#5614)
Co-authored-by: Patrick Honkonen <1883101+SaintPatrck@users.noreply.github.com>
Co-authored-by: Patrick Honkonen <phonkonen@bitwarden.com>
2025-08-01 15:28:14 +00:00
David Perez
8876418177 Add fingerprint to flight recorder (#5625) 2025-08-01 15:07:00 +00:00
David Perez
67b64034ff Update Junit to v5.13.4 (#5624) 2025-08-01 15:02:05 +00:00
bw-ghapp[bot]
79a232919a Crowdin Pull (#5628)
Co-authored-by: bitwarden-devops-bot <106330231+bitwarden-devops-bot@users.noreply.github.com>
2025-08-01 14:34:06 +00:00
David Perez
2fa9ea18b5 PM-15229: Accomidate system bars on specific Android 15 revisions (#5617) 2025-07-30 21:07:06 +00:00
Patrick Honkonen
9b297286e5 [PM-24112] Remove Password Manager strings and translations (#5590) 2025-07-30 20:51:04 +00:00
David Perez
376a62edaf Fix lint warnings and imports (#5623) 2025-07-30 20:32:18 +00:00
Patrick Honkonen
01570c2555 [PM-24113] Remove Authenticator strings and translations (#5589) 2025-07-30 20:06:30 +00:00
Patrick Honkonen
4a3db4fea7 [PM-24175] Refactor Crowdin workflow (#5587) 2025-07-30 18:03:13 +00:00
David Perez
3c0818232f Add logging for Biometric errors (#5621) 2025-07-30 16:56:29 +00:00
David Perez
1799d0b716 PM-24303: Master password reprompt fix (#5620) 2025-07-30 16:27:29 +00:00
Patrick Honkonen
cf896d6bf1 [PM-24206] Fix filtered verification code search (#5619) 2025-07-30 15:34:57 +00:00
Patrick Honkonen
40dff74d3f [PM-22814] Migrate BitwardenCard to the ui module (#5615) 2025-07-30 00:27:26 +00:00
David Perez
ddd2d7fad5 PM-24275: Move content package to 'ui' module (#5613) 2025-07-29 17:45:10 +00:00
David Perez
b4efc0e59d PM-24267: Move indicators to 'ui' module (#5612) 2025-07-29 16:30:04 +00:00
David Perez
4ffd41c33f PM-24245: Remove the restrict-item-deletion-to-can-manage-permission feature flag (#5606) 2025-07-29 14:31:13 +00:00
David Perez
a70f441064 PM-24240: Remove email verification feature flag (#5605) 2025-07-28 20:45:45 +00:00
Carlos Gonçalves
867e2287dc [PM-24157] Update Bitwarden SDK to 1.0.0-20250728.143558-250 (#5602) 2025-07-28 20:25:13 +00:00
Patrick Honkonen
912f734cae [PM-24205] Fix Fido2CredentialStore to save new credentials correctly (#5601) 2025-07-28 19:13:09 +00:00
Patrick Honkonen
02b5cbb199 [PM-24204] Correct TOTP generation to use cipherId instead of totpCode (#5599) 2025-07-28 18:45:37 +00:00
David Perez
f589546e6a PM-24176: Consolidate all FlagKeys (#5593) 2025-07-28 18:05:55 +00:00
David Perez
517198b265 Fix crash in Android 13 (#5588) 2025-07-25 18:42:38 +00:00
David Perez
91f1180be7 PM-20150, PM-20151: Remove single tap passkey feature flags (#5585) 2025-07-25 18:05:18 +00:00
bw-ghapp[bot]
8589a37e5a Crowdin Pull - Password Manager (#5586)
Co-authored-by: bitwarden-devops-bot <106330231+bitwarden-devops-bot@users.noreply.github.com>
2025-07-25 17:00:45 +00:00
bw-ghapp[bot]
e4678cc7df Crowdin Pull - Authenticator (#5584)
Co-authored-by: bitwarden-devops-bot <106330231+bitwarden-devops-bot@users.noreply.github.com>
2025-07-25 15:35:34 +00:00
David Perez
e665c386ff PM-20152: Remove import logins flow feature flag (#5580) 2025-07-25 14:14:48 +00:00
bw-ghapp[bot]
2f2ec71fc4 Crowdin Pull - Authenticator (#5581)
Co-authored-by: bitwarden-devops-bot <106330231+bitwarden-devops-bot@users.noreply.github.com>
2025-07-25 14:13:00 +00:00
bw-ghapp[bot]
7b115df83a Crowdin Pull - Password Manager (#5582)
Co-authored-by: bitwarden-devops-bot <106330231+bitwarden-devops-bot@users.noreply.github.com>
2025-07-25 14:12:53 +00:00
bw-ghapp[bot]
edd1763198 Crowdin Pull - Password Manager (#5578)
Co-authored-by: bitwarden-devops-bot <106330231+bitwarden-devops-bot@users.noreply.github.com>
2025-07-24 21:11:13 +00:00
Patrick Honkonen
37d3ff30e4 [PM-24002] Copy Authenticator strings to ui module (#5576) 2025-07-24 21:10:58 +00:00
David Perez
258a58aa25 PM-24137, PM-24138: Remove host alias feature flags (#5575) 2025-07-24 20:46:46 +00:00
Patrick Honkonen
da5dcef41e [PM-24111] Copy Password Manager strings to ui module (#5569) 2025-07-24 19:30:05 +00:00
David Perez
7a578ff2c5 Update the version name to 2025.7.0 (#5572) 2025-07-24 16:34:18 +00:00
Nailik
355facc36b [PM-13789] add credential manager provider for passwords (#4110)
Co-authored-by: Patrick Honkonen <phonkonen@bitwarden.com>
Co-authored-by: Patrick Honkonen <rizzin@gmail.com>
Co-authored-by: Patrick Honkonen <1883101+SaintPatrck@users.noreply.github.com>
2025-07-24 15:29:55 +00:00
David Perez
c60f3131b6 PM-24090: Remove ChromeAutofill feature flag (#5567) 2025-07-24 14:49:02 +00:00
David Perez
bb950c8c59 PM-24089: Remove Mutual TLS feature flag (#5566) 2025-07-24 13:33:32 +00:00
David Perez
c7df80ff00 PM-24088: Remove the MobileErrorReporting feature flag (#5565) 2025-07-24 13:33:22 +00:00
David Perez
d308b84943 PM-24087: Update the add/edit ssh key title (#5564) 2025-07-23 21:06:44 +00:00
David Perez
79ad18877d Update Androidx and Hilt dependencies (#5563) 2025-07-23 20:05:36 +00:00
David Perez
4f51507e4b Update Mockk to v1.14.5 (#5562) 2025-07-23 20:05:22 +00:00
David Perez
88fcd35d1a Update Firebase to v34.0.0 (#5561) 2025-07-23 20:05:04 +00:00
Patrick Honkonen
987639b2a3 [PM-23817] Move PM string to UI module and update Crowdin configuration (#5550) 2025-07-23 19:49:54 +00:00
David Perez
d32b4c7c7e PM-24075: Update Dynamic colors copy (#5560) 2025-07-23 16:20:27 +00:00
David Perez
9ed59e61a3 PM-24035: Add tooltip for website icons (#5554) 2025-07-22 20:06:54 +00:00
David Perez
3342ebf139 PM-19185: Persist pin after a soft-logout (#5555) 2025-07-22 20:06:34 +00:00
Patrick Honkonen
4050215145 Disable MissingTranslation and ExtraTranslation lint checks in UI module (#5558) 2025-07-22 20:03:49 +00:00
Patrick Honkonen
3e0ee5fcd8 [PM-22744] Refactor to use CipherListView as primary cipher source (#5494) 2025-07-22 20:00:08 +00:00
Andy Pixley
fcd7326f2c [BRE-831] Switching to use AKV instead of GitHub secrets (#5553) 2025-07-22 14:53:14 +00:00
David Perez
c94fe56b47 PM-24004: Push notification for sync should bypass 30 minute interval (#5552) 2025-07-21 19:44:13 +00:00
Patrick Honkonen
17287680d9 Allow asterisk in email validation (#5549) 2025-07-21 15:49:16 +00:00
renovate[bot]
e4935318de [deps]: Lock file maintenance (#5548)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-21 14:38:12 +00:00
Amy Galles
f22643fec1 [BRE-768] Automate Google Play publishing (#5256)
Co-authored-by: Álison Fernandes <vvolkgang@users.noreply.github.com>
2025-07-21 14:11:30 +00:00
David Perez
6454dc1a58 PM-23878: Move filterTouchesWhenObscured to avoid actionbar issues (#5546) 2025-07-18 22:14:06 +00:00
David Perez
411e359600 PM-23878: Add filter touches when obscured (#5545) 2025-07-18 20:45:12 +00:00
David Perez
e75d7844de PM-23910: Disallow file sends for non-premium users (#5544) 2025-07-18 20:44:52 +00:00
David Perez
25680f9255 PM-18405: Update the AboutScreen copy info (#5538) 2025-07-18 15:19:55 +00:00
David Perez
628cb12081 VULN-261: Filter out send intents that use our own content provider (#5539) 2025-07-18 14:56:01 +00:00
bw-ghapp[bot]
710e35680b Crowdin Pull - Authenticator (#5541)
Co-authored-by: bitwarden-devops-bot <106330231+bitwarden-devops-bot@users.noreply.github.com>
2025-07-18 12:42:38 +00:00
bw-ghapp[bot]
b5cd0c9d9d Crowdin Pull - Password Manager (#5542)
Co-authored-by: bitwarden-devops-bot <106330231+bitwarden-devops-bot@users.noreply.github.com>
2025-07-18 12:42:22 +00:00
Carlos Gonçalves
9995fa92f1 [PM-23871] Update Bitwarden SDK (#5537) 2025-07-17 18:39:55 +00:00
André Bispo
44aae70fe4 [PM-23314] Enforce HTTPS (#5533) 2025-07-17 18:27:10 +00:00
Patrick Honkonen
fca4ebe023 [PM-23681] Update TotpCodeManager to use CipherListView (#5532) 2025-07-17 16:10:41 +00:00
Patrick Honkonen
2d2a5e74da Fix unmockkStatic usage in SdkCipherRepositoryTest (#5534) 2025-07-17 00:42:41 +00:00
Michał Chęciński
b53ca30974 [BRE-769] Use Fastlane to keep github releases in sync with mobile deploy versions (#5219)
Co-authored-by: Patrick Honkonen <1883101+SaintPatrck@users.noreply.github.com>
Co-authored-by: Álison Fernandes <vvolkgang@users.noreply.github.com>
2025-07-16 15:32:21 +00:00
mpbw2
8178a61dba [PM-22335] Support fastlane dev via rbenv (#5390) 2025-07-16 14:13:21 +00:00
Patrick Honkonen
f0bdc8ede3 Update authenticatorbridge README (#5423) 2025-07-16 13:53:17 +00:00
Andy Pixley
145c19da22 [BRE-831] migrate secrets akv (#5347) 2025-07-15 20:05:10 +00:00
André Bispo
39b1409cbd [PM-22399] Send 2FA email when view appears (#5498)
Co-authored-by: Patrick Honkonen <1883101+SaintPatrck@users.noreply.github.com>
2025-07-15 16:31:37 +00:00
André Bispo
f26d54a2e2 [PM-23696] Hide cards from export when policy is enabled. (#5520) 2025-07-15 15:21:39 +00:00
David Perez
33cfaa5e95 PM-23774: Simplify AuthenticatorBridgeRepositoryImpl (#5529) 2025-07-15 14:15:01 +00:00
David Perez
9274e0f349 Update the Androidx Crypto library (#5527) 2025-07-14 21:36:13 +00:00
David Perez
46656d659e PM-23666: Construct unique SDK client for Authenticator Sync feature (#5510) 2025-07-14 20:53:09 +00:00
Patrick Honkonen
811f0f2757 [PM-23608] Add SDK method for generating TOTP for CipherListView (#5519) 2025-07-14 20:02:20 +00:00
David Perez
8f783a43e4 Update OkHttp to v5.1.0 (#5524) 2025-07-14 19:23:37 +00:00
David Perez
b8f74cdefa Update to Junit v5.13.3 (#5523) 2025-07-14 19:23:21 +00:00
David Perez
5e6dcb5b58 Update to AGP v8.11.1 (#5522) 2025-07-14 19:23:08 +00:00
André Bispo
c5a40a89d9 [PM-23546] Update 2FA verification code accept any length (#5500)
Co-authored-by: Patrick Honkonen <1883101+SaintPatrck@users.noreply.github.com>
2025-07-14 17:18:18 +00:00
Carlos Gonçalves
929233081c [PM- 22735] Unsafe deserialization parcel data intent (#5419)
Co-authored-by: David Perez <david@livefront.com>
2025-07-14 14:34:26 +00:00
aj-rosado
37af6a1773 [PM-23710] Fixed logic to getServerConfig and added new test on Authenticator (#5518) 2025-07-11 14:03:48 +00:00
bw-ghapp[bot]
557c5b46a5 Crowdin Pull - Password Manager (#5517)
Co-authored-by: bitwarden-devops-bot <106330231+bitwarden-devops-bot@users.noreply.github.com>
2025-07-11 13:28:24 +00:00
bw-ghapp[bot]
390ef34398 Crowdin Pull - Authenticator (#5516)
Co-authored-by: bitwarden-devops-bot <106330231+bitwarden-devops-bot@users.noreply.github.com>
2025-07-11 13:27:43 +00:00
David Perez
d2f7d52132 PM-23693: Remove Authenticator Sync flag from Authenticator app (#5515) 2025-07-11 13:27:10 +00:00
David Perez
0feac46711 PM-23692: Remove auth sync feature flag from password manager (#5514) 2025-07-11 13:17:22 +00:00
David Perez
bc50c0d873 PM-23690: Remove pre-login settings feature flag (#5513) 2025-07-10 21:32:50 +00:00
David Perez
fb3b9c9ea7 PM-23691: remove Flight Recorder feature flag (#5512) 2025-07-10 21:18:14 +00:00
David Perez
9a81e18cb4 PM-23625: Remove truncation logic for default deletion date of send (#5511) 2025-07-10 21:07:39 +00:00
Patrick Honkonen
f9914e5b46 [PM-21750] Only show dynamic colors option on Android 12+ (#5507) 2025-07-10 19:40:28 +00:00
David Perez
e193661f5f PM-23667: Optimize authenticator sync with totp database query (#5508) 2025-07-10 19:03:02 +00:00
Patrick Honkonen
532fcbb40e [PM-23605] Add decryptCipherListWithFailures to VaultSdkSource (#5505) 2025-07-10 13:03:50 +00:00
David Perez
187d50faa2 Update navigation library to v2.9.1 (#5503) 2025-07-09 20:26:00 +00:00
Patrick Honkonen
8f5376c2de [PM-23606] Update Bitwarden SDK (#5504) 2025-07-09 20:23:38 +00:00
David Perez
56192a7e8b Add 'getCipher' helper method (#5501) 2025-07-09 19:23:40 +00:00
David Perez
70350746ce Update the version at which we display the clipboard toast (#5502) 2025-07-09 19:07:18 +00:00
André Bispo
febfc82a53 [PM-19309] Fix search when restrict item policy is enabled (#5497) 2025-07-09 17:30:46 +00:00
David Perez
5f5c71979f PM-23557: Replace login with device toasts with snackbars (#5495) 2025-07-09 14:48:28 +00:00
David Perez
ba49a3e91f PM-23553: Replace Environment toasts with snackbars (#5493) 2025-07-09 14:04:22 +00:00
David Perez
965ab67e58 PM-14063: SDK persistance state (#5491) 2025-07-08 21:14:22 +00:00
David Perez
2932ed831b PM-23549: Remove Authenticator app name localizations (#5492) 2025-07-08 20:40:29 +00:00
David Perez
2ff3f3e23d PM-23503: Update Move to Organization toasts to be snackbars (#5489) 2025-07-07 21:43:08 +00:00
David Perez
eb5893dde4 Update Chrome Autofill compatibility mode (#5490) 2025-07-07 21:42:51 +00:00
github-actions[bot]
1165e7002b Update Google privileged browsers list (#5483)
Co-authored-by: GitHub Actions Bot <actions@github.com>
2025-07-07 16:35:35 +00:00
renovate[bot]
5fa7239130 [deps]: Update Azure/login action to v2 (#5484)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-07 16:04:23 +00:00
David Perez
fd9bdfa228 PM-19780: Fix incorrect sub header on authenticator search screen (#5488) 2025-07-07 15:57:25 +00:00
renovate[bot]
7db8f040e4 [deps]: Lock file maintenance (#5485)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-07 15:55:37 +00:00
David Perez
790331e058 PM-23365: Create ToastManager to simplify displaying toasts from a Manager or ViewModel (#5479) 2025-07-07 14:05:20 +00:00
bw-ghapp[bot]
d0640b7e20 Crowdin Pull - Password Manager (#5482)
Co-authored-by: bitwarden-devops-bot <106330231+bitwarden-devops-bot@users.noreply.github.com>
2025-07-04 02:01:23 +00:00
bw-ghapp[bot]
5429e27228 Crowdin Pull - Authenticator (#5481)
Co-authored-by: bitwarden-devops-bot <106330231+bitwarden-devops-bot@users.noreply.github.com>
2025-07-04 02:00:28 +00:00
David Perez
917aaac3a6 PM-23354: Replace Login Approval toasts with snackbar (#5478) 2025-07-03 19:09:13 +00:00
David Perez
0b7209b3c9 Minor-cleanup of StartRegistration classes (#5477) 2025-07-03 18:36:27 +00:00
David Perez
a7b3201015 PM-23320: Replace Export Vault screen toasts with snackbars (#5472) 2025-07-03 18:11:04 +00:00
David Perez
348e14e52d PM-23321: Replace two-factor screen toasts with snackbars (#5473) 2025-07-03 18:10:48 +00:00
David Perez
ef9dda5159 PM-23318: Replace OtherScreen toast with snackbar (#5471) 2025-07-03 18:10:19 +00:00
Patrick Honkonen
b0309e876e [PM-23121] Update privileged app list item subtext (#5475) 2025-07-03 14:41:43 +00:00
David Perez
59a49355fd Clean up lint warnings (#5470) 2025-07-03 13:46:47 +00:00
David Perez
901184db45 PM-23322: Replace VaultItemScreen toasts with snackbars (#5474) 2025-07-03 00:56:32 +00:00
David Perez
a2507c317d PM-23308: Replace Toasts with Snackbar in AttachmentsScreen (#5469) 2025-07-02 19:25:28 +00:00
David Perez
f608852dc7 PM-23305: Replace Vault Screen Toasts with Snackbars (#5468) 2025-07-02 19:13:58 +00:00
David Perez
e44d63229c Update to the latest Bitwarden SDK (#5466) 2025-07-02 19:13:36 +00:00
David Perez
f7b876f204 PM-22972: Replace send Toasts with Snackbars (#5464) 2025-07-02 19:13:14 +00:00
Patrick Honkonen
1268afaef8 [PM-23212] Move bitwarden.pw intent filter to debug and beta builds (#5467) 2025-07-02 19:11:19 +00:00
David Perez
3f1c1dec17 PM-23293: Remove unused Toast events from the app (#5463) 2025-07-02 19:10:03 +00:00
David Perez
5eea55f173 Update various dependencies (#5465) 2025-07-02 19:05:24 +00:00
Amy Galles
1a8cf4055a log inputs to job summary for build workflows (#5453) 2025-07-02 19:03:26 +00:00
aj-rosado
defdf8eb58 [PM-22640] Re-added isScreenCaptureAllowed to the MainViewModel state (#5462) 2025-07-02 18:18:33 +00:00
David Perez
9940c8cf9e Update to AGP v8.11.0 (#5460) 2025-07-02 15:57:32 +00:00
David Perez
e1058f5021 PM-23275: Update the display name for UK English (#5461) 2025-07-02 15:40:42 +00:00
Patrick Honkonen
986cd2ee30 [PM-19779] Make Authenticator TOTP codes collapsible (#5452) 2025-07-02 14:15:03 +00:00
David Perez
eae870cb3a Fix flicker on TextField autocomplete (#5456) 2025-07-02 13:57:00 +00:00
David Perez
79493a55bd Add generic logging to Autofill process (#5457) 2025-07-02 13:56:41 +00:00
David Perez
18bafaba8a PM-22213: Hide current access count when editing and there is not max access count (#5451) 2025-07-01 16:23:50 +00:00
David Perez
896be911a4 Update Junit and Mockk libraries (#5455) 2025-07-01 16:13:08 +00:00
David Perez
85a86106f6 PM-19780: Authenticator source headers (#5450) 2025-07-01 16:12:48 +00:00
David Perez
edb7996c28 PM-23186: Move 'BitwardenSwitch' to the 'ui' module (#5454) 2025-07-01 15:42:24 +00:00
Patrick Honkonen
a806109380 [PM-23132] Update capitalization and wording in privileged apps strings (#5449) 2025-07-01 15:11:54 +00:00
Patrick Honkonen
4f5c28e248 [PM-23131] Make "About privileged apps" screen scrollable (#5448) 2025-06-30 21:37:41 +00:00
Amy Galles
b22f06cbf9 [BRE-768] Rename store publish workflow to avoid confusion (#5439) 2025-06-30 20:28:37 +00:00
Patrick Honkonen
1070c9d46e [PM-23125] Move authenticator drawables to ui module (#5440) 2025-06-30 15:55:24 +00:00
David Perez
b1dc894fe8 PM-23136: Only apply 'always' display cutout mode on API 30 and up (#5446) 2025-06-30 15:31:19 +00:00
aj-rosado
c76945161a [PM-22640] Updating screen capture flag when the setting is changed (#5426) 2025-06-30 13:57:33 +00:00
Patrick Honkonen
789cd80eba [PM-23122] Make BitwardenTextRows in PrivilegedAppsListScreen unclickable (#5441) 2025-06-30 13:21:06 +00:00
Patrick Honkonen
9482890102 [PM-23121] Capitalize "You" in passkey trust string (#5437) 2025-06-27 19:56:34 +00:00
David Perez
ed2d6ca585 Move item listing models to common location for reuse with search (#5438) 2025-06-27 19:06:31 +00:00
Patrick Honkonen
d279f6acae [PM-22786] Migrate BitwardenTextSelectionButton to ui module (#5436) 2025-06-27 17:46:23 +00:00
Andy Pixley
6ebcab7b86 [BRE-848] Add Workflow Permissions (#5389) 2025-06-27 17:00:05 +00:00
David Perez
3ee74d3ec5 PM-19776: Change 'Move to Bitwarden' to 'Copy to Bitwarden vault' (#5435) 2025-06-27 16:50:14 +00:00
Patrick Honkonen
288efb3611 [PM-19108] Fix untrusted privileged app origin validation error handling (#5432) 2025-06-27 15:53:19 +00:00
bw-ghapp[bot]
bbdf8552c9 Crowdin Pull - Password Manager (#5434)
Co-authored-by: bitwarden-devops-bot <106330231+bitwarden-devops-bot@users.noreply.github.com>
2025-06-27 14:49:15 +00:00
bw-ghapp[bot]
44ef598df3 Crowdin Pull - Authenticator (#5433)
Co-authored-by: bitwarden-devops-bot <106330231+bitwarden-devops-bot@users.noreply.github.com>
2025-06-27 14:48:48 +00:00
David Perez
73a8e241d4 Update Androidx Room and WorkManager libraries (#5430) 2025-06-26 20:43:29 +00:00
David Perez
4d6260ea02 Update Robolectric to the latest version (#5428) 2025-06-26 20:43:07 +00:00
David Perez
569bb4f110 Update Compose BOM to latest version (2025.06.01) (#5431) 2025-06-26 20:42:51 +00:00
Patrick Honkonen
ffc71371a9 [BWA-156] Allow TOTP syncing with Authenticator release APKs (#5429) 2025-06-26 20:40:19 +00:00
David Perez
8d0b23d166 PM-23092: Update the Autofill settings UI for better communication (#5427) 2025-06-26 17:53:47 +00:00
Patrick Honkonen
5f525d9d95 [BWA-162] Add getPackageInstallationSourceOrNull to BitwardenPackageManager (#5418) 2025-06-25 21:19:17 +00:00
Patrick Honkonen
b94d59ba6b Upgrade KSP to 2.2.0-2.0.2 (#5422) 2025-06-25 19:45:28 +00:00
David Perez
4ff1a9ba94 Improve autofill version checking (#5421) 2025-06-25 17:01:18 +00:00
Patrick Honkonen
9c1673f603 [PM-22998] Fix isBuildVersionAtLeast check (#5420) 2025-06-25 17:00:25 +00:00
Patrick Honkonen
ddc099f727 [PM-19108] Add Privileged Apps List Screen (#5372) 2025-06-25 16:41:48 +00:00
André Bispo
fbfcfcd683 [PM-19309] Handle restrict item types policy (#5357) 2025-06-25 15:46:44 +00:00
Patrick Honkonen
1234898786 [PM-22998] Migrate isBuildVersionBelow to core module (#5417) 2025-06-25 13:55:19 +00:00
David Perez
182e6475c0 PM-22997: Update compatibility versions for Chrome and Brave (#5415) 2025-06-24 19:17:26 +00:00
David Perez
f27590a4d6 Do not allow Bitwarden to autofill itself (#5416) 2025-06-24 18:33:06 +00:00
Patrick Honkonen
807c76f8ec [PM-22831] Migrate IconData and BitwardenIcon to ui module (#5385) 2025-06-24 17:15:28 +00:00
David Perez
3877c4bd64 PM-22213: Update the order of items in the Send and Cipher overflows (#5407) 2025-06-24 14:45:39 +00:00
David Perez
8c88fd9d53 Add Brave integration toggle (#5411) 2025-06-24 14:45:17 +00:00
Patrick Honkonen
b92493611e [PM-22827] Move drawable resources to ui module and enable resource shrinking (#5388) 2025-06-24 14:26:03 +00:00
Nailik
9235f92206 [PM-22903] fix unit test execution (#5401) 2025-06-24 13:16:07 +00:00
David Perez
a3610c22dd Rename Chrome Autofill to Browser Autofill (#5409) 2025-06-23 21:10:52 +00:00
David Perez
1e4fc31ed4 Update Kotlin to v2.2.0 (#5408) 2025-06-23 20:57:10 +00:00
David Perez
ac1a9a2dc0 PM-22875: Done button on keyboard should submit pin or password from dialog (#5392) 2025-06-23 18:14:14 +00:00
David Perez
fe0e6bc67b Replace toObjectRoute with custom ParcelableRouteSerializer (#5393) 2025-06-23 18:13:22 +00:00
David Perez
419e5ca918 Update to latest Bitwarden SDK (#5403) 2025-06-23 16:51:18 +00:00
David Perez
be1a6e2097 Update Turbine to v1.2.1 (#5398) 2025-06-23 13:57:07 +00:00
David Perez
4fe989ce68 Add Room Gradle plugin (#5399) 2025-06-23 13:56:48 +00:00
Maciej Zieniuk
8be7410302 [PM-15087] Update the device push token every 7 days (#4386) 2025-06-20 21:06:41 +00:00
bw-ghapp[bot]
16225f0d68 Crowdin Pull - Password Manager (#5395)
Co-authored-by: bitwarden-devops-bot <106330231+bitwarden-devops-bot@users.noreply.github.com>
2025-06-20 13:55:30 +00:00
bw-ghapp[bot]
08679a8973 Crowdin Pull - Authenticator (#5394)
Co-authored-by: bitwarden-devops-bot <106330231+bitwarden-devops-bot@users.noreply.github.com>
2025-06-20 13:53:46 +00:00
David Perez
4d3e782b69 PM-22874: Fix Events service domain (#5391) 2025-06-20 13:52:33 +00:00
Patrick Honkonen
4d8fe722d1 [PM-22786] Migrate TooltipData to ui module (#5382) 2025-06-18 20:16:55 +00:00
David Perez
c5600c1d84 PM-22551: Update remove password copy (#5387) 2025-06-18 19:19:01 +00:00
David Perez
9816321d93 PM-22835: Update the passkey creation date format style (#5386) 2025-06-18 19:05:57 +00:00
Patrick Honkonen
56e8acf81f [PM-22786] Migrate PersistentListExtensions to core module (#5380) 2025-06-18 18:42:54 +00:00
Patrick Honkonen
08b07a0050 [PM-22778] Migrate BitwardenTextButton to ui module (#5378) 2025-06-18 17:47:43 +00:00
Patrick Honkonen
25d7c1e72c [PM-22786] Migrate BitwardenRowOfActions to ui module (#5381) 2025-06-18 16:17:18 +00:00
Patrick Honkonen
e311a4f618 [PM-19625] Move DataStateExtensionsTest to data module (#5377) 2025-06-18 16:03:59 +00:00
Patrick Honkonen
0eea6b07a3 [PM-22780] Migrate BitwardenHorizontalDivider to ui module (#5379) 2025-06-18 15:33:25 +00:00
Patrick Honkonen
c52e769327 [PM-21363] Migrate ZonedDateTime utils to core module (#5375) 2025-06-18 15:05:36 +00:00
David Perez
292a28d155 PM-22776: Update logic for determining base domains (#5374) 2025-06-18 15:05:24 +00:00
Patrick Honkonen
6c41c358ac [PM-22815] Migrate BitwardenContentBlock to ui module (#5383) 2025-06-18 15:05:21 +00:00
Patrick Honkonen
e7cf5a7efa [PM-22777] Migrate AnimateNullableContentVisibility to ui module (#5376) 2025-06-17 21:38:50 +00:00
Patrick Honkonen
f64364c1b8 [PM-19108] Update passkey prompt for unrecognized browser (#5371) 2025-06-17 01:03:07 +00:00
David Perez
d42b8ecd2d Update version constant names for consistency (#5369) 2025-06-16 18:26:09 +00:00
David Perez
a6f7b1e176 Update AndroidX AppCompat and Autofill libraries (#5368) 2025-06-16 17:16:38 +00:00
David Perez
d56b9fc0ff Update to Junit v5.13.1 (#5367) 2025-06-16 17:09:11 +00:00
Patrick Honkonen
f290ae411b [PM-22552] Update alg type in PasskeyAttestationOptions (#5363) 2025-06-16 16:47:40 +00:00
David Perez
508566f06f Update the Firebase BOM to 33.15.0 (#5366) 2025-06-16 15:52:14 +00:00
Patrick Honkonen
95f146fb3e [PM-21782] Improve create cipher error handling (#5362) 2025-06-16 14:23:30 +00:00
aj-rosado
469df4495a [PM-22568] Change totp seed field to a password field (#5350) 2025-06-13 16:32:12 +00:00
David Perez
053dfc1647 PM-22643: Do not clear error dialogs when updating TOTP data (#5361) 2025-06-13 16:25:39 +00:00
Patrick Honkonen
7de770ca03 [PM-22441] Refactor DigitalAssetLinkService to use source website (#5351) 2025-06-13 16:03:27 +00:00
David Perez
861a4281fa PM-22642, PM-22644: Add MP reprompt for TOTP code and secure note (#5359) 2025-06-13 16:01:58 +00:00
Patrick Honkonen
265014fd64 [PM-22665] Add BitwardenPackageManager abstraction (#5360) 2025-06-13 16:01:56 +00:00
bw-ghapp[bot]
5d32fe9caf Crowdin Pull - Password Manager (#5356)
Co-authored-by: bitwarden-devops-bot <106330231+bitwarden-devops-bot@users.noreply.github.com>
2025-06-13 13:53:17 +00:00
bw-ghapp[bot]
44ba0f548a Crowdin Pull - Authenticator (#5355)
Co-authored-by: bitwarden-devops-bot <106330231+bitwarden-devops-bot@users.noreply.github.com>
2025-06-13 13:52:33 +00:00
David Perez
694443f2e1 Password field tooltip support (#5354) 2025-06-12 21:56:05 +00:00
David Perez
0ade60025c PM-22634: Fix parsing of system language (#5353) 2025-06-12 20:47:12 +00:00
David Perez
5adccca823 PM-22477: Update the timestamp format for ciphers (#5352) 2025-06-12 18:49:20 +00:00
David Perez
3c86bb425b Add tests for the EditItemScreen and EditItemViewModel (#5348) 2025-06-12 14:30:00 +00:00
Patrick Honkonen
3474e0b608 [PM-22461] Add About privileged apps screen (#5335) 2025-06-12 14:19:19 +00:00
Patrick Honkonen
0f2476bebf Update BitwardenContentBlock divider padding logic (#5346) 2025-06-11 19:44:41 +00:00
Patrick Honkonen
edffb8dd6f Add tooltip to BitwardenTextRow (#5344) 2025-06-11 17:36:15 +00:00
David Perez
2dc6c170f5 PM-22551: Update toasts to snackbars for Sends (#5339) 2025-06-11 16:33:59 +00:00
David Perez
2a8a16ab3f BWA-160: Modernize QrCodeScanScreen (#5342) 2025-06-10 19:04:39 +00:00
Patrick Honkonen
76995a28ad [deps] Update googleProtoBufJava to 4.31.1 (#5343) 2025-06-10 19:02:55 +00:00
David Perez
7e146800a8 PM-22522: Update time picker language (#5338) 2025-06-10 19:02:31 +00:00
Patrick Honkonen
7a2f1c294f [deps] Update sonarqube plugin (#5307) 2025-06-10 18:22:15 +00:00
David Perez
44d4926300 Remove unused dialogs (#5337) 2025-06-10 15:49:15 +00:00
Álison Fernandes
e4c160d1e0 [PM-22437] Add product release notes to GitHub Releases (#5318) 2025-06-09 20:46:25 +00:00
Álison Fernandes
0f9f9d9dce [PM-22389] GitHub Release workflow supports releasing BWPM and BWA (#5312) 2025-06-09 19:40:34 +00:00
David Perez
a0c2600517 PM-10286: VerificationCodeScreen should not show MP reprompt if there is no master password (#5336) 2025-06-09 19:32:02 +00:00
Patrick Honkonen
c60df56648 [PM-21458] Add UserManagedPrivilegedApps feature flag (#5325) 2025-06-09 19:04:38 +00:00
David Perez
9cdfe0c5d6 PM-22502: Format dates and times correctly for locale (#5333) 2025-06-09 18:30:30 +00:00
renovate[bot]
d822be62e1 [deps]: Lock file maintenance (#5331)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-09 16:17:25 +00:00
Matt Andreko
7adbfdcc84 Fix permissions for check-run action (#5316) 2025-06-09 13:34:28 +00:00
David Perez
beb4c533c8 Update the SnackbarRelayManager (#5317) 2025-06-06 18:28:08 +00:00
Patrick Honkonen
e1cd813445 [PM-19107] Introduce user-trusted privileged apps for Credential Manager (#4848) 2025-06-06 17:51:06 +00:00
David Perez
f769900976 PM-22456: Move Temporal Accessor Extensions to 'Core' module (#5324) 2025-06-06 17:29:02 +00:00
David Perez
a0ff94195f Update Junit to v5.13.0 (#5323) 2025-06-06 16:31:05 +00:00
bw-ghapp[bot]
9853f137d2 Crowdin Pull - Authenticator (#5319)
Co-authored-by: bitwarden-devops-bot <106330231+bitwarden-devops-bot@users.noreply.github.com>
2025-06-06 14:07:19 +00:00
bw-ghapp[bot]
b591534bd9 Crowdin Pull - Password Manager (#5320)
Co-authored-by: bitwarden-devops-bot <106330231+bitwarden-devops-bot@users.noreply.github.com>
2025-06-06 14:06:57 +00:00
David Perez
d2c329264c PM-22397: Remove custom deletion date (#5311) 2025-06-05 19:20:12 +00:00
David Perez
a9791c3f9f PM-22402: Update File Send error message (#5313) 2025-06-05 18:39:28 +00:00
1451 changed files with 64451 additions and 64563 deletions

20
.github/actions/log-inputs/action.yml vendored Normal file
View File

@@ -0,0 +1,20 @@
name: 'Log Inputs to Job Summary'
description: 'Log workflow inputs to the GitHub Actions job summary'
inputs:
inputs:
description: 'Workflow inputs as JSON'
required: true
runs:
using: 'composite'
steps:
- name: Log inputs to job summary
shell: bash
run: |
echo "<details><summary>Job Inputs</summary>" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo '```json' >> $GITHUB_STEP_SUMMARY
echo '${{ inputs.inputs }}' >> $GITHUB_STEP_SUMMARY
echo '```' >> $GITHUB_STEP_SUMMARY
echo "</details>" >> $GITHUB_STEP_SUMMARY

View File

@@ -0,0 +1,49 @@
name: 'Setup Android Build'
description: 'Setup Android build environment with Gradle, Ruby, and Fastlane'
inputs:
java-version:
description: 'Java version to use'
required: false
default: '21'
runs:
using: 'composite'
steps:
- name: Validate Gradle wrapper
uses: gradle/actions/wrapper-validation@017a9effdb900e5b5b2fddfb590a105619dca3c3 # v4.4.2
- name: Cache Gradle files
uses: actions/cache@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4
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@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4
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@44511735964dcb71245e7e55f72539531f7bc0eb # v1.257.0
with:
bundler-cache: true
- name: Configure JDK
uses: actions/setup-java@dded0888837ed1f317902acf8a20df0ad188d165 # v5.0.0
with:
distribution: "temurin"
java-version: ${{ inputs.java-version }}
- name: Install Fastlane
shell: bash
run: |
gem install bundler:2.2.27
bundle config path vendor/bundle
bundle install --jobs 4 --retry 3

View File

@@ -0,0 +1,133 @@
# Get Release Notes from Jira script
Fetches release notes from Jira issues.
## Prerequisites
- Python dev environment - use [uv](https://github.com/astral-sh/uv)
- Jira API token. Generate one at: https://id.atlassian.com/manage-profile/security/api-tokens
- Install dependencies:
```bash
uv pip install -r pyproject.toml
```
## Usage
```bash
./jira_release_notes.py RELEASE-1762 example@example.com T0k3n123
```
# Output Format
The script retrieves the content from a custom field and handles two types of Jira release notes formats:
1. Bullet Points:
```
• Point 1
• Point 2
• Point 3
```
2. Single Line:
```
Single line of release notes text
```
## Jira JSON format example
### Single line
```json
...
"customfield_10335": {
"type": "doc",
"version": 1,
"content": [
{
"type": "paragraph",
"content": [
{
"type": "text",
"text": "Single line release notes"
}
]
}
]
},
...
```
### Bullet points
```json
...
"customfield_10335": {
"type": "doc",
"version": 1,
"content": [
{
"type": "bulletList",
"content": [
{
"type": "listItem",
"content": [
{
"type": "paragraph",
"content": [
{
"type": "text",
"text": "Release notes list item 1"
}
]
}
]
},
{
"type": "listItem",
"content": [
{
"type": "paragraph",
"content": [
{
"type": "text",
"text": "Release notes list item 2"
}
]
}
]
},
{
"type": "listItem",
"content": [
{
"type": "paragraph",
"content": [
{
"type": "text",
"text": "Release notes list item 3"
}
]
}
]
},
{
"type": "listItem",
"content": [
{
"type": "paragraph",
"content": [
{
"type": "text",
"text": "Release notes list item 4"
}
]
}
]
}
]
}
]
},
...
```

View File

@@ -0,0 +1,70 @@
#!/usr/bin/env python3
import sys
import base64
import json
import requests
def extract_text_from_content(content):
if isinstance(content, list):
texts = [extract_text_from_content(item) for item in content]
return '\n'.join(text for text in texts if text.strip())
if isinstance(content, dict):
if content.get('type') == 'text':
return content.get('text', '')
elif content.get('type') == 'paragraph':
return extract_text_from_content(content.get('content', []))
elif content.get('type') == 'bulletList':
return extract_text_from_content(content.get('content', []))
elif content.get('type') == 'listItem':
item_text = extract_text_from_content(content.get('content', []))
return f"* {item_text.strip()}"
return ''
def parse_release_notes(response_json):
try:
fields = response_json.get('fields', {})
release_notes_field = fields.get('customfield_10335', {})
if not release_notes_field or not release_notes_field.get('content'):
return ''
release_notes = extract_text_from_content(release_notes_field.get('content', []))
return release_notes
except Exception as e:
print(f"Error parsing release notes: {str(e)}", file=sys.stderr)
return ''
def main():
if len(sys.argv) != 4:
print(f"Usage: {sys.argv[0]} <issue_id> <jira_email> <jira_api_token>")
sys.exit(1)
jira_issue_id = sys.argv[1]
jira_email = sys.argv[2]
jira_api_token = sys.argv[3]
jira_base_url = "https://bitwarden.atlassian.net"
auth = base64.b64encode(f"{jira_email}:{jira_api_token}".encode()).decode()
headers = {
"Authorization": f"Basic {auth}",
"Content-Type": "application/json"
}
response = requests.get(
f"{jira_base_url}/rest/api/3/issue/{jira_issue_id}",
headers=headers
)
if response.status_code != 200:
print(f"Error fetching Jira issue: {response.status_code}", file=sys.stderr)
sys.exit(1)
release_notes = parse_release_notes(response.json())
print(release_notes)
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,9 @@
[project]
name = "jira-get-release-notes"
version = "0.1.0"
description = "Add your description here"
readme = "README.md"
requires-python = ">=3.12"
dependencies = [
"requests>=2.32.3",
]

91
.github/scripts/jira-get-release-notes/uv.lock generated vendored Normal file
View File

@@ -0,0 +1,91 @@
version = 1
revision = 2
requires-python = ">=3.12"
[[package]]
name = "certifi"
version = "2025.4.26"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/e8/9e/c05b3920a3b7d20d3d3310465f50348e5b3694f4f88c6daf736eef3024c4/certifi-2025.4.26.tar.gz", hash = "sha256:0a816057ea3cdefcef70270d2c515e4506bbc954f417fa5ade2021213bb8f0c6", size = 160705, upload-time = "2025-04-26T02:12:29.51Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/4a/7e/3db2bd1b1f9e95f7cddca6d6e75e2f2bd9f51b1246e546d88addca0106bd/certifi-2025.4.26-py3-none-any.whl", hash = "sha256:30350364dfe371162649852c63336a15c70c6510c2ad5015b21c2345311805f3", size = 159618, upload-time = "2025-04-26T02:12:27.662Z" },
]
[[package]]
name = "charset-normalizer"
version = "3.4.2"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/e4/33/89c2ced2b67d1c2a61c19c6751aa8902d46ce3dacb23600a283619f5a12d/charset_normalizer-3.4.2.tar.gz", hash = "sha256:5baececa9ecba31eff645232d59845c07aa030f0c81ee70184a90d35099a0e63", size = 126367, upload-time = "2025-05-02T08:34:42.01Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/d7/a4/37f4d6035c89cac7930395a35cc0f1b872e652eaafb76a6075943754f095/charset_normalizer-3.4.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0c29de6a1a95f24b9a1aa7aefd27d2487263f00dfd55a77719b530788f75cff7", size = 199936, upload-time = "2025-05-02T08:32:33.712Z" },
{ url = "https://files.pythonhosted.org/packages/ee/8a/1a5e33b73e0d9287274f899d967907cd0bf9c343e651755d9307e0dbf2b3/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cddf7bd982eaa998934a91f69d182aec997c6c468898efe6679af88283b498d3", size = 143790, upload-time = "2025-05-02T08:32:35.768Z" },
{ url = "https://files.pythonhosted.org/packages/66/52/59521f1d8e6ab1482164fa21409c5ef44da3e9f653c13ba71becdd98dec3/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fcbe676a55d7445b22c10967bceaaf0ee69407fbe0ece4d032b6eb8d4565982a", size = 153924, upload-time = "2025-05-02T08:32:37.284Z" },
{ url = "https://files.pythonhosted.org/packages/86/2d/fb55fdf41964ec782febbf33cb64be480a6b8f16ded2dbe8db27a405c09f/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d41c4d287cfc69060fa91cae9683eacffad989f1a10811995fa309df656ec214", size = 146626, upload-time = "2025-05-02T08:32:38.803Z" },
{ url = "https://files.pythonhosted.org/packages/8c/73/6ede2ec59bce19b3edf4209d70004253ec5f4e319f9a2e3f2f15601ed5f7/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4e594135de17ab3866138f496755f302b72157d115086d100c3f19370839dd3a", size = 148567, upload-time = "2025-05-02T08:32:40.251Z" },
{ url = "https://files.pythonhosted.org/packages/09/14/957d03c6dc343c04904530b6bef4e5efae5ec7d7990a7cbb868e4595ee30/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cf713fe9a71ef6fd5adf7a79670135081cd4431c2943864757f0fa3a65b1fafd", size = 150957, upload-time = "2025-05-02T08:32:41.705Z" },
{ url = "https://files.pythonhosted.org/packages/0d/c8/8174d0e5c10ccebdcb1b53cc959591c4c722a3ad92461a273e86b9f5a302/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a370b3e078e418187da8c3674eddb9d983ec09445c99a3a263c2011993522981", size = 145408, upload-time = "2025-05-02T08:32:43.709Z" },
{ url = "https://files.pythonhosted.org/packages/58/aa/8904b84bc8084ac19dc52feb4f5952c6df03ffb460a887b42615ee1382e8/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:a955b438e62efdf7e0b7b52a64dc5c3396e2634baa62471768a64bc2adb73d5c", size = 153399, upload-time = "2025-05-02T08:32:46.197Z" },
{ url = "https://files.pythonhosted.org/packages/c2/26/89ee1f0e264d201cb65cf054aca6038c03b1a0c6b4ae998070392a3ce605/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:7222ffd5e4de8e57e03ce2cef95a4c43c98fcb72ad86909abdfc2c17d227fc1b", size = 156815, upload-time = "2025-05-02T08:32:48.105Z" },
{ url = "https://files.pythonhosted.org/packages/fd/07/68e95b4b345bad3dbbd3a8681737b4338ff2c9df29856a6d6d23ac4c73cb/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:bee093bf902e1d8fc0ac143c88902c3dfc8941f7ea1d6a8dd2bcb786d33db03d", size = 154537, upload-time = "2025-05-02T08:32:49.719Z" },
{ url = "https://files.pythonhosted.org/packages/77/1a/5eefc0ce04affb98af07bc05f3bac9094513c0e23b0562d64af46a06aae4/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:dedb8adb91d11846ee08bec4c8236c8549ac721c245678282dcb06b221aab59f", size = 149565, upload-time = "2025-05-02T08:32:51.404Z" },
{ url = "https://files.pythonhosted.org/packages/37/a0/2410e5e6032a174c95e0806b1a6585eb21e12f445ebe239fac441995226a/charset_normalizer-3.4.2-cp312-cp312-win32.whl", hash = "sha256:db4c7bf0e07fc3b7d89ac2a5880a6a8062056801b83ff56d8464b70f65482b6c", size = 98357, upload-time = "2025-05-02T08:32:53.079Z" },
{ url = "https://files.pythonhosted.org/packages/6c/4f/c02d5c493967af3eda9c771ad4d2bbc8df6f99ddbeb37ceea6e8716a32bc/charset_normalizer-3.4.2-cp312-cp312-win_amd64.whl", hash = "sha256:5a9979887252a82fefd3d3ed2a8e3b937a7a809f65dcb1e068b090e165bbe99e", size = 105776, upload-time = "2025-05-02T08:32:54.573Z" },
{ url = "https://files.pythonhosted.org/packages/ea/12/a93df3366ed32db1d907d7593a94f1fe6293903e3e92967bebd6950ed12c/charset_normalizer-3.4.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:926ca93accd5d36ccdabd803392ddc3e03e6d4cd1cf17deff3b989ab8e9dbcf0", size = 199622, upload-time = "2025-05-02T08:32:56.363Z" },
{ url = "https://files.pythonhosted.org/packages/04/93/bf204e6f344c39d9937d3c13c8cd5bbfc266472e51fc8c07cb7f64fcd2de/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eba9904b0f38a143592d9fc0e19e2df0fa2e41c3c3745554761c5f6447eedabf", size = 143435, upload-time = "2025-05-02T08:32:58.551Z" },
{ url = "https://files.pythonhosted.org/packages/22/2a/ea8a2095b0bafa6c5b5a55ffdc2f924455233ee7b91c69b7edfcc9e02284/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3fddb7e2c84ac87ac3a947cb4e66d143ca5863ef48e4a5ecb83bd48619e4634e", size = 153653, upload-time = "2025-05-02T08:33:00.342Z" },
{ url = "https://files.pythonhosted.org/packages/b6/57/1b090ff183d13cef485dfbe272e2fe57622a76694061353c59da52c9a659/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:98f862da73774290f251b9df8d11161b6cf25b599a66baf087c1ffe340e9bfd1", size = 146231, upload-time = "2025-05-02T08:33:02.081Z" },
{ url = "https://files.pythonhosted.org/packages/e2/28/ffc026b26f441fc67bd21ab7f03b313ab3fe46714a14b516f931abe1a2d8/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c9379d65defcab82d07b2a9dfbfc2e95bc8fe0ebb1b176a3190230a3ef0e07c", size = 148243, upload-time = "2025-05-02T08:33:04.063Z" },
{ url = "https://files.pythonhosted.org/packages/c0/0f/9abe9bd191629c33e69e47c6ef45ef99773320e9ad8e9cb08b8ab4a8d4cb/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e635b87f01ebc977342e2697d05b56632f5f879a4f15955dfe8cef2448b51691", size = 150442, upload-time = "2025-05-02T08:33:06.418Z" },
{ url = "https://files.pythonhosted.org/packages/67/7c/a123bbcedca91d5916c056407f89a7f5e8fdfce12ba825d7d6b9954a1a3c/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:1c95a1e2902a8b722868587c0e1184ad5c55631de5afc0eb96bc4b0d738092c0", size = 145147, upload-time = "2025-05-02T08:33:08.183Z" },
{ url = "https://files.pythonhosted.org/packages/ec/fe/1ac556fa4899d967b83e9893788e86b6af4d83e4726511eaaad035e36595/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ef8de666d6179b009dce7bcb2ad4c4a779f113f12caf8dc77f0162c29d20490b", size = 153057, upload-time = "2025-05-02T08:33:09.986Z" },
{ url = "https://files.pythonhosted.org/packages/2b/ff/acfc0b0a70b19e3e54febdd5301a98b72fa07635e56f24f60502e954c461/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:32fc0341d72e0f73f80acb0a2c94216bd704f4f0bce10aedea38f30502b271ff", size = 156454, upload-time = "2025-05-02T08:33:11.814Z" },
{ url = "https://files.pythonhosted.org/packages/92/08/95b458ce9c740d0645feb0e96cea1f5ec946ea9c580a94adfe0b617f3573/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:289200a18fa698949d2b39c671c2cc7a24d44096784e76614899a7ccf2574b7b", size = 154174, upload-time = "2025-05-02T08:33:13.707Z" },
{ url = "https://files.pythonhosted.org/packages/78/be/8392efc43487ac051eee6c36d5fbd63032d78f7728cb37aebcc98191f1ff/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4a476b06fbcf359ad25d34a057b7219281286ae2477cc5ff5e3f70a246971148", size = 149166, upload-time = "2025-05-02T08:33:15.458Z" },
{ url = "https://files.pythonhosted.org/packages/44/96/392abd49b094d30b91d9fbda6a69519e95802250b777841cf3bda8fe136c/charset_normalizer-3.4.2-cp313-cp313-win32.whl", hash = "sha256:aaeeb6a479c7667fbe1099af9617c83aaca22182d6cf8c53966491a0f1b7ffb7", size = 98064, upload-time = "2025-05-02T08:33:17.06Z" },
{ url = "https://files.pythonhosted.org/packages/e9/b0/0200da600134e001d91851ddc797809e2fe0ea72de90e09bec5a2fbdaccb/charset_normalizer-3.4.2-cp313-cp313-win_amd64.whl", hash = "sha256:aa6af9e7d59f9c12b33ae4e9450619cf2488e2bbe9b44030905877f0b2324980", size = 105641, upload-time = "2025-05-02T08:33:18.753Z" },
{ url = "https://files.pythonhosted.org/packages/20/94/c5790835a017658cbfabd07f3bfb549140c3ac458cfc196323996b10095a/charset_normalizer-3.4.2-py3-none-any.whl", hash = "sha256:7f56930ab0abd1c45cd15be65cc741c28b1c9a34876ce8c17a2fa107810c0af0", size = 52626, upload-time = "2025-05-02T08:34:40.053Z" },
]
[[package]]
name = "jira-get-release-notes"
version = "0.1.0"
source = { virtual = "." }
dependencies = [
{ name = "requests" },
]
[package.metadata]
requires-dist = [{ name = "requests", specifier = ">=2.32.3" }]
[[package]]
name = "idna"
version = "3.10"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490, upload-time = "2024-09-15T18:07:39.745Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442, upload-time = "2024-09-15T18:07:37.964Z" },
]
[[package]]
name = "requests"
version = "2.32.3"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "certifi" },
{ name = "charset-normalizer" },
{ name = "idna" },
{ name = "urllib3" },
]
sdist = { url = "https://files.pythonhosted.org/packages/63/70/2bf7780ad2d390a8d301ad0b550f1581eadbd9a20f896afe06353c2a2913/requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760", size = 131218, upload-time = "2024-05-29T15:37:49.536Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/f9/9b/335f9764261e915ed497fcdeb11df5dfd6f7bf257d4a6a2a686d80da4d54/requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6", size = 64928, upload-time = "2024-05-29T15:37:47.027Z" },
]
[[package]]
name = "urllib3"
version = "2.4.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/8a/78/16493d9c386d8e60e442a35feac5e00f0913c0f4b7c217c11e8ec2ff53e0/urllib3-2.4.0.tar.gz", hash = "sha256:414bc6535b787febd7567804cc015fee39daab8ad86268f1310a9250697de466", size = 390672, upload-time = "2025-04-10T15:23:39.232Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/6b/11/cc635220681e93a0183390e26485430ca2c7b5f9d33b15c74c2861cb8091/urllib3-2.4.0-py3-none-any.whl", hash = "sha256:4e16665048960a0900c702d4a66415956a584919c03361cac9f1df5c5dd7e813", size = 128680, upload-time = "2025-04-10T15:23:37.377Z" },
]

View File

@@ -4,6 +4,7 @@ on:
push:
branches:
- main
- release/**/*
workflow_dispatch:
inputs:
version-name:
@@ -27,11 +28,12 @@ on:
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
JAVA_VERSION: 17
JAVA_VERSION: 21
permissions:
contents: read
packages: read
id-token: write
jobs:
build:
@@ -39,14 +41,23 @@ jobs:
runs-on: ubuntu-24.04
steps:
- name: Log inputs to job summary
run: |
echo "<details><summary>Job Inputs</summary>" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo '```json' >> $GITHUB_STEP_SUMMARY
echo '${{ toJson(inputs) }}' >> $GITHUB_STEP_SUMMARY
echo '```' >> $GITHUB_STEP_SUMMARY
echo "</details>" >> $GITHUB_STEP_SUMMARY
- name: Check out repo
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Validate Gradle wrapper
uses: gradle/actions/wrapper-validation@06832c7b30a0129d7fb559bcc6e43d26f6374244 # v4.3.1
uses: gradle/actions/wrapper-validation@017a9effdb900e5b5b2fddfb590a105619dca3c3 # v4.4.2
- name: Cache Gradle files
uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3
uses: actions/cache@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4
with:
path: |
~/.gradle/caches
@@ -56,7 +67,7 @@ jobs:
${{ runner.os }}-gradle-v2-
- name: Cache build output
uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3
uses: actions/cache@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4
with:
path: |
${{ github.workspace }}/build-cache
@@ -65,13 +76,13 @@ jobs:
${{ runner.os }}-build-
- name: Configure JDK
uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # v4.7.1
uses: actions/setup-java@dded0888837ed1f317902acf8a20df0ad188d165 # v5.0.0
with:
distribution: "temurin"
java-version: ${{ env.JAVA_VERSION }}
- name: Configure Ruby
uses: ruby/setup-ruby@ca041f971d66735f3e5ff1e21cc13e2d51e7e535 # v1.233.0
uses: ruby/setup-ruby@44511735964dcb71245e7e55f72539531f7bc0eb # v1.257.0
with:
bundler-cache: true
@@ -99,10 +110,10 @@ jobs:
steps:
- name: Check out repo
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Configure Ruby
uses: ruby/setup-ruby@ca041f971d66735f3e5ff1e21cc13e2d51e7e535 # v1.233.0
uses: ruby/setup-ruby@44511735964dcb71245e7e55f72539531f7bc0eb # v1.257.0
with:
bundler-cache: true
@@ -113,9 +124,18 @@ jobs:
bundle install --jobs 4 --retry 3
- name: Log in to Azure
uses: Azure/login@cb79c773a3cfa27f31f25eb3f677781210c9ce3d # v1.6.1
uses: bitwarden/gh-actions/azure-login@main
with:
creds: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }}
subscription_id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
tenant_id: ${{ secrets.AZURE_TENANT_ID }}
client_id: ${{ secrets.AZURE_CLIENT_ID }}
- name: Get Azure Key Vault secrets
id: get-kv-secrets
uses: bitwarden/gh-actions/get-keyvault-secrets@main
with:
keyvault: gh-android
secrets: "BWA-AAB-KEYSTORE-STORE-PASSWORD,BWA-AAB-KEYSTORE-KEY-PASSWORD,BWA-APK-KEYSTORE-STORE-PASSWORD,BWA-APK-KEYSTORE-KEY-PASSWORD"
- name: Retrieve secrets
env:
@@ -159,6 +179,9 @@ jobs:
az storage blob download --account-name $ACCOUNT_NAME --container-name $CONTAINER_NAME \
--name authenticator_play_store-creds.json --file ${{ github.workspace }}/secrets/authenticator_play_store-creds.json --output none
- name: AZ Logout
uses: bitwarden/gh-actions/azure-logout@main
- name: Verify Play Store credentials
if: ${{ inputs.publish-to-play-store }}
run: |
@@ -166,10 +189,10 @@ jobs:
json_key:${{ github.workspace }}/secrets/authenticator_play_store-creds.json }}
- name: Validate Gradle wrapper
uses: gradle/actions/wrapper-validation@06832c7b30a0129d7fb559bcc6e43d26f6374244 # v4.3.1
uses: gradle/actions/wrapper-validation@017a9effdb900e5b5b2fddfb590a105619dca3c3 # v4.4.2
- name: Cache Gradle files
uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3
uses: actions/cache@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4
with:
path: |
~/.gradle/caches
@@ -179,7 +202,7 @@ jobs:
${{ runner.os }}-gradle-v2-
- name: Cache build output
uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3
uses: actions/cache@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4
with:
path: |
${{ github.workspace }}/build-cache
@@ -188,21 +211,30 @@ jobs:
${{ runner.os }}-build-
- name: Configure JDK
uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # v4.7.1
uses: actions/setup-java@dded0888837ed1f317902acf8a20df0ad188d165 # v5.0.0
with:
distribution: "temurin"
java-version: ${{ env.JAVA_VERSION }}
- name: Update app CI Build info
run: |
./scripts/update_app_ci_build_info.sh \
$GITHUB_REPOSITORY \
$GITHUB_REF_NAME \
$GITHUB_SHA \
$GITHUB_RUN_ID \
$GITHUB_RUN_ATTEMPT
- name: Increment version
run: |
DEFAULT_VERSION_CODE=$GITHUB_RUN_NUMBER
VERSION_CODE="${{ inputs.version-code || '$DEFAULT_VERSION_CODE' }}"
bundle exec fastlane setAuthenticatorBuildVersionInfo \
bundle exec fastlane setBuildVersionInfo \
versionCode:$VERSION_CODE \
versionName:${{ inputs.version-name || '' }}
regex='versionName = "([^"]+)"'
if [[ "$(cat authenticator/build.gradle.kts)" =~ $regex ]]; then
regex='appVersionName = "([^"]+)"'
if [[ "$(cat gradle/libs.versions.toml)" =~ $regex ]]; then
VERSION_NAME="${BASH_REMATCH[1]}"
fi
echo "Version Name: ${VERSION_NAME}" >> $GITHUB_STEP_SUMMARY
@@ -213,18 +245,18 @@ jobs:
run: |
bundle exec fastlane bundleAuthenticatorRelease \
storeFile:${{ github.workspace }}/keystores/authenticator_aab-keystore.jks \
storePassword:'${{ secrets.BWA_AAB_KEYSTORE_STORE_PASSWORD }}' \
storePassword:'${{ steps.get-kv-secrets.outputs.BWA-AAB-KEYSTORE-STORE-PASSWORD }}' \
keyAlias:authenticatorupload \
keyPassword:'${{ secrets.BWA_AAB_KEYSTORE_KEY_PASSWORD }}'
keyPassword:'${{ steps.get-kv-secrets.outputs.BWA-AAB-KEYSTORE-KEY-PASSWORD }}'
- name: Generate release Play Store APK
if: ${{ matrix.variant == 'apk' }}
run: |
bundle exec fastlane buildAuthenticatorRelease \
storeFile:${{ github.workspace }}/keystores/authenticator_apk-keystore.jks \
storePassword:'${{ secrets.BWA_APK_KEYSTORE_STORE_PASSWORD }}' \
storePassword:'${{ steps.get-kv-secrets.outputs.BWA-APK-KEYSTORE-STORE-PASSWORD }}' \
keyAlias:bitwardenauthenticator \
keyPassword:'${{ secrets.BWA_APK_KEYSTORE_KEY_PASSWORD }}'
keyPassword:'${{ steps.get-kv-secrets.outputs.BWA-APK-KEYSTORE-KEY-PASSWORD }}'
- name: Upload release Play Store .aab artifact
if: ${{ matrix.variant == 'aab' }}

View File

@@ -4,6 +4,7 @@ on:
push:
branches:
- main
- release/**/*
workflow_dispatch:
inputs:
version-name:
@@ -17,22 +18,23 @@ on:
distribute-to-firebase:
description: "Optional. Distribute artifacts to Firebase."
required: false
default: false
default: true
type: boolean
publish-to-play-store:
description: "Optional. Deploy bundle artifact to Google Play Store"
required: false
default: false
default: true
type: boolean
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
JAVA_VERSION: 17
JAVA_VERSION: 21
GITHUB_ACTION_RUN_URL: "https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}"
permissions:
contents: read
packages: read
id-token: write
jobs:
build:
@@ -40,14 +42,23 @@ jobs:
runs-on: ubuntu-24.04
steps:
- name: Log inputs to job summary
run: |
echo "<details><summary>Job Inputs</summary>" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo '```json' >> $GITHUB_STEP_SUMMARY
echo '${{ toJson(inputs) }}' >> $GITHUB_STEP_SUMMARY
echo '```' >> $GITHUB_STEP_SUMMARY
echo "</details>" >> $GITHUB_STEP_SUMMARY
- name: Check out repo
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Validate Gradle wrapper
uses: gradle/actions/wrapper-validation@06832c7b30a0129d7fb559bcc6e43d26f6374244 # v4.3.1
uses: gradle/actions/wrapper-validation@017a9effdb900e5b5b2fddfb590a105619dca3c3 # v4.4.2
- name: Cache Gradle files
uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3
uses: actions/cache@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4
with:
path: |
~/.gradle/caches
@@ -57,7 +68,7 @@ jobs:
${{ runner.os }}-gradle-v2-
- name: Cache build output
uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3
uses: actions/cache@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4
with:
path: |
${{ github.workspace }}/build-cache
@@ -66,13 +77,13 @@ jobs:
${{ runner.os }}-build-
- name: Configure JDK
uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # v4.7.1
uses: actions/setup-java@dded0888837ed1f317902acf8a20df0ad188d165 # v5.0.0
with:
distribution: "temurin"
java-version: ${{ env.JAVA_VERSION }}
- name: Configure Ruby
uses: ruby/setup-ruby@ca041f971d66735f3e5ff1e21cc13e2d51e7e535 # v1.233.0
uses: ruby/setup-ruby@44511735964dcb71245e7e55f72539531f7bc0eb # v1.257.0
with:
bundler-cache: true
@@ -107,10 +118,10 @@ jobs:
artifact: ["apk", "aab"]
steps:
- name: Check out repo
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Configure Ruby
uses: ruby/setup-ruby@ca041f971d66735f3e5ff1e21cc13e2d51e7e535 # v1.233.0
uses: ruby/setup-ruby@44511735964dcb71245e7e55f72539531f7bc0eb # v1.257.0
with:
bundler-cache: true
@@ -121,9 +132,18 @@ jobs:
bundle install --jobs 4 --retry 3
- name: Log in to Azure
uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0
uses: bitwarden/gh-actions/azure-login@main
with:
creds: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }}
subscription_id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
tenant_id: ${{ secrets.AZURE_TENANT_ID }}
client_id: ${{ secrets.AZURE_CLIENT_ID }}
- name: Get Azure Key Vault secrets
id: get-kv-secrets
uses: bitwarden/gh-actions/get-keyvault-secrets@main
with:
keyvault: gh-android
secrets: "UPLOAD-KEYSTORE-PASSWORD,UPLOAD-BETA-KEYSTORE-PASSWORD,UPLOAD-BETA-KEY-PASSWORD,PLAY-KEYSTORE-PASSWORD,PLAY-BETA-KEYSTORE-PASSWORD,PLAY-BETA-KEY-PASSWORD"
- name: Retrieve secrets
env:
@@ -160,11 +180,14 @@ jobs:
az storage blob download --account-name $ACCOUNT_NAME --container-name $CONTAINER_NAME \
--name app_play_prod_firebase-creds.json --file ${{ github.workspace }}/secrets/app_play_prod_firebase-creds.json --output none
- name: Log out from Azure
uses: bitwarden/gh-actions/azure-logout@main
- name: Validate Gradle wrapper
uses: gradle/actions/wrapper-validation@06832c7b30a0129d7fb559bcc6e43d26f6374244 # v4.3.1
uses: gradle/actions/wrapper-validation@017a9effdb900e5b5b2fddfb590a105619dca3c3 # v4.4.2
- name: Cache Gradle files
uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3
uses: actions/cache@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4
with:
path: |
~/.gradle/caches
@@ -174,7 +197,7 @@ jobs:
${{ runner.os }}-gradle-v2-
- name: Cache build output
uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3
uses: actions/cache@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4
with:
path: |
${{ github.workspace }}/build-cache
@@ -183,7 +206,7 @@ jobs:
${{ runner.os }}-build-
- name: Configure JDK
uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # v4.7.1
uses: actions/setup-java@dded0888837ed1f317902acf8a20df0ad188d165 # v5.0.0
with:
distribution: "temurin"
java-version: ${{ env.JAVA_VERSION }}
@@ -207,48 +230,48 @@ jobs:
- name: Generate release Play Store bundle
if: ${{ matrix.variant == 'prod' && matrix.artifact == 'aab' }}
env:
UPLOAD_KEYSTORE_PASSWORD: ${{ secrets.UPLOAD_KEYSTORE_PASSWORD }}
UPLOAD-KEYSTORE-PASSWORD: ${{ steps.get-kv-secrets.outputs.UPLOAD-KEYSTORE-PASSWORD }}
run: |
bundle exec fastlane bundlePlayStoreRelease \
storeFile:app_upload-keystore.jks \
storePassword:${{ env.UPLOAD_KEYSTORE_PASSWORD }} \
storePassword:${{ env.UPLOAD-KEYSTORE-PASSWORD }} \
keyAlias:upload \
keyPassword:${{ env.UPLOAD_KEYSTORE_PASSWORD }}
keyPassword:${{ env.UPLOAD-KEYSTORE-PASSWORD }}
- name: Generate beta Play Store bundle
if: ${{ (matrix.variant == 'prod') && (matrix.artifact == 'aab') }}
env:
UPLOAD_BETA_KEYSTORE_PASSWORD: ${{ secrets.UPLOAD_BETA_KEYSTORE_PASSWORD }}
UPLOAD_BETA_KEY_PASSWORD: ${{ secrets.UPLOAD_BETA_KEY_PASSWORD }}
UPLOAD-BETA-KEYSTORE-PASSWORD: ${{ steps.get-kv-secrets.outputs.UPLOAD-BETA-KEYSTORE-PASSWORD }}
UPLOAD-BETA-KEY-PASSWORD: ${{ steps.get-kv-secrets.outputs.UPLOAD-BETA-KEY-PASSWORD }}
run: |
bundle exec fastlane bundlePlayStoreBeta \
storeFile:app_beta_upload-keystore.jks \
storePassword:${{ env.UPLOAD_BETA_KEYSTORE_PASSWORD }} \
storePassword:${{ env.UPLOAD-BETA-KEYSTORE-PASSWORD }} \
keyAlias:bitwarden-beta-upload \
keyPassword:${{ env.UPLOAD_BETA_KEY_PASSWORD }}
keyPassword:${{ env.UPLOAD-BETA-KEY-PASSWORD }}
- name: Generate release Play Store APK
if: ${{ (matrix.variant == 'prod') && (matrix.artifact == 'apk') }}
env:
PLAY_KEYSTORE_PASSWORD: ${{ secrets.PLAY_KEYSTORE_PASSWORD }}
PLAY-KEYSTORE-PASSWORD: ${{ steps.get-kv-secrets.outputs.PLAY-KEYSTORE-PASSWORD }}
run: |
bundle exec fastlane assemblePlayStoreReleaseApk \
storeFile:app_play-keystore.jks \
storePassword:${{ env.PLAY_KEYSTORE_PASSWORD }} \
storePassword:${{ env.PLAY-KEYSTORE-PASSWORD }} \
keyAlias:bitwarden \
keyPassword:${{ env.PLAY_KEYSTORE_PASSWORD }}
keyPassword:${{ env.PLAY-KEYSTORE-PASSWORD }}
- name: Generate beta Play Store APK
if: ${{ (matrix.variant == 'prod') && (matrix.artifact == 'apk') }}
env:
PLAY_BETA_KEYSTORE_PASSWORD: ${{ secrets.PLAY_BETA_KEYSTORE_PASSWORD }}
PLAY_BETA_KEY_PASSWORD: ${{ secrets.PLAY_BETA_KEY_PASSWORD }}
PLAY-BETA-KEYSTORE-PASSWORD: ${{ steps.get-kv-secrets.outputs.PLAY-BETA-KEYSTORE-PASSWORD }}
PLAY-BETA-KEY-PASSWORD: ${{ steps.get-kv-secrets.outputs.PLAY-BETA-KEY-PASSWORD }}
run: |
bundle exec fastlane assemblePlayStoreBetaApk \
storeFile:app_beta_play-keystore.jks \
storePassword:${{ env.PLAY_BETA_KEYSTORE_PASSWORD }} \
storePassword:${{ env.PLAY-BETA-KEYSTORE-PASSWORD }} \
keyAlias:bitwarden-beta \
keyPassword:${{ env.PLAY_BETA_KEY_PASSWORD }}
keyPassword:${{ env.PLAY-BETA-KEY-PASSWORD }}
- name: Generate debug Play Store APKs
if: ${{ (matrix.variant != 'prod') && (matrix.artifact == 'apk') }}
@@ -394,7 +417,7 @@ jobs:
bundle exec fastlane run validate_play_store_json_key
- name: Publish Play Store bundle
if: ${{ matrix.variant == 'prod' && matrix.artifact == 'aab' && (inputs.publish-to-play-store || github.ref_name == 'main') }}
if: ${{ matrix.variant == 'prod' && matrix.artifact == 'aab' && (inputs.publish-to-play-store || github.event_name == 'push') }}
run: |
bundle exec fastlane publishProdToPlayStore
bundle exec fastlane publishBetaToPlayStore
@@ -406,10 +429,10 @@ jobs:
runs-on: ubuntu-24.04
steps:
- name: Check out repo
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Configure Ruby
uses: ruby/setup-ruby@ca041f971d66735f3e5ff1e21cc13e2d51e7e535 # v1.233.0
uses: ruby/setup-ruby@44511735964dcb71245e7e55f72539531f7bc0eb # v1.257.0
with:
bundler-cache: true
@@ -420,9 +443,18 @@ jobs:
bundle install --jobs 4 --retry 3
- name: Log in to Azure
uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0
uses: bitwarden/gh-actions/azure-login@main
with:
creds: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }}
subscription_id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
tenant_id: ${{ secrets.AZURE_TENANT_ID }}
client_id: ${{ secrets.AZURE_CLIENT_ID }}
- name: Get Azure Key Vault secrets
id: get-kv-secrets
uses: bitwarden/gh-actions/get-keyvault-secrets@main
with:
keyvault: gh-android
secrets: "FDROID-KEYSTORE-PASSWORD,FDROID-BETA-KEYSTORE-PASSWORD,FDROID-BETA-KEY-PASSWORD"
- name: Retrieve secrets
env:
@@ -445,11 +477,14 @@ jobs:
az storage blob download --account-name $ACCOUNT_NAME --container-name $CONTAINER_NAME \
--name app_fdroid_firebase-creds.json --file ${{ github.workspace }}/secrets/app_fdroid_firebase-creds.json --output none
- name: Log out from Azure
uses: bitwarden/gh-actions/azure-logout@main
- name: Validate Gradle wrapper
uses: gradle/actions/wrapper-validation@06832c7b30a0129d7fb559bcc6e43d26f6374244 # v4.3.1
uses: gradle/actions/wrapper-validation@017a9effdb900e5b5b2fddfb590a105619dca3c3 # v4.4.2
- name: Cache Gradle files
uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3
uses: actions/cache@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4
with:
path: |
~/.gradle/caches
@@ -459,7 +494,7 @@ jobs:
${{ runner.os }}-gradle-v2-
- name: Cache build output
uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3
uses: actions/cache@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4
with:
path: |
${{ github.workspace }}/build-cache
@@ -468,7 +503,7 @@ jobs:
${{ runner.os }}-build-
- name: Configure JDK
uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # v4.7.1
uses: actions/setup-java@dded0888837ed1f317902acf8a20df0ad188d165 # v5.0.0
with:
distribution: "temurin"
java-version: ${{ env.JAVA_VERSION }}
@@ -491,15 +526,15 @@ jobs:
versionCode:$VERSION_CODE \
versionName:${{ inputs.version-name || '' }}
regex='versionName = "([^"]+)"'
if [[ "$(cat app/build.gradle.kts)" =~ $regex ]]; then
regex='appVersionName = "([^"]+)"'
if [[ "$(cat gradle/libs.versions.toml)" =~ $regex ]]; then
VERSION_NAME="${BASH_REMATCH[1]}"
fi
echo "Version Name: ${VERSION_NAME}" >> $GITHUB_STEP_SUMMARY
echo "Version Number: $VERSION_CODE" >> $GITHUB_STEP_SUMMARY
- name: Generate F-Droid artifacts
env:
FDROID_STORE_PASSWORD: ${{ secrets.FDROID_KEYSTORE_PASSWORD }}
FDROID_STORE_PASSWORD: ${{ steps.get-kv-secrets.outputs.FDROID-KEYSTORE-PASSWORD }}
run: |
bundle exec fastlane assembleFDroidReleaseApk \
storeFile:app_fdroid-keystore.jks \
@@ -509,14 +544,14 @@ jobs:
- name: Generate F-Droid Beta Artifacts
env:
FDROID_BETA_KEYSTORE_PASSWORD: ${{ secrets.FDROID_BETA_KEYSTORE_PASSWORD }}
FDROID_BETA_KEY_PASSWORD: ${{ secrets.FDROID_BETA_KEY_PASSWORD }}
FDROID-BETA-KEYSTORE-PASSWORD: ${{ steps.get-kv-secrets.outputs.FDROID-BETA-KEYSTORE-PASSWORD }}
FDROID-BETA-KEY-PASSWORD: ${{ steps.get-kv-secrets.outputs.FDROID-BETA-KEY-PASSWORD }}
run: |
bundle exec fastlane assembleFDroidBetaApk \
storeFile:app_beta_fdroid-keystore.jks \
storePassword:"${{ env.FDROID_BETA_KEYSTORE_PASSWORD }}" \
storePassword:"${{ env.FDROID-BETA-KEYSTORE-PASSWORD }}" \
keyAlias:bitwarden-beta \
keyPassword:"${{ env.FDROID_BETA_KEY_PASSWORD }}"
keyPassword:"${{ env.FDROID-BETA-KEY-PASSWORD }}"
- name: Upload F-Droid .apk artifact
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2

View File

@@ -21,7 +21,7 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # 4.2.2
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Download Google Privileged Browsers List
run: curl -s $SOURCE_URL -o $GOOGLE_FILE

View File

@@ -8,30 +8,29 @@ on:
jobs:
crowdin-sync:
name: Crowdin Pull - ${{ matrix.name }} - ${{ github.event_name }}
name: Crowdin Pull - ${{ github.event_name }}
runs-on: ubuntu-24.04
permissions:
contents: write
pull-requests: write
strategy:
matrix:
include:
- name: Password Manager
project_id: 269690
config: crowdin-bwpm.yml
branch: crowdin-pull-bwpm
- name: Authenticator
project_id: 673718
config: crowdin-bwa.yml
branch: crowdin-pull-bwa
id-token: write
steps:
- name: Checkout repo
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Login to Azure - CI Subscription
uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0
- name: Log in to Azure
uses: bitwarden/gh-actions/azure-login@main
with:
creds: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }}
subscription_id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
tenant_id: ${{ secrets.AZURE_TENANT_ID }}
client_id: ${{ secrets.AZURE_CLIENT_ID }}
- name: Get Azure Key Vault secrets
id: get-kv-secrets
uses: bitwarden/gh-actions/get-keyvault-secrets@main
with:
keyvault: gh-org-bitwarden
secrets: "BW-GHAPP-ID,BW-GHAPP-KEY"
- name: Retrieve secrets
id: retrieve-secrets
@@ -40,30 +39,33 @@ jobs:
keyvault: "bitwarden-ci"
secrets: "crowdin-api-token, github-gpg-private-key, github-gpg-private-key-passphrase"
- name: Log out from Azure
uses: bitwarden/gh-actions/azure-logout@main
- name: Generate GH App token
uses: actions/create-github-app-token@3ff1caaa28b64c9cc276ce0a02e2ff584f3900c5 # v2.0.2
uses: actions/create-github-app-token@a8d616148505b5069dccd32f177bb87d7f39123b # v2.1.1
id: app-token
with:
app-id: ${{ secrets.BW_GHAPP_ID }}
private-key: ${{ secrets.BW_GHAPP_KEY }}
app-id: ${{ steps.get-kv-secrets.outputs.BW-GHAPP-ID }}
private-key: ${{ steps.get-kv-secrets.outputs.BW-GHAPP-KEY }}
- name: Download translations for ${{ matrix.name }}
uses: crowdin/github-action@b8012bd5491b8aa8578b73ab5b5f5e7c94aaa6e2 # v2.7.0
- name: Download translations
uses: crowdin/github-action@0749939f635900a2521aa6aac7a3766642b2dc71 # v2.11.0
env:
GITHUB_TOKEN: ${{ steps.app-token.outputs.token }}
CROWDIN_API_TOKEN: ${{ steps.retrieve-secrets.outputs.crowdin-api-token }}
_CROWDIN_PROJECT_ID: ${{ matrix.project_id }}
_CROWDIN_PROJECT_ID: "269690"
with:
config: ${{ matrix.config }}
config: crowdin.yml
upload_sources: false
upload_translations: false
download_translations: true
github_user_name: "bitwarden-devops-bot"
github_user_email: "106330231+bitwarden-devops-bot@users.noreply.github.com"
commit_message: "Crowdin Pull - ${{ matrix.name }}"
localization_branch_name: ${{ matrix.branch }}
commit_message: "Crowdin Pull"
localization_branch_name: "crowdin-pull"
create_pull_request: true
pull_request_title: "Crowdin Pull - ${{ matrix.name }}"
pull_request_body: ":inbox_tray: New translations for ${{ matrix.name }} received!"
pull_request_title: "Crowdin Pull"
pull_request_body: ":inbox_tray: New translations received!"
gpg_private_key: ${{ steps.retrieve-secrets.outputs.github-gpg-private-key }}
gpg_passphrase: ${{ steps.retrieve-secrets.outputs.github-gpg-private-key-passphrase }}

View File

@@ -13,14 +13,17 @@ jobs:
runs-on: ubuntu-24.04
permissions:
contents: read
id-token: write
steps:
- name: Check out repo
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Log in to Azure
uses: Azure/login@cb79c773a3cfa27f31f25eb3f677781210c9ce3d # v1.6.1
uses: bitwarden/gh-actions/azure-login@main
with:
creds: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }}
subscription_id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
tenant_id: ${{ secrets.AZURE_TENANT_ID }}
client_id: ${{ secrets.AZURE_CLIENT_ID }}
- name: Retrieve secrets
id: retrieve-secrets
@@ -29,24 +32,16 @@ jobs:
keyvault: "bitwarden-ci"
secrets: "crowdin-api-token"
- name: Upload sources for Password Manager
uses: crowdin/github-action@b8012bd5491b8aa8578b73ab5b5f5e7c94aaa6e2 # v2.7.0
- name: Upload sources
uses: crowdin/github-action@0749939f635900a2521aa6aac7a3766642b2dc71 # v2.11.0
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
CROWDIN_API_TOKEN: ${{ steps.retrieve-secrets.outputs.crowdin-api-token }}
_CROWDIN_PROJECT_ID: "269690"
with:
config: crowdin-bwpm.yml
config: crowdin.yml
upload_sources: true
upload_translations: false
- name: Upload sources for Authenticator
uses: crowdin/github-action@b8012bd5491b8aa8578b73ab5b5f5e7c94aaa6e2 # v2.7.0
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
CROWDIN_API_TOKEN: ${{ steps.retrieve-secrets.outputs.crowdin-api-token }}
_CROWDIN_PROJECT_ID: "673718"
with:
config: crowdin-bwa.yml
upload_sources: true
upload_translations: false
- name: Log out from Azure
uses: bitwarden/gh-actions/azure-logout@main

View File

@@ -3,81 +3,116 @@ name: Create GitHub Release
on:
workflow_dispatch:
inputs:
version-name:
description: 'Version Name - E.g. "2024.11.1"'
required: true
type: string
version-number:
description: 'Version Number - E.g. "123456"'
required: true
type: string
artifact-run-id:
description: 'GitHub Action Run ID containing artifacts'
required: true
type: string
draft:
description: 'Create as draft release'
type: boolean
default: true
prerelease:
description: 'Mark as pre-release'
type: boolean
default: true
make-latest:
description: 'Set as the latest release'
type: boolean
branch-protection-type:
description: 'Branch protection type'
type: choice
options:
- Branch Name
- GitHub API
default: Branch Name
release-ticket-id:
description: 'Release Ticket ID - e.g. RELEASE-1762'
required: true
type: string
env:
ARTIFACTS_PATH: artifacts
jobs:
create-release:
name: Create GitHub Release
runs-on: ubuntu-24.04
permissions:
contents: write
id-token: write
steps:
- name: Check out repository
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with:
fetch-depth: 0
- name: Log inputs to job summary
uses: ./.github/actions/log-inputs
with:
inputs: ${{ toJson(inputs) }}
- name: Get branch from workflow run
id: get_release_branch
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
ARTIFACT_RUN_ID: ${{ inputs.artifact-run-id }}
BRANCH_PROTECTION_TYPE: ${{ inputs.branch-protection-type }}
run: |
release_branch=$(gh run view $ARTIFACT_RUN_ID --json headBranch -q .headBranch)
workflow_data=$(gh run view $ARTIFACT_RUN_ID --json headBranch,workflowName)
release_branch=$(echo "$workflow_data" | jq -r .headBranch)
workflow_name=$(echo "$workflow_data" | jq -r .workflowName)
case "$BRANCH_PROTECTION_TYPE" in
"Branch Name")
if [[ "$release_branch" != "main" && ! "$release_branch" =~ ^release/ ]]; then
echo "::error::Branch '$release_branch' is not 'main' or a release branch starting with 'release/'. Releases must be created from protected branches."
exit 1
fi
# branch protection check
if [[ "$release_branch" != "main" && ! "$release_branch" =~ ^release/ ]]; then
echo "::error::Branch '$release_branch' is not 'main' or a release branch starting with 'release/'. Releases must be created from protected branches."
exit 1
fi
echo "🔖 Release branch: $release_branch"
echo "🔖 Workflow name: $workflow_name"
echo "release_branch=$release_branch" >> $GITHUB_OUTPUT
echo "workflow_name=$workflow_name" >> $GITHUB_OUTPUT
case "$workflow_name" in
*"Password Manager"* | "Build")
app_name="Password Manager"
app_name_suffix="bwpm"
;;
"GitHub API")
#NOTE requires token with "administration:read" scope
if ! gh api "repos/${{ github.repository }}/branches/$release_branch/protection" | grep -q "required_status_checks"; then
echo "::error::Branch '$release_branch' is not protected. Releases must be created from protected branches. If that's not correct, confirm if the github token user has the 'administration:read' scope."
exit 1
fi
*"Authenticator"*)
app_name="Authenticator"
app_name_suffix="bwa"
;;
*)
echo "::error::Unsupported branch protection type: $BRANCH_PROTECTION_TYPE"
echo "::error::Unknown workflow name: $workflow_name"
exit 1
;;
esac
echo "🔖 App name: $app_name"
echo "🔖 App name suffix: $app_name_suffix"
echo "app_name=$app_name" >> $GITHUB_OUTPUT
echo "app_name_suffix=$app_name_suffix" >> $GITHUB_OUTPUT
echo "release_branch=$release_branch" >> $GITHUB_OUTPUT
- name: Get version info from run logs and set release tag name
id: get_release_info
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
ARTIFACT_RUN_ID: ${{ inputs.artifact-run-id }}
_APP_NAME_SUFFIX: ${{ steps.get_release_branch.outputs.app_name_suffix }}
run: |
workflow_log=$(gh run view $ARTIFACT_RUN_ID --log)
version_number_with_trailing_dot=$(grep -m 1 "Setting version code to" <<< "$workflow_log" | sed 's/.*Setting version code to //')
version_number=${version_number_with_trailing_dot%.} # remove trailing dot
version_name_with_trailing_dot=$(grep -m 1 "Setting version name to" <<< "$workflow_log" | sed 's/.*Setting version name to //')
version_name=${version_name_with_trailing_dot%.} # remove trailing dot
if [[ -z "$version_name" ]]; then
echo "::warning::Version name not found. Using default value - 0.0.0"
version_name="0.0.0"
else
echo "✅ Found version name: $version_name"
fi
if [[ -z "$version_number" ]]; then
echo "::warning::Version number not found. Using default value - 0"
version_number="0"
else
echo "✅ Found version number: $version_number"
fi
echo "version_number=$version_number" >> $GITHUB_OUTPUT
echo "version_name=$version_name" >> $GITHUB_OUTPUT
tag_name="v$version_name-$_APP_NAME_SUFFIX" # e.g. v2025.6.0-bwpm
echo "🔖 New tag name: $tag_name"
echo "tag_name=$tag_name" >> $GITHUB_OUTPUT
last_release_tag=$(git tag -l --sort=-authordate | grep "$_APP_NAME_SUFFIX" | head -n 1)
echo "🔖 Last release tag: $last_release_tag"
echo "last_release_tag=$last_release_tag" >> $GITHUB_OUTPUT
- name: Download artifacts
env:
@@ -92,37 +127,156 @@ jobs:
find $ARTIFACTS_PATH -type f
fi
- name: Create Release
id: create_release
uses: softprops/action-gh-release@da05d552573ad5aba039eaac05058a918a7bf631 # v2.2.2
with:
tag_name: "v${{ inputs.version-name }}"
name: "${{ inputs.version-name }} (${{ inputs.version-number }})"
prerelease: ${{ inputs.prerelease }}
draft: ${{ inputs.draft }}
make_latest: ${{ inputs.make-latest }}
target_commitish: ${{ steps.get_release_branch.outputs.release_branch }}
generate_release_notes: true
files: |
artifacts/**/*
# Files that won't be included in any release
files_to_remove=(
"com.x8bit.bitwarden.aab"
"com.x8bit.bitwarden.aab-sha256.txt"
- name: Update Release Description
"com.x8bit.bitwarden.beta.apk"
"com.x8bit.bitwarden.beta.apk-sha256.txt"
"com.x8bit.bitwarden.beta.aab"
"com.x8bit.bitwarden.beta.aab-sha256.txt"
"com.x8bit.bitwarden.beta-fdroid.apk"
"com.x8bit.bitwarden.beta-fdroid.apk-sha256.txt"
"com.x8bit.bitwarden.dev.apk"
"com.x8bit.bitwarden.dev.apk-sha256.txt"
"com.bitwarden.authenticator.aab"
"authenticator-android-aab-sha256.txt"
)
for file in "${files_to_remove[@]}"; do
find $ARTIFACTS_PATH -name "$file" -type f -delete
done
echo "🔖 Removed internal artifacts."
echo ""
echo "🔖 Files to be included in the release:"
find $ARTIFACTS_PATH -type f
- name: Log in to Azure
uses: bitwarden/gh-actions/azure-login@main
with:
subscription_id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
tenant_id: ${{ secrets.AZURE_TENANT_ID }}
client_id: ${{ secrets.AZURE_CLIENT_ID }}
- name: Get Azure Key Vault secrets
id: get-kv-secrets
uses: bitwarden/gh-actions/get-keyvault-secrets@main
with:
keyvault: gh-android
secrets: "JIRA-API-EMAIL,JIRA-API-TOKEN"
- name: Log out from Azure
uses: bitwarden/gh-actions/azure-logout@main
- name: Get product release notes
id: get_release_notes
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
RELEASE_ID: ${{ steps.create_release.outputs.id }}
RELEASE_URL: ${{ steps.create_release.outputs.url }}
ARTIFACT_RUN_ID: ${{ inputs.artifact-run-id }}
_VERSION_NAME: ${{ steps.get_release_info.outputs.version_name }}
_RELEASE_TICKET_ID: ${{ inputs.release-ticket-id }}
_JIRA_API_EMAIL: ${{ steps.get-kv-secrets.outputs.JIRA-API-EMAIL }}
_JIRA_API_TOKEN: ${{ steps.get-kv-secrets.outputs.JIRA-API-TOKEN }}
run: |
# Get current release body
current_body=$(gh api /repos/${{ github.repository }}/releases/$RELEASE_ID --jq .body)
echo "Getting product release notes"
product_release_notes=$(python3 .github/scripts/jira-get-release-notes/jira_release_notes.py $_RELEASE_TICKET_ID $_JIRA_API_EMAIL $_JIRA_API_TOKEN)
# Append build source to the end
updated_body="${current_body}
if [[ -z "$product_release_notes" || $product_release_notes == "Error checking"* ]]; then
echo "::warning::Failed to fetch release notes from Jira. Output: $product_release_notes"
product_release_notes="<insert product release notes here>"
else
echo "✅ Product release notes:"
echo "$product_release_notes"
fi
echo "$product_release_notes" > product_release_notes.txt
- name: Create Release
id: create_release
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
_APP_NAME: ${{ steps.get_release_branch.outputs.app_name }}
_VERSION_NAME: ${{ steps.get_release_info.outputs.version_name }}
_VERSION_NUMBER: ${{ steps.get_release_info.outputs.version_number }}
_TARGET_COMMIT: ${{ steps.get_release_branch.outputs.release_branch }}
_TAG_NAME: ${{ steps.get_release_info.outputs.tag_name }}
_LAST_RELEASE_TAG: ${{ steps.get_release_info.outputs.last_release_tag }}
run: |
is_latest_release=false
if [[ "$_APP_NAME" == "Password Manager" ]]; then
is_latest_release=true
fi
echo "⌛️ Creating release for $_APP_NAME $_VERSION_NAME ($_VERSION_NUMBER) on $_TARGET_COMMIT"
release_url=$(gh release create "$_TAG_NAME" \
--title "$_APP_NAME $_VERSION_NAME ($_VERSION_NUMBER)" \
--target "$_TARGET_COMMIT" \
--generate-notes \
--notes-start-tag "$_LAST_RELEASE_TAG" \
--latest=$is_latest_release \
--draft \
$ARTIFACTS_PATH/*/*)
# Extract release tag from URL
release_id_from_url=$(echo "$release_url" | sed 's/.*\/tag\///')
echo "release_id_from_url=$release_id_from_url" >> $GITHUB_OUTPUT
echo "url=$release_url" >> $GITHUB_OUTPUT
echo "✅ Release created: $release_url"
echo "🔖 Release ID from URL: $release_id_from_url"
- name: Update Release Description
id: update_release_description
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
ARTIFACT_RUN_ID: ${{ inputs.artifact-run-id }}
_VERSION_NAME: ${{ steps.get_release_info.outputs.version_name }}
_RELEASE_ID: ${{ steps.create_release.outputs.release_id_from_url }}
run: |
echo "Getting current release body. Release ID: $_RELEASE_ID"
current_body=$(gh release view "$_RELEASE_ID" --json body --jq .body)
product_release_notes=$(cat product_release_notes.txt)
# Update release description with product release notes and builds source
updated_body="# Overview
${product_release_notes}
${current_body}
**Builds Source:** https://github.com/${{ github.repository }}/actions/runs/$ARTIFACT_RUN_ID"
# Update release
gh api --method PATCH /repos/${{ github.repository }}/releases/$RELEASE_ID \
-f body="$updated_body"
new_release_url=$(gh release edit "$_RELEASE_ID" --notes "$updated_body")
echo "# :rocket: Release ready at:" >> $GITHUB_STEP_SUMMARY
echo "$RELEASE_URL" >> $GITHUB_STEP_SUMMARY
# draft release links change after editing
echo "release_url=$new_release_url" >> $GITHUB_OUTPUT
- name: Add Release Summary
env:
_RELEASE_TAG: ${{ steps.get_release_info.outputs.tag_name }}
_LAST_RELEASE_TAG: ${{ steps.get_release_info.outputs.last_release_tag }}
_VERSION_NAME: ${{ steps.get_release_info.outputs.version_name }}
_VERSION_NUMBER: ${{ steps.get_release_info.outputs.version_number }}
_RELEASE_BRANCH: ${{ steps.get_release_branch.outputs.release_branch }}
_RELEASE_URL: ${{ steps.update_release_description.outputs.release_url }}
run: |
echo "# :fish_cake: Release ready at:" >> $GITHUB_STEP_SUMMARY
echo "$_RELEASE_URL" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
if [[ "$_VERSION_NAME" == "0.0.0" || "$_VERSION_NUMBER" == "0" ]]; then
echo "> [!CAUTION]" >> $GITHUB_STEP_SUMMARY
echo "> Version name or number wasn't previously found and a default value was used. You'll need to manually update the release Title, Tag and Description, specifically, the "Full Changelog" link." >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
fi
echo ":clipboard: Confirm that the defined GitHub Release options are correct:" >> $GITHUB_STEP_SUMMARY
echo " * :bookmark: New tag name: \`$_RELEASE_TAG\`" >> $GITHUB_STEP_SUMMARY
echo " * :palm_tree: Target branch: \`$_RELEASE_BRANCH\`" >> $GITHUB_STEP_SUMMARY
echo " * :ocean: Previous tag set in the description \"Full Changelog\" link: \`$_LAST_RELEASE_TAG\`" >> $GITHUB_STEP_SUMMARY
echo " * :white_check_mark: Description has automated release notes and they match the commits in the release branch" >> $GITHUB_STEP_SUMMARY
echo "> [!NOTE]" >> $GITHUB_STEP_SUMMARY
echo "> Commits directly pushed to branches without a Pull Request won't appear in the automated release notes." >> $GITHUB_STEP_SUMMARY

View File

@@ -1,12 +1,36 @@
name: Publish GitHub Release as newest
name: Publish Password Manager and Authenticator GitHub Release as newest
on:
workflow_dispatch:
schedule:
- cron: '0 3 * * 1-5'
permissions:
contents: write
id-token: write
actions: read
jobs:
stub:
runs-on: ubuntu-24.04
name: Stub
steps:
- name: Stub
run: echo "This is a stub job to trigger the workflow."
publish-release-password-manager:
name: Publish Password Manager Release
uses: bitwarden/gh-actions/.github/workflows/_publish-mobile-github-release.yml@main
with:
release_name: "Password Manager"
workflow_name: "publish-github-release.yml"
credentials_filename: "play_creds.json"
project_type: android
check_release_command: >
bundle exec fastlane getLatestPlayStoreVersion package_name:com.x8bit.bitwarden track:production
secrets: inherit
publish-release-authenticator:
name: Publish Authenticator Release
uses: bitwarden/gh-actions/.github/workflows/_publish-mobile-github-release.yml@main
with:
release_name: "Authenticator"
workflow_name: "publish-github-release.yml"
credentials_filename: "authenticator_play_store-creds.json"
project_type: android
check_release_command: >
bundle exec fastlane getLatestPlayStoreVersion package_name:com.bitwarden.authenticator track:production
secrets: inherit

160
.github/workflows/publish-store.yml vendored Normal file
View File

@@ -0,0 +1,160 @@
name: Publish to Google Play
run-name: "Promoting ${{ inputs.product }} ${{ inputs.version-code }} from ${{ inputs.track-from }} to ${{ inputs.track-target }}"
on:
workflow_dispatch:
inputs:
product:
description: "Which app is being released."
type: choice
options:
- Password Manager
- Authenticator
version-name:
description: "Version name to promote to production ex 2025.1.1"
type: string
version-code:
description: "Build number to promote to production."
required: true
type: string
rollout-percentage:
description: "Percentage of users who will receive this version update."
required: true
type: choice
options:
- 10%
- 30%
- 50%
- 100%
default: 10%
release-notes:
description: "Change notes to be included with this release."
type: string
default: "Bug fixes."
required: true
track-from:
description: "Track to promote from."
type: choice
options:
- internal
- Fastlane Automation Source
required: true
default: "internal"
track-target:
description: "Track to promote to."
type: choice
options:
- production
- Fastlane Automation Target
required: true
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GITHUB_ACTION_RUN_URL: "https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}"
permissions:
contents: read
packages: read
id-token: write
jobs:
promote:
runs-on: ubuntu-24.04
name: Promote build to Production in Play Store
steps:
- name: Log inputs to job summary
run: |
echo "<details><summary>Job Inputs</summary>" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo '```json' >> $GITHUB_STEP_SUMMARY
echo '${{ toJson(inputs) }}' >> $GITHUB_STEP_SUMMARY
echo '```' >> $GITHUB_STEP_SUMMARY
echo "</details>" >> $GITHUB_STEP_SUMMARY
- name: Check out repo
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Configure Ruby
uses: ruby/setup-ruby@44511735964dcb71245e7e55f72539531f7bc0eb # v1.257.0
with:
bundler-cache: true
- name: Install Fastlane
run: |
gem install bundler:2.2.27
bundle config path vendor/bundle
bundle install --jobs 4 --retry 3
- name: Log in to Azure
uses: bitwarden/gh-actions/azure-login@main
with:
subscription_id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
tenant_id: ${{ secrets.AZURE_TENANT_ID }}
client_id: ${{ secrets.AZURE_CLIENT_ID }}
- name: Get Azure Key Vault secrets
id: get-kv-secrets
uses: bitwarden/gh-actions/get-keyvault-secrets@main
with:
keyvault: gh-android
secrets: "PLAY-BETA-KEYSTORE-PASSWORD,PLAY-BETA-KEY-PASSWORD"
- name: Retrieve secrets
env:
ACCOUNT_NAME: bitwardenci
CONTAINER_NAME: mobile
run: |
mkdir -p ${{ github.workspace }}/secrets
mkdir -p ${{ github.workspace }}/app/src/standardRelease
az storage blob download --account-name $ACCOUNT_NAME --container-name $CONTAINER_NAME \
--name play_creds.json --file ${{ github.workspace }}/secrets/play_creds.json --output none
az storage blob download --account-name $ACCOUNT_NAME --container-name $CONTAINER_NAME \
--name authenticator_play_store-creds.json --file ${{ github.workspace }}/secrets/authenticator_play_store-creds.json --output none
- name: Log out from Azure
uses: bitwarden/gh-actions/azure-logout@main
- name: Format Release Notes
run: |
FORMATTED_MESSAGE="$(echo "${{ inputs.release-notes }}" | sed 's/ /\n/g')"
echo "RELEASE_NOTES<<EOF" >> $GITHUB_ENV
echo "$FORMATTED_MESSAGE" >> $GITHUB_ENV
echo "EOF" >> $GITHUB_ENV
- name: Promote Play Store version to production
env:
PLAY_KEYSTORE_PASSWORD: ${{ steps.get-kv-secrets.outputs.PLAY-BETA-KEYSTORE-PASSWORD }}
PLAY_KEY_PASSWORD: ${{ steps.get-kv-secrets.outputs.PLAY-BETA-KEY-PASSWORD }}
VERSION_CODE_INPUT: ${{ inputs.version-code }}
VERSION_NAME: ${{inputs.version-name}}
ROLLOUT_PERCENTAGE: ${{ inputs.rollout-percentage }}
PRODUCT: ${{ inputs.product }}
TRACK_FROM: ${{ inputs.track-from }}
TRACK_TARGET: ${{ inputs.track-target }}
run: |
if [ "$PRODUCT" = "Password Manager" ]; then
PACKAGE_NAME="com.x8bit.bitwarden"
elif [ "$PRODUCT" = "Authenticator" ]; then
PACKAGE_NAME="com.bitwarden.authenticator"
else
echo "Unsupported product: $PRODUCT"
exit 1
fi
VERSION_CODE=$(echo "${VERSION_CODE_INPUT}" | tr -d ',')
decimal=$(echo "scale=2; ${ROLLOUT_PERCENTAGE/\%/} / 100" | bc)
bundle exec fastlane updateReleaseNotes \
releaseNotes:"$RELEASE_NOTES" \
versionCode:"$VERSION_CODE" \
packageName:"$PACKAGE_NAME"
bundle exec fastlane promoteToProduction \
versionCode:"$VERSION_CODE" \
versionName:"$VERSION_NAME" \
rolloutPercentage:"$decimal" \
packageName:"$PACKAGE_NAME" \
releaseNotes:"$RELEASE_NOTES" \
track:"$TRACK_FROM" \
trackPromoteTo:"$TRACK_TARGET"

View File

@@ -1,14 +0,0 @@
name: Publish
on:
workflow_dispatch:
jobs:
publish:
runs-on: ubuntu-24.04
name: Promote build to Production in Play Store
steps:
- name: TEST STEP
run: exit 0

View File

@@ -9,7 +9,9 @@ on:
type: choice
options:
- RC
- Hotfix
- Hotfix Password Manager
- Hotfix Authenticator
- Test
jobs:
create-release-branch:
@@ -17,38 +19,54 @@ jobs:
runs-on: ubuntu-24.04
permissions:
contents: write
actions: write
steps:
- name: Check out repository
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with:
fetch-depth: 0
- name: Create RC Branch
if: inputs.release_type == 'RC'
- name: Create RC or Test Branch
id: rc_branch
if: inputs.release_type == 'RC' || inputs.release_type == 'Test'
env:
RC_PREFIX_DATE: "true" # replace with input if needed
_TEST_MODE: ${{ inputs.release_type == 'Test' }}
_RELEASE_TYPE: ${{ inputs.release_type }}
run: |
if [ "$RC_PREFIX_DATE" = "true" ]; then
current_date=$(date +'%Y.%m')
branch_name="release/${current_date}-rc${{ github.run_number }}"
else
branch_name="release/rc${{ github.run_number }}"
current_date=$(date +'%Y.%-m')
branch_name="${current_date}-rc${{ github.run_number }}"
if [ "$_TEST_MODE" = "true" ]; then
branch_name="WORKFLOW-TEST-${branch_name}"
fi
branch_name="release/${branch_name}"
git switch main
git switch -c $branch_name
git push origin $branch_name
echo "# :cherry_blossom: RC branch: ${branch_name}" >> $GITHUB_STEP_SUMMARY
echo "# :cherry_blossom: ${_RELEASE_TYPE} branch: ${branch_name}" >> $GITHUB_STEP_SUMMARY
echo "branch_name=$branch_name" >> $GITHUB_OUTPUT
- name: Create Hotfix Branch
if: inputs.release_type == 'Hotfix'
id: hotfix_branch
if: startsWith(inputs.release_type, 'Hotfix')
env:
_RELEASE_TYPE: ${{ inputs.release_type }}
run: |
latest_tag=$(git tag -l --sort=-creatordate | head -n 1)
app_codename="bwpm"
if [ "$_RELEASE_TYPE" == "Hotfix Authenticator" ]; then
app_codename="bwa"
fi
echo "🌿 app codename: $app_codename"
latest_tag=$(git tag -l --sort=-creatordate | grep "$app_codename" | head -n 1)
if [ -z "$latest_tag" ]; then
echo "::error::No tags found in the repository"
exit 1
fi
branch_name="release/hotfix-${latest_tag}"
echo "🌿 branch name: $branch_name"
echo "branch_name=$branch_name" >> $GITHUB_OUTPUT
if git show-ref --verify --quiet "refs/remotes/origin/$branch_name"; then
echo "# :fire: :warning: Hotfix branch already exists: ${branch_name}" >> $GITHUB_STEP_SUMMARY
exit 0
@@ -56,3 +74,12 @@ jobs:
git switch -c $branch_name $latest_tag
git push origin $branch_name
echo "# :fire: Hotfix branch: ${branch_name}" >> $GITHUB_STEP_SUMMARY
- name: Trigger CI Workflows
env:
GH_TOKEN: ${{ github.token }}
_BRANCH_NAME: ${{ steps.rc_branch.outputs.branch_name || steps.hotfix_branch.outputs.branch_name }}
run: |
echo "🌿 branch name: $_BRANCH_NAME"
gh workflow run build.yml --ref $_BRANCH_NAME -f distribute-to-firebase=true -f publish-to-play-store=true
gh workflow run build-authenticator.yml --ref $_BRANCH_NAME -f distribute-to-firebase=true -f publish-to-play-store=true

View File

@@ -6,55 +6,30 @@ on:
branches:
- "main"
permissions: {}
jobs:
sast:
name: SAST scan
runs-on: ubuntu-24.04
name: Checkmarx
uses: bitwarden/gh-actions/.github/workflows/_checkmarx.yml@main
secrets:
AZURE_SUBSCRIPTION_ID: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }}
AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }}
permissions:
contents: read
pull-requests: write
security-events: write
steps:
- name: Check out repo
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
fetch-depth: 0
- name: Scan with Checkmarx
uses: checkmarx/ast-github-action@ef93013c95adc60160bc22060875e90800d3ecfc # 2.3.19
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 .
- name: Upload Checkmarx results to GitHub
uses: github/codeql-action/upload-sarif@45775bd8235c68ba998cffa5171334d58593da47 # v3.28.15
with:
sarif_file: cx_result.sarif
id-token: write
quality:
name: Quality scan
runs-on: ubuntu-24.04
name: Sonar
uses: bitwarden/gh-actions/.github/workflows/_sonar.yml@main
secrets:
AZURE_SUBSCRIPTION_ID: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }}
AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }}
permissions:
contents: read
steps:
- name: Check out repo
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
fetch-depth: 0
- name: Scan with SonarCloud
uses: sonarsource/sonarqube-scan-action@aa494459d7c39c106cc77b166de8b4250a32bb97 # v5.1.0
env:
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
with:
args: >
-Dsonar.organization=${{ github.repository_owner }}
-Dsonar.projectKey=${{ github.repository_owner }}_${{ github.event.repository.name }}
pull-requests: write
id-token: write

View File

@@ -11,70 +11,38 @@ on:
branches:
- main
permissions: {}
jobs:
check-run:
name: Check PR run
uses: bitwarden/gh-actions/.github/workflows/check-run.yml@main
permissions: read-all
permissions:
contents: read
sast:
name: SAST scan
runs-on: ubuntu-24.04
name: Checkmarx
uses: bitwarden/gh-actions/.github/workflows/_checkmarx.yml@main
needs: check-run
secrets:
AZURE_SUBSCRIPTION_ID: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }}
AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }}
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@ef93013c95adc60160bc22060875e90800d3ecfc # 2.3.19
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@45775bd8235c68ba998cffa5171334d58593da47 # v3.28.15
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 }}
id-token: write
quality:
name: Quality scan
runs-on: ubuntu-24.04
name: Sonar
uses: bitwarden/gh-actions/.github/workflows/_sonar.yml@main
needs: check-run
secrets:
AZURE_SUBSCRIPTION_ID: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }}
AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }}
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@aa494459d7c39c106cc77b166de8b4250a32bb97 # v5.1.0
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 }}
id-token: write

227
.github/workflows/sdlc-sdk-update.yml vendored Normal file
View File

@@ -0,0 +1,227 @@
name: SDLC / SDK Update
run-name: "SDK ${{inputs.run-mode == 'Update' && format('Update - {0}', inputs.sdk-version) || format('Test #{0} - {1}', inputs.pr-id, inputs.sdk-version)}}"
on:
workflow_dispatch:
inputs:
run-mode:
description: "Run Mode"
type: choice
options:
- Test # used for testing sdk-internal repo PRs
- Update # opens a PR in this repo updating the SDK
default: Test
sdk-package:
description: "SDK Package ID"
required: true
default: "com.bitwarden:sdk-android.dev"
sdk-version:
description: "SDK Version"
required: true
default: "1.0.0-2686-km-update-kdf-sdk"
pr-id:
description: "Pull Request ID"
env:
_BOT_NAME: "bw-ghapp[bot]"
_BOT_EMAIL: "178206702+bw-ghapp[bot]@users.noreply.github.com"
jobs:
update:
name: Update and PR
if: ${{ inputs.run-mode == 'Update' }}
runs-on: ubuntu-24.04
permissions:
id-token: write
steps:
- name: Log in to Azure
uses: bitwarden/gh-actions/azure-login@main
with:
subscription_id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
tenant_id: ${{ secrets.AZURE_TENANT_ID }}
client_id: ${{ secrets.AZURE_CLIENT_ID }}
- name: Get Azure Key Vault secrets
id: get-kv-secrets
uses: bitwarden/gh-actions/get-keyvault-secrets@main
with:
keyvault: gh-org-bitwarden
secrets: "BW-GHAPP-ID,BW-GHAPP-KEY"
- name: Log out from Azure
uses: bitwarden/gh-actions/azure-logout@main
- name: Generate GH App token
uses: actions/create-github-app-token@a8d616148505b5069dccd32f177bb87d7f39123b # v2.1.1
id: app-token
with:
app-id: ${{ steps.get-kv-secrets.outputs.BW-GHAPP-ID }}
private-key: ${{ steps.get-kv-secrets.outputs.BW-GHAPP-KEY }}
permission-pull-requests: write
permission-actions: read
permission-contents: write
- name: Check out repo
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with:
token: ${{ steps.app-token.outputs.token }}
fetch-depth: 0
- name: Log inputs to job summary
uses: ./.github/actions/log-inputs
with:
inputs: ${{ toJson(inputs) }}
- name: Switch to branch
id: switch-branch
run: |
BRANCH_NAME="sdlc/sdk-update"
echo "branch_name=$BRANCH_NAME" >> $GITHUB_OUTPUT
if git switch $BRANCH_NAME; then
echo "✅ Switched to existing branch: $BRANCH_NAME"
echo "updating_existing_branch=true" >> $GITHUB_OUTPUT
else
echo "📝 Creating new branch: $BRANCH_NAME"
git switch -c $BRANCH_NAME
echo "updating_existing_branch=false" >> $GITHUB_OUTPUT
fi
- name: Prevent updating the branch when the last committer isn't the bot
if: ${{ steps.switch-branch.outputs.updating_existing_branch == 'true' }}
env:
GH_TOKEN: ${{ steps.app-token.outputs.token }}
_BRANCH_NAME: ${{ steps.switch-branch.outputs.branch_name }}
run: |
LATEST_COMMIT_AUTHOR=$(git log -1 --format='%ae' $_BRANCH_NAME)
echo "Latest commit author in branch ($_BRANCH_NAME): $LATEST_COMMIT_AUTHOR"
echo "Expected bot email: $_BOT_EMAIL"
if [ "$LATEST_COMMIT_AUTHOR" != "$_BOT_EMAIL" ]; then
echo "::error::Branch $_BRANCH_NAME has a commit not made by the bot." \
"This indicates manual changes have been made to the branch," \
"PR has to be merged or closed before running this workflow again."
echo "👀 Fetching existing PR..."
gh pr list --head $_BRANCH_NAME --base main --state open --json number --jq '.[0].number // empty'
EXISTING_PR=$(gh pr list --head $_BRANCH_NAME --base main --state open --json number --jq '.[0].number // empty')
if [ -z "$EXISTING_PR" ]; then
echo "::error::Couldn't find an existing PR for branch $_BRANCH_NAME."
exit 1
fi
PR_URL="https://github.com/${{ github.repository }}/pull/$EXISTING_PR"
echo "## ❌ Merge or close: $PR_URL" >> $GITHUB_STEP_SUMMARY
exit 1
fi
echo "✅ Branch tip commit was made by the bot. Safe to proceed."
# Using main to retrieve the changelog on consecutive updates of the same PR.
- name: Get current SDK version from main branch
id: get-current-sdk
run: |
git show origin/main:gradle/libs.versions.toml
SDK_VERSION=$(git show origin/main:gradle/libs.versions.toml | grep "bitwardenSdk =" | cut -d'"' -f2)
if [ -z "$SDK_VERSION" ]; then
echo "::error::Failed to get current SDK version from main branch."
exit 1
fi
GIT_REF=$(echo "$SDK_VERSION" | cut -d'-' -f3-) # handles both commit hashes and branch names
echo "Current SDK version (from main): $SDK_VERSION"
echo "Current SDK git ref: $GIT_REF"
echo "version=$SDK_VERSION" >> $GITHUB_OUTPUT
echo "git_ref=$GIT_REF" >> $GITHUB_OUTPUT
- name: Update SDK Version
env:
_SDK_PACKAGE: ${{ inputs.sdk-package }}
_SDK_VERSION: ${{ inputs.sdk-version }}
run: |
./scripts/update-sdk-version.sh "$_SDK_PACKAGE" "$_SDK_VERSION"
- name: Create branch and commit
env:
_SDK_PACKAGE: ${{ inputs.sdk-package }}
_SDK_VERSION: ${{ inputs.sdk-version }}
_BRANCH_NAME: ${{ steps.switch-branch.outputs.branch_name }}
run: |
echo "👀 Committing SDK version update..."
git config user.name "$_BOT_NAME"
git config user.email "$_BOT_EMAIL"
git add gradle/libs.versions.toml
git commit -m "SDK Update - $_SDK_PACKAGE $_SDK_VERSION"
git push origin $_BRANCH_NAME
- name: Create or Update Pull Request
env:
GH_TOKEN: ${{ steps.app-token.outputs.token }}
_BRANCH_NAME: ${{ steps.switch-branch.outputs.branch_name }}
_SDK_PACKAGE: ${{ inputs.sdk-package }}
_SDK_VERSION: ${{ inputs.sdk-version }}
_OLD_SDK_VERSION: ${{ steps.get-current-sdk.outputs.version }}
_OLD_SDK_GIT_REF: ${{ steps.get-current-sdk.outputs.git_ref }}
run: |
NEW_SDK_GIT_REF=$(echo "$_SDK_VERSION" | cut -d'-' -f3-)
CHANGELOG=$(./scripts/get-repo-changelog.sh "bitwarden/sdk-internal" "$_OLD_SDK_GIT_REF" "$NEW_SDK_GIT_REF")
PR_BODY="Updates the SDK version from \`$_OLD_SDK_VERSION\` to \`$_SDK_PACKAGE $_SDK_VERSION\`
## What's Changed
$CHANGELOG"
EXISTING_PR=$(gh pr list --head $_BRANCH_NAME --base main --state open --json number --jq '.[0].number // empty')
if [ -n "$EXISTING_PR" ]; then
echo "🔄 Updating existing PR #$EXISTING_PR..."
echo -e "$PR_BODY" | gh pr edit $EXISTING_PR \
--title "Update SDK to $_SDK_VERSION" \
--body-file -
PR_URL="https://github.com/${{ github.repository }}/pull/$EXISTING_PR"
echo "## ✅ Updated PR: $PR_URL" >> $GITHUB_STEP_SUMMARY
else
echo "📝 Creating new PR..."
PR_URL=$(echo -e "$PR_BODY" | gh pr create \
--title "Update SDK to $_SDK_VERSION" \
--body-file - \
--base main \
--head $_BRANCH_NAME \
--label "automated-pr" \
--label "t:ci")
echo "## 🚀 Created PR: $PR_URL" >> $GITHUB_STEP_SUMMARY
fi
test:
name: Test Update
if: ${{ inputs.run-mode == 'Test' }}
runs-on: ubuntu-24.04
permissions:
contents: read
packages: read
steps:
- name: Check out repo
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Log inputs to job summary
uses: ./.github/actions/log-inputs
with:
inputs: ${{ toJson(inputs) }}
- name: Setup Android Build
uses: ./.github/actions/setup-android-build
- name: Update SDK Version
env:
_SDK_PACKAGE: ${{ inputs.sdk-package }}
_SDK_VERSION: ${{ inputs.sdk-version }}
run: |
./scripts/update-sdk-version.sh "$_SDK_PACKAGE" "$_SDK_VERSION"
- name: Build
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # Used in settings.gradle.kts to download the SDK from GitHub Maven Packages
run: |
./gradlew assembleDebug --warn

View File

@@ -13,7 +13,7 @@ on:
workflow_dispatch:
env:
_JAVA_VERSION: 17
_JAVA_VERSION: 21
_GITHUB_ACTION_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}/attempts/${{ github.run_attempt }}
@@ -27,13 +27,13 @@ jobs:
steps:
- name: Check out repo
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Validate Gradle wrapper
uses: gradle/actions/wrapper-validation@06832c7b30a0129d7fb559bcc6e43d26f6374244 # v4.3.1
uses: gradle/actions/wrapper-validation@017a9effdb900e5b5b2fddfb590a105619dca3c3 # v4.4.2
- name: Cache Gradle files
uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3
uses: actions/cache@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4
with:
path: |
~/.gradle/caches
@@ -43,7 +43,7 @@ jobs:
${{ runner.os }}-gradle-v2-
- name: Cache build output
uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3
uses: actions/cache@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4
with:
path: |
${{ github.workspace }}/build-cache
@@ -52,12 +52,12 @@ jobs:
${{ runner.os }}-build-
- name: Configure Ruby
uses: ruby/setup-ruby@ca041f971d66735f3e5ff1e21cc13e2d51e7e535 # v1.233.0
uses: ruby/setup-ruby@44511735964dcb71245e7e55f72539531f7bc0eb # v1.257.0
with:
bundler-cache: true
- name: Configure JDK
uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # v4.7.1
uses: actions/setup-java@dded0888837ed1f317902acf8a20df0ad188d165 # v5.0.0
with:
distribution: "temurin"
java-version: ${{ env._JAVA_VERSION }}
@@ -91,7 +91,7 @@ jobs:
- name: Upload to codecov.io
id: upload-to-codecov
uses: codecov/codecov-action@ad3126e916f78f00edff4ed0317cf185271ccc2d # v5.4.2
uses: codecov/codecov-action@5a1091511ad55cbe89839c7260b706298ca349f7 # v5.5.1
if: github.event_name == 'push' || github.event_name == 'pull_request'
continue-on-error: true
with:

7
.gitignore vendored
View File

@@ -3,6 +3,13 @@
fastlane/report.xml
fastlane/README.md
# Ruby / Bundler
.bundle/
vendor/
# Backup files
*.bak
# General
.DS_Store
Thumbs.db

View File

@@ -1 +1 @@
3.3.1
3.4.2

View File

@@ -7,3 +7,12 @@ gem 'time'
plugins_path = File.join(File.dirname(__FILE__), 'fastlane', 'Pluginfile')
eval_gemfile(plugins_path) if File.exist?(plugins_path)
# Since ruby 3.4.0 these are not included in the standard library
gem 'abbrev'
gem 'logger'
gem 'mutex_m'
gem 'csv'
# Starting with Ruby 3.5.0, these are not included in the standard library
gem 'ostruct'

View File

@@ -5,35 +5,39 @@ GEM
base64
nkf
rexml
abbrev (0.1.2)
addressable (2.8.7)
public_suffix (>= 2.0.2, < 7.0)
artifactory (3.0.17)
atomos (0.1.3)
aws-eventstream (1.3.2)
aws-partitions (1.1107.0)
aws-sdk-core (3.224.0)
aws-eventstream (1.4.0)
aws-partitions (1.1159.0)
aws-sdk-core (3.232.0)
aws-eventstream (~> 1, >= 1.3.0)
aws-partitions (~> 1, >= 1.992.0)
aws-sigv4 (~> 1.9)
base64
bigdecimal
jmespath (~> 1, >= 1.6.1)
logger
aws-sdk-kms (1.101.0)
aws-sdk-core (~> 3, >= 3.216.0)
aws-sdk-kms (1.112.0)
aws-sdk-core (~> 3, >= 3.231.0)
aws-sigv4 (~> 1.5)
aws-sdk-s3 (1.186.1)
aws-sdk-core (~> 3, >= 3.216.0)
aws-sdk-s3 (1.199.0)
aws-sdk-core (~> 3, >= 3.231.0)
aws-sdk-kms (~> 1)
aws-sigv4 (~> 1.5)
aws-sigv4 (1.11.0)
aws-sigv4 (1.12.1)
aws-eventstream (~> 1, >= 1.0.2)
babosa (1.0.4)
base64 (0.2.0)
base64 (0.3.0)
bigdecimal (3.2.3)
claide (1.1.0)
colored (1.2)
colored2 (3.1.2)
commander (4.6.0)
highline (~> 2.0.0)
csv (3.3.5)
date (3.4.1)
declarative (0.0.20)
digest-crc (0.7.0)
@@ -58,10 +62,10 @@ GEM
faraday (>= 0.8.0)
http-cookie (~> 1.0.0)
faraday-em_http (1.0.0)
faraday-em_synchrony (1.0.0)
faraday-em_synchrony (1.0.1)
faraday-excon (1.1.0)
faraday-httpclient (1.0.1)
faraday-multipart (1.1.0)
faraday-multipart (1.1.1)
multipart-post (~> 2.0)
faraday-net_http (1.0.2)
faraday-net_http_persistent (1.2.0)
@@ -71,7 +75,7 @@ GEM
faraday_middleware (1.2.1)
faraday (~> 1.0)
fastimage (2.4.0)
fastlane (2.227.2)
fastlane (2.228.0)
CFPropertyList (>= 2.3, < 4.0.0)
addressable (>= 2.8, < 3.0.0)
artifactory (~> 3.0)
@@ -165,37 +169,38 @@ GEM
httpclient (2.9.0)
mutex_m
jmespath (1.6.2)
json (2.12.2)
jwt (2.10.1)
json (2.13.2)
jwt (2.10.2)
base64
logger (1.7.0)
mini_magick (4.13.2)
mini_mime (1.1.5)
multi_json (1.15.0)
multi_json (1.17.0)
multipart-post (2.4.1)
mutex_m (0.3.0)
nanaimo (0.4.0)
naturally (2.2.1)
naturally (2.3.0)
nkf (0.2.0)
optparse (0.6.0)
os (1.1.4)
ostruct (0.6.3)
plist (3.7.2)
public_suffix (6.0.2)
rake (13.2.1)
rake (13.3.0)
representable (3.2.0)
declarative (< 0.1.0)
trailblazer-option (>= 0.1.1, < 0.2.0)
uber (< 0.2.0)
retriable (3.1.2)
rexml (3.4.1)
rexml (3.4.4)
rouge (3.28.0)
ruby2_keywords (0.0.5)
rubyzip (2.4.1)
security (0.1.5)
signet (0.20.0)
signet (0.21.0)
addressable (~> 2.8)
faraday (>= 0.17.5, < 3.a)
jwt (>= 1.5, < 3.0)
jwt (>= 1.5, < 4.0)
multi_json (~> 1.10)
simctl (1.6.10)
CFPropertyList
@@ -230,12 +235,17 @@ PLATFORMS
ruby
DEPENDENCIES
abbrev
csv
fastlane
fastlane-plugin-firebase_app_distribution
logger
mutex_m
ostruct
time
RUBY VERSION
ruby 3.3.1p55
ruby 3.4.2p28
BUNDLED WITH
2.6.6
2.6.9

View File

@@ -52,6 +52,47 @@
Please avoid mixing formatting and logical changes in the same commit/PR. When possible, fix any large formatting issues in a separate PR before opening one to make logical changes to the same code. This helps others focus on the meaningful code changes when reviewing the code.
4. Setup JDK `Version` `21`:
- Navigate to `Preferences > Build, Execution, Deployment > Build Tools > Gradle`.
- Hit the selected Gradle JDK next to `Gradle JDK:`.
- Select a `21.x` version or hit `Download JDK...` if not present.
- Select `Version` `21`.
- Select your preferred `Vendor`.
- Hit `Download`.
- Hit `Apply`.
5. Setup `detekt` pre-commit hook (optional):
Run the following script from the root of the repository to install the hook. This will overwrite any existing pre-commit hook if present.
```shell
echo "Writing detekt pre-commit hook..."
cat << 'EOL' > .git/hooks/pre-commit
#!/usr/bin/env bash
echo "Running detekt check..."
OUTPUT="/tmp/detekt-$(date +%s)"
./gradlew -Pprecommit=true detekt > $OUTPUT
EXIT_CODE=$?
if [ $EXIT_CODE -ne 0 ]; then
cat $OUTPUT
rm $OUTPUT
echo "***********************************************"
echo " detekt failed "
echo " Please fix the above issues before committing "
echo "***********************************************"
exit $EXIT_CODE
fi
rm $OUTPUT
EOL
echo "detekt pre-commit hook written to .git/hooks/pre-commit"
echo "Making the hook executable"
chmod +x .git/hooks/pre-commit
echo "detekt pre-commit hook installed successfully to .git/hooks/pre-commit"
```
## Theme
### Icons & Illustrations

View File

@@ -37,6 +37,6 @@ android {
kotlin {
compilerOptions {
jvmTarget.set(JvmTarget.fromTarget(libs.versions.jvmTarget.get()))
jvmTarget = JvmTarget.fromTarget(libs.versions.jvmTarget.get())
}
}

View File

@@ -10,6 +10,7 @@ import java.util.Properties
plugins {
alias(libs.plugins.android.application)
alias(libs.plugins.androidx.room)
// 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)
@@ -46,26 +47,32 @@ android {
namespace = "com.x8bit.bitwarden"
compileSdk = libs.versions.compileSdk.get().toInt()
room {
schemaDirectory("$projectDir/schemas")
}
defaultConfig {
applicationId = "com.x8bit.bitwarden"
minSdk = libs.versions.minSdk.get().toInt()
targetSdk = libs.versions.targetSdk.get().toInt()
versionCode = 1
versionName = "2025.4.0"
setProperty("archivesBaseName", "com.x8bit.bitwarden")
ksp {
// The location in which the generated Room Database Schemas will be stored in the repo.
arg("room.schemaLocation", "$projectDir/schemas")
}
versionCode = libs.versions.appVersionCode.get().toInt()
versionName = libs.versions.appVersionName.get()
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
// Set the base archive name for publishing purposes. This is used to derive the APK and AAB
// artifact names when uploading to Firebase and Play Store.
base.archivesName = "com.x8bit.bitwarden"
buildConfigField(
type = "String",
name = "CI_INFO",
value = "${ciProperties.getOrDefault("ci.info", "\"local\"")}",
value = "${ciProperties.getOrDefault("ci.info", "\"\uD83D\uDCBB local\"")}",
)
buildConfigField(
type = "String",
name = "SDK_VERSION",
value = "\"${libs.versions.bitwardenSdk.get()}\"",
)
}
@@ -99,6 +106,7 @@ android {
applicationIdSuffix = ".beta"
isDebuggable = false
isMinifyEnabled = true
isShrinkResources = true
matchingFallbacks += listOf("release")
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
@@ -111,6 +119,7 @@ android {
release {
isDebuggable = false
isMinifyEnabled = true
isShrinkResources = true
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro",
@@ -193,7 +202,7 @@ android {
kotlin {
compilerOptions {
jvmTarget.set(JvmTarget.fromTarget(libs.versions.jvmTarget.get()))
jvmTarget = JvmTarget.fromTarget(libs.versions.jvmTarget.get())
}
}
@@ -215,6 +224,7 @@ dependencies {
implementation(project(":annotation"))
implementation(project(":core"))
implementation(project(":cxf"))
implementation(project(":data"))
implementation(project(":network"))
implementation(project(":ui"))
@@ -236,6 +246,8 @@ dependencies {
implementation(libs.androidx.compose.ui.graphics)
implementation(libs.androidx.compose.ui.tooling.preview)
implementation(libs.androidx.core.ktx)
implementation(libs.androidx.credentials)
implementation(libs.androidx.credentials.providerevents)
implementation(libs.androidx.hilt.navigation.compose)
implementation(libs.androidx.lifecycle.process)
implementation(libs.androidx.lifecycle.runtime.compose)
@@ -249,17 +261,15 @@ dependencies {
implementation(libs.androidx.work.runtime.ktx)
implementation(libs.bitwarden.sdk)
implementation(libs.bumptech.glide)
implementation(libs.androidx.credentials)
implementation(libs.google.hilt.android)
ksp(libs.google.hilt.compiler)
implementation(libs.kotlinx.collections.immutable)
implementation(libs.kotlinx.coroutines.android)
implementation(libs.kotlinx.serialization)
implementation(platform(libs.square.okhttp.bom))
implementation(libs.square.okhttp)
implementation(libs.square.okhttp.logging)
implementation(platform(libs.square.retrofit.bom))
implementation(libs.square.retrofit)
implementation(libs.square.retrofit.kotlinx.serialization)
implementation(libs.timber)
implementation(libs.zxing.zxing.core)
@@ -287,7 +297,6 @@ dependencies {
testImplementation(libs.kotlinx.coroutines.test)
testImplementation(libs.mockk.mockk)
testImplementation(libs.robolectric.robolectric)
testImplementation(libs.square.okhttp.mockwebserver)
testImplementation(libs.square.turbine)
}
@@ -296,8 +305,7 @@ tasks {
useJUnitPlatform()
maxHeapSize = "2g"
maxParallelForks = Runtime.getRuntime().availableProcessors()
jvmArgs = jvmArgs.orEmpty() + "-XX:+UseParallelGC"
android.sourceSets["main"].res.srcDirs("src/test/res")
jvmArgs = jvmArgs.orEmpty() + "-XX:+UseParallelGC" + "-Duser.country=US"
}
}
@@ -322,6 +330,7 @@ private fun renameFile(path: String, newName: String) {
if (originalFile.renameTo(newFile)) {
println("Renamed $originalFile to $newFile")
} else {
@Suppress("TooGenericExceptionThrown")
throw RuntimeException("Failed to rename $originalFile to $newFile")
}
}

View File

@@ -0,0 +1,38 @@
{
"formatVersion": 1,
"database": {
"version": 1,
"identityHash": "ce40856ec88770d11b7afb587c7deabc",
"entities": [
{
"tableName": "privileged_apps",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`package_name` TEXT NOT NULL, `signature` TEXT NOT NULL, PRIMARY KEY(`package_name`, `signature`))",
"fields": [
{
"fieldPath": "packageName",
"columnName": "package_name",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "signature",
"columnName": "signature",
"affinity": "TEXT",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"package_name",
"signature"
]
}
}
],
"setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'ce40856ec88770d11b7afb587c7deabc')"
]
}
}

View File

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

View File

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

View File

@@ -0,0 +1,21 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<application tools:ignore="MissingApplicationIcon">
<activity
android:name=".MainActivity">
<intent-filter android:autoVerify="true">
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="https" />
<data android:host="*.bitwarden.pw" />
<data android:pathPattern="/redirect-connector.*" />
</intent-filter>
</activity>
</application>
</manifest>

View File

@@ -7,6 +7,20 @@
<meta-data
android:name="firebase_crashlytics_collection_enabled"
android:value="false" />
<activity
android:name=".MainActivity"
tools:ignore="IntentFilterExportedReceiver">
<intent-filter android:autoVerify="true">
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="https" />
<data android:host="*.bitwarden.pw" />
<data android:pathPattern="/redirect-connector.*" />
</intent-filter>
</activity>
</application>
</manifest>

View File

@@ -0,0 +1,27 @@
<?xml version="1.0" encoding="utf-8"?>
<network-security-config xmlns:tools="http://schemas.android.com/tools">
<base-config
cleartextTrafficPermitted="true"
tools:ignore="InsecureBaseConfiguration">
<trust-anchors>
<!-- Trust pre-installed CAs -->
<certificates src="system" />
<!-- Additionally trust user added CAs -->
<certificates
src="user"
tools:ignore="AcceptsUserCertificates" />
</trust-anchors>
</base-config>
<domain-config cleartextTrafficPermitted="false">
<domain includeSubdomains="true">bitwarden.com</domain>
<domain includeSubdomains="true">bitwarden.eu</domain>
<domain includeSubdomains="true">bitwarden.pw</domain>
<trust-anchors>
<!-- Only trust pre-installed CAs for Bitwarden domains and all subdomains -->
<certificates src="system" />
</trust-anchors>
</domain-config>
</network-security-config>

View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<credential-provider>
<capabilities>
<capability name="android.credentials.TYPE_PASSWORD_CREDENTIAL" />
<capability name="androidx.credentials.TYPE_PUBLIC_KEY_CREDENTIAL" />
</capabilities>
</credential-provider>

View File

@@ -81,12 +81,12 @@
<data android:scheme="https" />
<data android:host="*.bitwarden.com" />
<data android:host="*.bitwarden.eu" />
<data android:host="*.bitwarden.pw" />
<data android:pathPattern="/redirect-connector.*" />
</intent-filter>
<intent-filter>
<action android:name="com.x8bit.bitwarden.credentials.ACTION_CREATE_PASSKEY" />
<action android:name="com.x8bit.bitwarden.credentials.ACTION_GET_PASSKEY" />
<action android:name="com.x8bit.bitwarden.credentials.ACTION_GET_PASSWORD" />
<action android:name="com.x8bit.bitwarden.credentials.ACTION_UNLOCK_ACCOUNT" />
<category android:name="android.intent.category.DEFAULT" />
@@ -115,11 +115,11 @@
android:theme="@android:style/Theme.NoDisplay" />
<activity
android:name=".AutofillTotpCopyActivity"
android:name=".AutofillCallbackActivity"
android:exported="true"
android:launchMode="singleTop"
android:noHistory="true"
android:theme="@style/AutofillTotpCopyTheme" />
android:theme="@style/AutofillCallbackTheme" />
<activity
android:name=".AuthCallbackActivity"
@@ -133,16 +133,6 @@
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data
android:host="captcha-callback"
android:scheme="bitwarden" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data
android:host="duo-callback"
android:scheme="bitwarden" />
@@ -259,7 +249,7 @@
android:name="com.x8bit.bitwarden.AutofillTileService"
android:exported="true"
android:icon="@drawable/ic_notification"
android:label="@string/autofill"
android:label="@string/autofill_title"
android:permission="android.permission.BIND_QUICK_SETTINGS_TILE"
tools:ignore="MissingClass">
<intent-filter>
@@ -330,11 +320,19 @@
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.HOME" />
</intent>
<!-- To Query Privileged Apps -->
<intent>
<action android:name="android.intent.action.VIEW" />
<data android:scheme="http" />
</intent>
<!-- To Query Chrome Beta: -->
<package android:name="com.chrome.beta" />
<!-- To Query Chrome Stable: -->
<package android:name="com.android.chrome" />
<!-- To Query Brave Stable: -->
<package android:name="com.brave.browser" />
</queries>
</manifest>

View File

@@ -779,6 +779,42 @@
}
]
}
},
{
"type": "android",
"info": {
"package_name": "cz.seznam.sbrowser",
"signatures": [
{
"build": "release",
"cert_fingerprint_sha256": "DB:95:40:66:10:78:83:6E:4E:B1:66:F6:9E:F4:07:30:9E:8D:AE:33:34:68:5E:C8:F6:FA:2F:13:81:B9:AC:F6"
}
]
}
},
{
"type": "android",
"info": {
"package_name": "com.opera.mini.native",
"signatures": [
{
"build": "release",
"cert_fingerprint_sha256": "57:AC:BC:52:5F:1B:2E:BD:19:19:6C:D6:F0:14:39:7C:C9:10:FD:18:84:1E:0A:E8:50:FE:BC:3E:1E:59:3F:F2"
}
]
}
},
{
"type": "android",
"info": {
"package_name": "com.opera.mini.native.beta",
"signatures": [
{
"build": "release",
"cert_fingerprint_sha256": "57:AC:BC:52:5F:1B:2E:BD:19:19:6C:D6:F0:14:39:7C:C9:10:FD:18:84:1E:0A:E8:50:FE:BC:3E:1E:59:3F:F2"
}
]
}
}
]
}

View File

@@ -1,8 +1,11 @@
package com.x8bit.bitwarden
import android.app.ComponentCaller
import android.content.Intent
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import com.bitwarden.annotation.OmitFromCoverage
import com.bitwarden.ui.platform.util.validate
/**
* An activity to be launched and then immediately closed so that the OS Shade can be collapsed
@@ -11,7 +14,16 @@ import com.bitwarden.annotation.OmitFromCoverage
@OmitFromCoverage
class AccessibilityActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
intent = intent.validate()
super.onCreate(savedInstanceState)
finish()
}
override fun onNewIntent(intent: Intent) {
super.onNewIntent(intent.validate())
}
override fun onNewIntent(intent: Intent, caller: ComponentCaller) {
super.onNewIntent(intent.validate(), caller)
}
}

View File

@@ -1,10 +1,12 @@
package com.x8bit.bitwarden
import android.app.ComponentCaller
import android.content.Intent
import android.os.Bundle
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
import com.bitwarden.annotation.OmitFromCoverage
import com.bitwarden.ui.platform.util.validate
import dagger.hilt.android.AndroidEntryPoint
/**
@@ -21,6 +23,7 @@ class AuthCallbackActivity : AppCompatActivity() {
private val viewModel: AuthCallbackViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
intent = intent.validate()
super.onCreate(savedInstanceState)
viewModel.trySendAction(AuthCallbackAction.IntentReceive(intent = intent))
@@ -35,4 +38,12 @@ class AuthCallbackActivity : AppCompatActivity() {
startActivity(intent)
finish()
}
override fun onNewIntent(intent: Intent) {
super.onNewIntent(intent.validate())
}
override fun onNewIntent(intent: Intent, caller: ComponentCaller) {
super.onNewIntent(intent.validate(), caller)
}
}

View File

@@ -3,7 +3,6 @@ package com.x8bit.bitwarden
import android.content.Intent
import com.bitwarden.ui.platform.base.BaseViewModel
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
import com.x8bit.bitwarden.data.auth.repository.util.getCaptchaCallbackTokenResult
import com.x8bit.bitwarden.data.auth.repository.util.getDuoCallbackTokenResult
import com.x8bit.bitwarden.data.auth.repository.util.getSsoCallbackResult
import com.x8bit.bitwarden.data.auth.repository.util.getWebAuthResultOrNull
@@ -27,7 +26,6 @@ class AuthCallbackViewModel @Inject constructor(
private fun handleIntentReceived(action: AuthCallbackAction.IntentReceive) {
val yubiKeyResult = action.intent.getYubiKeyResultOrNull()
val webAuthResult = action.intent.getWebAuthResultOrNull()
val captchaCallbackTokenResult = action.intent.getCaptchaCallbackTokenResult()
val duoCallbackTokenResult = action.intent.getDuoCallbackTokenResult()
val ssoCallbackResult = action.intent.getSsoCallbackResult()
when {
@@ -35,12 +33,6 @@ class AuthCallbackViewModel @Inject constructor(
authRepository.setYubiKeyResult(yubiKeyResult = yubiKeyResult)
}
captchaCallbackTokenResult != null -> {
authRepository.setCaptchaCallbackTokenResult(
tokenResult = captchaCallbackTokenResult,
)
}
duoCallbackTokenResult != null -> {
authRepository.setDuoCallbackTokenResult(
tokenResult = duoCallbackTokenResult,

View File

@@ -1,10 +1,13 @@
package com.x8bit.bitwarden
import android.app.ComponentCaller
import android.content.Intent
import android.os.Bundle
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.lifecycleScope
import com.bitwarden.annotation.OmitFromCoverage
import com.bitwarden.ui.platform.util.validate
import com.x8bit.bitwarden.data.autofill.manager.AutofillCompletionManager
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.flow.launchIn
@@ -12,43 +15,43 @@ import kotlinx.coroutines.flow.onEach
import javax.inject.Inject
/**
* An activity for copying a TOTP code to the clipboard. This is done when an autofill item is
* selected and it requires TOTP authentication. Due to the constraints of the autofill framework,
* we also have to re-fulfill the autofill for the views that are being filled.
* An activity that is launched to complete Autofill. This is done when an autofill item is selected
* and is associated with a valid cipher. Due to the constraints of the autofill framework, we also
* have to re-fulfill the autofill for the views that are being filled.
*/
@OmitFromCoverage
@AndroidEntryPoint
class AutofillTotpCopyActivity : AppCompatActivity() {
class AutofillCallbackActivity : AppCompatActivity() {
@Inject
lateinit var autofillCompletionManager: AutofillCompletionManager
private val autofillTotpCopyViewModel: AutofillTotpCopyViewModel by viewModels()
private val viewModel: AutofillCallbackViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
intent = intent.validate()
super.onCreate(savedInstanceState)
observeViewModelEvents()
autofillTotpCopyViewModel.trySendAction(
AutofillTotpCopyAction.IntentReceived(
intent = intent,
),
)
viewModel.trySendAction(AutofillCallbackAction.IntentReceived(intent = intent))
}
override fun onNewIntent(intent: Intent) {
super.onNewIntent(intent.validate())
}
override fun onNewIntent(intent: Intent, caller: ComponentCaller) {
super.onNewIntent(intent.validate(), caller)
}
private fun observeViewModelEvents() {
autofillTotpCopyViewModel
viewModel
.eventFlow
.onEach { event ->
when (event) {
is AutofillTotpCopyEvent.CompleteAutofill -> {
handleCompleteAutofill(event)
}
is AutofillTotpCopyEvent.FinishActivity -> {
finishActivity()
}
is AutofillCallbackEvent.CompleteAutofill -> handleCompleteAutofill(event)
is AutofillCallbackEvent.FinishActivity -> finishActivity()
}
}
.launchIn(lifecycleScope)
@@ -57,7 +60,7 @@ class AutofillTotpCopyActivity : AppCompatActivity() {
/**
* Complete autofill with the provided data.
*/
private fun handleCompleteAutofill(event: AutofillTotpCopyEvent.CompleteAutofill) {
private fun handleCompleteAutofill(event: AutofillCallbackEvent.CompleteAutofill) {
autofillCompletionManager.completeAutofill(
activity = this,
cipherView = event.cipherView,

View File

@@ -5,14 +5,15 @@ import androidx.lifecycle.viewModelScope
import com.bitwarden.ui.platform.base.BaseViewModel
import com.bitwarden.vault.CipherView
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
import com.x8bit.bitwarden.data.autofill.util.getTotpCopyIntentOrNull
import com.x8bit.bitwarden.data.autofill.util.getAutofillCallbackIntentOrNull
import com.x8bit.bitwarden.data.platform.util.launchWithTimeout
import com.x8bit.bitwarden.data.vault.manager.model.GetCipherResult
import com.x8bit.bitwarden.data.vault.repository.VaultRepository
import com.x8bit.bitwarden.data.vault.repository.model.VaultUnlockData
import com.x8bit.bitwarden.data.vault.repository.util.statusFor
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.mapNotNull
import timber.log.Timber
import javax.inject.Inject
/**
@@ -21,53 +22,65 @@ import javax.inject.Inject
private const val CIPHER_WAIT_TIMEOUT_MILLIS: Long = 500
/**
* A view model that handles logic for the [AutofillTotpCopyActivity].
* A view model that handles logic for the [AutofillCallbackActivity].
*/
@HiltViewModel
class AutofillTotpCopyViewModel @Inject constructor(
class AutofillCallbackViewModel @Inject constructor(
private val authRepository: AuthRepository,
private val vaultRepository: VaultRepository,
) : BaseViewModel<Unit, AutofillTotpCopyEvent, AutofillTotpCopyAction>(Unit) {
) : BaseViewModel<Unit, AutofillCallbackEvent, AutofillCallbackAction>(Unit) {
private val activeUserId: String? get() = authRepository.activeUserId
override fun handleAction(action: AutofillTotpCopyAction): Unit = when (action) {
is AutofillTotpCopyAction.IntentReceived -> handleIntentReceived(action)
override fun handleAction(action: AutofillCallbackAction): Unit = when (action) {
is AutofillCallbackAction.IntentReceived -> handleIntentReceived(action)
}
/**
* Process the received intent and alert the activity of what to do next.
*/
private fun handleIntentReceived(action: AutofillTotpCopyAction.IntentReceived) {
private fun handleIntentReceived(action: AutofillCallbackAction.IntentReceived) {
viewModelScope
.launchWithTimeout(
timeoutBlock = { finishActivity() },
timeoutBlock = {
Timber.w("Autofill -- Timeout")
finishActivity()
},
timeoutDuration = CIPHER_WAIT_TIMEOUT_MILLIS,
) {
// Extract TOTP copy data from the intent.
val cipherId = action
.intent
.getTotpCopyIntentOrNull()
.getAutofillCallbackIntentOrNull()
?.cipherId
if (cipherId == null || isVaultLocked()) {
if (cipherId == null) {
Timber.w("Autofill -- Cipher was not provided")
finishActivity()
return@launchWithTimeout
}
if (isVaultLocked()) {
Timber.w("Autofill -- Vault is locked")
finishActivity()
return@launchWithTimeout
}
// Try and find the matching cipher.
vaultRepository
.ciphersStateFlow
.mapNotNull { it.data }
.first()
.find { it.id == cipherId }
?.let { cipherView ->
sendEvent(
AutofillTotpCopyEvent.CompleteAutofill(
cipherView = cipherView,
),
)
when (val result = vaultRepository.getCipher(cipherId = cipherId)) {
GetCipherResult.CipherNotFound -> {
Timber.w("Autofill -- Cipher not found")
finishActivity()
}
?: finishActivity()
is GetCipherResult.Failure -> {
Timber.w(result.error, "Autofill -- Get cipher failure")
finishActivity()
}
is GetCipherResult.Success -> {
Timber.d("Autofill -- Cipher found")
sendEvent(AutofillCallbackEvent.CompleteAutofill(result.cipherView))
}
}
}
}
@@ -75,7 +88,7 @@ class AutofillTotpCopyViewModel @Inject constructor(
* Send an event to the activity that signals it to finish.
*/
private fun finishActivity() {
sendEvent(AutofillTotpCopyEvent.FinishActivity)
sendEvent(AutofillCallbackEvent.FinishActivity)
}
private suspend fun isVaultLocked(): Boolean {
@@ -92,30 +105,30 @@ class AutofillTotpCopyViewModel @Inject constructor(
}
/**
* Represents actions that can be sent to the [AutofillTotpCopyViewModel].
* Represents actions that can be sent to the [AutofillCallbackViewModel].
*/
sealed class AutofillTotpCopyAction {
sealed class AutofillCallbackAction {
/**
* An [intent] has been received and is ready to be processed.
*/
data class IntentReceived(
val intent: Intent,
) : AutofillTotpCopyAction()
) : AutofillCallbackAction()
}
/**
* Represents events emitted by the [AutofillTotpCopyViewModel].
* Represents events emitted by the [AutofillCallbackViewModel].
*/
sealed class AutofillTotpCopyEvent {
sealed class AutofillCallbackEvent {
/**
* Complete autofill with the provided [cipherView].
*/
data class CompleteAutofill(
val cipherView: CipherView,
) : AutofillTotpCopyEvent()
) : AutofillCallbackEvent()
/**
* Finish the activity.
*/
data object FinishActivity : AutofillTotpCopyEvent()
data object FinishActivity : AutofillCallbackEvent()
}

View File

@@ -8,6 +8,7 @@ import com.x8bit.bitwarden.data.platform.manager.event.OrganizationEventManager
import com.x8bit.bitwarden.data.platform.manager.network.NetworkConfigManager
import com.x8bit.bitwarden.data.platform.manager.network.NetworkConnectionManager
import com.x8bit.bitwarden.data.platform.manager.restriction.RestrictionManager
import com.x8bit.bitwarden.data.platform.repository.EnvironmentRepository
import dagger.hilt.android.HiltAndroidApp
import timber.log.Timber
import javax.inject.Inject
@@ -38,6 +39,17 @@ class BitwardenApplication : Application() {
@Inject
lateinit var restrictionManager: RestrictionManager
@Inject
lateinit var environmentRepository: EnvironmentRepository
override fun onCreate() {
super.onCreate()
// These must be initialized in order to ensure that the restrictionManager does not
// override the environmentRepository values.
restrictionManager.initialize()
environmentRepository.initialize()
}
override fun onLowMemory() {
super.onLowMemory()
Timber.w("onLowMemory")

View File

@@ -1,12 +1,12 @@
package com.x8bit.bitwarden
import android.app.ComponentCaller
import android.content.Intent
import android.os.Build
import android.os.Bundle
import android.view.KeyEvent
import android.view.MotionEvent
import android.view.WindowManager
import android.widget.Toast
import androidx.activity.compose.setContent
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
@@ -14,6 +14,7 @@ import androidx.appcompat.app.AppCompatDelegate
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.core.app.ActivityCompat
import androidx.core.os.LocaleListCompat
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
import androidx.lifecycle.compose.collectAsStateWithLifecycle
@@ -23,6 +24,7 @@ import com.bitwarden.annotation.OmitFromCoverage
import com.bitwarden.ui.platform.base.util.EventsEffect
import com.bitwarden.ui.platform.theme.BitwardenTheme
import com.bitwarden.ui.platform.util.setupEdgeToEdge
import com.bitwarden.ui.platform.util.validate
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
@@ -67,10 +69,11 @@ class MainActivity : AppCompatActivity() {
lateinit var debugLaunchManager: DebugMenuLaunchManager
override fun onCreate(savedInstanceState: Bundle?) {
intent = intent.validate()
var shouldShowSplashScreen = true
installSplashScreen().setKeepOnScreenCondition { shouldShowSplashScreen }
super.onCreate(savedInstanceState)
window.decorView.filterTouchesWhenObscured = true
if (savedInstanceState == null) {
mainViewModel.trySendAction(MainAction.ReceiveFirstIntent(intent = intent))
}
@@ -114,8 +117,15 @@ class MainActivity : AppCompatActivity() {
}
override fun onNewIntent(intent: Intent) {
super.onNewIntent(intent)
mainViewModel.trySendAction(action = MainAction.ReceiveNewIntent(intent = intent))
val newIntent = intent.validate()
super.onNewIntent(newIntent)
mainViewModel.trySendAction(action = MainAction.ReceiveNewIntent(intent = newIntent))
}
override fun onNewIntent(intent: Intent, caller: ComponentCaller) {
val newIntent = intent.validate()
super.onNewIntent(newIntent, caller)
mainViewModel.trySendAction(action = MainAction.ReceiveNewIntent(intent = newIntent))
}
override fun onResume() {
@@ -173,12 +183,6 @@ class MainActivity : AppCompatActivity() {
is MainEvent.CompleteAutofill -> handleCompleteAutofill(event)
MainEvent.Recreate -> handleRecreate()
MainEvent.NavigateToDebugMenu -> navController.navigateToDebugMenuScreen()
is MainEvent.ShowToast -> {
Toast
.makeText(baseContext, event.message.invoke(resources), Toast.LENGTH_SHORT)
.show()
}
is MainEvent.UpdateAppLocale -> {
AppCompatDelegate.setApplicationLocales(
LocaleListCompat.forLanguageTags(event.localeName),
@@ -211,7 +215,7 @@ class MainActivity : AppCompatActivity() {
}
private fun handleRecreate() {
recreate()
ActivityCompat.recreate(this)
}
private fun updateScreenCapture(isScreenCaptureAllowed: Boolean) {

View File

@@ -4,10 +4,11 @@ import android.content.Intent
import android.os.Parcelable
import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.viewModelScope
import com.bitwarden.core.data.manager.toast.ToastManager
import com.bitwarden.ui.platform.base.BaseViewModel
import com.bitwarden.ui.platform.feature.settings.appearance.model.AppTheme
import com.bitwarden.ui.util.Text
import com.bitwarden.ui.util.asText
import com.bitwarden.ui.platform.manager.IntentManager
import com.bitwarden.ui.platform.resource.BitwardenString
import com.bitwarden.vault.CipherView
import com.x8bit.bitwarden.data.auth.manager.AddTotpItemFromAuthenticatorManager
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
@@ -22,13 +23,12 @@ import com.x8bit.bitwarden.data.credentials.manager.BitwardenCredentialManager
import com.x8bit.bitwarden.data.credentials.util.getCreateCredentialRequestOrNull
import com.x8bit.bitwarden.data.credentials.util.getFido2AssertionRequestOrNull
import com.x8bit.bitwarden.data.credentials.util.getGetCredentialsRequestOrNull
import com.x8bit.bitwarden.data.credentials.util.getProviderGetPasswordRequestOrNull
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
@@ -36,7 +36,6 @@ 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.feature.settings.appearance.model.AppLanguage
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
@@ -44,12 +43,15 @@ import com.x8bit.bitwarden.ui.platform.util.isPasswordGeneratorShortcut
import com.x8bit.bitwarden.ui.vault.model.TotpData
import com.x8bit.bitwarden.ui.vault.util.getTotpDataOrNull
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.delay
import kotlinx.coroutines.FlowPreview
import kotlinx.coroutines.flow.debounce
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.drop
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.merge
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
@@ -58,17 +60,17 @@ import java.time.Clock
import javax.inject.Inject
private const val SPECIAL_CIRCUMSTANCE_KEY = "special-circumstance"
private const val ANIMATION_REFRESH_DELAY = 500L
private const val ANIMATION_DEBOUNCE_DELAY_MS = 500L
/**
* A view model that helps launch actions for the [MainActivity].
*/
@OptIn(FlowPreview::class)
@Suppress("LongParameterList", "TooManyFunctions")
@HiltViewModel
class MainViewModel @Inject constructor(
accessibilitySelectionManager: AccessibilitySelectionManager,
autofillSelectionManager: AutofillSelectionManager,
featureFlagManager: FeatureFlagManager,
private val addTotpItemFromAuthenticatorManager: AddTotpItemFromAuthenticatorManager,
private val specialCircumstanceManager: SpecialCircumstanceManager,
private val garbageCollectionManager: GarbageCollectionManager,
@@ -81,13 +83,11 @@ class MainViewModel @Inject constructor(
private val savedStateHandle: SavedStateHandle,
private val appResumeManager: AppResumeManager,
private val clock: Clock,
private val toastManager: ToastManager,
) : BaseViewModel<MainState, MainEvent, MainAction>(
initialState = MainState(
theme = settingsRepository.appTheme,
isScreenCaptureAllowed = settingsRepository.isScreenCaptureAllowed,
isErrorReportingDialogEnabled = featureFlagManager.getFeatureFlag(
key = FlagKey.MobileErrorReporting,
),
isDynamicColorsEnabled = settingsRepository.isDynamicColorsEnabled,
),
) {
@@ -106,12 +106,6 @@ 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) }
@@ -145,36 +139,23 @@ class MainViewModel @Inject constructor(
.onEach(::trySendAction)
.launchIn(viewModelScope)
authRepository
.userStateFlow
.drop(count = 1)
// Trigger an action whenever the current user changes or we go into/out of a pending
// account state (which acts like switching to a temporary user).
.map { it?.activeUserId to it?.hasPendingAccountAddition }
.distinctUntilChanged()
.onEach {
// 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.
delay(ANIMATION_REFRESH_DELAY)
trySendAction(MainAction.Internal.CurrentUserStateChange)
}
.launchIn(viewModelScope)
vaultRepository
.vaultStateEventFlow
.onEach {
when (it) {
is VaultStateEvent.Locked -> {
// Similar to account switching, triggering this action too soon can
// interfere with animations or navigation logic, so we will delay slightly.
delay(ANIMATION_REFRESH_DELAY)
trySendAction(MainAction.Internal.VaultUnlockStateChange)
}
is VaultStateEvent.Unlocked -> Unit
}
}
merge(
authRepository
.userStateFlow
.drop(count = 1)
// Trigger an action whenever the current user changes or we go into/out of a
// pending account state (which acts like switching to a temporary user).
.map { it?.activeUserId to it?.hasPendingAccountAddition }
.distinctUntilChanged(),
vaultRepository
.vaultStateEventFlow
.filter { it is VaultStateEvent.Locked },
)
// This debounce ensure we do not emit multiple times rapidly and also acts as a short
// delay to give animations time to finish (ex: account switcher).
.debounce(timeoutMillis = ANIMATION_DEBOUNCE_DELAY_MS)
.map { MainAction.Internal.CurrentUserOrVaultStateChange }
.onEach(::sendAction)
.launchIn(viewModelScope)
// On app launch, mark all active users as having previously logged in.
@@ -212,22 +193,13 @@ class MainViewModel @Inject constructor(
handleAutofillSelectionReceive(action)
}
is MainAction.Internal.CurrentUserStateChange -> handleCurrentUserStateChange()
is MainAction.Internal.CurrentUserOrVaultStateChange -> {
handleCurrentUserOrVaultStateChange()
}
is MainAction.Internal.ScreenCaptureUpdate -> handleScreenCaptureUpdate(action)
is MainAction.Internal.ThemeUpdate -> handleAppThemeUpdated(action)
is MainAction.Internal.VaultUnlockStateChange -> handleVaultUnlockStateChange()
is MainAction.Internal.DynamicColorsUpdate -> handleDynamicColorsUpdate(action)
is MainAction.Internal.OnMobileErrorReportingReceive -> {
handleOnMobileErrorReportingReceive(action)
}
}
}
private fun handleOnMobileErrorReportingReceive(
action: MainAction.Internal.OnMobileErrorReportingReceive,
) {
mutableStateFlow.update {
it.copy(isErrorReportingDialogEnabled = action.isErrorReportingEnabled)
}
}
@@ -260,8 +232,9 @@ class MainViewModel @Inject constructor(
sendEvent(MainEvent.CompleteAutofill(cipherView = action.cipherView))
}
private fun handleCurrentUserStateChange() {
recreateUiAndGarbageCollect()
private fun handleCurrentUserOrVaultStateChange() {
sendEvent(MainEvent.Recreate)
garbageCollectionManager.tryCollect()
}
private fun handleScreenCaptureUpdate(action: MainAction.Internal.ScreenCaptureUpdate) {
@@ -273,10 +246,6 @@ class MainViewModel @Inject constructor(
sendEvent(MainEvent.UpdateAppTheme(osTheme = action.theme.osValue))
}
private fun handleVaultUnlockStateChange() {
recreateUiAndGarbageCollect()
}
private fun handleDynamicColorsUpdate(action: MainAction.Internal.DynamicColorsUpdate) {
mutableStateFlow.update { it.copy(isDynamicColorsEnabled = action.isDynamicColorsEnabled) }
}
@@ -325,6 +294,7 @@ class MainViewModel @Inject constructor(
val createCredentialRequest = intent.getCreateCredentialRequestOrNull()
val getCredentialsRequest = intent.getGetCredentialsRequestOrNull()
val fido2AssertCredentialRequest = intent.getFido2AssertionRequestOrNull()
val providerGetPasswordRequest = intent.getProviderGetPasswordRequestOrNull()
when {
passwordlessRequestData != null -> {
authRepository.activeUserId?.let {
@@ -415,6 +385,19 @@ class MainViewModel @Inject constructor(
)
}
providerGetPasswordRequest != null -> {
// Set the user's verification status when a new GetPassword request is
// received to force explicit verification if the user's vault is
// unlocked when the request is received.
bitwardenCredentialManager.isUserVerified =
providerGetPasswordRequest.isUserPreVerified
specialCircumstanceManager.specialCircumstance =
SpecialCircumstance.ProviderGetPasswordRequest(
passwordGetRequest = providerGetPasswordRequest,
)
}
getCredentialsRequest != null -> {
specialCircumstanceManager.specialCircumstance =
SpecialCircumstance.ProviderGetCredentials(
@@ -438,11 +421,6 @@ class MainViewModel @Inject constructor(
}
}
private fun recreateUiAndGarbageCollect() {
sendEvent(MainEvent.Recreate)
garbageCollectionManager.tryCollect()
}
private fun handleCompleteRegistrationData(data: CompleteRegistrationData) {
viewModelScope.launch {
// Attempt to load the environment for the user if they have a pre-auth environment
@@ -455,15 +433,15 @@ class MainViewModel @Inject constructor(
)
when (emailTokenResult) {
is EmailTokenResult.Error -> {
sendEvent(
MainEvent.ShowToast(
message = emailTokenResult
.message
?.asText()
?: R.string.there_was_an_issue_validating_the_registration_token
.asText(),
),
)
emailTokenResult
.message
?.let { toastManager.show(message = it) }
?: run {
toastManager.show(
messageId = BitwardenString
.there_was_an_issue_validating_the_registration_token,
)
}
}
EmailTokenResult.Expired -> {
@@ -495,15 +473,12 @@ data class MainState(
val theme: AppTheme,
val isScreenCaptureAllowed: Boolean,
val isDynamicColorsEnabled: Boolean,
private val isErrorReportingDialogEnabled: Boolean,
) : Parcelable {
/**
* Contains all feature flags that are available to the UI.
*/
val featureFlagsState: FeatureFlagsState
get() = FeatureFlagsState(
isErrorReportingDialogEnabled = isErrorReportingDialogEnabled,
)
get() = FeatureFlagsState
}
/**
@@ -548,13 +523,6 @@ 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.
*/
@@ -563,9 +531,9 @@ sealed class MainAction {
) : Internal()
/**
* Indicates a relevant change in the current user state.
* Indicates a relevant change in the current user state or vault locked state.
*/
data object CurrentUserStateChange : Internal()
data object CurrentUserOrVaultStateChange : Internal()
/**
* Indicates that the screen capture state has changed.
@@ -581,11 +549,6 @@ sealed class MainAction {
val theme: AppTheme,
) : Internal()
/**
* Indicates a relevant change in the current vault lock state.
*/
data object VaultUnlockStateChange : Internal()
/**
* Indicates that the dynamic colors state has changed.
*/
@@ -621,11 +584,6 @@ sealed class MainEvent {
*/
data object NavigateToDebugMenu : MainEvent()
/**
* Show a toast with the given [message].
*/
data class ShowToast(val message: Text) : MainEvent()
/**
* Indicates that the app language has been updated.
*/

View File

@@ -1,5 +1,6 @@
package com.x8bit.bitwarden.data.auth.datasource.disk
import com.bitwarden.network.model.AccountKeysJson
import com.bitwarden.network.model.SyncResponseJson
import com.bitwarden.network.provider.AppIdProvider
import com.x8bit.bitwarden.data.auth.datasource.disk.model.AccountTokensJson
@@ -126,13 +127,34 @@ interface AuthDiskSource : AppIdProvider {
/**
* Retrieves a private key using a [userId].
*/
@Deprecated(
message = "Use getAccountKeys instead.",
replaceWith = ReplaceWith("getAccountKeys"),
)
fun getPrivateKey(userId: String): String?
/**
* Stores a private key using a [userId].
*/
@Deprecated(
message = "Use storeAccountKeys instead.",
replaceWith = ReplaceWith("storeAccountKeys"),
)
fun storePrivateKey(userId: String, privateKey: String?)
/**
* Returns the profile account keys for the given [userId].
*/
fun getAccountKeys(userId: String): AccountKeysJson?
/**
* Stores the profile account keys for the given [userId].
*/
fun storeAccountKeys(
userId: String,
accountKeys: AccountKeysJson?,
)
/**
* Retrieves a user auto-unlock key for the given [userId].
*/

View File

@@ -4,6 +4,7 @@ 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.AccountKeysJson
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.OnboardingStatus
@@ -48,6 +49,7 @@ 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 LAST_LOCK_TIMESTAMP = "lastLockTimestamp"
private const val PROFILE_ACCOUNT_KEYS_KEY = "profileAccountKeys"
/**
* Primary implementation of [AuthDiskSource].
@@ -142,6 +144,7 @@ class AuthDiskSourceImpl(
storePinProtectedUserKey(userId = userId, pinProtectedUserKey = null)
storeEncryptedPin(userId = userId, encryptedPin = null)
storePrivateKey(userId = userId, privateKey = null)
storeAccountKeys(userId = userId, accountKeys = null)
storeOrganizationKeys(userId = userId, organizationKeys = null)
storeOrganizations(userId = userId, organizations = null)
storeUserBiometricInitVector(userId = userId, iv = null)
@@ -228,9 +231,11 @@ class AuthDiskSourceImpl(
)
}
@Deprecated("Use getAccountKeys instead.", replaceWith = ReplaceWith("getAccountKeys"))
override fun getPrivateKey(userId: String): String? =
getString(key = MASTER_KEY_ENCRYPTION_PRIVATE_KEY.appendIdentifier(userId))
@Deprecated("Use storeAccountKeys instead.", replaceWith = ReplaceWith("storeAccountKeys"))
override fun storePrivateKey(userId: String, privateKey: String?) {
putString(
key = MASTER_KEY_ENCRYPTION_PRIVATE_KEY.appendIdentifier(userId),
@@ -238,6 +243,20 @@ class AuthDiskSourceImpl(
)
}
override fun getAccountKeys(userId: String): AccountKeysJson? =
getEncryptedString(key = PROFILE_ACCOUNT_KEYS_KEY.appendIdentifier(userId))
?.let { json.decodeFromStringOrNull(it) }
override fun storeAccountKeys(
userId: String,
accountKeys: AccountKeysJson?,
) {
putEncryptedString(
key = PROFILE_ACCOUNT_KEYS_KEY.appendIdentifier(userId),
value = accountKeys?.let { json.encodeToString(it) },
)
}
override fun getUserAutoUnlockKey(userId: String): String? =
getEncryptedString(
key = USER_AUTO_UNLOCK_KEY_KEY.appendIdentifier(userId),

View File

@@ -2,12 +2,14 @@ package com.x8bit.bitwarden.data.auth.datasource.disk.model
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import java.time.Instant
/**
* Container for the user's API tokens.
*
* @property accessToken The user's primary access token.
* @property refreshToken The user's refresh token.
* @property expiresAtSec The time at which the token expires in epoch seconds.
*/
@Serializable
data class AccountTokensJson(
@@ -16,6 +18,9 @@ data class AccountTokensJson(
@SerialName("refreshToken")
val refreshToken: String?,
@SerialName("expiresAtSec")
val expiresAtSec: Long = Instant.MAX.epochSecond,
) {
/**
* Returns `true` if the user is logged in, `false otherwise.

View File

@@ -8,11 +8,12 @@ import androidx.core.app.NotificationChannelCompat
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
import com.bitwarden.annotation.OmitFromCoverage
import com.bitwarden.core.util.toPendingIntentMutabilityFlag
import com.bitwarden.data.manager.DispatcherManager
import com.x8bit.bitwarden.R
import com.bitwarden.ui.platform.resource.BitwardenDrawable
import com.bitwarden.ui.platform.resource.BitwardenString
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.manager.PushManager
import com.x8bit.bitwarden.data.platform.manager.model.PasswordlessRequestData
import kotlinx.coroutines.CoroutineScope
@@ -48,14 +49,14 @@ class AuthRequestNotificationManagerImpl(
NOTIFICATION_CHANNEL_ID,
NotificationManagerCompat.IMPORTANCE_DEFAULT,
)
.setName(context.getString(R.string.pending_log_in_requests))
.setName(context.getString(BitwardenString.pending_log_in_requests))
.build(),
)
if (!notificationManager.areNotificationsEnabled(NOTIFICATION_CHANNEL_ID)) return
// Create the notification
val builder = NotificationCompat.Builder(context, NOTIFICATION_CHANNEL_ID)
.setContentIntent(createContentIntent(data))
.setContentTitle(context.getString(R.string.log_in_requested))
.setContentTitle(context.getString(BitwardenString.log_in_requested))
.setContentText(
authDiskSource
.userState
@@ -63,10 +64,10 @@ class AuthRequestNotificationManagerImpl(
?.get(data.userId)
?.profile
?.email
?.let { context.getString(R.string.confim_log_in_attemp_for_x, it) }
?: context.getString(R.string.confirm_log_in),
?.let { context.getString(BitwardenString.confim_log_in_attemp_for_x, it) }
?: context.getString(BitwardenString.confirm_log_in),
)
.setSmallIcon(R.drawable.ic_notification)
.setSmallIcon(BitwardenDrawable.ic_notification)
.setColor(Color.White.value.toInt())
.setAutoCancel(true)
.setTimeoutAfter(NOTIFICATION_DEFAULT_TIMEOUT_MILLIS)

View File

@@ -1,5 +1,6 @@
package com.x8bit.bitwarden.data.auth.manager
import com.bitwarden.network.model.AuthTokenData
import com.x8bit.bitwarden.data.auth.datasource.disk.AuthDiskSource
/**
@@ -9,9 +10,19 @@ class AuthTokenManagerImpl(
private val authDiskSource: AuthDiskSource,
) : AuthTokenManager {
override fun getActiveAccessTokenOrNull(): String? = authDiskSource
override fun getAuthTokenDataOrNull(): AuthTokenData? = authDiskSource
.userState
?.activeUserId
?.let { authDiskSource.getAccountTokens(it) }
?.accessToken
?.let { userId ->
authDiskSource
.getAccountTokens(userId = userId)
?.takeIf { it.accessToken != null }
?.let {
AuthTokenData(
userId = userId,
accessToken = requireNotNull(it.accessToken),
expiresAtSec = it.expiresAtSec,
)
}
}
}

View File

@@ -1,11 +1,10 @@
package com.x8bit.bitwarden.data.auth.manager
import android.content.Context
import android.widget.Toast
import androidx.annotation.StringRes
import com.bitwarden.core.data.manager.toast.ToastManager
import com.bitwarden.core.data.repository.util.bufferedMutableSharedFlow
import com.bitwarden.data.manager.DispatcherManager
import com.x8bit.bitwarden.R
import com.bitwarden.ui.platform.resource.BitwardenString
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
@@ -27,15 +26,15 @@ import timber.log.Timber
*/
@Suppress("LongParameterList")
class UserLogoutManagerImpl(
private val context: Context,
private val authDiskSource: AuthDiskSource,
private val generatorDiskSource: GeneratorDiskSource,
private val passwordHistoryDiskSource: PasswordHistoryDiskSource,
private val pushDiskSource: PushDiskSource,
private val settingsDiskSource: SettingsDiskSource,
private val toastManager: ToastManager,
private val vaultDiskSource: VaultDiskSource,
dispatcherManager: DispatcherManager,
private val vaultSdkSource: VaultSdkSource,
dispatcherManager: DispatcherManager,
) : UserLogoutManager {
private val scope = CoroutineScope(dispatcherManager.unconfined)
private val mainScope = CoroutineScope(dispatcherManager.main)
@@ -49,7 +48,7 @@ class UserLogoutManagerImpl(
Timber.d("logout reason=$reason")
val isExpired = reason == LogoutReason.SecurityStamp
if (isExpired) {
showToast(message = R.string.login_expired)
showToast(message = BitwardenString.login_expired)
}
val ableToSwitchToNewAccount = switchUserIfAvailable(
@@ -71,7 +70,7 @@ class UserLogoutManagerImpl(
Timber.d("softLogout reason=$reason")
val isExpired = reason == LogoutReason.SecurityStamp
if (isExpired) {
showToast(message = R.string.login_expired)
showToast(message = BitwardenString.login_expired)
}
authDiskSource.storeAccountTokens(
userId = userId,
@@ -81,6 +80,7 @@ class UserLogoutManagerImpl(
// Save any data that will still need to be retained after otherwise clearing all dat
val vaultTimeoutInMinutes = settingsDiskSource.getVaultTimeoutInMinutes(userId = userId)
val vaultTimeoutAction = settingsDiskSource.getVaultTimeoutAction(userId = userId)
val pinProtectedUserKey = authDiskSource.getPinProtectedUserKey(userId = userId)
switchUserIfAvailable(
currentUserId = userId,
@@ -102,6 +102,10 @@ class UserLogoutManagerImpl(
vaultTimeoutAction = vaultTimeoutAction,
)
}
authDiskSource.storePinProtectedUserKey(
userId = userId,
pinProtectedUserKey = pinProtectedUserKey,
)
}
private fun clearData(userId: String) {
@@ -117,7 +121,7 @@ class UserLogoutManagerImpl(
}
private fun showToast(@StringRes message: Int) {
mainScope.launch { Toast.makeText(context, message, Toast.LENGTH_SHORT).show() }
mainScope.launch { toastManager.show(messageId = message) }
}
private fun switchUserIfAvailable(
@@ -136,7 +140,7 @@ class UserLogoutManagerImpl(
// Check if there is a new active user
return if (updatedAccounts.isNotEmpty()) {
if (currentUserId == currentUserState.activeUserId && !isExpired) {
showToast(message = R.string.account_switched_automatically)
showToast(message = BitwardenString.account_switched_automatically)
}
// If we logged out a non-active user, we want to leave the active user unchanged.

View File

@@ -0,0 +1,43 @@
package com.x8bit.bitwarden.data.auth.manager
import com.x8bit.bitwarden.data.auth.repository.model.UserState
import kotlinx.coroutines.flow.StateFlow
/**
* Manages the global state of all users.
*/
interface UserStateManager {
/**
* Emits updates for changes to the [UserState].
*/
val userStateFlow: StateFlow<UserState?>
/**
* Tracks whether there is an additional account that is pending login/registration in order to
* have multiple accounts available.
*
* This allows a direct view into and modification of [UserState.hasPendingAccountAddition].
* Note that this call has no effect when there is no [UserState] information available.
*/
var hasPendingAccountAddition: Boolean
/**
* Emits updates for changes to the [UserState.hasPendingAccountAddition] flag.
*/
val hasPendingAccountAdditionStateFlow: StateFlow<Boolean>
/**
* Tracks whether there is an account that is pending deletion in order to allow the account to
* remain active until the deletion is finalized.
*/
var hasPendingAccountDeletion: Boolean
/**
* Run the given [block] while preventing any updates to [UserState]. This is useful in cases
* where many individual changes might occur that would normally affect the [UserState] but we
* only want a single final emission. In the rare case that multiple threads are running
* transactions simultaneously, there will be no [UserState] updates until the last
* transaction completes.
*/
suspend fun <T> userStateTransaction(block: suspend () -> T): T
}

View File

@@ -0,0 +1,162 @@
package com.x8bit.bitwarden.data.auth.manager
import com.bitwarden.data.manager.DispatcherManager
import com.x8bit.bitwarden.data.auth.datasource.disk.AuthDiskSource
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.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.auth.repository.util.currentOnboardingStatus
import com.x8bit.bitwarden.data.auth.repository.util.onboardingStatusChangesFlow
import com.x8bit.bitwarden.data.auth.repository.util.toUserState
import com.x8bit.bitwarden.data.auth.repository.util.userAccountTokens
import com.x8bit.bitwarden.data.auth.repository.util.userAccountTokensFlow
import com.x8bit.bitwarden.data.auth.repository.util.userKeyConnectorStateFlow
import com.x8bit.bitwarden.data.auth.repository.util.userKeyConnectorStateList
import com.x8bit.bitwarden.data.auth.repository.util.userOrganizationsList
import com.x8bit.bitwarden.data.auth.repository.util.userOrganizationsListFlow
import com.x8bit.bitwarden.data.platform.manager.FirstTimeActionManager
import com.x8bit.bitwarden.data.platform.manager.model.FirstTimeState
import com.x8bit.bitwarden.data.vault.manager.VaultLockManager
import com.x8bit.bitwarden.data.vault.repository.model.VaultUnlockData
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.filterNot
import kotlinx.coroutines.flow.merge
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.flow.update
/**
* The default implementation of the [UserStateManager].
*/
class UserStateManagerImpl(
private val authDiskSource: AuthDiskSource,
firstTimeActionManager: FirstTimeActionManager,
vaultLockManager: VaultLockManager,
dispatcherManager: DispatcherManager,
) : UserStateManager {
private val unconfinedScope = CoroutineScope(dispatcherManager.unconfined)
//region Pending Account Addition
private val mutableHasPendingAccountAdditionStateFlow = MutableStateFlow(value = false)
override val hasPendingAccountAdditionStateFlow: StateFlow<Boolean>
get() = mutableHasPendingAccountAdditionStateFlow
override var hasPendingAccountAddition: Boolean
by mutableHasPendingAccountAdditionStateFlow::value
//endregion Pending Account Addition
//region Pending Account Deletion
/**
* If there is a pending account deletion, continue showing the original UserState until it
* is confirmed. This is accomplished by blocking the emissions of the [userStateFlow]
* whenever set to `true`.
*/
private val mutableHasPendingAccountDeletionStateFlow = MutableStateFlow(value = false)
override var hasPendingAccountDeletion: Boolean
by mutableHasPendingAccountDeletionStateFlow::value
//endregion Pending Account Deletion
/**
* Whenever a function needs to update multiple underlying data-points that contribute to the
* [UserState], we update this [MutableStateFlow] and continue to show the original `UserState`
* until the transaction is complete. This is accomplished by blocking the emissions of the
* [userStateFlow] whenever this is set to a value above 0 (a count is used if more than one
* process is updating data simultaneously).
*/
private val mutableUserStateTransactionCountStateFlow = MutableStateFlow(0)
@Suppress("UNCHECKED_CAST", "MagicNumber")
override val userStateFlow: StateFlow<UserState?> = combine(
authDiskSource.userStateFlow,
authDiskSource.userAccountTokensFlow,
authDiskSource.userOrganizationsListFlow,
authDiskSource.userKeyConnectorStateFlow,
authDiskSource.onboardingStatusChangesFlow,
firstTimeActionManager.firstTimeStateFlow,
vaultLockManager.vaultUnlockDataStateFlow,
hasPendingAccountAdditionStateFlow,
// Ignore the data in the merge, but trigger an update when they emit.
merge(
mutableHasPendingAccountDeletionStateFlow,
mutableUserStateTransactionCountStateFlow,
vaultLockManager.isActiveUserUnlockingFlow,
),
) { array ->
val userStateJson = array[0] as UserStateJson?
val userAccountTokens = array[1] as List<UserAccountTokens>
val userOrganizationsList = array[2] as List<UserOrganizations>
val userIsUsingKeyConnectorList = array[3] as List<UserKeyConnectorState>
val onboardingStatus = array[4] as OnboardingStatus?
val firstTimeState = array[5] as FirstTimeState
val vaultState = array[6] as List<VaultUnlockData>
val hasPendingAccountAddition = array[7] as Boolean
userStateJson?.toUserState(
vaultState = vaultState,
userAccountTokens = userAccountTokens,
userOrganizationsList = userOrganizationsList,
userIsUsingKeyConnectorList = userIsUsingKeyConnectorList,
hasPendingAccountAddition = hasPendingAccountAddition,
onboardingStatus = onboardingStatus,
isBiometricsEnabledProvider = ::isBiometricsEnabled,
vaultUnlockTypeProvider = ::getVaultUnlockType,
isDeviceTrustedProvider = ::isDeviceTrusted,
firstTimeState = firstTimeState,
)
}
.filterNot {
mutableHasPendingAccountDeletionStateFlow.value ||
mutableUserStateTransactionCountStateFlow.value > 0 ||
vaultLockManager.isActiveUserUnlockingFlow.value
}
.stateIn(
scope = unconfinedScope,
started = SharingStarted.Eagerly,
initialValue = authDiskSource
.userState
?.toUserState(
vaultState = vaultLockManager.vaultUnlockDataStateFlow.value,
userAccountTokens = authDiskSource.userAccountTokens,
userOrganizationsList = authDiskSource.userOrganizationsList,
userIsUsingKeyConnectorList = authDiskSource.userKeyConnectorStateList,
hasPendingAccountAddition = mutableHasPendingAccountAdditionStateFlow.value,
onboardingStatus = authDiskSource.currentOnboardingStatus,
isBiometricsEnabledProvider = ::isBiometricsEnabled,
vaultUnlockTypeProvider = ::getVaultUnlockType,
isDeviceTrustedProvider = ::isDeviceTrusted,
firstTimeState = firstTimeActionManager.currentOrDefaultUserFirstTimeState,
),
)
override suspend fun <T> userStateTransaction(block: suspend () -> T): T {
mutableUserStateTransactionCountStateFlow.update { it.inc() }
return try {
block()
} finally {
mutableUserStateTransactionCountStateFlow.update { it.dec() }
}
}
private fun isBiometricsEnabled(
userId: String,
): Boolean = authDiskSource.getUserBiometricUnlockKey(userId = userId) != null
private fun isDeviceTrusted(
userId: String,
): Boolean = authDiskSource.getDeviceKey(userId = userId) != null
private fun getVaultUnlockType(
userId: String,
): VaultUnlockType = authDiskSource
.getPinProtectedUserKey(userId = userId)
?.let { VaultUnlockType.PIN }
?: VaultUnlockType.MASTER_PASSWORD
}

View File

@@ -1,6 +1,7 @@
package com.x8bit.bitwarden.data.auth.manager.di
import android.content.Context
import com.bitwarden.core.data.manager.toast.ToastManager
import com.bitwarden.data.manager.DispatcherManager
import com.bitwarden.network.service.AccountsService
import com.bitwarden.network.service.AuthRequestsService
@@ -107,23 +108,23 @@ object AuthManagerModule {
@Provides
@Singleton
fun provideUserLogoutManager(
@ApplicationContext context: Context,
authDiskSource: AuthDiskSource,
generatorDiskSource: GeneratorDiskSource,
passwordHistoryDiskSource: PasswordHistoryDiskSource,
pushDiskSource: PushDiskSource,
settingsDiskSource: SettingsDiskSource,
toastManager: ToastManager,
vaultDiskSource: VaultDiskSource,
vaultSdkSource: VaultSdkSource,
dispatcherManager: DispatcherManager,
): UserLogoutManager =
UserLogoutManagerImpl(
context = context,
authDiskSource = authDiskSource,
generatorDiskSource = generatorDiskSource,
passwordHistoryDiskSource = passwordHistoryDiskSource,
pushDiskSource = pushDiskSource,
settingsDiskSource = settingsDiskSource,
toastManager = toastManager,
vaultDiskSource = vaultDiskSource,
vaultSdkSource = vaultSdkSource,
dispatcherManager = dispatcherManager,

View File

@@ -6,6 +6,7 @@ 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.OnboardingStatus
import com.x8bit.bitwarden.data.auth.manager.AuthRequestManager
import com.x8bit.bitwarden.data.auth.manager.UserStateManager
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
@@ -27,12 +28,10 @@ import com.x8bit.bitwarden.data.auth.repository.model.ResetPasswordResult
import com.x8bit.bitwarden.data.auth.repository.model.SendVerificationEmailResult
import com.x8bit.bitwarden.data.auth.repository.model.SetPasswordResult
import com.x8bit.bitwarden.data.auth.repository.model.SwitchAccountResult
import com.x8bit.bitwarden.data.auth.repository.model.UserState
import com.x8bit.bitwarden.data.auth.repository.model.ValidatePasswordResult
import com.x8bit.bitwarden.data.auth.repository.model.ValidatePinResult
import com.x8bit.bitwarden.data.auth.repository.model.VerifiedOrganizationDomainSsoDetailsResult
import com.x8bit.bitwarden.data.auth.repository.model.VerifyOtpResult
import com.x8bit.bitwarden.data.auth.repository.util.CaptchaCallbackTokenResult
import com.x8bit.bitwarden.data.auth.repository.util.DuoCallbackTokenResult
import com.x8bit.bitwarden.data.auth.repository.util.SsoCallbackResult
import com.x8bit.bitwarden.data.auth.repository.util.WebAuthResult
@@ -45,23 +44,12 @@ import kotlinx.coroutines.flow.StateFlow
* Provides an API for observing an modifying authentication state.
*/
@Suppress("TooManyFunctions")
interface AuthRepository : AuthenticatorProvider, AuthRequestManager {
interface AuthRepository : AuthenticatorProvider, AuthRequestManager, UserStateManager {
/**
* Models the current auth state.
*/
val authStateFlow: StateFlow<AuthState>
/**
* Emits updates for changes to the [UserState].
*/
val userStateFlow: StateFlow<UserState?>
/**
* Flow of the current [CaptchaCallbackTokenResult]. Subscribers should listen to the flow
* in order to receive updates whenever [setCaptchaCallbackTokenResult] is called.
*/
val captchaTokenResultFlow: Flow<CaptchaCallbackTokenResult>
/**
* Flow of the current [DuoCallbackTokenResult]. Subscribers should listen to the flow
* in order to receive updates whenever [setDuoCallbackTokenResult] is called.
@@ -117,15 +105,6 @@ interface AuthRepository : AuthenticatorProvider, AuthRequestManager {
*/
var shouldTrustDevice: Boolean
/**
* Tracks whether there is an additional account that is pending login/registration in order to
* have multiple accounts available.
*
* This allows a direct view into and modification of [UserState.hasPendingAccountAddition].
* Note that this call has no effect when there is no [UserState] information available.
*/
var hasPendingAccountAddition: Boolean
/**
* Return the cached password policies for the current user.
*/
@@ -147,11 +126,6 @@ interface AuthRepository : AuthenticatorProvider, AuthRequestManager {
*/
val showWelcomeCarousel: Boolean
/**
* Clears the pending deletion state that occurs when the an account is successfully deleted.
*/
fun clearPendingAccountDeletion()
/**
* Attempt to delete the current account using the [masterPassword] and log them out
* upon success.
@@ -186,7 +160,6 @@ interface AuthRepository : AuthenticatorProvider, AuthRequestManager {
suspend fun login(
email: String,
password: String,
captchaToken: String?,
): LoginResult
/**
@@ -201,7 +174,6 @@ interface AuthRepository : AuthenticatorProvider, AuthRequestManager {
asymmetricalKey: String,
requestPrivateKey: String,
masterPasswordHash: String?,
captchaToken: String?,
): LoginResult
/**
@@ -213,7 +185,6 @@ interface AuthRepository : AuthenticatorProvider, AuthRequestManager {
email: String,
password: String?,
twoFactorData: TwoFactorDataModel,
captchaToken: String?,
orgIdentifier: String?,
): LoginResult
@@ -226,7 +197,6 @@ interface AuthRepository : AuthenticatorProvider, AuthRequestManager {
ssoCode: String,
ssoCodeVerifier: String,
ssoRedirectUri: String,
captchaToken: String?,
organizationIdentifier: String,
): LoginResult
@@ -239,7 +209,6 @@ interface AuthRepository : AuthenticatorProvider, AuthRequestManager {
email: String,
password: String?,
newDeviceOtp: String,
captchaToken: String?,
orgIdentifier: String?,
): LoginResult
@@ -294,7 +263,6 @@ interface AuthRepository : AuthenticatorProvider, AuthRequestManager {
masterPassword: String,
masterPasswordHint: String?,
emailVerificationToken: String? = null,
captchaToken: String?,
shouldCheckDataBreaches: Boolean,
isMasterPasswordStrong: Boolean,
): RegisterResult
@@ -332,11 +300,6 @@ interface AuthRepository : AuthenticatorProvider, AuthRequestManager {
passwordHint: String?,
): SetPasswordResult
/**
* Set the value of [captchaTokenResultFlow].
*/
fun setCaptchaCallbackTokenResult(tokenResult: CaptchaCallbackTokenResult)
/**
* Set the value of [duoTokenResultFlow].
*/

View File

@@ -33,6 +33,8 @@ 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.VerificationCodeResponseJson
import com.bitwarden.network.model.VerificationOtpResponseJson
import com.bitwarden.network.model.VerifyEmailTokenRequestJson
import com.bitwarden.network.model.VerifyEmailTokenResponseJson
import com.bitwarden.network.service.AccountsService
@@ -46,7 +48,6 @@ 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.OnboardingStatus
import com.x8bit.bitwarden.data.auth.datasource.disk.model.UserStateJson
import com.x8bit.bitwarden.data.auth.datasource.network.model.DeviceDataModel
import com.x8bit.bitwarden.data.auth.datasource.sdk.AuthSdkSource
import com.x8bit.bitwarden.data.auth.datasource.sdk.util.toInt
@@ -55,6 +56,7 @@ import com.x8bit.bitwarden.data.auth.manager.AuthRequestManager
import com.x8bit.bitwarden.data.auth.manager.KeyConnectorManager
import com.x8bit.bitwarden.data.auth.manager.TrustedDeviceManager
import com.x8bit.bitwarden.data.auth.manager.UserLogoutManager
import com.x8bit.bitwarden.data.auth.manager.UserStateManager
import com.x8bit.bitwarden.data.auth.manager.model.MigrateExistingUserToKeyConnectorResult
import com.x8bit.bitwarden.data.auth.repository.model.AuthState
import com.x8bit.bitwarden.data.auth.repository.model.BreachCountResult
@@ -77,51 +79,35 @@ import com.x8bit.bitwarden.data.auth.repository.model.ResetPasswordResult
import com.x8bit.bitwarden.data.auth.repository.model.SendVerificationEmailResult
import com.x8bit.bitwarden.data.auth.repository.model.SetPasswordResult
import com.x8bit.bitwarden.data.auth.repository.model.SwitchAccountResult
import com.x8bit.bitwarden.data.auth.repository.model.UserAccountTokens
import com.x8bit.bitwarden.data.auth.repository.model.UserKeyConnectorState
import com.x8bit.bitwarden.data.auth.repository.model.UserOrganizations
import com.x8bit.bitwarden.data.auth.repository.model.UserState
import com.x8bit.bitwarden.data.auth.repository.model.ValidatePasswordResult
import com.x8bit.bitwarden.data.auth.repository.model.ValidatePinResult
import com.x8bit.bitwarden.data.auth.repository.model.VaultUnlockType
import com.x8bit.bitwarden.data.auth.repository.model.VerifiedOrganizationDomainSsoDetailsResult
import com.x8bit.bitwarden.data.auth.repository.model.VerifyOtpResult
import com.x8bit.bitwarden.data.auth.repository.model.toLoginErrorResult
import com.x8bit.bitwarden.data.auth.repository.util.CaptchaCallbackTokenResult
import com.x8bit.bitwarden.data.auth.repository.util.DuoCallbackTokenResult
import com.x8bit.bitwarden.data.auth.repository.util.SsoCallbackResult
import com.x8bit.bitwarden.data.auth.repository.util.WebAuthResult
import com.x8bit.bitwarden.data.auth.repository.util.activeUserIdChangesFlow
import com.x8bit.bitwarden.data.auth.repository.util.currentOnboardingStatus
import com.x8bit.bitwarden.data.auth.repository.util.onboardingStatusChangesFlow
import com.x8bit.bitwarden.data.auth.repository.util.policyInformation
import com.x8bit.bitwarden.data.auth.repository.util.toRemovedPasswordUserStateJson
import com.x8bit.bitwarden.data.auth.repository.util.toSdkParams
import com.x8bit.bitwarden.data.auth.repository.util.toUserState
import com.x8bit.bitwarden.data.auth.repository.util.toUserStateJsonWithPassword
import com.x8bit.bitwarden.data.auth.repository.util.userAccountTokens
import com.x8bit.bitwarden.data.auth.repository.util.userAccountTokensFlow
import com.x8bit.bitwarden.data.auth.repository.util.userKeyConnectorStateFlow
import com.x8bit.bitwarden.data.auth.repository.util.userKeyConnectorStateList
import com.x8bit.bitwarden.data.auth.repository.util.userOrganizationsList
import com.x8bit.bitwarden.data.auth.repository.util.userOrganizationsListFlow
import com.x8bit.bitwarden.data.auth.repository.util.userSwitchingChangesFlow
import com.x8bit.bitwarden.data.auth.util.KdfParamsConstants.DEFAULT_PBKDF2_ITERATIONS
import com.x8bit.bitwarden.data.auth.util.YubiKeyResult
import com.x8bit.bitwarden.data.auth.util.toSdkParams
import com.x8bit.bitwarden.data.platform.datasource.disk.SettingsDiskSource
import com.x8bit.bitwarden.data.platform.error.MissingPropertyException
import com.x8bit.bitwarden.data.platform.error.NoActiveUserException
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.model.FirstTimeState
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.vault.datasource.sdk.VaultSdkSource
import com.x8bit.bitwarden.data.vault.repository.VaultRepository
import com.x8bit.bitwarden.data.vault.repository.model.VaultUnlockData
import com.x8bit.bitwarden.data.vault.repository.model.VaultUnlockError
import com.x8bit.bitwarden.data.vault.repository.model.VaultUnlockResult
import kotlinx.coroutines.CoroutineScope
@@ -129,13 +115,11 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asSharedFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.filterNot
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.launchIn
@@ -145,7 +129,7 @@ import kotlinx.coroutines.flow.merge
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.receiveAsFlow
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.flow.update
import java.time.Clock
import javax.inject.Singleton
/**
@@ -154,6 +138,7 @@ import javax.inject.Singleton
@Suppress("LargeClass", "LongParameterList", "TooManyFunctions")
@Singleton
class AuthRepositoryImpl(
private val clock: Clock,
private val accountsService: AccountsService,
private val devicesService: DevicesService,
private val haveIBeenPwnedService: HaveIBeenPwnedService,
@@ -162,6 +147,7 @@ class AuthRepositoryImpl(
private val authSdkSource: AuthSdkSource,
private val vaultSdkSource: VaultSdkSource,
private val authDiskSource: AuthDiskSource,
private val settingsDiskSource: SettingsDiskSource,
private val configDiskSource: ConfigDiskSource,
private val environmentRepository: EnvironmentRepository,
private val settingsRepository: SettingsRepository,
@@ -171,12 +157,13 @@ class AuthRepositoryImpl(
private val trustedDeviceManager: TrustedDeviceManager,
private val userLogoutManager: UserLogoutManager,
private val policyManager: PolicyManager,
firstTimeActionManager: FirstTimeActionManager,
private val userStateManager: UserStateManager,
logsManager: LogsManager,
pushManager: PushManager,
dispatcherManager: DispatcherManager,
) : AuthRepository,
AuthRequestManager by authRequestManager {
AuthRequestManager by authRequestManager,
UserStateManager by userStateManager {
/**
* A scope intended for use when simply collecting multiple flows in order to combine them. The
* use of [Dispatchers.Unconfined] allows for this to happen synchronously whenever any of
@@ -189,24 +176,6 @@ class AuthRepositoryImpl(
*/
private val ioScope = CoroutineScope(dispatcherManager.io)
private val mutableHasPendingAccountAdditionStateFlow = MutableStateFlow(false)
/**
* If there is a pending account deletion, continue showing the original UserState until it
* is confirmed. This is accomplished by blocking the emissions of the [userStateFlow]
* whenever set to `true`.
*/
private val mutableHasPendingAccountDeletionStateFlow = MutableStateFlow(false)
/**
* Whenever a function needs to update multiple underlying data-points that contribute to the
* [UserState], we update this [MutableStateFlow] and continue to show the original `UserState`
* until the transaction is complete. This is accomplished by blocking the emissions of the
* [userStateFlow] whenever this is set to a value above 0 (a count is used if more than one
* process is updating data simultaneously).
*/
private val mutableUserStateTransactionCountStateFlow = MutableStateFlow(0)
/**
* The auth information to make the identity token request will need to be
* cached to make the request again in the case of two-factor authentication.
@@ -267,72 +236,6 @@ class AuthRepositoryImpl(
initialValue = AuthState.Uninitialized,
)
@Suppress("UNCHECKED_CAST", "MagicNumber")
override val userStateFlow: StateFlow<UserState?> = combine(
authDiskSource.userStateFlow,
authDiskSource.userAccountTokensFlow,
authDiskSource.userOrganizationsListFlow,
authDiskSource.userKeyConnectorStateFlow,
authDiskSource.onboardingStatusChangesFlow,
firstTimeActionManager.firstTimeStateFlow,
vaultRepository.vaultUnlockDataStateFlow,
mutableHasPendingAccountAdditionStateFlow,
// Ignore the data in the merge, but trigger an update when they emit.
merge(
mutableHasPendingAccountDeletionStateFlow,
mutableUserStateTransactionCountStateFlow,
vaultRepository.isActiveUserUnlockingFlow,
),
) { array ->
val userStateJson = array[0] as UserStateJson?
val userAccountTokens = array[1] as List<UserAccountTokens>
val userOrganizationsList = array[2] as List<UserOrganizations>
val userIsUsingKeyConnectorList = array[3] as List<UserKeyConnectorState>
val onboardingStatus = array[4] as OnboardingStatus?
val firstTimeState = array[5] as FirstTimeState
val vaultState = array[6] as List<VaultUnlockData>
val hasPendingAccountAddition = array[7] as Boolean
userStateJson?.toUserState(
vaultState = vaultState,
userAccountTokens = userAccountTokens,
userOrganizationsList = userOrganizationsList,
userIsUsingKeyConnectorList = userIsUsingKeyConnectorList,
hasPendingAccountAddition = hasPendingAccountAddition,
onboardingStatus = onboardingStatus,
isBiometricsEnabledProvider = ::isBiometricsEnabled,
vaultUnlockTypeProvider = ::getVaultUnlockType,
isDeviceTrustedProvider = ::isDeviceTrusted,
firstTimeState = firstTimeState,
)
}
.filterNot {
mutableHasPendingAccountDeletionStateFlow.value ||
mutableUserStateTransactionCountStateFlow.value > 0 ||
vaultRepository.isActiveUserUnlockingFlow.value
}
.stateIn(
scope = unconfinedScope,
started = SharingStarted.Eagerly,
initialValue = authDiskSource
.userState
?.toUserState(
vaultState = vaultRepository.vaultUnlockDataStateFlow.value,
userAccountTokens = authDiskSource.userAccountTokens,
userOrganizationsList = authDiskSource.userOrganizationsList,
userIsUsingKeyConnectorList = authDiskSource.userKeyConnectorStateList,
hasPendingAccountAddition = mutableHasPendingAccountAdditionStateFlow.value,
onboardingStatus = authDiskSource.currentOnboardingStatus,
isBiometricsEnabledProvider = ::isBiometricsEnabled,
vaultUnlockTypeProvider = ::getVaultUnlockType,
isDeviceTrustedProvider = ::isDeviceTrusted,
firstTimeState = firstTimeActionManager.currentOrDefaultUserFirstTimeState,
),
)
private val captchaTokenChannel = Channel<CaptchaCallbackTokenResult>(capacity = Int.MAX_VALUE)
override val captchaTokenResultFlow: Flow<CaptchaCallbackTokenResult> =
captchaTokenChannel.receiveAsFlow()
private val duoTokenChannel = Channel<DuoCallbackTokenResult>(capacity = Int.MAX_VALUE)
override val duoTokenResultFlow: Flow<DuoCallbackTokenResult> = duoTokenChannel.receiveAsFlow()
@@ -361,9 +264,6 @@ class AuthRepositoryImpl(
}
}
override var hasPendingAccountAddition: Boolean
by mutableHasPendingAccountAdditionStateFlow::value
override val passwordPolicies: List<PolicyInformation.MasterPassword>
get() = policyManager.getActivePolicies()
@@ -382,7 +282,7 @@ class AuthRepositoryImpl(
init {
combine(
mutableHasPendingAccountAdditionStateFlow,
userStateManager.hasPendingAccountAdditionStateFlow,
authDiskSource.userStateFlow,
environmentRepository.environmentStateFlow,
) { hasPendingAddition, userState, environment ->
@@ -401,14 +301,16 @@ class AuthRepositoryImpl(
.launchIn(unconfinedScope)
pushManager
.syncOrgKeysFlow
.onEach {
val userId = activeUserId ?: return@onEach
// TODO: [PM-20593] Investigate why tokens are explicitly refreshed.
refreshAccessTokenSynchronouslyInternal(
userId = userId,
logOutOnFailure = false,
)
vaultRepository.sync(forced = true)
.onEach { userId ->
if (userId == activeUserId) {
// TODO: [PM-20593] Investigate why tokens are explicitly refreshed.
refreshAccessTokenSynchronously(userId = userId)
// We just sync now to get the latest data
vaultRepository.sync(forced = true)
} else {
// We clear the last sync time to ensure we sync when we become the active user
settingsDiskSource.storeLastSyncTime(userId = userId, lastSyncTime = null)
}
}
// This requires the ioScope to ensure that refreshAccessTokenSynchronously
// happens on a background thread
@@ -466,16 +368,12 @@ class AuthRepositoryImpl(
.launchIn(unconfinedScope)
}
override fun clearPendingAccountDeletion() {
mutableHasPendingAccountDeletionStateFlow.value = false
}
override suspend fun deleteAccountWithMasterPassword(
masterPassword: String,
): DeleteAccountResult {
val profile = authDiskSource.userState?.activeAccount?.profile
?: return DeleteAccountResult.Error(message = null, error = NoActiveUserException())
mutableHasPendingAccountDeletionStateFlow.value = true
userStateManager.hasPendingAccountDeletion = true
return authSdkSource
.hashPassword(
email = profile.email,
@@ -495,7 +393,7 @@ class AuthRepositoryImpl(
override suspend fun deleteAccountWithOneTimePassword(
oneTimePassword: String,
): DeleteAccountResult {
mutableHasPendingAccountDeletionStateFlow.value = true
userStateManager.hasPendingAccountDeletion = true
return accountsService
.deleteAccount(
masterPasswordHash = null,
@@ -507,13 +405,13 @@ class AuthRepositoryImpl(
private fun Result<DeleteAccountResponseJson>.finalizeDeleteAccount(): DeleteAccountResult =
fold(
onFailure = {
clearPendingAccountDeletion()
userStateManager.hasPendingAccountDeletion = false
DeleteAccountResult.Error(error = it, message = null)
},
onSuccess = { response ->
when (response) {
is DeleteAccountResponseJson.Invalid -> {
clearPendingAccountDeletion()
userStateManager.hasPendingAccountDeletion = false
DeleteAccountResult.Error(message = response.message, error = null)
}
@@ -564,6 +462,10 @@ class AuthRepositoryImpl(
.map { keys }
}
.onSuccess { keys ->
// TDE and SSO user creation still uses crypto-v1. These users are not
// expected to have the AEAD keys so we only store the private key for now.
// See https://github.com/bitwarden/android/pull/5682#discussion_r2273940332
// for more details.
authDiskSource.storePrivateKey(
userId = userId,
privateKey = keys.privateKey,
@@ -592,11 +494,15 @@ class AuthRepositoryImpl(
val profile = authDiskSource.userState?.activeAccount?.profile
?: return LoginResult.Error(errorMessage = null, error = NoActiveUserException())
val userId = profile.userId
val privateKey = authDiskSource.getPrivateKey(userId = userId)
val accountKeys = authDiskSource.getAccountKeys(userId = userId)
val privateKey = accountKeys?.publicKeyEncryptionKeyPair?.wrappedPrivateKey
?: authDiskSource.getPrivateKey(userId = userId)
?: return LoginResult.Error(
errorMessage = null,
error = MissingPropertyException("Private Key"),
)
val signingKey = accountKeys?.signatureKeyPair?.wrappedSigningKey
val securityState = accountKeys?.securityState?.securityState
checkForVaultUnlockError(
onVaultUnlockError = { error ->
@@ -606,6 +512,8 @@ class AuthRepositoryImpl(
unlockVault(
accountProfile = profile,
privateKey = privateKey,
signingKey = signingKey,
securityState = securityState,
initUserCryptoMethod = InitUserCryptoMethod.AuthRequest(
requestPrivateKey = requestPrivateKey,
method = AuthRequestMethod.UserKey(protectedUserKey = asymmetricalKey),
@@ -620,7 +528,6 @@ class AuthRepositoryImpl(
override suspend fun login(
email: String,
password: String,
captchaToken: String?,
): LoginResult = identityService
.preLogin(email = email)
.flatMap {
@@ -639,7 +546,6 @@ class AuthRepositoryImpl(
username = email,
password = passwordHash,
),
captchaToken = captchaToken,
)
}
.fold(
@@ -659,7 +565,6 @@ class AuthRepositoryImpl(
asymmetricalKey: String,
requestPrivateKey: String,
masterPasswordHash: String?,
captchaToken: String?,
): LoginResult =
loginCommon(
email = email,
@@ -674,14 +579,12 @@ class AuthRepositoryImpl(
asymmetricalKey = asymmetricalKey,
privateKey = requestPrivateKey,
),
captchaToken = captchaToken,
)
override suspend fun login(
email: String,
password: String?,
twoFactorData: TwoFactorDataModel,
captchaToken: String?,
orgIdentifier: String?,
): LoginResult = identityTokenAuthModel
?.let {
@@ -690,7 +593,6 @@ class AuthRepositoryImpl(
password = password,
authModel = it,
twoFactorData = twoFactorData,
captchaToken = captchaToken ?: twoFactorResponse?.captchaToken,
deviceData = twoFactorDeviceData,
orgIdentifier = orgIdentifier,
)
@@ -704,7 +606,6 @@ class AuthRepositoryImpl(
email: String,
password: String?,
newDeviceOtp: String,
captchaToken: String?,
orgIdentifier: String?,
): LoginResult = identityTokenAuthModel
?.let {
@@ -713,7 +614,6 @@ class AuthRepositoryImpl(
password = password,
authModel = it,
newDeviceOtp = newDeviceOtp,
captchaToken = captchaToken ?: twoFactorResponse?.captchaToken,
deviceData = twoFactorDeviceData,
orgIdentifier = orgIdentifier,
)
@@ -747,7 +647,6 @@ class AuthRepositoryImpl(
ssoCode: String,
ssoCodeVerifier: String,
ssoRedirectUri: String,
captchaToken: String?,
organizationIdentifier: String,
): LoginResult = loginCommon(
email = email,
@@ -756,15 +655,62 @@ class AuthRepositoryImpl(
ssoCodeVerifier = ssoCodeVerifier,
ssoRedirectUri = ssoRedirectUri,
),
captchaToken = captchaToken,
orgIdentifier = organizationIdentifier,
)
override fun refreshAccessTokenSynchronously(userId: String): Result<RefreshTokenResponseJson> =
refreshAccessTokenSynchronouslyInternal(
userId = userId,
logOutOnFailure = true,
)
override fun refreshAccessTokenSynchronously(
userId: String,
): Result<String> {
val refreshToken = authDiskSource
.getAccountTokens(userId = userId)
?.refreshToken
?: return IllegalStateException("Must be logged in.").asFailure()
return identityService
.refreshTokenSynchronously(refreshToken)
.flatMap { refreshTokenResponse ->
// Check to make sure the user is still logged in after making the request
authDiskSource
.userState
?.accounts
?.get(userId)
?.let { refreshTokenResponse.asSuccess() }
?: IllegalStateException("Must be logged in.").asFailure()
}
.flatMap { refreshTokenResponse ->
when (refreshTokenResponse) {
is RefreshTokenResponseJson.Error -> {
if (refreshTokenResponse.isInvalidGrant) {
logout(userId = userId, reason = LogoutReason.InvalidGrant)
}
IllegalStateException(refreshTokenResponse.error).asFailure()
}
is RefreshTokenResponseJson.Forbidden -> {
logout(userId = userId, reason = LogoutReason.RefreshForbidden)
refreshTokenResponse.error.asFailure()
}
is RefreshTokenResponseJson.Unauthorized -> {
logout(userId = userId, reason = LogoutReason.RefreshUnauthorized)
refreshTokenResponse.error.asFailure()
}
is RefreshTokenResponseJson.Success -> {
// Store the new token information
authDiskSource.storeAccountTokens(
userId = userId,
accountTokens = AccountTokensJson(
accessToken = refreshTokenResponse.accessToken,
refreshToken = refreshTokenResponse.refreshToken,
expiresAtSec = clock.instant().epochSecond +
refreshTokenResponse.expiresIn,
),
)
refreshTokenResponse.accessToken.asSuccess()
}
}
}
}
override fun logout(reason: LogoutReason) {
activeUserId?.let { userId -> logout(userId = userId, reason = reason) }
@@ -796,7 +742,17 @@ class AuthRepositoryImpl(
?.let { jsonRequest ->
accountsService.resendVerificationCodeEmail(body = jsonRequest).fold(
onFailure = { ResendEmailResult.Error(message = it.message, error = it) },
onSuccess = { ResendEmailResult.Success },
onSuccess = {
when (it) {
VerificationCodeResponseJson.Success -> ResendEmailResult.Success
is VerificationCodeResponseJson.Invalid -> {
ResendEmailResult.Error(
message = it.firstValidationErrorMessage,
error = null,
)
}
}
},
)
}
?: ResendEmailResult.Error(
@@ -809,7 +765,17 @@ class AuthRepositoryImpl(
?.let { jsonRequest ->
accountsService.resendNewDeviceOtp(body = jsonRequest).fold(
onFailure = { ResendEmailResult.Error(message = it.message, error = it) },
onSuccess = { ResendEmailResult.Success },
onSuccess = {
when (it) {
VerificationOtpResponseJson.Success -> ResendEmailResult.Success
is VerificationOtpResponseJson.Invalid -> {
ResendEmailResult.Error(
message = it.firstValidationErrorMessage,
error = null,
)
}
}
},
)
}
?: ResendEmailResult.Error(
@@ -832,7 +798,7 @@ class AuthRepositoryImpl(
// We need to make sure that the environment is set back to the correct spot.
updateEnvironment()
// No switching to do but clear any pending account additions
hasPendingAccountAddition = false
userStateManager.hasPendingAccountAddition = false
return SwitchAccountResult.NoChange
}
@@ -847,7 +813,7 @@ class AuthRepositoryImpl(
authDiskSource.userState = currentUserState.copy(activeUserId = userId)
// Clear any pending account additions
hasPendingAccountAddition = false
userStateManager.hasPendingAccountAddition = false
return SwitchAccountResult.AccountSwitched
}
@@ -858,7 +824,6 @@ class AuthRepositoryImpl(
masterPassword: String,
masterPasswordHint: String?,
emailVerificationToken: String?,
captchaToken: String?,
shouldCheckDataBreaches: Boolean,
isMasterPasswordStrong: Boolean,
): RegisterResult {
@@ -893,7 +858,6 @@ class AuthRepositoryImpl(
email = email,
masterPasswordHash = registerKeyResponse.masterPasswordHash,
masterPasswordHint = masterPasswordHint,
captchaResponse = captchaToken,
key = registerKeyResponse.encryptedUserKey,
keys = RegisterRequestJson.Keys(
publicKey = registerKeyResponse.keys.public,
@@ -910,7 +874,6 @@ class AuthRepositoryImpl(
masterPasswordHash = registerKeyResponse.masterPasswordHash,
masterPasswordHint = masterPasswordHint,
emailVerificationToken = emailVerificationToken,
captchaResponse = captchaToken,
userSymmetricKey = registerKeyResponse.encryptedUserKey,
userAsymmetricKeys = RegisterFinishRequestJson.Keys(
publicKey = registerKeyResponse.keys.public,
@@ -925,18 +888,9 @@ class AuthRepositoryImpl(
.fold(
onSuccess = {
when (it) {
is RegisterResponseJson.CaptchaRequired -> {
it.validationErrors.captchaKeys.firstOrNull()
?.let { key -> RegisterResult.CaptchaRequired(captchaId = key) }
?: RegisterResult.Error(
errorMessage = null,
error = MissingPropertyException("Captcha ID"),
)
}
is RegisterResponseJson.Success -> {
settingsRepository.hasUserLoggedInOrCreatedAccount = true
RegisterResult.Success(captchaToken = it.captchaBypassToken)
RegisterResult.Success
}
is RegisterResponseJson.Invalid -> {
@@ -1150,6 +1104,9 @@ class AuthRepositoryImpl(
)
.onSuccess {
rsaKeys?.private?.let {
// This process is used by TDE and Enterprise accounts during initial
// login. We continue to store the locally generated keys
// until TDE and Enterprise accounts support AEAD keys.
authDiskSource.storePrivateKey(userId = userId, privateKey = it)
}
authDiskSource.storeUserKey(userId = userId, userKey = encryptedUserKey)
@@ -1182,10 +1139,6 @@ class AuthRepositoryImpl(
)
}
override fun setCaptchaCallbackTokenResult(tokenResult: CaptchaCallbackTokenResult) {
captchaTokenChannel.trySend(tokenResult)
}
override fun setDuoCallbackTokenResult(tokenResult: DuoCallbackTokenResult) {
duoTokenChannel.trySend(tokenResult)
}
@@ -1422,42 +1375,6 @@ class AuthRepositoryImpl(
onFailure = { LeaveOrganizationResult.Error(error = it) },
)
private fun refreshAccessTokenSynchronouslyInternal(
userId: String,
logOutOnFailure: Boolean,
): Result<RefreshTokenResponseJson> {
val refreshToken = authDiskSource
.getAccountTokens(userId = userId)
?.refreshToken
?: return IllegalStateException("Must be logged in.").asFailure()
return identityService
.refreshTokenSynchronously(refreshToken)
.flatMap { refreshTokenResponse ->
// Check to make sure the user is still logged in after making the request
authDiskSource
.userState
?.accounts
?.get(userId)
?.let { refreshTokenResponse.asSuccess() }
?: IllegalStateException("Must be logged in.").asFailure()
}
.onFailure {
if (logOutOnFailure) {
logout(userId = userId, reason = LogoutReason.TokenRefreshFail)
}
}
.onSuccess { refreshTokenResponse ->
// Update the existing UserState with updated token information
authDiskSource.storeAccountTokens(
userId = userId,
accountTokens = AccountTokensJson(
accessToken = refreshTokenResponse.accessToken,
refreshToken = refreshTokenResponse.refreshToken,
),
)
}
}
@Suppress("CyclomaticComplexMethod")
private suspend fun validatePasswordAgainstPolicy(
password: String,
@@ -1559,27 +1476,6 @@ class AuthRepositoryImpl(
)
}
private fun isBiometricsEnabled(
userId: String,
): Boolean = authDiskSource.getUserBiometricUnlockKey(userId = userId) != null
private fun isDeviceTrusted(
userId: String,
): Boolean = authDiskSource.getDeviceKey(userId = userId) != null
private fun getVaultUnlockType(
userId: String,
): VaultUnlockType =
when {
authDiskSource.getPinProtectedUserKey(userId = userId) != null -> {
VaultUnlockType.PIN
}
else -> {
VaultUnlockType.MASTER_PASSWORD
}
}
/**
* Update the saved state with the force password reset reason.
*/
@@ -1613,7 +1509,6 @@ class AuthRepositoryImpl(
twoFactorData: TwoFactorDataModel? = null,
deviceData: DeviceDataModel? = null,
orgIdentifier: String? = null,
captchaToken: String?,
newDeviceOtp: String? = null,
): LoginResult = identityService
.getToken(
@@ -1621,7 +1516,6 @@ class AuthRepositoryImpl(
email = email,
authModel = authModel,
twoFactorData = twoFactorData ?: getRememberedTwoFactorData(email),
captchaToken = captchaToken,
newDeviceOtp = newDeviceOtp,
)
.fold(
@@ -1640,10 +1534,6 @@ class AuthRepositoryImpl(
},
onSuccess = { loginResponse ->
when (loginResponse) {
is GetTokenResponseJson.CaptchaRequired -> LoginResult.CaptchaRequired(
captchaId = loginResponse.captchaKey,
)
is GetTokenResponseJson.TwoFactorRequired -> handleLoginCommonTwoFactorRequired(
loginResponse = loginResponse,
email = email,
@@ -1696,7 +1586,7 @@ class AuthRepositoryImpl(
deviceData: DeviceDataModel?,
orgIdentifier: String?,
userConfirmedKeyConnector: Boolean,
): LoginResult = userStateTransaction {
): LoginResult = userStateManager.userStateTransaction {
val userStateJson = loginResponse.toUserState(
previousUserState = authDiskSource.userState,
environmentUrlData = environmentRepository.environment.environmentUrlData,
@@ -1735,7 +1625,7 @@ class AuthRepositoryImpl(
// we should ask him to confirm the domain
if (isNewKeyConnectorUser && isNotConfirmed) {
keyConnectorResponse = loginResponse
return LoginResult.ConfirmKeyConnectorDomain(
return@userStateTransaction LoginResult.ConfirmKeyConnectorDomain(
domain = keyConnectorUrl,
)
}
@@ -1780,6 +1670,7 @@ class AuthRepositoryImpl(
accountTokens = AccountTokensJson(
accessToken = loginResponse.accessToken,
refreshToken = loginResponse.refreshToken,
expiresAtSec = clock.instant().epochSecond + loginResponse.expiresInSeconds,
),
)
settingsRepository.hasUserLoggedInOrCreatedAccount = true
@@ -1790,11 +1681,18 @@ class AuthRepositoryImpl(
// when we completed the pending admin auth request.
authDiskSource.storeUserKey(userId = userId, userKey = it)
}
// We continue to store the private key for backwards compatibility. Key connector
// conversion still relies on the private key.
loginResponse.privateKey?.let {
// Only set the value if it's present, since we may have set it already
// when we completed the key connector conversion.
authDiskSource.storePrivateKey(userId = userId, privateKey = it)
}
loginResponse.accountKeys?.let {
// Only set the value if it's present, since we may have set it already
// when we completed the key connector conversion.
authDiskSource.storeAccountKeys(userId = userId, accountKeys = it)
}
// If the user just authenticated with a two-factor code and selected the option to
// remember it, then the API response will return a token that will be used in place
// of the two-factor code on the next login attempt.
@@ -1895,6 +1793,8 @@ class AuthRepositoryImpl(
masterKey = it.masterKey,
userKey = key,
),
securityState = loginResponse.accountKeys?.securityState?.securityState,
signingKey = loginResponse.accountKeys?.signatureKeyPair?.wrappedSigningKey,
)
}
.fold(
@@ -1918,6 +1818,8 @@ class AuthRepositoryImpl(
val result = unlockVault(
accountProfile = profile,
privateKey = keyConnectorResponse.keys.private,
securityState = loginResponse.accountKeys?.securityState?.securityState,
signingKey = loginResponse.accountKeys?.signatureKeyPair?.wrappedSigningKey,
initUserCryptoMethod = InitUserCryptoMethod.KeyConnector(
masterKey = keyConnectorResponse.masterKey,
userKey = keyConnectorResponse.encryptedUserKey,
@@ -1930,10 +1832,16 @@ class AuthRepositoryImpl(
userId = profile.userId,
userKey = keyConnectorResponse.encryptedUserKey,
)
// We continue to store the private key for backwards compatibility since
// key connector conversion still relies on the private key.
authDiskSource.storePrivateKey(
userId = profile.userId,
privateKey = keyConnectorResponse.keys.private,
)
authDiskSource.storeAccountKeys(
userId = profile.userId,
accountKeys = loginResponse.accountKeys,
)
}
result
}
@@ -1955,11 +1863,13 @@ class AuthRepositoryImpl(
): VaultUnlockResult? {
// Attempt to unlock the vault with password if possible.
val masterPassword = password ?: return null
val privateKey = loginResponse.privateKey ?: return null
val privateKey = loginResponse.privateKeyOrNull() ?: return null
val key = loginResponse.key ?: return null
return unlockVault(
accountProfile = profile,
privateKey = privateKey,
securityState = loginResponse.accountKeys?.securityState?.securityState,
signingKey = loginResponse.accountKeys?.signatureKeyPair?.wrappedSigningKey,
initUserCryptoMethod = InitUserCryptoMethod.Password(
password = masterPassword,
userKey = key,
@@ -1977,13 +1887,15 @@ 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.
val privateKey = loginResponse.privateKey
val privateKey = loginResponse.privateKeyOrNull()
val key = loginResponse.key
if (privateKey != null && key != null) {
deviceData?.let { model ->
return unlockVault(
accountProfile = profile,
privateKey = privateKey,
securityState = loginResponse.accountKeys?.securityState?.securityState,
signingKey = loginResponse.accountKeys?.signatureKeyPair?.wrappedSigningKey,
initUserCryptoMethod = InitUserCryptoMethod.AuthRequest(
requestPrivateKey = model.privateKey,
method = model
@@ -2008,13 +1920,26 @@ class AuthRepositoryImpl(
.userDecryptionOptions
?.trustedDeviceUserDecryptionOptions
?.let { options ->
loginResponse.privateKey?.let { privateKey ->
unlockVaultWithTrustedDeviceUserDecryptionOptionsAndStoreKeys(
options = options,
profile = profile,
privateKey = privateKey,
)
}
loginResponse.accountKeys
?.let { accountKeys ->
unlockVaultWithTrustedDeviceUserDecryptionOptionsAndStoreKeys(
options = options,
profile = profile,
privateKey = accountKeys.publicKeyEncryptionKeyPair.wrappedPrivateKey,
securityState = accountKeys.securityState?.securityState,
signingKey = accountKeys.signatureKeyPair?.wrappedSigningKey,
)
}
?: loginResponse.privateKey
?.let { privateKey ->
unlockVaultWithTrustedDeviceUserDecryptionOptionsAndStoreKeys(
options = options,
profile = profile,
privateKey = privateKey,
securityState = null,
signingKey = null,
)
}
}
}
@@ -2026,6 +1951,8 @@ class AuthRepositoryImpl(
options: TrustedDeviceUserDecryptionOptionsJson,
profile: AccountJson.Profile,
privateKey: String,
securityState: String?,
signingKey: String?,
): VaultUnlockResult? {
var vaultUnlockResult: VaultUnlockResult? = null
val userId = profile.userId
@@ -2044,6 +1971,8 @@ class AuthRepositoryImpl(
vaultUnlockResult = unlockVault(
accountProfile = profile,
privateKey = privateKey,
signingKey = signingKey,
securityState = securityState,
initUserCryptoMethod = InitUserCryptoMethod.AuthRequest(
requestPrivateKey = pendingRequest.requestPrivateKey,
method = AuthRequestMethod.UserKey(protectedUserKey = userKey),
@@ -2071,6 +2000,8 @@ class AuthRepositoryImpl(
vaultUnlockResult = unlockVault(
accountProfile = profile,
privateKey = privateKey,
securityState = securityState,
signingKey = signingKey,
initUserCryptoMethod = InitUserCryptoMethod.DeviceKey(
deviceKey = deviceKey,
protectedDevicePrivateKey = encryptedPrivateKey,
@@ -2090,6 +2021,8 @@ class AuthRepositoryImpl(
private suspend fun unlockVault(
accountProfile: AccountJson.Profile,
privateKey: String,
securityState: String?,
signingKey: String?,
initUserCryptoMethod: InitUserCryptoMethod,
): VaultUnlockResult {
val userId = accountProfile.userId
@@ -2098,6 +2031,8 @@ class AuthRepositoryImpl(
email = accountProfile.email,
kdf = accountProfile.toSdkParams(),
privateKey = privateKey,
signingKey = signingKey,
securityState = securityState,
initUserCryptoMethod = initUserCryptoMethod,
// The value for the organization keys here will typically be null. We can separately
// unlock the vault for organization data after receiving the sync response if this
@@ -2124,20 +2059,12 @@ class AuthRepositoryImpl(
}
//endregion LoginCommon
/**
* Run the given [block] while preventing any updates to [UserState]. This is useful in cases
* where many individual changes might occur that would normally affect the [UserState] but we
* only want a single final emission. In the rare case that multiple threads are running
* transactions simultaneously, there will be no [UserState] updates until the last
* transaction completes.
*/
private inline fun <T> userStateTransaction(block: () -> T): T {
mutableUserStateTransactionCountStateFlow.update { it.inc() }
return try {
block()
} finally {
mutableUserStateTransactionCountStateFlow.update { it.dec() }
}
}
}
/**
* Convenience function to extract the private key from the
* [GetTokenResponseJson.Success] response.
*/
private fun GetTokenResponseJson.Success.privateKeyOrNull(): String? =
this.accountKeys?.publicKeyEncryptionKeyPair?.wrappedPrivateKey
?: this.privateKey

View File

@@ -13,8 +13,11 @@ import com.x8bit.bitwarden.data.auth.manager.AuthRequestManager
import com.x8bit.bitwarden.data.auth.manager.KeyConnectorManager
import com.x8bit.bitwarden.data.auth.manager.TrustedDeviceManager
import com.x8bit.bitwarden.data.auth.manager.UserLogoutManager
import com.x8bit.bitwarden.data.auth.manager.UserStateManager
import com.x8bit.bitwarden.data.auth.manager.UserStateManagerImpl
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
import com.x8bit.bitwarden.data.auth.repository.AuthRepositoryImpl
import com.x8bit.bitwarden.data.platform.datasource.disk.SettingsDiskSource
import com.x8bit.bitwarden.data.platform.manager.FirstTimeActionManager
import com.x8bit.bitwarden.data.platform.manager.LogsManager
import com.x8bit.bitwarden.data.platform.manager.PolicyManager
@@ -22,11 +25,13 @@ import com.x8bit.bitwarden.data.platform.manager.PushManager
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
import com.x8bit.bitwarden.data.vault.manager.VaultLockManager
import com.x8bit.bitwarden.data.vault.repository.VaultRepository
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import java.time.Clock
import javax.inject.Singleton
/**
@@ -39,6 +44,7 @@ object AuthRepositoryModule {
@Provides
@Singleton
fun providesAuthRepository(
clock: Clock,
accountsService: AccountsService,
devicesService: DevicesService,
identityService: IdentityService,
@@ -47,6 +53,7 @@ object AuthRepositoryModule {
authSdkSource: AuthSdkSource,
vaultSdkSource: VaultSdkSource,
authDiskSource: AuthDiskSource,
settingsDiskSource: SettingsDiskSource,
configDiskSource: ConfigDiskSource,
dispatcherManager: DispatcherManager,
environmentRepository: EnvironmentRepository,
@@ -58,9 +65,10 @@ object AuthRepositoryModule {
userLogoutManager: UserLogoutManager,
pushManager: PushManager,
policyManager: PolicyManager,
firstTimeActionManager: FirstTimeActionManager,
logsManager: LogsManager,
userStateManager: UserStateManager,
): AuthRepository = AuthRepositoryImpl(
clock = clock,
accountsService = accountsService,
devicesService = devicesService,
identityService = identityService,
@@ -68,6 +76,7 @@ object AuthRepositoryModule {
authSdkSource = authSdkSource,
vaultSdkSource = vaultSdkSource,
authDiskSource = authDiskSource,
settingsDiskSource = settingsDiskSource,
configDiskSource = configDiskSource,
haveIBeenPwnedService = haveIBeenPwnedService,
dispatcherManager = dispatcherManager,
@@ -80,7 +89,21 @@ object AuthRepositoryModule {
userLogoutManager = userLogoutManager,
pushManager = pushManager,
policyManager = policyManager,
firstTimeActionManager = firstTimeActionManager,
logsManager = logsManager,
userStateManager = userStateManager,
)
@Provides
@Singleton
fun providesUserStateManager(
authDiskSource: AuthDiskSource,
firstTimeActionManager: FirstTimeActionManager,
vaultLockManager: VaultLockManager,
dispatcherManager: DispatcherManager,
): UserStateManager = UserStateManagerImpl(
authDiskSource = authDiskSource,
firstTimeActionManager = firstTimeActionManager,
vaultLockManager = vaultLockManager,
dispatcherManager = dispatcherManager,
)
}

View File

@@ -9,11 +9,6 @@ sealed class LoginResult {
*/
data object Success : LoginResult()
/**
* Captcha verification is required.
*/
data class CaptchaRequired(val captchaId: String) : LoginResult()
/**
* Encryption key migration is required.
*/

View File

@@ -29,6 +29,24 @@ sealed class LogoutReason {
data object NoLongerSupported : Biometrics()
}
/**
* Indicates that the logout is happening because the there was an "invalid_grant" response
* from the network.
*/
data object InvalidGrant : LogoutReason()
/**
* Indicates that the logout is happening because the there was a "Forbidden" response from
* token refresh API.
*/
data object RefreshForbidden : LogoutReason()
/**
* Indicates that the logout is happening because the there was a "Unauthorized" response from
* token refresh API.
*/
data object RefreshUnauthorized : LogoutReason()
/**
* Indicates that the logout is happening because of an invalid state.
*/
@@ -58,11 +76,6 @@ sealed class LogoutReason {
*/
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.

View File

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

View File

@@ -7,16 +7,8 @@ sealed class RegisterResult {
/**
* Register succeeded.
*
* @param captchaToken the captcha bypass token to bypass future captcha verifications.
*/
data class Success(val captchaToken: String?) : RegisterResult()
/**
* Captcha verification is required.
*
* @param captchaId the captcha id for performing the captcha verification.
*/
data class CaptchaRequired(val captchaId: String) : RegisterResult()
data object Success : RegisterResult()
/**
* There was an error logging in.

View File

@@ -15,6 +15,6 @@ sealed class ResendEmailResult {
*/
data class Error(
val message: String?,
val error: Throwable,
val error: Throwable?,
) : ResendEmailResult()
}

View File

@@ -1,74 +0,0 @@
package com.x8bit.bitwarden.data.auth.repository.util
import android.content.Intent
import android.net.Uri
import kotlinx.serialization.json.buildJsonObject
import kotlinx.serialization.json.put
import java.net.URLEncoder
import java.util.Base64
import java.util.Locale
private const val CAPTCHA_HOST: String = "captcha-callback"
private const val CALLBACK_URI = "bitwarden://$CAPTCHA_HOST"
/**
* Generates a [Uri] to display a CAPTCHA challenge for Bitwarden authentication.
*/
fun generateUriForCaptcha(captchaId: String): Uri {
val json = buildJsonObject {
put(key = "siteKey", value = captchaId)
put(key = "locale", value = Locale.getDefault().toString())
put(key = "callbackUri", value = CALLBACK_URI)
put(key = "captchaRequiredText", value = "Captcha required")
}
val base64Data = Base64
.getEncoder()
.encodeToString(
json
.toString()
.toByteArray(Charsets.UTF_8),
)
val parentParam = URLEncoder.encode(CALLBACK_URI, "UTF-8")
val url = "https://vault.bitwarden.com/captcha-mobile-connector.html" +
"?data=$base64Data&parent=$parentParam&v=1"
return Uri.parse(url)
}
/**
* Retrieves a [CaptchaCallbackTokenResult] from an Intent. There are three possible cases.
*
* - `null`: Intent is not a captcha callback, or data is null.
*
* - [CaptchaCallbackTokenResult.MissingToken]:
* Intent is the captcha callback, but its missing a token value.
*
* - [CaptchaCallbackTokenResult.Success]:
* Intent is the captcha callback, and it has a token.
*/
fun Intent.getCaptchaCallbackTokenResult(): CaptchaCallbackTokenResult? {
val localData = data
return if (
action == Intent.ACTION_VIEW && localData != null && localData.host == CAPTCHA_HOST
) {
localData.getQueryParameter("token")?.let {
CaptchaCallbackTokenResult.Success(token = it)
} ?: CaptchaCallbackTokenResult.MissingToken
} else {
null
}
}
/**
* Sealed class representing the result of captcha callback token extraction.
*/
sealed class CaptchaCallbackTokenResult {
/**
* Represents a missing token in the captcha callback.
*/
data object MissingToken : CaptchaCallbackTokenResult()
/**
* Represents a token present in the captcha callback.
*/
data class Success(val token: String) : CaptchaCallbackTokenResult()
}

View File

@@ -2,9 +2,9 @@ package com.x8bit.bitwarden.data.auth.util
import android.content.Context
import android.content.Intent
import com.bitwarden.ui.platform.util.getSafeParcelableExtra
import com.x8bit.bitwarden.MainActivity
import com.x8bit.bitwarden.data.platform.manager.model.PasswordlessRequestData
import com.x8bit.bitwarden.data.platform.util.getSafeParcelableExtra
private const val NOTIFICATION_DATA: String = "notificationData"

View File

@@ -4,6 +4,7 @@ import android.content.Context
import android.content.pm.PackageManager
import android.os.PowerManager
import android.view.accessibility.AccessibilityManager
import com.bitwarden.core.data.manager.toast.ToastManager
import com.bitwarden.data.manager.DispatcherManager
import com.x8bit.bitwarden.data.autofill.accessibility.manager.AccessibilityAutofillManager
import com.x8bit.bitwarden.data.autofill.accessibility.manager.AccessibilityAutofillManagerImpl
@@ -89,6 +90,7 @@ object AccessibilityModule {
accessibilityAutofillManager: AccessibilityAutofillManager,
launcherPackageNameManager: LauncherPackageNameManager,
powerManager: PowerManager,
toastManager: ToastManager,
): BitwardenAccessibilityProcessor =
BitwardenAccessibilityProcessorImpl(
context = context,
@@ -96,6 +98,7 @@ object AccessibilityModule {
accessibilityAutofillManager = accessibilityAutofillManager,
launcherPackageNameManager = launcherPackageNameManager,
powerManager = powerManager,
toastManager = toastManager,
)
@Singleton

View File

@@ -5,7 +5,8 @@ import android.os.PowerManager
import android.view.accessibility.AccessibilityEvent
import android.view.accessibility.AccessibilityNodeInfo
import android.widget.Toast
import com.x8bit.bitwarden.R
import com.bitwarden.core.data.manager.toast.ToastManager
import com.bitwarden.ui.platform.resource.BitwardenString
import com.x8bit.bitwarden.data.autofill.accessibility.manager.AccessibilityAutofillManager
import com.x8bit.bitwarden.data.autofill.accessibility.manager.LauncherPackageNameManager
import com.x8bit.bitwarden.data.autofill.accessibility.model.AccessibilityAction
@@ -26,6 +27,7 @@ class BitwardenAccessibilityProcessorImpl(
private val accessibilityAutofillManager: AccessibilityAutofillManager,
private val launcherPackageNameManager: LauncherPackageNameManager,
private val powerManager: PowerManager,
private val toastManager: ToastManager,
) : BitwardenAccessibilityProcessor {
override fun processAccessibilityEvent(
event: AccessibilityEvent,
@@ -110,13 +112,10 @@ class BitwardenAccessibilityProcessorImpl(
)
}
?: run {
Toast
.makeText(
context,
R.string.autofill_tile_uri_not_found,
Toast.LENGTH_LONG,
)
.show()
toastManager.show(
messageId = BitwardenString.autofill_tile_uri_not_found,
duration = Toast.LENGTH_LONG,
)
}
}

View File

@@ -8,8 +8,9 @@ import com.x8bit.bitwarden.data.autofill.model.FilledData
import com.x8bit.bitwarden.data.autofill.model.FilledPartition
import com.x8bit.bitwarden.data.autofill.util.buildDataset
import com.x8bit.bitwarden.data.autofill.util.buildVaultItemDataset
import com.x8bit.bitwarden.data.autofill.util.createTotpCopyIntentSender
import com.x8bit.bitwarden.data.autofill.util.createAutofillCallbackIntentSender
import com.x8bit.bitwarden.data.autofill.util.fillableAutofillIds
import timber.log.Timber
/**
* The default implementation for [FillResponseBuilder]. This is a component for compiling fulfilled
@@ -22,12 +23,9 @@ class FillResponseBuilderImpl : FillResponseBuilder {
saveInfo: SaveInfo?,
): FillResponse? =
if (filledData.fillableAutofillIds.isNotEmpty()) {
Timber.d("Autofill request constructing FillResponse")
val fillResponseBuilder = FillResponse.Builder()
saveInfo
?.let { nonNullSaveInfo ->
fillResponseBuilder.setSaveInfo(nonNullSaveInfo)
}
saveInfo?.let { nonNullSaveInfo -> fillResponseBuilder.setSaveInfo(nonNullSaveInfo) }
filledData
.filledPartitions
@@ -52,12 +50,7 @@ class FillResponseBuilderImpl : FillResponseBuilder {
fillResponseBuilder
// Add the Vault Item
.addDataset(
filledData
.buildVaultItemDataset(
autofillAppInfo = autofillAppInfo,
),
)
.addDataset(filledData.buildVaultItemDataset(autofillAppInfo = autofillAppInfo))
.setIgnoredIds(*filledData.ignoreAutofillIds.toTypedArray())
.build()
} else {
@@ -66,13 +59,14 @@ class FillResponseBuilderImpl : FillResponseBuilder {
// with a presentation view. Neither of these make sense in the case where we have no
// views to fill. What we are supposed to do when we cannot fulfill a request is
// replace [FillResponse] with null in order to avoid this crash.
Timber.w("Autofill request has no fillable ids")
null
}
}
/**
* Convert this [FilledPartition] and [autofillAppInfo] into an [IntentSender] if totp is enabled
* and there the [FilledPartition.autofillCipher] has a valid cipher id.
* Convert this [FilledPartition] and [autofillAppInfo] into an [IntentSender] if the
* [FilledPartition.autofillCipher] has a valid cipher id.
*/
private fun FilledPartition.toAuthIntentSenderOrNull(
autofillAppInfo: AutofillAppInfo,
@@ -80,8 +74,7 @@ private fun FilledPartition.toAuthIntentSenderOrNull(
autofillCipher
.cipherId
?.let { cipherId ->
// We always do this even if there is no TOTP code because we want to log the events
createTotpCopyIntentSender(
createAutofillCallbackIntentSender(
cipherId = cipherId,
context = autofillAppInfo.context,
)

View File

@@ -9,6 +9,7 @@ import com.x8bit.bitwarden.data.autofill.model.FilledData
import com.x8bit.bitwarden.data.autofill.model.FilledPartition
import com.x8bit.bitwarden.data.autofill.provider.AutofillCipherProvider
import com.x8bit.bitwarden.data.autofill.util.buildFilledItemOrNull
import timber.log.Timber
/**
* The maximum amount of filled partitions the user will see. Viewing the rest will require opening
@@ -34,6 +35,7 @@ class FilledDataBuilderImpl(
private val autofillCipherProvider: AutofillCipherProvider,
) : FilledDataBuilder {
override suspend fun build(autofillRequest: AutofillRequest.Fillable): FilledData {
Timber.d("Autofill request constructing FilledData")
val isVaultLocked = autofillCipherProvider.isVaultLocked()
// Subtract one to make sure there is space for the vault item.
@@ -84,7 +86,7 @@ class FilledDataBuilderImpl(
)
}
}
?: emptyList()
.orEmpty()
}
}
@@ -114,15 +116,13 @@ class FilledDataBuilderImpl(
): FilledPartition {
val filledItems = autofillViews
.mapNotNull { autofillView ->
val value = when (autofillView) {
is AutofillView.Card.ExpirationMonth -> autofillCipher.expirationMonth
is AutofillView.Card.ExpirationYear -> autofillCipher.expirationYear
is AutofillView.Card.Number -> autofillCipher.number
is AutofillView.Card.SecurityCode -> autofillCipher.code
}
autofillView.buildFilledItemOrNull(
value = value,
)
autofillCipher
.getAutofillValueOrNull(autofillView)
?.let { value ->
autofillView.buildFilledItemOrNull(
value = value,
)
}
}
return FilledPartition(
@@ -160,6 +160,48 @@ class FilledDataBuilderImpl(
}
}
/**
* Get the autofill value for the given [autofillView], or null if no value is available.
*/
private fun AutofillCipher.Card.getAutofillValueOrNull(autofillView: AutofillView.Card): String? =
when (autofillView) {
is AutofillView.Card.CardholderName -> {
cardholderName.takeIf { it.isNotEmpty() }
}
is AutofillView.Card.ExpirationMonth -> {
expirationMonth.takeIf { it.isNotEmpty() }
}
is AutofillView.Card.ExpirationYear -> {
expirationYear.takeIf { it.isNotEmpty() }
}
is AutofillView.Card.Number -> {
number
.filter { it.isDigit() }
.takeIf { it.isNotEmpty() }
}
is AutofillView.Card.SecurityCode -> {
code
.filter { it.isDigit() }
.takeIf { it.isNotEmpty() }
}
is AutofillView.Card.ExpirationDate -> {
if (expirationMonth.isNotBlank() && expirationYear.isNotBlank()) {
expirationMonth.padStart(2, '0') + expirationYear.takeLast(2)
} else {
null
}
}
is AutofillView.Card.Brand -> {
brand.takeIf { it.isNotEmpty() }
}
}
/**
* Get the item at the [index]. If that fails, return the last item in the list. If that also fails,
* return null.

View File

@@ -4,6 +4,7 @@ import android.service.autofill.FillRequest
import android.service.autofill.SaveInfo
import com.x8bit.bitwarden.data.autofill.model.AutofillPartition
import com.x8bit.bitwarden.data.platform.repository.SettingsRepository
import timber.log.Timber
/**
* The primary implementation of [SaveInfoBuilder].This is used for converting autofill data into
@@ -18,6 +19,7 @@ class SaveInfoBuilderImpl(
fillRequest: FillRequest,
packageName: String?,
): SaveInfo? {
Timber.d("Autofill request constructing SaveInfo -- ${fillRequest.id}")
// Make sure that the save prompt is possible.
val canPerformSaveRequest = autofillPartition.canPerformSaveRequest
if (settingsRepository.isAutofillSavePromptDisabled || !canPerformSaveRequest) return null
@@ -26,6 +28,7 @@ class SaveInfoBuilderImpl(
// in Compat mode since they show as masked values.
val isInCompatMode = (fillRequest.flags or
FillRequest.FLAG_COMPATIBILITY_MODE_REQUEST) == fillRequest.flags
Timber.d("Autofill request isInCompatMode=$isInCompatMode -- ${fillRequest.id}")
// If login and compat mode, the password might be obfuscated,
// in which case we should skip the save request.

View File

@@ -8,9 +8,9 @@ import androidx.lifecycle.lifecycleScope
import com.x8bit.bitwarden.data.autofill.manager.AutofillActivityManager
import com.x8bit.bitwarden.data.autofill.manager.AutofillActivityManagerImpl
import com.x8bit.bitwarden.data.autofill.manager.AutofillEnabledManager
import com.x8bit.bitwarden.data.autofill.manager.chrome.ChromeThirdPartyAutofillEnabledManager
import com.x8bit.bitwarden.data.autofill.manager.chrome.ChromeThirdPartyAutofillManager
import com.x8bit.bitwarden.data.autofill.manager.chrome.ChromeThirdPartyAutofillManagerImpl
import com.x8bit.bitwarden.data.autofill.manager.browser.BrowserThirdPartyAutofillEnabledManager
import com.x8bit.bitwarden.data.autofill.manager.browser.BrowserThirdPartyAutofillManager
import com.x8bit.bitwarden.data.autofill.manager.browser.BrowserThirdPartyAutofillManagerImpl
import com.x8bit.bitwarden.data.platform.manager.AppStateManager
import dagger.Module
import dagger.Provides
@@ -29,9 +29,9 @@ object ActivityAutofillModule {
@ActivityScoped
@ActivityScopedManager
@Provides
fun provideActivityScopedChromeThirdPartyAutofillManager(
fun provideActivityScopedBrowserThirdPartyAutofillManager(
activity: Activity,
): ChromeThirdPartyAutofillManager = ChromeThirdPartyAutofillManagerImpl(
): BrowserThirdPartyAutofillManager = BrowserThirdPartyAutofillManagerImpl(
context = activity.baseContext,
)
@@ -39,19 +39,19 @@ object ActivityAutofillModule {
@Provides
fun provideAutofillActivityManager(
@ActivityScopedManager autofillManager: AutofillManager,
@ActivityScopedManager chromeThirdPartyAutofillManager: ChromeThirdPartyAutofillManager,
@ActivityScopedManager browserThirdPartyAutofillManager: BrowserThirdPartyAutofillManager,
appStateManager: AppStateManager,
autofillEnabledManager: AutofillEnabledManager,
lifecycleScope: LifecycleCoroutineScope,
chromeThirdPartyAutofillEnabledManager: ChromeThirdPartyAutofillEnabledManager,
browserThirdPartyAutofillEnabledManager: BrowserThirdPartyAutofillEnabledManager,
): AutofillActivityManager =
AutofillActivityManagerImpl(
autofillManager = autofillManager,
chromeThirdPartyAutofillManager = chromeThirdPartyAutofillManager,
browserThirdPartyAutofillManager = browserThirdPartyAutofillManager,
appStateManager = appStateManager,
autofillEnabledManager = autofillEnabledManager,
lifecycleScope = lifecycleScope,
chromeThirdPartyAutofillEnabledManager = chromeThirdPartyAutofillEnabledManager,
browserThirdPartyAutofillEnabledManager = browserThirdPartyAutofillEnabledManager,
)
/**

View File

@@ -16,15 +16,17 @@ import com.x8bit.bitwarden.data.autofill.manager.AutofillEnabledManager
import com.x8bit.bitwarden.data.autofill.manager.AutofillEnabledManagerImpl
import com.x8bit.bitwarden.data.autofill.manager.AutofillTotpManager
import com.x8bit.bitwarden.data.autofill.manager.AutofillTotpManagerImpl
import com.x8bit.bitwarden.data.autofill.manager.chrome.ChromeThirdPartyAutofillEnabledManager
import com.x8bit.bitwarden.data.autofill.manager.chrome.ChromeThirdPartyAutofillEnabledManagerImpl
import com.x8bit.bitwarden.data.autofill.manager.browser.BrowserAutofillDialogManager
import com.x8bit.bitwarden.data.autofill.manager.browser.BrowserAutofillDialogManagerImpl
import com.x8bit.bitwarden.data.autofill.manager.browser.BrowserThirdPartyAutofillEnabledManager
import com.x8bit.bitwarden.data.autofill.manager.browser.BrowserThirdPartyAutofillEnabledManagerImpl
import com.x8bit.bitwarden.data.autofill.parser.AutofillParser
import com.x8bit.bitwarden.data.autofill.parser.AutofillParserImpl
import com.x8bit.bitwarden.data.autofill.processor.AutofillProcessor
import com.x8bit.bitwarden.data.autofill.processor.AutofillProcessorImpl
import com.x8bit.bitwarden.data.autofill.provider.AutofillCipherProvider
import com.x8bit.bitwarden.data.autofill.provider.AutofillCipherProviderImpl
import com.x8bit.bitwarden.data.platform.manager.FeatureFlagManager
import com.x8bit.bitwarden.data.platform.datasource.disk.SettingsDiskSource
import com.x8bit.bitwarden.data.platform.manager.PolicyManager
import com.x8bit.bitwarden.data.platform.manager.ciphermatching.CipherMatchingManager
import com.x8bit.bitwarden.data.platform.manager.clipboard.BitwardenClipboardManager
@@ -59,12 +61,22 @@ object AutofillModule {
@Singleton
@Provides
fun providesChromeAutofillEnabledManager(
featureFlagManager: FeatureFlagManager,
): ChromeThirdPartyAutofillEnabledManager =
ChromeThirdPartyAutofillEnabledManagerImpl(
featureFlagManager = featureFlagManager,
)
fun providesBrowserAutofillEnabledManager(): BrowserThirdPartyAutofillEnabledManager =
BrowserThirdPartyAutofillEnabledManagerImpl()
@Singleton
@Provides
fun providesBrowserAutofillDialogManager(
autofillEnabledManager: AutofillEnabledManager,
browserThirdPartyAutofillEnabledManager: BrowserThirdPartyAutofillEnabledManager,
clock: Clock,
settingsDiskSource: SettingsDiskSource,
): BrowserAutofillDialogManager = BrowserAutofillDialogManagerImpl(
autofillEnabledManager = autofillEnabledManager,
browserThirdPartyAutofillEnabledManager = browserThirdPartyAutofillEnabledManager,
clock = clock,
settingsDiskSource = settingsDiskSource,
)
@Singleton
@Provides
@@ -93,7 +105,6 @@ object AutofillModule {
@Singleton
@Provides
fun providesAutofillTotpManager(
@ApplicationContext context: Context,
clock: Clock,
clipboardManager: BitwardenClipboardManager,
authRepository: AuthRepository,
@@ -101,7 +112,6 @@ object AutofillModule {
vaultRepository: VaultRepository,
): AutofillTotpManager =
AutofillTotpManagerImpl(
context = context,
clock = clock,
clipboardManager = clipboardManager,
authRepository = authRepository,
@@ -115,11 +125,13 @@ object AutofillModule {
authRepository: AuthRepository,
cipherMatchingManager: CipherMatchingManager,
vaultRepository: VaultRepository,
policyManager: PolicyManager,
): AutofillCipherProvider =
AutofillCipherProviderImpl(
authRepository = authRepository,
cipherMatchingManager = cipherMatchingManager,
vaultRepository = vaultRepository,
policyManager = policyManager,
)
@Singleton

View File

@@ -2,9 +2,9 @@ package com.x8bit.bitwarden.data.autofill.manager
import android.view.autofill.AutofillManager
import androidx.lifecycle.LifecycleCoroutineScope
import com.x8bit.bitwarden.data.autofill.manager.chrome.ChromeThirdPartyAutofillEnabledManager
import com.x8bit.bitwarden.data.autofill.manager.chrome.ChromeThirdPartyAutofillManager
import com.x8bit.bitwarden.data.autofill.model.chrome.ChromeThirdPartyAutofillStatus
import com.x8bit.bitwarden.data.autofill.manager.browser.BrowserThirdPartyAutofillEnabledManager
import com.x8bit.bitwarden.data.autofill.manager.browser.BrowserThirdPartyAutofillManager
import com.x8bit.bitwarden.data.autofill.model.browser.BrowserThirdPartyAutofillStatus
import com.x8bit.bitwarden.data.platform.manager.AppStateManager
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
@@ -14,21 +14,22 @@ import kotlinx.coroutines.flow.onEach
*/
class AutofillActivityManagerImpl(
private val autofillManager: AutofillManager,
private val chromeThirdPartyAutofillManager: ChromeThirdPartyAutofillManager,
private val browserThirdPartyAutofillManager: BrowserThirdPartyAutofillManager,
autofillEnabledManager: AutofillEnabledManager,
appStateManager: AppStateManager,
lifecycleScope: LifecycleCoroutineScope,
chromeThirdPartyAutofillEnabledManager: ChromeThirdPartyAutofillEnabledManager,
browserThirdPartyAutofillEnabledManager: BrowserThirdPartyAutofillEnabledManager,
) : AutofillActivityManager {
private val isAutofillEnabledAndSupported: Boolean
get() = autofillManager.isEnabled &&
autofillManager.hasEnabledAutofillServices() &&
autofillManager.isAutofillSupported
private val chromeAutofillStatus: ChromeThirdPartyAutofillStatus
get() = ChromeThirdPartyAutofillStatus(
stableStatusData = chromeThirdPartyAutofillManager.stableChromeAutofillStatus,
betaChannelStatusData = chromeThirdPartyAutofillManager.betaChromeAutofillStatus,
private val browserAutofillStatus: BrowserThirdPartyAutofillStatus
get() = BrowserThirdPartyAutofillStatus(
braveStableStatusData = browserThirdPartyAutofillManager.stableBraveAutofillStatus,
chromeStableStatusData = browserThirdPartyAutofillManager.stableChromeAutofillStatus,
chromeBetaChannelStatusData = browserThirdPartyAutofillManager.betaChromeAutofillStatus,
)
init {
@@ -36,8 +37,8 @@ class AutofillActivityManagerImpl(
.appForegroundStateFlow
.onEach {
autofillEnabledManager.isAutofillEnabled = isAutofillEnabledAndSupported
chromeThirdPartyAutofillEnabledManager.chromeThirdPartyAutofillStatus =
chromeAutofillStatus
browserThirdPartyAutofillEnabledManager.browserThirdPartyAutofillStatus =
browserAutofillStatus
}
.launchIn(lifecycleScope)
}

View File

@@ -17,6 +17,7 @@ import com.x8bit.bitwarden.data.platform.manager.event.OrganizationEventManager
import com.x8bit.bitwarden.data.platform.manager.model.OrganizationEvent
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import timber.log.Timber
/**
* Primary implementation of [AutofillCompletionManager].
@@ -41,6 +42,7 @@ class AutofillCompletionManagerImpl(
.intent
?.getAutofillAssistStructureOrNull()
?: run {
Timber.w("Assist structure not found")
activity.cancelAndFinish()
return
}
@@ -51,6 +53,7 @@ class AutofillCompletionManagerImpl(
assistStructure = assistStructure,
)
if (autofillRequest !is AutofillRequest.Fillable) {
Timber.w("Request is not fillable")
activity.cancelAndFinish()
return
}
@@ -68,11 +71,13 @@ class AutofillCompletionManagerImpl(
authIntentSender = null,
)
?: run {
Timber.w("Dataset not found")
activity.cancelAndFinish()
return@launch
}
totpManager.tryCopyTotpToClipboard(cipherView = cipherView)
val resultIntent = createAutofillSelectionResultIntent(dataset)
Timber.d("Autofill success")
activity.setResultAndFinish(resultIntent = resultIntent)
cipherView.id?.let {
organizationEventManager.trackEvent(

View File

@@ -1,10 +1,8 @@
package com.x8bit.bitwarden.data.autofill.manager
import android.content.Context
import android.widget.Toast
import com.bitwarden.ui.platform.resource.BitwardenString
import com.bitwarden.ui.util.asText
import com.bitwarden.vault.CipherView
import com.x8bit.bitwarden.R
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
import com.x8bit.bitwarden.data.platform.manager.clipboard.BitwardenClipboardManager
import com.x8bit.bitwarden.data.platform.repository.SettingsRepository
@@ -16,7 +14,6 @@ import java.time.Clock
* Default implementation of the [AutofillTotpManager].
*/
class AutofillTotpManagerImpl(
private val context: Context,
private val clock: Clock,
private val clipboardManager: BitwardenClipboardManager,
private val authRepository: AuthRepository,
@@ -27,25 +24,19 @@ class AutofillTotpManagerImpl(
if (settingsRepository.isAutoCopyTotpDisabled) return
val isPremium = authRepository.userStateFlow.value?.activeAccount?.isPremium == true
if (!isPremium && !cipherView.organizationUseTotp) return
val totpCode = cipherView.login?.totp ?: return
cipherView.login?.totp ?: return
val cipherId = cipherView.id ?: return
val totpResult = vaultRepository.generateTotp(
time = clock.instant(),
totpCode = totpCode,
cipherId = cipherId,
)
if (totpResult is GenerateTotpResult.Success) {
clipboardManager.setText(
text = totpResult.code,
toastDescriptorOverride = R.string.verification_code_totp.asText(),
toastDescriptorOverride = BitwardenString.verification_code_totp.asText(),
)
Toast
.makeText(
context.applicationContext,
R.string.verification_code_totp,
Toast.LENGTH_LONG,
)
.show()
}
}
}

View File

@@ -0,0 +1,16 @@
package com.x8bit.bitwarden.data.autofill.manager.browser
/**
* Manager to handle whether the Browser Autofill Dialog should be displayed.
*/
interface BrowserAutofillDialogManager {
/**
* Indicates whether the dialog should be displayed to the user.
*/
val shouldShowDialog: Boolean
/**
* The dialog has been dismissed and we should delay displaying it again.
*/
fun delayDialog()
}

View File

@@ -0,0 +1,32 @@
package com.x8bit.bitwarden.data.autofill.manager.browser
import com.x8bit.bitwarden.data.autofill.manager.AutofillEnabledManager
import com.x8bit.bitwarden.data.platform.datasource.disk.SettingsDiskSource
import java.time.Clock
/**
* We only show the dialog once per 24 hour period.
*/
private const val SHOW_DIALOG_DELAY_MS: Long = 24L * 60L * 60L * 1000L
/**
* The default implementation of the [BrowserAutofillDialogManager].
*/
internal class BrowserAutofillDialogManagerImpl(
private val autofillEnabledManager: AutofillEnabledManager,
private val browserThirdPartyAutofillEnabledManager: BrowserThirdPartyAutofillEnabledManager,
private val clock: Clock,
private val settingsDiskSource: SettingsDiskSource,
) : BrowserAutofillDialogManager {
override val shouldShowDialog: Boolean
get() = autofillEnabledManager.isAutofillEnabled &&
browserThirdPartyAutofillEnabledManager
.browserThirdPartyAutofillStatus
.isAnyIsAvailableAndDisabled &&
settingsDiskSource.browserAutofillDialogReshowTime?.isBefore(clock.instant()) != false
override fun delayDialog() {
settingsDiskSource.browserAutofillDialogReshowTime =
clock.instant().plusMillis(SHOW_DIALOG_DELAY_MS)
}
}

View File

@@ -0,0 +1,22 @@
package com.x8bit.bitwarden.data.autofill.manager.browser
import com.x8bit.bitwarden.data.autofill.model.browser.BrowserThirdPartyAutofillStatus
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.StateFlow
/**
* Manager which provides whether specific browser versions have third party autofill available and
* enabled.
*/
interface BrowserThirdPartyAutofillEnabledManager {
/**
* Combined status for all concerned browser versions.
*/
var browserThirdPartyAutofillStatus: BrowserThirdPartyAutofillStatus
/**
* An observable [StateFlow] of the combined third party autofill status of all concerned
* browser versions.
*/
val browserThirdPartyAutofillStatusFlow: Flow<BrowserThirdPartyAutofillStatus>
}

View File

@@ -0,0 +1,42 @@
package com.x8bit.bitwarden.data.autofill.manager.browser
import com.x8bit.bitwarden.data.autofill.model.browser.BrowserThirdPartyAutoFillData
import com.x8bit.bitwarden.data.autofill.model.browser.BrowserThirdPartyAutofillStatus
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.update
/**
* Default implementation of [BrowserThirdPartyAutofillEnabledManager].
*/
class BrowserThirdPartyAutofillEnabledManagerImpl : BrowserThirdPartyAutofillEnabledManager {
override var browserThirdPartyAutofillStatus: BrowserThirdPartyAutofillStatus = DEFAULT_STATUS
set(value) {
field = value
mutableBrowserThirdPartyAutofillStatusStateFlow.update {
value
}
}
private val mutableBrowserThirdPartyAutofillStatusStateFlow = MutableStateFlow(
value = browserThirdPartyAutofillStatus,
)
override val browserThirdPartyAutofillStatusFlow: Flow<BrowserThirdPartyAutofillStatus>
get() = mutableBrowserThirdPartyAutofillStatusStateFlow
}
private val DEFAULT_STATUS = BrowserThirdPartyAutofillStatus(
braveStableStatusData = BrowserThirdPartyAutoFillData(
isAvailable = false,
isThirdPartyEnabled = false,
),
chromeStableStatusData = BrowserThirdPartyAutoFillData(
isAvailable = false,
isThirdPartyEnabled = false,
),
chromeBetaChannelStatusData = BrowserThirdPartyAutoFillData(
isAvailable = false,
isThirdPartyEnabled = false,
),
)

View File

@@ -0,0 +1,25 @@
package com.x8bit.bitwarden.data.autofill.manager.browser
import com.x8bit.bitwarden.data.autofill.model.browser.BrowserThirdPartyAutoFillData
/**
* Manager class used to determine if a device has installed versions of a browser (either the
* stable release or beta channel) which support and require opt in to third party autofill.
*/
interface BrowserThirdPartyAutofillManager {
/**
* The data representing the status of the stable Brave version
*/
val stableBraveAutofillStatus: BrowserThirdPartyAutoFillData
/**
* The data representing the status of the stable Chrome version
*/
val stableChromeAutofillStatus: BrowserThirdPartyAutoFillData
/**
* The data representing the status of the beta Chrome version
*/
val betaChromeAutofillStatus: BrowserThirdPartyAutoFillData
}

View File

@@ -1,35 +1,36 @@
package com.x8bit.bitwarden.data.autofill.manager.chrome
package com.x8bit.bitwarden.data.autofill.manager.browser
import android.content.ContentResolver
import android.content.Context
import android.net.Uri
import com.bitwarden.annotation.OmitFromCoverage
import com.x8bit.bitwarden.data.autofill.model.chrome.ChromeReleaseChannel
import com.x8bit.bitwarden.data.autofill.model.chrome.ChromeThirdPartyAutoFillData
import com.x8bit.bitwarden.data.autofill.model.browser.BrowserPackage
import com.x8bit.bitwarden.data.autofill.model.browser.BrowserThirdPartyAutoFillData
private const val CONTENT_PROVIDER_NAME = ".AutofillThirdPartyModeContentProvider"
private const val THIRD_PARTY_MODE_COLUMN = "autofill_third_party_state"
private const val THIRD_PARTY_MODE_ACTIONS_URI_PATH = "autofill_third_party_mode"
/**
* Default implementation of the [ChromeThirdPartyAutofillManager] which uses a
* [ContentResolver] to determine if the installed Chrome packages support and enable
* third party autofill services.
* Default implementation of the [BrowserThirdPartyAutofillManager] which uses a [ContentResolver]
* to determine if the installed browser packages support and enable third party autofill services.
*
* Based off of [this blog post](https://android-developers.googleblog.com/2025/02/chrome-3p-autofill-services-update.html)
*/
@OmitFromCoverage
class ChromeThirdPartyAutofillManagerImpl(
class BrowserThirdPartyAutofillManagerImpl(
private val context: Context,
) : ChromeThirdPartyAutofillManager {
override val stableChromeAutofillStatus: ChromeThirdPartyAutoFillData
get() = getThirdPartyAutoFillStatusForChannel(ChromeReleaseChannel.STABLE)
override val betaChromeAutofillStatus: ChromeThirdPartyAutoFillData
get() = getThirdPartyAutoFillStatusForChannel(ChromeReleaseChannel.BETA)
) : BrowserThirdPartyAutofillManager {
override val stableBraveAutofillStatus: BrowserThirdPartyAutoFillData
get() = getThirdPartyAutoFillStatusForChannel(BrowserPackage.BRAVE_RELEASE)
override val stableChromeAutofillStatus: BrowserThirdPartyAutoFillData
get() = getThirdPartyAutoFillStatusForChannel(BrowserPackage.CHROME_STABLE)
override val betaChromeAutofillStatus: BrowserThirdPartyAutoFillData
get() = getThirdPartyAutoFillStatusForChannel(BrowserPackage.CHROME_BETA)
private fun getThirdPartyAutoFillStatusForChannel(
releaseChannel: ChromeReleaseChannel,
): ChromeThirdPartyAutoFillData {
releaseChannel: BrowserPackage,
): BrowserThirdPartyAutoFillData {
val uri = Uri.Builder()
.scheme(ContentResolver.SCHEME_CONTENT)
.authority(releaseChannel.packageName + CONTENT_PROVIDER_NAME)
@@ -54,7 +55,7 @@ class ChromeThirdPartyAutofillManagerImpl(
true
}
?: false
return ChromeThirdPartyAutoFillData(
return BrowserThirdPartyAutoFillData(
isAvailable = isThirdPartyAvailable,
isThirdPartyEnabled = thirdPartyEnabled,
)

View File

@@ -1,22 +0,0 @@
package com.x8bit.bitwarden.data.autofill.manager.chrome
import com.x8bit.bitwarden.data.autofill.model.chrome.ChromeThirdPartyAutofillStatus
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.StateFlow
/**
* Manager which provides whether specific Chrome versions have third party autofill available and
* enabled.
*/
interface ChromeThirdPartyAutofillEnabledManager {
/**
* Combined status for all concerned Chrome versions.
*/
var chromeThirdPartyAutofillStatus: ChromeThirdPartyAutofillStatus
/**
* An observable [StateFlow] of the combined third party autofill status of all concerned
* chrome versions.
*/
val chromeThirdPartyAutofillStatusFlow: Flow<ChromeThirdPartyAutofillStatus>
}

View File

@@ -1,52 +0,0 @@
package com.x8bit.bitwarden.data.autofill.manager.chrome
import com.x8bit.bitwarden.data.autofill.model.chrome.ChromeThirdPartyAutoFillData
import com.x8bit.bitwarden.data.autofill.model.chrome.ChromeThirdPartyAutofillStatus
import com.x8bit.bitwarden.data.platform.manager.FeatureFlagManager
import com.x8bit.bitwarden.data.platform.manager.model.FlagKey
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.update
/**
* Default implementation of [ChromeThirdPartyAutofillEnabledManager].
*/
class ChromeThirdPartyAutofillEnabledManagerImpl(
private val featureFlagManager: FeatureFlagManager,
) : ChromeThirdPartyAutofillEnabledManager {
override var chromeThirdPartyAutofillStatus: ChromeThirdPartyAutofillStatus = DEFAULT_STATUS
set(value) {
field = value
mutableChromeThirdPartyAutofillStatusStateFlow.update {
value
}
}
private val mutableChromeThirdPartyAutofillStatusStateFlow = MutableStateFlow(
chromeThirdPartyAutofillStatus,
)
override val chromeThirdPartyAutofillStatusFlow: Flow<ChromeThirdPartyAutofillStatus>
get() = mutableChromeThirdPartyAutofillStatusStateFlow
.combine(
featureFlagManager.getFeatureFlagFlow(FlagKey.ChromeAutofill),
) { data, enabled ->
if (enabled) {
data
} else {
DEFAULT_STATUS
}
}
}
private val DEFAULT_STATUS = ChromeThirdPartyAutofillStatus(
ChromeThirdPartyAutoFillData(
isAvailable = false,
isThirdPartyEnabled = false,
),
ChromeThirdPartyAutoFillData(
isAvailable = false,
isThirdPartyEnabled = false,
),
)

View File

@@ -1,20 +0,0 @@
package com.x8bit.bitwarden.data.autofill.manager.chrome
import com.x8bit.bitwarden.data.autofill.model.chrome.ChromeThirdPartyAutoFillData
/**
* Manager class used to determine if a device has installed versions of Chrome (either the
* stable release or beta channel) which support and require opt in to third party autofill.
*/
interface ChromeThirdPartyAutofillManager {
/**
* The data representing the status of the stable chrome version
*/
val stableChromeAutofillStatus: ChromeThirdPartyAutoFillData
/**
* The data representing the status of the beta chrome version
*/
val betaChromeAutofillStatus: ChromeThirdPartyAutoFillData
}

View File

@@ -1,6 +1,7 @@
package com.x8bit.bitwarden.data.autofill.model
import android.content.Context
import androidx.annotation.ChecksSdkIntAtLeast
/**
* The app information required for the autofill service.
@@ -9,4 +10,10 @@ data class AutofillAppInfo(
val context: Context,
val packageName: String,
val sdkInt: Int,
)
) {
/**
* Returns true if the current [sdkInt] version is at least the provided [version].
*/
@ChecksSdkIntAtLeast(parameter = 0)
fun isVersionAtLeast(version: Int): Boolean = sdkInt >= version
}

View File

@@ -0,0 +1,14 @@
package com.x8bit.bitwarden.data.autofill.model
import android.os.Parcelable
import kotlinx.parcelize.Parcelize
/**
* Represents data for the autofill flow via authentication intents.
*
* @property cipherId The ID of the cipher associated with this Autofill instance.
*/
@Parcelize
data class AutofillCallbackData(
val cipherId: String,
) : Parcelable

View File

@@ -2,7 +2,7 @@ package com.x8bit.bitwarden.data.autofill.model
import androidx.annotation.DrawableRes
import com.bitwarden.core.Uuid
import com.x8bit.bitwarden.R
import com.bitwarden.ui.platform.resource.BitwardenDrawable
/**
* A paired down model of the CipherView for use within the autofill feature.
@@ -46,9 +46,10 @@ sealed class AutofillCipher {
val expirationMonth: String,
val expirationYear: String,
val number: String,
val brand: String,
) : AutofillCipher() {
override val iconRes: Int
@DrawableRes get() = R.drawable.ic_payment_card
@DrawableRes get() = BitwardenDrawable.ic_payment_card
override val isTotpEnabled: Boolean
get() = false
@@ -67,6 +68,6 @@ sealed class AutofillCipher {
val username: String,
) : AutofillCipher() {
override val iconRes: Int
@DrawableRes get() = R.drawable.ic_globe
@DrawableRes get() = BitwardenDrawable.ic_globe
}
}

View File

@@ -0,0 +1,16 @@
package com.x8bit.bitwarden.data.autofill.model
/**
* Autofill hints used to determine what data an input field is associated with.
*/
enum class AutofillHint {
CARD_CARDHOLDER,
CARD_EXPIRATION_DATE,
CARD_EXPIRATION_MONTH,
CARD_EXPIRATION_YEAR,
CARD_NUMBER,
CARD_SECURITY_CODE,
CARD_BRAND,
PASSWORD,
USERNAME,
}

View File

@@ -16,13 +16,16 @@ sealed class AutofillSaveItem : Parcelable {
* @property expirationMonth The expiration month in string form (if applicable).
* @property expirationYear The expiration year in string form (if applicable).
* @property securityCode The security code for the card (if applicable).
* @property cardholderName The name on the card (if applicable).
*/
@Parcelize
data class Card(
val cardholderName: String?,
val number: String?,
val expirationMonth: String?,
val expirationYear: String?,
val securityCode: String?,
val brand: String?,
) : AutofillSaveItem()
/**

View File

@@ -1,14 +0,0 @@
package com.x8bit.bitwarden.data.autofill.model
import android.os.Parcelable
import kotlinx.parcelize.Parcelize
/**
* Represents data for a TOTP copying during the autofill flow via authentication intents.
*
* @property cipherId The cipher for which we are copying a TOTP to the clipboard.
*/
@Parcelize
data class AutofillTotpCopyData(
val cipherId: String,
) : Parcelable

View File

@@ -48,10 +48,28 @@ sealed class AutofillView {
) : Card()
/**
* The expiration year [AutofillView] for the [Card] data partition.
* The expiration year [AutofillView] for the [Card] data partition. This implementation
* also has its own [yearValue] because it can be present in lists, in which case there
* is specialized logic for determining its [yearValue]. The [Data.textValue] is very
* likely going to be a very different value.
*/
data class ExpirationYear(
override val data: Data,
val yearValue: String?,
) : Card()
/**
* The expiration date [AutofillView] for the [Card] data partition.
*/
data class ExpirationDate(
override val data: Data,
) : Card()
/**
* The cardholder name [AutofillView] for the [Card] data partition.
*/
data class CardholderName(
override val data: Data,
) : Card()
/**
@@ -67,6 +85,17 @@ sealed class AutofillView {
data class SecurityCode(
override val data: Data,
) : Card()
/**
* The brand [AutofillView] for the [Card] data partition. This implementation also has its
* own [brandValue] because it can be present in lists, in which case there is specialized
* logic for determining its [brandValue]. The [Data.textValue] is very likely going to be
* a very different value.
*/
data class Brand(
override val data: Data,
val brandValue: String?,
) : Card()
}
/**

View File

@@ -0,0 +1,16 @@
package com.x8bit.bitwarden.data.autofill.model.browser
private const val BRAVE_CHANNEL_PACKAGE = "com.brave.browser"
private const val CHROME_BETA_CHANNEL_PACKAGE = "com.chrome.beta"
private const val CHROME_RELEASE_CHANNEL_PACKAGE = "com.android.chrome"
/**
* Enumerated values of each browser that supports third party autofill checks.
*
* @property packageName the package name of the release channel for the browser version.
*/
enum class BrowserPackage(val packageName: String) {
BRAVE_RELEASE(BRAVE_CHANNEL_PACKAGE),
CHROME_STABLE(CHROME_RELEASE_CHANNEL_PACKAGE),
CHROME_BETA(CHROME_BETA_CHANNEL_PACKAGE),
}

View File

@@ -0,0 +1,28 @@
package com.x8bit.bitwarden.data.autofill.model.browser
/**
* Relevant data relating to the third party autofill status of a specific browser app.
*/
data class BrowserThirdPartyAutoFillData(
val isAvailable: Boolean,
val isThirdPartyEnabled: Boolean,
) {
val isAvailableButDisabled: Boolean = isAvailable && !isThirdPartyEnabled
}
/**
* The overall status for all relevant browsers.
*/
data class BrowserThirdPartyAutofillStatus(
val braveStableStatusData: BrowserThirdPartyAutoFillData,
val chromeStableStatusData: BrowserThirdPartyAutoFillData,
val chromeBetaChannelStatusData: BrowserThirdPartyAutoFillData,
) {
/**
* Whether any of the available browsers have third party autofill disabled.
*/
val isAnyIsAvailableAndDisabled: Boolean
get() = braveStableStatusData.isAvailableButDisabled ||
chromeStableStatusData.isAvailableButDisabled ||
chromeBetaChannelStatusData.isAvailableButDisabled
}

View File

@@ -1,14 +0,0 @@
package com.x8bit.bitwarden.data.autofill.model.chrome
private const val BETA_CHANNEL_PACKAGE = "com.chrome.beta"
private const val CHROME_CHANNEL_PACKAGE = "com.android.chrome"
/**
* Enumerated values of each version of Chrome supported for third party autofill checks.
*
* @property packageName the package name of the release channel for the Chrome version.
*/
enum class ChromeReleaseChannel(val packageName: String) {
STABLE(CHROME_CHANNEL_PACKAGE),
BETA(BETA_CHANNEL_PACKAGE),
}

View File

@@ -1,17 +0,0 @@
package com.x8bit.bitwarden.data.autofill.model.chrome
/**
* Relevant data relating to the third party autofill status of a version of the Chrome browser app.
*/
data class ChromeThirdPartyAutoFillData(
val isAvailable: Boolean,
val isThirdPartyEnabled: Boolean,
)
/**
* The overall status for all relevant release channels of Chrome.
*/
data class ChromeThirdPartyAutofillStatus(
val stableStatusData: ChromeThirdPartyAutoFillData,
val betaChannelStatusData: ChromeThirdPartyAutoFillData,
)

View File

@@ -15,6 +15,7 @@ import com.x8bit.bitwarden.data.autofill.util.getMaxInlineSuggestionsCount
import com.x8bit.bitwarden.data.autofill.util.toAutofillView
import com.x8bit.bitwarden.data.autofill.util.website
import com.x8bit.bitwarden.data.platform.repository.SettingsRepository
import timber.log.Timber
/**
* A list of URIs that should never be autofilled.
@@ -23,6 +24,8 @@ private val BLOCK_LISTED_URIS: List<String> = listOf(
"androidapp://android",
"androidapp://com.android.settings",
"androidapp://com.x8bit.bitwarden",
"androidapp://com.x8bit.bitwarden.beta",
"androidapp://com.x8bit.bitwarden.dev",
"androidapp://com.oneplus.applocker",
)
@@ -70,6 +73,7 @@ class AutofillParserImpl(
autofillAppInfo: AutofillAppInfo,
fillRequest: FillRequest?,
): AutofillRequest {
Timber.d("Parsing AssistStructure -- ${fillRequest?.id}")
// Parse the `assistStructure` into internal models.
val traversalDataList = assistStructure.traverse()
// Take only the autofill views from the node that currently has focus.
@@ -131,6 +135,7 @@ class AutofillParserImpl(
// Get inline information if available
val isInlineAutofillEnabled = settingsRepository.isInlineAutofillEnabled
Timber.d("Autofill request isInlineEnabled=$isInlineAutofillEnabled -- ${fillRequest?.id}")
val maxInlineSuggestionsCount = fillRequest.getMaxInlineSuggestionsCount(
autofillAppInfo = autofillAppInfo,
isInlineAutofillEnabled = isInlineAutofillEnabled,

View File

@@ -53,8 +53,12 @@ class AutofillProcessorImpl(
fillCallback: FillCallback,
request: FillRequest,
) {
Timber.d("Begin processing Autofill fill request -- ${request.id}")
// Set the listener so that any long running work is cancelled when it is no longer needed.
cancellationSignal.setOnCancelListener { job.cancel() }
cancellationSignal.setOnCancelListener {
Timber.d("Autofill job cancelled")
job.cancel()
}
// Process the OS data and handle invoking the callback with the result.
job.cancel()
job = scope.launch {
@@ -122,6 +126,7 @@ class AutofillProcessorImpl(
)
when (autofillRequest) {
is AutofillRequest.Fillable -> {
Timber.d("Autofill request is Fillable -- ${fillRequest.id}")
// Fulfill the [autofillRequest].
val filledData = filledDataBuilder.build(
autofillRequest = autofillRequest,
@@ -141,6 +146,7 @@ class AutofillProcessorImpl(
@Suppress("TooGenericExceptionCaught")
try {
Timber.d("Autofill request success: Fillable -- ${fillRequest.id}")
fillCallback.onSuccess(response)
} catch (e: RuntimeException) {
// This is to catch any TransactionTooLargeExceptions that could occur here.
@@ -153,6 +159,7 @@ class AutofillProcessorImpl(
// If we are unable to fulfill the request, we should invoke the callback
// with null. This effectively disables autofill for this view set and
// allows the [AutofillService] to be unbound.
Timber.d("Autofill request success: Unfillable -- ${fillRequest.id}")
fillCallback.onSuccess(null)
}
}

View File

@@ -1,16 +1,21 @@
package com.x8bit.bitwarden.data.autofill.provider
import com.bitwarden.network.model.PolicyTypeJson
import com.bitwarden.vault.CipherListView
import com.bitwarden.vault.CipherListViewType
import com.bitwarden.vault.CipherRepromptType
import com.bitwarden.vault.CipherType
import com.bitwarden.vault.CipherView
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
import com.x8bit.bitwarden.data.autofill.model.AutofillCipher
import com.x8bit.bitwarden.data.platform.manager.PolicyManager
import com.x8bit.bitwarden.data.platform.manager.ciphermatching.CipherMatchingManager
import com.x8bit.bitwarden.data.platform.util.firstWithTimeoutOrNull
import com.x8bit.bitwarden.data.platform.util.subtitle
import com.x8bit.bitwarden.data.vault.manager.model.GetCipherResult
import com.x8bit.bitwarden.data.vault.repository.VaultRepository
import com.x8bit.bitwarden.data.vault.repository.model.VaultUnlockData
import com.x8bit.bitwarden.data.vault.repository.util.statusFor
import timber.log.Timber
/**
* The duration, in milliseconds, we should wait while waiting for the vault status to not be
@@ -31,6 +36,7 @@ class AutofillCipherProviderImpl(
private val authRepository: AuthRepository,
private val cipherMatchingManager: CipherMatchingManager,
private val vaultRepository: VaultRepository,
private val policyManager: PolicyManager,
) : AutofillCipherProvider {
private val activeUserId: String? get() = authRepository.activeUserId
@@ -49,31 +55,42 @@ class AutofillCipherProviderImpl(
}
override suspend fun getCardAutofillCiphers(): List<AutofillCipher.Card> {
val cipherViews = getUnlockedCiphersOrNull() ?: return emptyList()
return cipherViews
.mapNotNull { cipherView ->
cipherView
val cipherListViews = getUnlockedCipherListViewsOrNull() ?: return emptyList()
val organizationIdsWithCardTypeRestrictions = policyManager
.getActivePolicies(PolicyTypeJson.RESTRICT_ITEM_TYPES)
.map { it.organizationId }
return cipherListViews
.mapNotNull { cipherListView ->
cipherListView
// We only care about non-deleted card ciphers.
.takeIf {
// Must be card type.
cipherView.type == CipherType.CARD &&
it.type is CipherListViewType.Card &&
// Must not be deleted.
cipherView.deletedDate == null &&
it.deletedDate == null &&
// Must not require a reprompt.
it.reprompt == CipherRepromptType.NONE
it.reprompt == CipherRepromptType.NONE &&
// Must not be restricted by organization.
!it.isExcludedByOrgCardRestrictions(
organizationIdsWithCardTypeRestrictions,
)
}
?.let { nonNullCipherView ->
AutofillCipher.Card(
cipherId = cipherView.id,
name = nonNullCipherView.name,
subtitle = nonNullCipherView.subtitle.orEmpty(),
cardholderName = nonNullCipherView.card?.cardholderName.orEmpty(),
code = nonNullCipherView.card?.code.orEmpty(),
expirationMonth = nonNullCipherView.card?.expMonth.orEmpty(),
expirationYear = nonNullCipherView.card?.expYear.orEmpty(),
number = nonNullCipherView.card?.number.orEmpty(),
)
?.let { nonNullCipherListView ->
nonNullCipherListView.id?.let { cipherId ->
decryptCipherOrNull(cipherId = cipherId)?.let { cipherView ->
AutofillCipher.Card(
cipherId = cipherView.id,
name = cipherView.name,
subtitle = cipherView.subtitle.orEmpty(),
cardholderName = cipherView.card?.cardholderName.orEmpty(),
code = cipherView.card?.code.orEmpty(),
expirationMonth = cipherView.card?.expMonth.orEmpty(),
expirationYear = cipherView.card?.expYear.orEmpty(),
number = cipherView.card?.number.orEmpty(),
brand = cipherView.card?.brand.orEmpty(),
)
}
}
}
}
}
@@ -81,12 +98,12 @@ class AutofillCipherProviderImpl(
override suspend fun getLoginAutofillCiphers(
uri: String,
): List<AutofillCipher.Login> {
val cipherViews = getUnlockedCiphersOrNull() ?: return emptyList()
val cipherViews = getUnlockedCipherListViewsOrNull() ?: return emptyList()
// We only care about non-deleted login ciphers.
val loginCiphers = cipherViews
.filter {
// Must be login type
it.type == CipherType.LOGIN &&
it.type is CipherListViewType.Login &&
// Must not be deleted.
it.deletedDate == null &&
// Must not require a reprompt.
@@ -96,9 +113,12 @@ class AutofillCipherProviderImpl(
return cipherMatchingManager
// Filter for ciphers that match the uri in some way.
.filterCiphersForMatches(
ciphers = loginCiphers,
cipherListViews = loginCiphers,
matchUri = uri,
)
.mapNotNull { cipherListView ->
cipherListView.id?.let { decryptCipherOrNull(cipherId = it) }
}
.map { cipherView ->
AutofillCipher.Login(
cipherId = cipherView.id,
@@ -114,10 +134,47 @@ class AutofillCipherProviderImpl(
/**
* Get available [CipherView]s if possible.
*/
private suspend fun getUnlockedCiphersOrNull(): List<CipherView>? =
private suspend fun getUnlockedCipherListViewsOrNull(): List<CipherListView>? =
vaultRepository
.ciphersStateFlow
.decryptCipherListResultStateFlow
.takeUnless { isVaultLocked() }
?.firstWithTimeoutOrNull(timeMillis = GET_CIPHERS_TIMEOUT_MS) { it.data != null }
?.data
?.successes
private suspend fun decryptCipherOrNull(cipherId: String): CipherView? =
when (val result = vaultRepository.getCipher(cipherId = cipherId)) {
GetCipherResult.CipherNotFound -> {
Timber.e("Cipher not found for autofill.")
null
}
is GetCipherResult.Failure -> {
Timber.e(result.error, "Failed to decrypt cipher for autofill.")
null
}
is GetCipherResult.Success -> result.cipherView
}
/**
* Checks if this [CipherListView] item should be excluded from autofill due to
* organization-based card type restrictions.
*
* It's considered restricted if:
* 1. There are organizations with card type restrictions AND this item is a personal vault item
* (organizationId is null).
* 2. OR this item belongs to an organization that has card type restrictions.
*/
private fun CipherListView.isExcludedByOrgCardRestrictions(
restrictingOrgIds: List<String>,
): Boolean {
if (restrictingOrgIds.isEmpty()) {
return false
}
// If personal vault (no orgId), restricted if any org has restrictions.
return organizationId == null ||
// If part of an org, restricted if that org is in the restricting list.
organizationId in restrictingOrgIds
}
}

View File

@@ -12,18 +12,19 @@ import android.service.autofill.Dataset
import android.view.autofill.AutofillManager
import androidx.core.os.bundleOf
import com.bitwarden.annotation.OmitFromCoverage
import com.x8bit.bitwarden.AutofillTotpCopyActivity
import com.bitwarden.core.util.toPendingIntentMutabilityFlag
import com.bitwarden.ui.platform.util.getSafeParcelableExtra
import com.x8bit.bitwarden.AutofillCallbackActivity
import com.x8bit.bitwarden.MainActivity
import com.x8bit.bitwarden.data.autofill.model.AutofillAppInfo
import com.x8bit.bitwarden.data.autofill.model.AutofillCallbackData
import com.x8bit.bitwarden.data.autofill.model.AutofillSaveItem
import com.x8bit.bitwarden.data.autofill.model.AutofillSelectionData
import com.x8bit.bitwarden.data.autofill.model.AutofillTotpCopyData
import com.x8bit.bitwarden.data.platform.util.getSafeParcelableExtra
import kotlin.random.Random
private const val AUTOFILL_SAVE_ITEM_DATA_KEY = "autofill-save-item-data"
private const val AUTOFILL_SELECTION_DATA_KEY = "autofill-selection-data"
private const val AUTOFILL_TOTP_COPY_DATA_KEY = "autofill-totp-copy-data"
private const val AUTOFILL_CALLBACK_DATA_KEY = "autofill-callback-data"
private const val AUTOFILL_BUNDLE_KEY = "autofill-bundle-key"
/**
@@ -54,21 +55,21 @@ fun createAutofillSelectionIntent(
}
/**
* Creates an [IntentSender] built with the data required for performing a TOTP copying during
* the autofill flow.
* Creates an [IntentSender] built with the data required for performing an Autofill callback
* during the autofill flow.
*/
fun createTotpCopyIntentSender(
fun createAutofillCallbackIntentSender(
cipherId: String,
context: Context,
): IntentSender {
val intent = Intent(
context,
AutofillTotpCopyActivity::class.java,
AutofillCallbackActivity::class.java,
)
.putExtra(
AUTOFILL_BUNDLE_KEY,
bundleOf(
AUTOFILL_TOTP_COPY_DATA_KEY to AutofillTotpCopyData(cipherId = cipherId),
AUTOFILL_CALLBACK_DATA_KEY to AutofillCallbackData(cipherId = cipherId),
),
)
return PendingIntent
@@ -142,12 +143,12 @@ fun Intent.getAutofillSelectionDataOrNull(): AutofillSelectionData? =
?.getSafeParcelableExtra(AUTOFILL_SELECTION_DATA_KEY)
/**
* Checks if the given [Intent] contains data for TOTP copying. The [AutofillTotpCopyData] will be
* Checks if the given [Intent] contains Autofill callback data. The [AutofillCallbackData] will be
* returned when present.
*/
fun Intent.getTotpCopyIntentOrNull(): AutofillTotpCopyData? =
fun Intent.getAutofillCallbackIntentOrNull(): AutofillCallbackData? =
getBundleExtra(AUTOFILL_BUNDLE_KEY)
?.getSafeParcelableExtra(AUTOFILL_TOTP_COPY_DATA_KEY)
?.getSafeParcelableExtra(AUTOFILL_CALLBACK_DATA_KEY)
/**
* Checks if the given [Activity] was created for Autofill. This is useful to avoid locking the

View File

@@ -9,16 +9,19 @@ import com.x8bit.bitwarden.data.autofill.model.AutofillView
val AutofillPartition.Card.expirationMonthSaveValue: String?
get() = this
.views
.firstOrNull { it is AutofillView.Card.ExpirationMonth && it.monthValue != null }
?.data
?.textValue
.filterIsInstance<AutofillView.Card.ExpirationMonth>()
.firstOrNull { it.monthValue != null }
?.monthValue
/**
* The text value representation of the year from the [AutofillPartition.Card].
*/
val AutofillPartition.Card.expirationYearSaveValue: String?
get() = this
.extractNonNullTextValueOrNull { it is AutofillView.Card.ExpirationYear }
.views
.filterIsInstance<AutofillView.Card.ExpirationYear>()
.firstOrNull { it.yearValue != null }
?.yearValue
/**
* The text value representation of the card number from the [AutofillPartition.Card].
@@ -34,6 +37,24 @@ val AutofillPartition.Card.securityCodeSaveValue: String?
get() = this
.extractNonNullTextValueOrNull { it is AutofillView.Card.SecurityCode }
/**
* The text value representation of the cardholder name from the [AutofillPartition.Card].
*/
val AutofillPartition.Card.cardholderNameSaveValue: String?
get() = this
.extractNonNullTextValueOrNull { it is AutofillView.Card.CardholderName }
/**
* The text value representation of the brand from the [AutofillPartition.Card].
*/
val AutofillPartition.Card.brandSaveValue: String?
get() = this
.views
.filterIsInstance<AutofillView.Card.Brand>()
.firstOrNull { it.brandValue != null }
?.brandValue
?: this.extractNonNullTextValueOrNull { it is AutofillView.Card.Brand }
/**
* The text value representation of the password from the [AutofillPartition.Login].
*/

View File

@@ -11,10 +11,12 @@ fun AutofillRequest.Fillable.toAutofillSaveItem(): AutofillSaveItem =
when (this.partition) {
is AutofillPartition.Card -> {
AutofillSaveItem.Card(
cardholderName = partition.cardholderNameSaveValue,
number = partition.numberSaveValue,
expirationMonth = partition.expirationMonthSaveValue,
expirationYear = partition.expirationYearSaveValue,
securityCode = partition.securityCodeSaveValue,
brand = partition.brandSaveValue,
)
}

View File

@@ -35,3 +35,39 @@ fun AutofillValue.extractTextValue(): String? =
} else {
null
}
/**
* Extract a year value from this [AutofillValue].
*/
fun AutofillValue.extractYearValue(
autofillOptions: List<String>,
): String? =
when {
this.isList && autofillOptions.isNotEmpty() -> {
autofillOptions.getOrNull(listValue)
}
this.isText -> {
this.textValue.toString()
}
else -> null
}
/**
* Extract a card brand value from this [AutofillValue].
*/
fun AutofillValue.extractCardBrandValue(
autofillOptions: List<String>,
): String? =
when {
this.isList && autofillOptions.isNotEmpty() -> {
autofillOptions.getOrNull(listValue)
}
this.isText -> {
this.textValue.toString()
}
else -> null
}

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