Compare commits

...

120 Commits

Author SHA1 Message Date
David Perez
a721744a6b 🍒 PM-23666: Construct unique SDK client for Authentocator Sync feature (#5528) 2025-07-14 16:39:56 -05: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
805 changed files with 13284 additions and 6854 deletions

View File

@@ -39,6 +39,15 @@ 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
@@ -113,7 +122,7 @@ jobs:
bundle install --jobs 4 --retry 3
- name: Log in to Azure
uses: Azure/login@cb79c773a3cfa27f31f25eb3f677781210c9ce3d # v1.6.1
uses: Azure/login@a457da9ea143d694b1b9c7c869ebb04ebe844ef5 # v2.3.0
with:
creds: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }}

View File

@@ -40,6 +40,15 @@ 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
@@ -121,7 +130,7 @@ jobs:
bundle install --jobs 4 --retry 3
- name: Log in to Azure
uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0
uses: Azure/login@a457da9ea143d694b1b9c7c869ebb04ebe844ef5 # v2.3.0
with:
creds: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }}
@@ -420,7 +429,7 @@ jobs:
bundle install --jobs 4 --retry 3
- name: Log in to Azure
uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0
uses: Azure/login@a457da9ea143d694b1b9c7c869ebb04ebe844ef5 # v2.3.0
with:
creds: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }}

View File

@@ -29,7 +29,7 @@ jobs:
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Login to Azure - CI Subscription
uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0
uses: Azure/login@a457da9ea143d694b1b9c7c869ebb04ebe844ef5 # v2.3.0
with:
creds: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }}

View File

@@ -18,7 +18,7 @@ jobs:
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Log in to Azure
uses: Azure/login@cb79c773a3cfa27f31f25eb3f677781210c9ce3d # v1.6.1
uses: Azure/login@a457da9ea143d694b1b9c7c869ebb04ebe844ef5 # v2.3.0
with:
creds: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }}

View File

@@ -3,10 +3,12 @@ name: Publish GitHub Release as newest
on:
workflow_dispatch:
permissions: {}
jobs:
stub:
runs-on: ubuntu-24.04
name: Stub
steps:
- name: Stub
run: echo "This is a stub job to trigger the workflow."
run: echo "This is a stub job to trigger the workflow."

View File

@@ -4,6 +4,8 @@ name: Publish
on:
workflow_dispatch:
permissions: {}
jobs:
publish:
runs-on: ubuntu-24.04

View File

@@ -10,22 +10,22 @@ GEM
artifactory (3.0.17)
atomos (0.1.3)
aws-eventstream (1.4.0)
aws-partitions (1.1113.0)
aws-sdk-core (3.225.1)
aws-partitions (1.1125.0)
aws-sdk-core (3.226.2)
aws-eventstream (~> 1, >= 1.3.0)
aws-partitions (~> 1, >= 1.992.0)
aws-sigv4 (~> 1.9)
base64
jmespath (~> 1, >= 1.6.1)
logger
aws-sdk-kms (1.104.0)
aws-sdk-kms (1.106.0)
aws-sdk-core (~> 3, >= 3.225.0)
aws-sigv4 (~> 1.5)
aws-sdk-s3 (1.189.0)
aws-sdk-s3 (1.192.0)
aws-sdk-core (~> 3, >= 3.225.0)
aws-sdk-kms (~> 1)
aws-sigv4 (~> 1.5)
aws-sigv4 (1.12.0)
aws-sigv4 (1.12.1)
aws-eventstream (~> 1, >= 1.0.2)
babosa (1.0.4)
base64 (0.3.0)
@@ -58,10 +58,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 +71,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)
@@ -166,7 +166,7 @@ GEM
mutex_m
jmespath (1.6.2)
json (2.12.2)
jwt (2.10.1)
jwt (2.10.2)
base64
logger (1.7.0)
mini_magick (4.13.2)
@@ -175,7 +175,7 @@ GEM
multipart-post (2.4.1)
mutex_m (0.3.0)
nanaimo (0.4.0)
naturally (2.2.2)
naturally (2.3.0)
nkf (0.2.0)
optparse (0.6.0)
os (1.1.4)

View File

@@ -52,6 +52,16 @@
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` `17`:
- Navigate to `Preferences > Build, Execution, Deployment > Build Tools > Gradle`.
- Hit the selected Gradle JDK next to `Gradle JDK:`.
- Select a `17.x` version or hit `Download JDK...` if not present.
- Select `Version` `17`.
- Select your preferred `Vendor`.
- Hit `Download`.
- Hit `Apply`.
## 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,6 +47,10 @@ 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()
@@ -55,11 +60,6 @@ android {
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")
}
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
buildConfigField(
@@ -99,6 +99,7 @@ android {
applicationIdSuffix = ".beta"
isDebuggable = false
isMinifyEnabled = true
isShrinkResources = true
matchingFallbacks += listOf("release")
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
@@ -111,6 +112,7 @@ android {
release {
isDebuggable = false
isMinifyEnabled = true
isShrinkResources = true
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro",
@@ -193,7 +195,7 @@ android {
kotlin {
compilerOptions {
jvmTarget.set(JvmTarget.fromTarget(libs.versions.jvmTarget.get()))
jvmTarget = JvmTarget.fromTarget(libs.versions.jvmTarget.get())
}
}
@@ -296,8 +298,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"
}
}

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,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

@@ -81,7 +81,6 @@
<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>
@@ -330,11 +329,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

@@ -7,12 +7,12 @@ 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.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 javax.inject.Inject
/**
@@ -55,19 +55,13 @@ class AutofillTotpCopyViewModel @Inject constructor(
}
// 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 -> finishActivity()
is GetCipherResult.Failure -> finishActivity()
is GetCipherResult.Success -> {
sendEvent(AutofillTotpCopyEvent.CompleteAutofill(result.cipherView))
}
?: finishActivity()
}
}
}

View File

@@ -9,6 +9,7 @@ import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
import com.bitwarden.annotation.OmitFromCoverage
import com.bitwarden.data.manager.DispatcherManager
import com.bitwarden.ui.platform.resource.BitwardenDrawable
import com.x8bit.bitwarden.R
import com.x8bit.bitwarden.data.auth.datasource.disk.AuthDiskSource
import com.x8bit.bitwarden.data.auth.util.createPasswordlessRequestDataIntent
@@ -66,7 +67,7 @@ class AuthRequestNotificationManagerImpl(
?.let { context.getString(R.string.confim_log_in_attemp_for_x, it) }
?: context.getString(R.string.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,8 +1,7 @@
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
@@ -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)
@@ -117,7 +116,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(

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

@@ -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

@@ -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,6 +5,7 @@ import android.os.PowerManager
import android.view.accessibility.AccessibilityEvent
import android.view.accessibility.AccessibilityNodeInfo
import android.widget.Toast
import com.bitwarden.core.data.manager.toast.ToastManager
import com.x8bit.bitwarden.R
import com.x8bit.bitwarden.data.autofill.accessibility.manager.AccessibilityAutofillManager
import com.x8bit.bitwarden.data.autofill.accessibility.manager.LauncherPackageNameManager
@@ -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 = R.string.autofill_tile_uri_not_found,
duration = Toast.LENGTH_LONG,
)
}
}

View File

@@ -10,6 +10,7 @@ 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.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.w("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,6 +59,7 @@ 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
}
}

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()
}
}

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,8 +16,8 @@ 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.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
@@ -59,10 +59,10 @@ object AutofillModule {
@Singleton
@Provides
fun providesChromeAutofillEnabledManager(
fun providesBrowserAutofillEnabledManager(
featureFlagManager: FeatureFlagManager,
): ChromeThirdPartyAutofillEnabledManager =
ChromeThirdPartyAutofillEnabledManagerImpl(
): BrowserThirdPartyAutofillEnabledManager =
BrowserThirdPartyAutofillEnabledManagerImpl(
featureFlagManager = featureFlagManager,
)
@@ -93,7 +93,6 @@ object AutofillModule {
@Singleton
@Provides
fun providesAutofillTotpManager(
@ApplicationContext context: Context,
clock: Clock,
clipboardManager: BitwardenClipboardManager,
authRepository: AuthRepository,
@@ -101,7 +100,6 @@ object AutofillModule {
vaultRepository: VaultRepository,
): AutofillTotpManager =
AutofillTotpManagerImpl(
context = context,
clock = clock,
clipboardManager = clipboardManager,
authRepository = authRepository,

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

@@ -1,7 +1,5 @@
package com.x8bit.bitwarden.data.autofill.manager
import android.content.Context
import android.widget.Toast
import com.bitwarden.ui.util.asText
import com.bitwarden.vault.CipherView
import com.x8bit.bitwarden.R
@@ -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,
@@ -39,13 +36,6 @@ class AutofillTotpManagerImpl(
text = totpResult.code,
toastDescriptorOverride = R.string.verification_code_totp.asText(),
)
Toast
.makeText(
context.applicationContext,
R.string.verification_code_totp,
Toast.LENGTH_LONG,
)
.show()
}
}
}

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,56 @@
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 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 [BrowserThirdPartyAutofillEnabledManager].
*/
class BrowserThirdPartyAutofillEnabledManagerImpl(
private val featureFlagManager: FeatureFlagManager,
) : 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
.combine(
featureFlagManager.getFeatureFlagFlow(FlagKey.ChromeAutofill),
) { data, enabled ->
if (enabled) {
data
} else {
DEFAULT_STATUS
}
}
}
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

@@ -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.
@@ -48,7 +48,7 @@ sealed class AutofillCipher {
val number: 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 +67,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.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,18 @@
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,
)
/**
* The overall status for all relevant browsers.
*/
data class BrowserThirdPartyAutofillStatus(
val braveStableStatusData: BrowserThirdPartyAutoFillData,
val chromeStableStatusData: BrowserThirdPartyAutoFillData,
val chromeBetaChannelStatusData: BrowserThirdPartyAutoFillData,
)

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.e("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,6 +1,5 @@
package com.x8bit.bitwarden.data.autofill.util
import android.annotation.SuppressLint
import android.os.Build
import android.service.autofill.FillRequest
import android.widget.inline.InlinePresentationSpec
@@ -9,12 +8,11 @@ import com.x8bit.bitwarden.data.autofill.model.AutofillAppInfo
/**
* Extract the list of [InlinePresentationSpec]s. If it fails, return an empty list.
*/
@SuppressLint("NewApi")
fun FillRequest?.getInlinePresentationSpecs(
autofillAppInfo: AutofillAppInfo,
isInlineAutofillEnabled: Boolean,
): List<InlinePresentationSpec>? =
if (autofillAppInfo.sdkInt < Build.VERSION_CODES.R) {
if (!autofillAppInfo.isVersionAtLeast(version = Build.VERSION_CODES.R)) {
// When SDK version is bellow 30, InlinePresentationSpec is not available and null
// must be returned.
null
@@ -28,14 +26,13 @@ fun FillRequest?.getInlinePresentationSpecs(
* Extract the max inline suggestions count. If the OS is below Android R, this will always
* return 0.
*/
@SuppressLint("NewApi")
fun FillRequest?.getMaxInlineSuggestionsCount(
autofillAppInfo: AutofillAppInfo,
isInlineAutofillEnabled: Boolean,
): Int =
if (this != null &&
isInlineAutofillEnabled &&
autofillAppInfo.sdkInt >= Build.VERSION_CODES.R
autofillAppInfo.isVersionAtLeast(version = Build.VERSION_CODES.R)
) {
inlineSuggestionsRequest?.maxSuggestionCount ?: 0
} else {

View File

@@ -1,6 +1,5 @@
package com.x8bit.bitwarden.data.autofill.util
import android.annotation.SuppressLint
import android.app.PendingIntent
import android.os.Build
import android.service.autofill.Dataset
@@ -28,7 +27,6 @@ val FilledData.fillableAutofillIds: List<AutofillId>
/**
* Builds a [Dataset] for the Vault item.
*/
@SuppressLint("NewApi")
fun FilledData.buildVaultItemDataset(
autofillAppInfo: AutofillAppInfo,
): Dataset {
@@ -70,7 +68,7 @@ fun FilledData.buildVaultItemDataset(
return Dataset.Builder()
.setAuthentication(pendingIntent.intentSender)
.apply {
if (autofillAppInfo.sdkInt >= Build.VERSION_CODES.TIRAMISU) {
if (autofillAppInfo.isVersionAtLeast(version = Build.VERSION_CODES.TIRAMISU)) {
addVaultItemDataPostTiramisu(
autofillAppInfo = autofillAppInfo,
pendingIntent = pendingIntent,
@@ -132,8 +130,7 @@ private fun Dataset.Builder.addVaultItemDataPostTiramisu(
/**
* Adds the Vault data to the given [Dataset.Builder] for pre-Tiramisu versions.
*/
@Suppress("DEPRECATION", "LongParameterList")
@SuppressLint("NewApi")
@Suppress("LongParameterList")
private fun Dataset.Builder.addVaultItemDataPreTiramisu(
autofillAppInfo: AutofillAppInfo,
pendingIntent: PendingIntent,
@@ -142,7 +139,7 @@ private fun Dataset.Builder.addVaultItemDataPreTiramisu(
inlinePresentationSpec: InlinePresentationSpec?,
isLocked: Boolean,
): Dataset.Builder {
if (autofillAppInfo.sdkInt >= Build.VERSION_CODES.R) {
if (autofillAppInfo.isVersionAtLeast(version = Build.VERSION_CODES.R)) {
inlinePresentationSpec
?.createVaultItemInlinePresentationOrNull(
autofillAppInfo = autofillAppInfo,
@@ -150,6 +147,7 @@ private fun Dataset.Builder.addVaultItemDataPreTiramisu(
isLocked = isLocked,
)
?.let { inlinePresentation ->
@Suppress("DEPRECATION")
this.setInlinePresentation(inlinePresentation)
}
}

View File

@@ -1,17 +1,18 @@
package com.x8bit.bitwarden.data.autofill.util
import android.annotation.SuppressLint
import android.os.Build
import android.service.autofill.Dataset
import android.service.autofill.Field
import android.service.autofill.Presentations
import android.widget.RemoteViews
import androidx.annotation.RequiresApi
import com.x8bit.bitwarden.data.autofill.model.FilledItem
/**
* Set up an overlay presentation for this [FilledItem] in the [datasetBuilder] for Android devices
* running on API Tiramisu or greater.
*/
@SuppressLint("NewApi")
@RequiresApi(Build.VERSION_CODES.TIRAMISU)
fun FilledItem.applyToDatasetPostTiramisu(
datasetBuilder: Dataset.Builder,
presentations: Presentations,
@@ -29,11 +30,11 @@ fun FilledItem.applyToDatasetPostTiramisu(
* Set up an overlay presentation for this [FilledItem] in the [datasetBuilder] for Android devices
* running on APIs that predate Tiramisu.
*/
@Suppress("Deprecation")
fun FilledItem.applyToDatasetPreTiramisu(
datasetBuilder: Dataset.Builder,
remoteViews: RemoteViews,
) {
@Suppress("DEPRECATION")
datasetBuilder.setValue(
autofillId,
value,

View File

@@ -1,6 +1,5 @@
package com.x8bit.bitwarden.data.autofill.util
import android.annotation.SuppressLint
import android.content.IntentSender
import android.os.Build
import android.service.autofill.Dataset
@@ -16,7 +15,6 @@ import com.x8bit.bitwarden.ui.autofill.util.createCipherInlinePresentationOrNull
* Build a [Dataset] to represent the [FilledPartition]. This dataset includes an overlay UI
* presentation for each filled item. If an [authIntentSender] is present, add it to the dataset.
*/
@SuppressLint("NewApi")
fun FilledPartition.buildDataset(
authIntentSender: IntentSender?,
autofillAppInfo: AutofillAppInfo,
@@ -26,13 +24,9 @@ fun FilledPartition.buildDataset(
autofillCipher = autofillCipher,
)
val datasetBuilder = Dataset.Builder()
authIntentSender?.let { intentSender -> datasetBuilder.setAuthentication(intentSender) }
authIntentSender
?.let { intentSender ->
datasetBuilder.setAuthentication(intentSender)
}
if (autofillAppInfo.sdkInt >= Build.VERSION_CODES.TIRAMISU) {
if (autofillAppInfo.isVersionAtLeast(version = Build.VERSION_CODES.TIRAMISU)) {
applyToDatasetPostTiramisu(
autofillAppInfo = autofillAppInfo,
datasetBuilder = datasetBuilder,
@@ -85,20 +79,19 @@ private fun FilledPartition.applyToDatasetPostTiramisu(
* Apply this [FilledPartition] to the [datasetBuilder] on devices running OS versions that predate
* Tiramisu.
*/
@Suppress("DEPRECATION")
@SuppressLint("NewApi")
private fun FilledPartition.buildDatasetPreTiramisu(
autofillAppInfo: AutofillAppInfo,
datasetBuilder: Dataset.Builder,
remoteViews: RemoteViews,
) {
if (autofillAppInfo.sdkInt >= Build.VERSION_CODES.R) {
if (autofillAppInfo.isVersionAtLeast(version = Build.VERSION_CODES.R)) {
inlinePresentationSpec
?.createCipherInlinePresentationOrNull(
autofillAppInfo = autofillAppInfo,
autofillCipher = autofillCipher,
)
?.let { inlinePresentation ->
@Suppress("DEPRECATION")
datasetBuilder.setInlinePresentation(inlinePresentation)
}
}

View File

@@ -6,6 +6,7 @@ import androidx.core.graphics.drawable.IconCompat
import androidx.credentials.provider.BeginGetPublicKeyCredentialOption
import androidx.credentials.provider.PublicKeyCredentialEntry
import com.bitwarden.fido.Fido2CredentialAutofillView
import com.bitwarden.ui.platform.resource.BitwardenDrawable
import com.x8bit.bitwarden.R
import com.x8bit.bitwarden.data.credentials.processor.GET_PASSKEY_INTENT
import com.x8bit.bitwarden.data.credentials.util.setBiometricPromptDataIfSupported
@@ -87,9 +88,9 @@ class CredentialEntryBuilderImpl(
.createWithResource(
context,
if (isPasskey) {
R.drawable.ic_bw_passkey
BitwardenDrawable.ic_bw_passkey
} else {
R.drawable.ic_globe
BitwardenDrawable.ic_globe
},
)
.toIcon(context)

View File

@@ -118,9 +118,13 @@ object CredentialProviderModule {
@Singleton
fun providePrivilegedAppRepository(
privilegedAppDiskSource: PrivilegedAppDiskSource,
assetManager: AssetManager,
dispatcherManager: DispatcherManager,
json: Json,
): PrivilegedAppRepository = PrivilegedAppRepositoryImpl(
privilegedAppDiskSource = privilegedAppDiskSource,
assetManager = assetManager,
dispatcherManager = dispatcherManager,
json = json,
)

View File

@@ -73,7 +73,7 @@ data class PasskeyAttestationOptions(
@SerialName("type")
val type: String,
@SerialName("alg")
val alg: Long,
val alg: Double,
)
/**

View File

@@ -25,6 +25,7 @@ import androidx.credentials.provider.BeginGetCredentialResponse
import androidx.credentials.provider.BiometricPromptData
import androidx.credentials.provider.CreateEntry
import androidx.credentials.provider.ProviderClearCredentialStateRequest
import com.bitwarden.core.util.isBuildVersionAtLeast
import com.bitwarden.data.manager.DispatcherManager
import com.x8bit.bitwarden.R
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
@@ -34,7 +35,6 @@ import com.x8bit.bitwarden.data.credentials.model.GetCredentialsRequest
import com.x8bit.bitwarden.data.platform.manager.BiometricsEncryptionManager
import com.x8bit.bitwarden.data.platform.manager.FeatureFlagManager
import com.x8bit.bitwarden.data.platform.manager.model.FlagKey
import com.x8bit.bitwarden.data.platform.util.isBuildVersionBelow
import com.x8bit.bitwarden.ui.platform.manager.intent.IntentManager
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
@@ -214,7 +214,7 @@ class CredentialProviderProcessorImpl(
private fun CreateEntry.Builder.setBiometricPromptDataIfSupported(
cipher: Cipher,
): CreateEntry.Builder {
return if (isBuildVersionBelow(Build.VERSION_CODES.VANILLA_ICE_CREAM)) {
return if (!isBuildVersionAtLeast(Build.VERSION_CODES.VANILLA_ICE_CREAM)) {
this
} else {
setBiometricPromptData(

View File

@@ -1,22 +1,49 @@
package com.x8bit.bitwarden.data.credentials.repository
import com.bitwarden.core.data.repository.model.DataState
import com.x8bit.bitwarden.data.credentials.model.PrivilegedAppAllowListJson
import kotlinx.coroutines.flow.Flow
import com.x8bit.bitwarden.data.credentials.repository.model.PrivilegedAppData
import kotlinx.coroutines.flow.StateFlow
/**
* Repository for managing privileged apps trusted by the user.
*/
interface PrivilegedAppRepository {
/**
* Flow that represents the trusted privileged apps data.
*/
val trustedAppDataStateFlow: StateFlow<DataState<PrivilegedAppData>>
/**
* Flow of the user's trusted privileged apps.
*/
val userTrustedPrivilegedAppsFlow: Flow<PrivilegedAppAllowListJson>
val userTrustedAppsFlow: StateFlow<DataState<PrivilegedAppAllowListJson>>
/**
* Flow of the Google's trusted privileged apps.
*/
val googleTrustedPrivilegedAppsFlow: StateFlow<DataState<PrivilegedAppAllowListJson>>
/**
* Flow of the community's trusted privileged apps.
*/
val communityTrustedAppsFlow: StateFlow<DataState<PrivilegedAppAllowListJson>>
/**
* List the user's trusted privileged apps.
*/
suspend fun getAllUserTrustedPrivilegedApps(): PrivilegedAppAllowListJson
suspend fun getUserTrustedPrivilegedAppsOrNull(): PrivilegedAppAllowListJson?
/**
* List Google's trusted privileged apps.
*/
suspend fun getGoogleTrustedPrivilegedAppsOrNull(): PrivilegedAppAllowListJson?
/**
* List community's trusted privileged apps.
*/
suspend fun getCommunityTrustedPrivilegedAppsOrNull(): PrivilegedAppAllowListJson?
/**
* Returns true if the given [packageName] and [signature] are trusted.

View File

@@ -1,12 +1,35 @@
package com.x8bit.bitwarden.data.credentials.repository
import com.bitwarden.core.data.repository.model.DataState
import com.bitwarden.core.data.repository.util.combineDataStates
import com.bitwarden.core.data.util.decodeFromStringOrNull
import com.bitwarden.data.manager.DispatcherManager
import com.x8bit.bitwarden.data.credentials.datasource.disk.PrivilegedAppDiskSource
import com.x8bit.bitwarden.data.credentials.datasource.disk.entity.PrivilegedAppEntity
import com.x8bit.bitwarden.data.credentials.model.PrivilegedAppAllowListJson
import kotlinx.coroutines.flow.Flow
import com.x8bit.bitwarden.data.credentials.repository.model.PrivilegedAppData
import com.x8bit.bitwarden.data.platform.manager.AssetManager
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import kotlinx.serialization.json.Json
/**
* A "stop timeout delay" in milliseconds used to let a shared coroutine continue to run for the
* specified period of time after it no longer has subscribers.
*/
private const val STOP_TIMEOUT_DELAY_MS: Long = 1000L
private const val GOOGLE_ALLOW_LIST_FILE_NAME = "fido2_privileged_google.json"
private const val COMMUNITY_ALLOW_LIST_FILE_NAME = "fido2_privileged_community.json"
private const val ANDROID_TYPE = "android"
private const val RELEASE_BUILD = "release"
@@ -15,17 +38,102 @@ private const val RELEASE_BUILD = "release"
*/
class PrivilegedAppRepositoryImpl(
private val privilegedAppDiskSource: PrivilegedAppDiskSource,
private val assetManager: AssetManager,
dispatcherManager: DispatcherManager,
private val json: Json,
) : PrivilegedAppRepository {
override val userTrustedPrivilegedAppsFlow: Flow<PrivilegedAppAllowListJson> =
privilegedAppDiskSource.userTrustedPrivilegedAppsFlow
.map { it.toPrivilegedAppAllowListJson() }
private val unconfinedScope = CoroutineScope(dispatcherManager.unconfined)
private val ioScope = CoroutineScope(dispatcherManager.io)
override suspend fun getAllUserTrustedPrivilegedApps(): PrivilegedAppAllowListJson =
privilegedAppDiskSource.getAllUserTrustedPrivilegedApps()
private val mutableUserTrustedAppsFlow =
MutableStateFlow<DataState<PrivilegedAppAllowListJson>>(DataState.Loading)
private val mutableGoogleTrustedAppsFlow =
MutableStateFlow<DataState<PrivilegedAppAllowListJson>>(DataState.Loading)
private val mutableCommunityTrustedPrivilegedAppsFlow =
MutableStateFlow<DataState<PrivilegedAppAllowListJson>>(DataState.Loading)
override val trustedAppDataStateFlow: StateFlow<DataState<PrivilegedAppData>> =
combine(
userTrustedAppsFlow,
googleTrustedPrivilegedAppsFlow,
communityTrustedAppsFlow,
) { userAppsState, googleAppsState, communityAppsState ->
combineDataStates(
userAppsState,
googleAppsState,
communityAppsState,
) { userApps, googleApps, communityApps ->
PrivilegedAppData(
googleTrustedApps = googleApps,
communityTrustedApps = communityApps,
userTrustedApps = userApps,
)
}
}
.stateIn(
scope = unconfinedScope,
started = SharingStarted.WhileSubscribed(stopTimeoutMillis = STOP_TIMEOUT_DELAY_MS),
initialValue = DataState.Loading,
)
override val userTrustedAppsFlow: StateFlow<DataState<PrivilegedAppAllowListJson>>
get() = mutableUserTrustedAppsFlow.asStateFlow()
override val googleTrustedPrivilegedAppsFlow: StateFlow<DataState<PrivilegedAppAllowListJson>>
get() = mutableGoogleTrustedAppsFlow.asStateFlow()
override val communityTrustedAppsFlow: StateFlow<DataState<PrivilegedAppAllowListJson>>
get() = mutableCommunityTrustedPrivilegedAppsFlow.asStateFlow()
init {
ioScope.launch {
mutableGoogleTrustedAppsFlow.value = assetManager
.readAsset(fileName = GOOGLE_ALLOW_LIST_FILE_NAME)
.map { json.decodeFromString<PrivilegedAppAllowListJson>(it) }
.fold(
onSuccess = { DataState.Loaded(it) },
onFailure = { DataState.Error(it) },
)
mutableCommunityTrustedPrivilegedAppsFlow.value = assetManager
.readAsset(fileName = COMMUNITY_ALLOW_LIST_FILE_NAME)
.map { json.decodeFromString<PrivilegedAppAllowListJson>(it) }
.fold(
onSuccess = { DataState.Loaded(it) },
onFailure = { DataState.Error(it) },
)
}
privilegedAppDiskSource
.userTrustedPrivilegedAppsFlow
.map { DataState.Loaded(it.toPrivilegedAppAllowListJson()) }
.onEach { mutableUserTrustedAppsFlow.value = it }
.launchIn(ioScope)
}
override suspend fun getUserTrustedPrivilegedAppsOrNull(): PrivilegedAppAllowListJson =
privilegedAppDiskSource
.getAllUserTrustedPrivilegedApps()
.toPrivilegedAppAllowListJson()
override suspend fun getGoogleTrustedPrivilegedAppsOrNull(): PrivilegedAppAllowListJson? =
withContext(ioScope.coroutineContext) {
assetManager
.readAsset(fileName = GOOGLE_ALLOW_LIST_FILE_NAME)
.map { json.decodeFromStringOrNull<PrivilegedAppAllowListJson>(it) }
.getOrNull()
}
override suspend fun getCommunityTrustedPrivilegedAppsOrNull(): PrivilegedAppAllowListJson? {
return withContext(ioScope.coroutineContext) {
assetManager
.readAsset(fileName = COMMUNITY_ALLOW_LIST_FILE_NAME)
.map { json.decodeFromStringOrNull<PrivilegedAppAllowListJson>(it) }
.getOrNull()
}
}
override suspend fun isPrivilegedAppAllowed(
packageName: String,
signature: String,

View File

@@ -0,0 +1,12 @@
package com.x8bit.bitwarden.data.credentials.repository.model
import com.x8bit.bitwarden.data.credentials.model.PrivilegedAppAllowListJson
/**
* Represents privileged applications that are trusted by various sources.
*/
data class PrivilegedAppData(
val googleTrustedApps: PrivilegedAppAllowListJson,
val communityTrustedApps: PrivilegedAppAllowListJson,
val userTrustedApps: PrivilegedAppAllowListJson,
)

View File

@@ -7,10 +7,10 @@ import androidx.credentials.provider.BeginGetCredentialRequest
import androidx.credentials.provider.PendingIntentHandler
import androidx.credentials.provider.ProviderCreateCredentialRequest
import androidx.credentials.provider.ProviderGetCredentialRequest
import com.bitwarden.core.util.isBuildVersionAtLeast
import com.x8bit.bitwarden.data.credentials.model.CreateCredentialRequest
import com.x8bit.bitwarden.data.credentials.model.Fido2CredentialAssertionRequest
import com.x8bit.bitwarden.data.credentials.model.GetCredentialsRequest
import com.x8bit.bitwarden.data.platform.util.isBuildVersionBelow
import com.x8bit.bitwarden.ui.platform.manager.intent.EXTRA_KEY_CIPHER_ID
import com.x8bit.bitwarden.ui.platform.manager.intent.EXTRA_KEY_CREDENTIAL_ID
import com.x8bit.bitwarden.ui.platform.manager.intent.EXTRA_KEY_USER_ID
@@ -21,7 +21,7 @@ import com.x8bit.bitwarden.ui.platform.manager.intent.EXTRA_KEY_UV_PERFORMED_DUR
* [CredentialManager] creation process.
*/
fun Intent.getCreateCredentialRequestOrNull(): CreateCredentialRequest? {
if (isBuildVersionBelow(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)) return null
if (!isBuildVersionAtLeast(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)) return null
val systemRequest = PendingIntentHandler.retrieveProviderCreateCredentialRequest(this)
?: return null
@@ -48,7 +48,7 @@ fun Intent.getCreateCredentialRequestOrNull(): CreateCredentialRequest? {
* credential authentication process.
*/
fun Intent.getFido2AssertionRequestOrNull(): Fido2CredentialAssertionRequest? {
if (isBuildVersionBelow(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)) return null
if (!isBuildVersionAtLeast(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)) return null
val systemRequest = PendingIntentHandler
.retrieveProviderGetCredentialRequest(this)
@@ -84,7 +84,7 @@ fun Intent.getFido2AssertionRequestOrNull(): Fido2CredentialAssertionRequest? {
* [CredentialManager] credential lookup process.
*/
fun Intent.getGetCredentialsRequestOrNull(): GetCredentialsRequest? {
if (isBuildVersionBelow(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)) return null
if (!isBuildVersionAtLeast(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)) return null
val systemRequest = PendingIntentHandler
.retrieveBeginGetCredentialRequest(this)

View File

@@ -5,7 +5,7 @@ package com.x8bit.bitwarden.data.credentials.util
import android.os.Build
import androidx.credentials.provider.PublicKeyCredentialEntry
import com.bitwarden.annotation.OmitFromCoverage
import com.x8bit.bitwarden.data.platform.util.isBuildVersionBelow
import com.bitwarden.core.util.isBuildVersionAtLeast
import javax.crypto.Cipher
/**
@@ -15,7 +15,7 @@ fun PublicKeyCredentialEntry.Builder.setBiometricPromptDataIfSupported(
cipher: Cipher?,
isSingleTapAuthEnabled: Boolean,
): PublicKeyCredentialEntry.Builder =
if (!isBuildVersionBelow(Build.VERSION_CODES.VANILLA_ICE_CREAM) &&
if (isBuildVersionAtLeast(Build.VERSION_CODES.VANILLA_ICE_CREAM) &&
cipher != null &&
isSingleTapAuthEnabled
) {

View File

@@ -1,9 +1,9 @@
package com.x8bit.bitwarden.data.platform.datasource.disk
import android.content.SharedPreferences
import com.bitwarden.core.util.getBinaryLongFromZoneDateTime
import com.bitwarden.core.util.getZoneDateTimeFromBinaryLong
import com.bitwarden.data.datasource.disk.BaseDiskSource
import com.x8bit.bitwarden.data.platform.util.getBinaryLongFromZoneDateTime
import com.x8bit.bitwarden.data.platform.util.getZoneDateTimeFromBinaryLong
import java.time.ZonedDateTime
private const val CURRENT_PUSH_TOKEN_KEY = "pushCurrentToken"

View File

@@ -93,7 +93,7 @@ class SettingsDiskSourceImpl(
private val mutableHasSeenGeneratorCoachMarkFlow = bufferedMutableSharedFlow<Boolean?>()
private val mutableScreenCaptureAllowedFlow = MutableSharedFlow<Boolean?>()
private val mutableScreenCaptureAllowedFlow = bufferedMutableSharedFlow<Boolean?>()
private val mutableVaultRegisteredForExportFlow =
mutableMapOf<String, MutableSharedFlow<Boolean?>>()

View File

@@ -17,12 +17,6 @@ data class MutualTlsCertificate(
val leafCertificate: X509Certificate?
get() = certificateChain.lastOrNull()
/**
* Root certificate of the chain.
*/
val rootCertificate: X509Certificate?
get() = certificateChain.firstOrNull()
override fun toString(): String = leafCertificate
?.let {
buildString {
@@ -32,5 +26,5 @@ data class MutualTlsCertificate(
appendLine("Valid Until: ${it.notAfter}")
}
}
?: ""
.orEmpty()
}

View File

@@ -246,7 +246,7 @@ class FirstTimeActionManagerImpl @Inject constructor(
return authDiskSource
.getShowImportLoginsFlow(userId)
.combine(
vaultDiskSource.getCiphers(userId),
vaultDiskSource.getCiphersFlow(userId),
) { showImportLogins, ciphers ->
showImportLogins ?: true && ciphers.isEmpty()
}
@@ -260,7 +260,7 @@ class FirstTimeActionManagerImpl @Inject constructor(
return settingsDiskSource
.getShowImportLoginsSettingBadgeFlow(userId)
.combine(
vaultDiskSource.getCiphers(userId),
vaultDiskSource.getCiphersFlow(userId),
) { showImportLogins, ciphers ->
showImportLogins ?: false && ciphers.isEmpty()
}
@@ -297,7 +297,7 @@ class FirstTimeActionManagerImpl @Inject constructor(
.flatMapLatest { activeUserId ->
combine(
flow = this,
flow2 = vaultDiskSource.getCiphers(activeUserId),
flow2 = vaultDiskSource.getCiphersFlow(activeUserId),
) { receiverCurrentValue, ciphers ->
receiverCurrentValue && ciphers.none {
it.login != null && it.organizationId == null

View File

@@ -30,8 +30,15 @@ import kotlinx.serialization.json.Json
import java.time.Clock
import java.time.ZoneOffset
import java.time.ZonedDateTime
import java.time.temporal.ChronoUnit
import javax.inject.Inject
import kotlin.time.Duration
import kotlin.time.Duration.Companion.days
import kotlin.time.toJavaDuration
/**
* The amount of time to delay before updating the push token against Bitwarden server.
*/
private val PUSH_TOKEN_UPDATE_DELAY: Duration = 7.days
/**
* Primary implementation of [PushManager].
@@ -279,11 +286,6 @@ class PushManagerImpl @Inject constructor(
val userId = activeUserId ?: return
if (!isLoggedIn(userId)) return
// If the last registered token is from less than a day before, skip this for now
val lastRegistration = pushDiskSource.getLastPushTokenRegistrationDate(userId)?.toInstant()
val dayBefore = clock.instant().minus(1, ChronoUnit.DAYS)
if (lastRegistration?.isAfter(dayBefore) == true) return
ioScope.launch {
pushDiskSource.registeredPushToken?.let {
registerPushTokenIfNecessaryInternal(
@@ -296,14 +298,11 @@ class PushManagerImpl @Inject constructor(
private suspend fun registerPushTokenIfNecessaryInternal(userId: String, token: String) {
val currentToken = pushDiskSource.getCurrentPushToken(userId)
if (token == currentToken) {
// Our token is up-to-date, so just update the last registration date
pushDiskSource.storeLastPushTokenRegistrationDate(
userId = userId,
registrationDate = ZonedDateTime.ofInstant(clock.instant(), ZoneOffset.UTC),
)
return
val lastRegistration =
pushDiskSource.getLastPushTokenRegistrationDate(userId)?.toInstant() ?: return
val updateTime = clock.instant().minus(PUSH_TOKEN_UPDATE_DELAY.toJavaDuration())
if (updateTime.isBefore(lastRegistration)) return
}
pushService

View File

@@ -1,18 +1,25 @@
package com.x8bit.bitwarden.data.platform.manager
import android.os.Build
import com.bitwarden.core.util.isBuildVersionAtLeast
import com.bitwarden.sdk.Client
import com.x8bit.bitwarden.data.platform.util.isBuildVersionBelow
import com.x8bit.bitwarden.data.platform.manager.sdk.SdkRepositoryFactory
/**
* Primary implementation of [SdkClientManager].
*/
class SdkClientManagerImpl(
private val featureFlagManager: FeatureFlagManager,
nativeLibraryManager: NativeLibraryManager,
private val clientProvider: suspend () -> Client = {
sdkRepoFactory: SdkRepositoryFactory,
private val featureFlagManager: FeatureFlagManager,
private val clientProvider: suspend (userId: String?) -> Client = { userId ->
Client(settings = null).apply {
platform().loadFlags(featureFlagManager.sdkFeatureFlags)
userId?.let {
platform().state().apply {
registerCipherRepository(sdkRepoFactory.getCipherRepository(userId = it))
}
}
}
},
) : SdkClientManager {
@@ -22,14 +29,14 @@ class SdkClientManagerImpl(
// The SDK requires access to Android APIs that were not made public until API 31. In order
// to work around this limitation the SDK must be manually loaded prior to initializing any
// [Client] instance.
if (isBuildVersionBelow(Build.VERSION_CODES.S)) {
if (!isBuildVersionAtLeast(Build.VERSION_CODES.S)) {
nativeLibraryManager.loadLibrary("bitwarden_uniffi")
}
}
override suspend fun getOrCreateClient(
userId: String?,
): Client = userIdToClientMap.getOrPut(key = userId) { clientProvider() }
): Client = userIdToClientMap.getOrPut(key = userId) { clientProvider(userId) }
override fun destroyClient(
userId: String?,

View File

@@ -1,10 +1,10 @@
package com.x8bit.bitwarden.data.platform.manager.clipboard
import android.content.ClipData
import android.content.ClipDescription
import android.content.ClipboardManager
import android.content.Context
import android.os.Build
import android.widget.Toast
import androidx.compose.ui.text.AnnotatedString
import androidx.core.content.getSystemService
import androidx.core.os.persistableBundleOf
@@ -12,6 +12,8 @@ import androidx.work.ExistingWorkPolicy
import androidx.work.OneTimeWorkRequest
import androidx.work.WorkManager
import com.bitwarden.annotation.OmitFromCoverage
import com.bitwarden.core.data.manager.toast.ToastManager
import com.bitwarden.core.util.isBuildVersionAtLeast
import com.bitwarden.ui.platform.base.util.toAnnotatedString
import com.bitwarden.ui.util.Text
import com.x8bit.bitwarden.R
@@ -25,6 +27,7 @@ import java.util.concurrent.TimeUnit
class BitwardenClipboardManagerImpl(
private val context: Context,
private val settingsRepository: SettingsRepository,
private val toastManager: ToastManager,
) : BitwardenClipboardManager {
private val clipboardManager: ClipboardManager = requireNotNull(context.getSystemService())
@@ -41,18 +44,22 @@ class BitwardenClipboardManagerImpl(
.newPlainText("", text)
.apply {
description.extras = persistableBundleOf(
"android.content.extra.IS_SENSITIVE" to isSensitive,
if (isBuildVersionAtLeast(version = Build.VERSION_CODES.TIRAMISU)) {
ClipDescription.EXTRA_IS_SENSITIVE to isSensitive
} else {
"android.content.extra.IS_SENSITIVE" to isSensitive
},
)
},
)
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.S_V2) {
if (!isBuildVersionAtLeast(version = Build.VERSION_CODES.TIRAMISU)) {
val descriptor = toastDescriptorOverride
?.let { context.resources.getString(R.string.value_has_been_copied, it) }
?: context.resources.getString(
R.string.value_has_been_copied,
context.resources.getString(R.string.value),
)
Toast.makeText(context, descriptor, Toast.LENGTH_SHORT).show()
toastManager.show(message = descriptor)
}
val frequency = clearClipboardFrequencySeconds ?: return

View File

@@ -3,6 +3,8 @@ package com.x8bit.bitwarden.data.platform.manager.di
import android.app.Application
import android.content.Context
import androidx.core.content.getSystemService
import com.bitwarden.core.data.manager.toast.ToastManager
import com.bitwarden.core.data.manager.toast.ToastManagerImpl
import com.bitwarden.data.manager.DispatcherManager
import com.bitwarden.data.manager.DispatcherManagerImpl
import com.bitwarden.data.repository.ServerConfigRepository
@@ -67,6 +69,8 @@ import com.x8bit.bitwarden.data.platform.manager.network.NetworkConnectionManage
import com.x8bit.bitwarden.data.platform.manager.network.NetworkConnectionManagerImpl
import com.x8bit.bitwarden.data.platform.manager.restriction.RestrictionManager
import com.x8bit.bitwarden.data.platform.manager.restriction.RestrictionManagerImpl
import com.x8bit.bitwarden.data.platform.manager.sdk.SdkRepositoryFactory
import com.x8bit.bitwarden.data.platform.manager.sdk.SdkRepositoryFactoryImpl
import com.x8bit.bitwarden.data.platform.processor.AuthenticatorBridgeProcessor
import com.x8bit.bitwarden.data.platform.processor.AuthenticatorBridgeProcessorImpl
import com.x8bit.bitwarden.data.platform.repository.AuthenticatorBridgeRepository
@@ -134,13 +138,11 @@ object PlatformManagerModule {
addTotpItemFromAuthenticatorManager: AddTotpItemFromAuthenticatorManager,
@ApplicationContext context: Context,
dispatcherManager: DispatcherManager,
featureFlagManager: FeatureFlagManager,
): AuthenticatorBridgeProcessor = AuthenticatorBridgeProcessorImpl(
authenticatorBridgeRepository = authenticatorBridgeRepository,
addTotpItemFromAuthenticatorManager = addTotpItemFromAuthenticatorManager,
context = context,
dispatcherManager = dispatcherManager,
featureFlagManager = featureFlagManager,
)
@Provides
@@ -189,9 +191,19 @@ object PlatformManagerModule {
fun provideBitwardenClipboardManager(
@ApplicationContext context: Context,
settingsRepository: SettingsRepository,
toastManager: ToastManager,
): BitwardenClipboardManager = BitwardenClipboardManagerImpl(
context,
settingsRepository,
context = context,
settingsRepository = settingsRepository,
toastManager = toastManager,
)
@Provides
@Singleton
fun provideToastManager(
@ApplicationContext context: Context,
): ToastManager = ToastManagerImpl(
context = context,
)
@Provides
@@ -233,9 +245,11 @@ object PlatformManagerModule {
fun provideSdkClientManager(
featureFlagManager: FeatureFlagManager,
nativeLibraryManager: NativeLibraryManager,
sdkRepositoryFactory: SdkRepositoryFactory,
): SdkClientManager = SdkClientManagerImpl(
featureFlagManager = featureFlagManager,
nativeLibraryManager = nativeLibraryManager,
sdkRepoFactory = sdkRepositoryFactory,
)
@Provides
@@ -374,6 +388,14 @@ object PlatformManagerModule {
accessibilityEnabledManager = accessibilityEnabledManager,
)
@Provides
@Singleton
fun provideSdkRepositoryFactory(
vaultDiskSource: VaultDiskSource,
): SdkRepositoryFactory = SdkRepositoryFactoryImpl(
vaultDiskSource = vaultDiskSource,
)
@Provides
@Singleton
fun provideKeyManager(

View File

@@ -21,7 +21,6 @@ sealed class FlagKey<out T : Any> {
*/
val activeFlags: List<FlagKey<*>> by lazy {
listOf(
AuthenticatorSync,
EmailVerification,
ImportLoginsFlow,
CredentialExchangeProtocolImport,
@@ -33,22 +32,13 @@ sealed class FlagKey<out T : Any> {
SimpleLoginSelfHostAlias,
ChromeAutofill,
MobileErrorReporting,
FlightRecorder,
RestrictCipherItemDeletion,
PreAuthSettings,
UserManagedPrivilegedApps,
RemoveCardPolicy,
)
}
}
/**
* Data object holding the key for syncing with the Bitwarden Authenticator app.
*/
data object AuthenticatorSync : FlagKey<Boolean>() {
override val keyName: String = "enable-pm-bwa-sync"
override val defaultValue: Boolean = false
}
/**
* Data object holding the key for Email Verification feature.
*/
@@ -65,14 +55,6 @@ sealed class FlagKey<out T : Any> {
override val defaultValue: Boolean = false
}
/**
* Data object holding the key for enabling the flught recorder feature.
*/
data object FlightRecorder : FlagKey<Boolean>() {
override val keyName: String = "enable-pm-flight-recorder"
override val defaultValue: Boolean = false
}
/**
* Data object holding the feature flag key for the import logins feature.
*/
@@ -165,14 +147,6 @@ sealed class FlagKey<out T : Any> {
override val defaultValue: Boolean = false
}
/**
* Data object holding the feature flag key to enable the settings menu before login.
*/
data object PreAuthSettings : FlagKey<Boolean>() {
override val keyName: String = "enable-pm-prelogin-settings"
override val defaultValue: Boolean = false
}
/**
* Data object holding the feature flag key to enabled user-managed privileged apps.
*/
@@ -181,6 +155,15 @@ sealed class FlagKey<out T : Any> {
override val defaultValue: Boolean = false
}
/**
* Data object holding the feature flag key to enable the removal of card item types.
* This flag will hide card types from organizations with policy enable and individual vaults
*/
data object RemoveCardPolicy : FlagKey<Boolean>() {
override val keyName: String = "pm-16442-remove-card-item-type-policy"
override val defaultValue: Boolean = false
}
//region Dummy keys for testing
/**
* Data object holding the key for a [Boolean] flag to be used in tests.

View File

@@ -0,0 +1,13 @@
package com.x8bit.bitwarden.data.platform.manager.sdk
import com.bitwarden.sdk.CipherRepository
/**
* Creates and manages sdk repositories.
*/
interface SdkRepositoryFactory {
/**
* Retrieves or creates a [CipherRepository] for use with the Bitwarden SDK.
*/
fun getCipherRepository(userId: String): CipherRepository
}

View File

@@ -0,0 +1,20 @@
package com.x8bit.bitwarden.data.platform.manager.sdk
import com.bitwarden.sdk.CipherRepository
import com.x8bit.bitwarden.data.platform.manager.sdk.repository.SdkCipherRepository
import com.x8bit.bitwarden.data.vault.datasource.disk.VaultDiskSource
/**
* The default implementation for the [SdkRepositoryFactory].
*/
class SdkRepositoryFactoryImpl(
private val vaultDiskSource: VaultDiskSource,
) : SdkRepositoryFactory {
override fun getCipherRepository(
userId: String,
): CipherRepository =
SdkCipherRepository(
userId = userId,
vaultDiskSource = vaultDiskSource,
)
}

View File

@@ -0,0 +1,43 @@
package com.x8bit.bitwarden.data.platform.manager.sdk.repository
import com.bitwarden.sdk.CipherRepository
import com.bitwarden.vault.Cipher
import com.x8bit.bitwarden.data.vault.datasource.disk.VaultDiskSource
import com.x8bit.bitwarden.data.vault.repository.util.toEncryptedNetworkCipherResponse
import com.x8bit.bitwarden.data.vault.repository.util.toEncryptedSdkCipher
import timber.log.Timber
/**
* A user-scoped implementation of a Bitwarden SDK [CipherRepository].
*/
class SdkCipherRepository(
private val userId: String,
private val vaultDiskSource: VaultDiskSource,
) : CipherRepository {
override suspend fun get(id: String): Cipher? =
vaultDiskSource
.getCipher(userId = userId, cipherId = id)
?.toEncryptedSdkCipher()
override suspend fun has(id: String): Boolean = this.get(id = id) != null
override suspend fun list(): List<Cipher> =
vaultDiskSource
.getCiphers(userId = userId)
.map { it.toEncryptedSdkCipher() }
override suspend fun remove(id: String) {
vaultDiskSource.deleteCipher(userId = userId, cipherId = id)
}
override suspend fun set(id: String, value: Cipher) {
if (id != value.id) {
Timber.e("SDK Cipher 'set' operation: ID's do not match")
return
}
vaultDiskSource.saveCipher(
userId = userId,
cipher = value.toEncryptedNetworkCipherResponse(encryptedFor = userId),
)
}
}

View File

@@ -15,13 +15,11 @@ import com.bitwarden.authenticatorbridge.util.decrypt
import com.bitwarden.authenticatorbridge.util.encrypt
import com.bitwarden.authenticatorbridge.util.toFingerprint
import com.bitwarden.authenticatorbridge.util.toSymmetricEncryptionKeyData
import com.bitwarden.core.util.isBuildVersionAtLeast
import com.bitwarden.data.manager.DispatcherManager
import com.x8bit.bitwarden.data.auth.manager.AddTotpItemFromAuthenticatorManager
import com.x8bit.bitwarden.data.platform.manager.FeatureFlagManager
import com.x8bit.bitwarden.data.platform.manager.model.FlagKey
import com.x8bit.bitwarden.data.platform.repository.AuthenticatorBridgeRepository
import com.x8bit.bitwarden.data.platform.util.createAddTotpItemFromAuthenticatorIntent
import com.x8bit.bitwarden.data.platform.util.isBuildVersionBelow
import com.x8bit.bitwarden.ui.vault.util.getTotpDataOrNull
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
@@ -33,7 +31,6 @@ import timber.log.Timber
class AuthenticatorBridgeProcessorImpl(
private val authenticatorBridgeRepository: AuthenticatorBridgeRepository,
private val addTotpItemFromAuthenticatorManager: AddTotpItemFromAuthenticatorManager,
private val featureFlagManager: FeatureFlagManager,
dispatcherManager: DispatcherManager,
context: Context,
) : AuthenticatorBridgeProcessor {
@@ -44,12 +41,9 @@ class AuthenticatorBridgeProcessorImpl(
override val binder: IAuthenticatorBridgeService.Stub?
get() {
return if (
!featureFlagManager.getFeatureFlag(FlagKey.AuthenticatorSync) ||
isBuildVersionBelow(Build.VERSION_CODES.S)
) {
// If the feature flag is not enabled, OR if version is below Android 12,
// return a null binder which will no-op all service calls
return if (!isBuildVersionAtLeast(Build.VERSION_CODES.S)) {
// If version is below Android 12, return a null binder which will no-op all
// service calls
null
} else {
// Otherwise, return real binder implementation:

View File

@@ -1,17 +1,24 @@
package com.x8bit.bitwarden.data.platform.repository
import com.bitwarden.authenticatorbridge.model.SharedAccountData
import com.bitwarden.core.InitOrgCryptoRequest
import com.bitwarden.core.InitUserCryptoMethod
import com.bitwarden.core.InitUserCryptoRequest
import com.bitwarden.core.data.util.asSuccess
import com.bitwarden.core.data.util.flatMap
import com.bitwarden.data.repository.util.toEnvironmentUrlsOrDefault
import com.x8bit.bitwarden.data.auth.datasource.disk.AuthDiskSource
import com.x8bit.bitwarden.data.auth.datasource.disk.model.AccountJson
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
import com.x8bit.bitwarden.data.auth.repository.util.toSdkParams
import com.x8bit.bitwarden.data.platform.error.MissingPropertyException
import com.x8bit.bitwarden.data.platform.repository.util.sanitizeTotpUri
import com.x8bit.bitwarden.data.vault.datasource.disk.VaultDiskSource
import com.x8bit.bitwarden.data.vault.datasource.sdk.VaultSdkSource
import com.x8bit.bitwarden.data.vault.repository.VaultRepository
import com.x8bit.bitwarden.data.vault.repository.model.VaultUnlockData
import com.x8bit.bitwarden.data.vault.datasource.sdk.ScopedVaultSdkSource
import com.x8bit.bitwarden.data.vault.datasource.sdk.model.InitializeCryptoResult
import com.x8bit.bitwarden.data.vault.repository.model.VaultUnlockResult
import com.x8bit.bitwarden.data.vault.repository.util.statusFor
import com.x8bit.bitwarden.data.vault.repository.util.toEncryptedSdkCipher
import kotlinx.coroutines.flow.first
import com.x8bit.bitwarden.data.vault.repository.util.toVaultUnlockResult
/**
* Default implementation of [AuthenticatorBridgeRepository].
@@ -19,9 +26,8 @@ import kotlinx.coroutines.flow.first
class AuthenticatorBridgeRepositoryImpl(
private val authRepository: AuthRepository,
private val authDiskSource: AuthDiskSource,
private val vaultRepository: VaultRepository,
private val vaultDiskSource: VaultDiskSource,
private val vaultSdkSource: VaultSdkSource,
private val scopedVaultSdkSource: ScopedVaultSdkSource,
) : AuthenticatorBridgeRepository {
override val authenticatorSyncSymmetricKey: ByteArray?
@@ -45,62 +51,50 @@ class AuthenticatorBridgeRepositoryImpl(
@Suppress("LongMethod")
override suspend fun getSharedAccounts(): SharedAccountData {
val allAccounts = authRepository.userStateFlow.value?.accounts ?: emptyList()
val allAccounts = authDiskSource.userState?.accounts.orEmpty()
return allAccounts
.mapNotNull { account ->
val userId = account.userId
.mapNotNull { (userId, account) ->
// Grab the user's authenticator sync unlock key. If it is null,
// the user has not enabled authenticator sync.
// the user has not enabled authenticator sync and we skip the account.
val decryptedUserKey = authDiskSource.getAuthenticatorSyncUnlockKey(userId)
?: return@mapNotNull null
// Wait for any unlocking actions to finish:
vaultRepository.vaultUnlockDataStateFlow.first {
it.statusFor(userId) != VaultUnlockData.Status.UNLOCKING
}
// Unlock vault if necessary:
val isVaultAlreadyUnlocked = vaultRepository.isVaultUnlocked(userId = userId)
if (!isVaultAlreadyUnlocked) {
val unlockResult = vaultRepository
.unlockVaultWithDecryptedUserKey(
val vaultUnlockResult = unlockClient(
userId = userId,
account = account,
decryptedUserKey = decryptedUserKey,
)
when (vaultUnlockResult) {
is VaultUnlockResult.AuthenticationError,
is VaultUnlockResult.BiometricDecodingError,
is VaultUnlockResult.GenericError,
is VaultUnlockResult.InvalidStateError,
-> {
// Not being able to unlock the user's vault with the
// decrypted unlock key is an unexpected case, but if it does
// happen we omit the account from list of shared accounts
// and remove that user's authenticator sync unlock key.
// This gives the user a way to potentially re-enable syncing
// (going to Account Security and re-enabling the toggle)
authDiskSource.storeAuthenticatorSyncUnlockKey(
userId = userId,
decryptedUserKey = decryptedUserKey,
authenticatorSyncUnlockKey = null,
)
when (unlockResult) {
is VaultUnlockResult.AuthenticationError,
is VaultUnlockResult.BiometricDecodingError,
is VaultUnlockResult.GenericError,
is VaultUnlockResult.InvalidStateError,
-> {
// Not being able to unlock the user's vault with the
// decrypted unlock key is an unexpected case, but if it does
// happen we omit the account from list of shared accounts
// and remove that user's authenticator sync unlock key.
// This gives the user a way to potentially re-enable syncing
// (going to Account Security and re-enabling the toggle)
authDiskSource.storeAuthenticatorSyncUnlockKey(
userId = userId,
authenticatorSyncUnlockKey = null,
)
return@mapNotNull null
}
// Proceed
VaultUnlockResult.Success -> Unit
// Destroy our stand-alone instance of the vault.
scopedVaultSdkSource.clearCrypto(userId = userId)
return@mapNotNull null
}
// Proceed
VaultUnlockResult.Success -> Unit
}
// Vault is unlocked, query vault disk source for totp logins:
val totpUris = vaultDiskSource
.getCiphers(userId)
.first()
// Filter out any ciphers without a totp item and also deleted ciphers
.filter { it.login?.totp != null && it.deletedDate == null }
.getTotpCiphers(userId = userId)
// Filter out any deleted ciphers.
.filter { it.deletedDate == null }
.mapNotNull {
val decryptedCipher = vaultSdkSource
val decryptedCipher = scopedVaultSdkSource
.decryptCipher(
userId = userId,
cipher = it.toEncryptedSdkCipher(),
@@ -114,19 +108,18 @@ class AuthenticatorBridgeRepositoryImpl(
rawTotp.sanitizeTotpUri(cipherName, username)
}
// Lock the user's vault if we unlocked it for this operation:
if (!isVaultAlreadyUnlocked) {
vaultRepository.lockVault(
userId = userId,
isUserInitiated = false,
)
}
// Lock and destroy our stand-alone instance of the vault:
scopedVaultSdkSource.clearCrypto(userId = userId)
SharedAccountData.Account(
userId = account.userId,
name = account.name,
email = account.email,
environmentLabel = account.environment.label,
userId = userId,
name = account.profile.name,
email = account.profile.email,
environmentLabel = account
.settings
.environmentUrlData
.toEnvironmentUrlsOrDefault()
.label,
totpUris = totpUris,
)
}
@@ -134,4 +127,44 @@ class AuthenticatorBridgeRepositoryImpl(
SharedAccountData(it)
}
}
private suspend fun unlockClient(
userId: String,
account: AccountJson,
decryptedUserKey: String,
): VaultUnlockResult {
val privateKey = authDiskSource
.getPrivateKey(userId = userId)
?: return VaultUnlockResult.InvalidStateError(MissingPropertyException("Private key"))
return scopedVaultSdkSource
.initializeCrypto(
userId = userId,
request = InitUserCryptoRequest(
userId = userId,
kdfParams = account.profile.toSdkParams(),
email = account.profile.email,
privateKey = privateKey,
method = InitUserCryptoMethod.DecryptedKey(
decryptedUserKey = decryptedUserKey,
),
signingKey = null,
),
)
.flatMap { result ->
// Initialize the SDK for organizations if necessary
val organizationKeys = authDiskSource.getOrganizationKeys(userId = userId)
if (organizationKeys != null && result is InitializeCryptoResult.Success) {
scopedVaultSdkSource.initializeOrganizationCrypto(
userId = userId,
request = InitOrgCryptoRequest(organizationKeys = organizationKeys),
)
} else {
result.asSuccess()
}
}
.fold(
onFailure = { VaultUnlockResult.GenericError(error = it) },
onSuccess = { it.toVaultUnlockResult() },
)
}
}

View File

@@ -21,8 +21,8 @@ import com.x8bit.bitwarden.data.platform.repository.EnvironmentRepositoryImpl
import com.x8bit.bitwarden.data.platform.repository.SettingsRepository
import com.x8bit.bitwarden.data.platform.repository.SettingsRepositoryImpl
import com.x8bit.bitwarden.data.vault.datasource.disk.VaultDiskSource
import com.x8bit.bitwarden.data.vault.datasource.sdk.ScopedVaultSdkSource
import com.x8bit.bitwarden.data.vault.datasource.sdk.VaultSdkSource
import com.x8bit.bitwarden.data.vault.repository.VaultRepository
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
@@ -41,15 +41,13 @@ object PlatformRepositoryModule {
fun providesAuthenticatorBridgeRepository(
authRepository: AuthRepository,
authDiskSource: AuthDiskSource,
vaultRepository: VaultRepository,
vaultDiskSource: VaultDiskSource,
vaultSdkSource: VaultSdkSource,
scopedVaultSdkSource: ScopedVaultSdkSource,
): AuthenticatorBridgeRepository = AuthenticatorBridgeRepositoryImpl(
authRepository = authRepository,
authDiskSource = authDiskSource,
vaultRepository = vaultRepository,
vaultDiskSource = vaultDiskSource,
vaultSdkSource = vaultSdkSource,
scopedVaultSdkSource = scopedVaultSdkSource,
)
@Provides

View File

@@ -56,7 +56,7 @@ fun <T, R> MutableStateFlow<T>.observeWhenSubscribedAndUnlocked(
.filterNotNull()
.flatMapLatest { activeUserId ->
vaultUnlockFlow
.map { it.any { it.userId == activeUserId } }
.map { unlockData -> unlockData.any { it.userId == activeUserId } }
.distinctUntilChanged()
},
) { isSubscribed, activeUserId, isUnlocked ->

View File

@@ -9,12 +9,12 @@ import android.os.Build
import android.service.quicksettings.TileService
import androidx.annotation.Keep
import com.bitwarden.annotation.OmitFromCoverage
import com.bitwarden.core.util.isBuildVersionAtLeast
import com.x8bit.bitwarden.AccessibilityActivity
import com.x8bit.bitwarden.R
import com.x8bit.bitwarden.data.autofill.accessibility.manager.AccessibilityAutofillManager
import com.x8bit.bitwarden.data.autofill.accessibility.model.AccessibilityAction
import com.x8bit.bitwarden.data.autofill.accessibility.util.isAccessibilityServiceEnabled
import com.x8bit.bitwarden.data.platform.util.isBuildVersionBelow
import dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject
@@ -45,7 +45,7 @@ class BitwardenAutofillTileService : TileService() {
accessibilityAutofillManager.accessibilityAction = AccessibilityAction.AttemptParseUri
val intent = Intent(applicationContext, AccessibilityActivity::class.java)
.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK)
if (isBuildVersionBelow(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)) {
if (!isBuildVersionAtLeast(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)) {
@Suppress("DEPRECATION")
startActivityAndCollapse(intent)
} else {

View File

@@ -5,7 +5,7 @@ import android.os.Build
import android.service.quicksettings.TileService
import androidx.annotation.Keep
import com.bitwarden.annotation.OmitFromCoverage
import com.x8bit.bitwarden.data.platform.util.isBuildVersionBelow
import com.bitwarden.core.util.isBuildVersionAtLeast
import com.x8bit.bitwarden.ui.platform.manager.intent.IntentManager
import dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject
@@ -22,18 +22,17 @@ class BitwardenGeneratorTileService : TileService() {
override fun onClick() {
if (isLocked) {
unlockAndRun(Runnable { launchGenerator() })
unlockAndRun { launchGenerator() }
} else {
launchGenerator()
}
}
@Suppress("DEPRECATION")
@SuppressLint("StartActivityAndCollapseDeprecated")
private fun launchGenerator() {
val intent = intentManager.createTileIntent("bitwarden://password_generator")
if (isBuildVersionBelow(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)) {
if (!isBuildVersionAtLeast(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)) {
@Suppress("DEPRECATION")
@SuppressLint("StartActivityAndCollapseDeprecated")
startActivityAndCollapse(intent)
} else {
startActivityAndCollapse(intentManager.createTilePendingIntent(0, intent))

View File

@@ -5,7 +5,7 @@ import android.os.Build
import android.service.quicksettings.TileService
import androidx.annotation.Keep
import com.bitwarden.annotation.OmitFromCoverage
import com.x8bit.bitwarden.data.platform.util.isBuildVersionBelow
import com.bitwarden.core.util.isBuildVersionAtLeast
import com.x8bit.bitwarden.ui.platform.manager.intent.IntentManager
import dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject
@@ -22,18 +22,17 @@ class BitwardenVaultTileService : TileService() {
override fun onClick() {
if (isLocked) {
unlockAndRun(Runnable { launchVault() })
unlockAndRun { launchVault() }
} else {
launchVault()
}
}
@Suppress("DEPRECATION")
@SuppressLint("StartActivityAndCollapseDeprecated")
private fun launchVault() {
val intent = intentManager.createTileIntent("bitwarden://my_vault")
if (isBuildVersionBelow(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)) {
if (!isBuildVersionAtLeast(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)) {
@Suppress("DEPRECATION")
@SuppressLint("StartActivityAndCollapseDeprecated")
startActivityAndCollapse(intent)
} else {
startActivityAndCollapse(intentManager.createTilePendingIntent(0, intent))

View File

@@ -14,10 +14,25 @@ interface VaultDiskSource {
*/
suspend fun saveCipher(userId: String, cipher: SyncResponseJson.Cipher)
/**
* Retrieves all ciphers from the data source for a given [userId] as a [Flow].
*/
fun getCiphersFlow(userId: String): Flow<List<SyncResponseJson.Cipher>>
/**
* Retrieves all ciphers from the data source for a given [userId].
*/
fun getCiphers(userId: String): Flow<List<SyncResponseJson.Cipher>>
suspend fun getCiphers(userId: String): List<SyncResponseJson.Cipher>
/**
* Retrieves all ciphers from the data source for a given [userId] that contain TOTP codes.
*/
suspend fun getTotpCiphers(userId: String): List<SyncResponseJson.Cipher>
/**
* Retrieves a cipher from the data source for a given [userId] and [cipherId].
*/
suspend fun getCipher(userId: String, cipherId: String): SyncResponseJson.Cipher?
/**
* Deletes a cipher from the data source for the given [userId] and [cipherId].
@@ -72,13 +87,13 @@ interface VaultDiskSource {
/**
* Replaces all [vault] data for a given [userId] with the new `vault`.
*
* This will always cause the [getCiphers], [getCollections], and [getFolders] functions to
* This will always cause the [getCiphersFlow], [getCollections], and [getFolders] functions to
* re-emit even if the underlying data has not changed.
*/
suspend fun replaceVaultData(userId: String, vault: SyncResponseJson)
/**
* Trigger re-emissions from the [getCiphers], [getCollections], [getFolders], and [getSends]
* Trigger re-emissions from the [getCiphersFlow], [getCollections], [getFolders], and [getSends]
* functions.
*/
suspend fun resyncVaultData(userId: String)

View File

@@ -52,6 +52,7 @@ class VaultDiskSourceImpl(
CipherEntity(
id = cipher.id,
userId = userId,
hasTotp = cipher.login?.totp != null,
cipherType = json.encodeToString(cipher.type),
cipherJson = json.encodeToString(cipher),
),
@@ -59,13 +60,13 @@ class VaultDiskSourceImpl(
)
}
override fun getCiphers(
override fun getCiphersFlow(
userId: String,
): Flow<List<SyncResponseJson.Cipher>> =
merge(
forceCiphersFlow,
ciphersDao
.getAllCiphers(userId = userId)
.getAllCiphersFlow(userId = userId)
.map { entities ->
withContext(context = dispatcherManager.default) {
entities
@@ -81,6 +82,55 @@ class VaultDiskSourceImpl(
},
)
override suspend fun getCiphers(userId: String): List<SyncResponseJson.Cipher> {
val entities = ciphersDao.getAllCiphers(userId = userId)
return withContext(context = dispatcherManager.default) {
entities
.map { entity ->
async {
json.decodeFromStringWithErrorCallback<SyncResponseJson.Cipher>(
string = entity.cipherJson,
) { Timber.e(it, "Failed to deserialize Cipher in Vault") }
}
}
.awaitAll()
}
}
override suspend fun getTotpCiphers(userId: String): List<SyncResponseJson.Cipher> {
val entities = ciphersDao.getAllTotpCiphers(userId = userId)
return withContext(context = dispatcherManager.default) {
entities
.map { entity ->
async {
json.decodeFromStringWithErrorCallback<SyncResponseJson.Cipher>(
string = entity.cipherJson,
) { Timber.e(it, "Failed to deserialize TOTP Cipher in Vault") }
}
}
.awaitAll()
.filter {
// A safety-check since after the DB migration, we will temporarily think
// all ciphers contain a totp code
it.login?.totp != null
}
}
}
override suspend fun getCipher(
userId: String,
cipherId: String,
): SyncResponseJson.Cipher? =
ciphersDao
.getCipher(userId = userId, cipherId = cipherId)
?.let { entity ->
withContext(context = dispatcherManager.default) {
json.decodeFromStringWithErrorCallback<SyncResponseJson.Cipher>(
string = entity.cipherJson,
) { Timber.e(it, "Failed to deserialize Cipher in Vault") }
}
}
override suspend fun deleteCipher(userId: String, cipherId: String) {
ciphersDao.deleteCipher(userId, cipherId)
}
@@ -220,6 +270,7 @@ class VaultDiskSourceImpl(
CipherEntity(
id = cipher.id,
userId = userId,
hasTotp = cipher.login?.totp != null,
cipherType = json.encodeToString(cipher.type),
cipherJson = json.encodeToString(cipher),
)
@@ -296,7 +347,7 @@ class VaultDiskSourceImpl(
override suspend fun resyncVaultData(userId: String) {
coroutineScope {
val deferredCiphers = async { getCiphers(userId = userId).first() }
val deferredCiphers = async { getCiphersFlow(userId = userId).first() }
val deferredCollections = async { getCollections(userId = userId).first() }
val deferredFolders = async { getFolders(userId = userId).first() }
val deferredSends = async { getSends(userId = userId).first() }

View File

@@ -21,13 +21,38 @@ interface CiphersDao {
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertCiphers(ciphers: List<CipherEntity>)
/**
* Retrieves all ciphers from the database for a given [userId] as a [Flow].
*/
@Query("SELECT * FROM ciphers WHERE user_id = :userId")
fun getAllCiphersFlow(
userId: String,
): Flow<List<CipherEntity>>
/**
* Retrieves all ciphers from the database for a given [userId].
*/
@Query("SELECT * FROM ciphers WHERE user_id = :userId")
fun getAllCiphers(
suspend fun getAllCiphers(
userId: String,
): Flow<List<CipherEntity>>
): List<CipherEntity>
/**
* Retrieves all ciphers from the database for a given [userId].
*/
@Query("SELECT * FROM ciphers WHERE user_id = :userId AND has_totp = 1")
suspend fun getAllTotpCiphers(
userId: String,
): List<CipherEntity>
/**
* Retrieves a cipher from the database for a given [userId] and [cipherId].
*/
@Query("SELECT * FROM ciphers WHERE user_id = :userId AND id = :cipherId LIMIT 1")
suspend fun getCipher(
userId: String,
cipherId: String,
): CipherEntity?
/**
* Deletes all the stored ciphers associated with the given [userId]. This will return the

View File

@@ -1,5 +1,6 @@
package com.x8bit.bitwarden.data.vault.datasource.disk.database
import androidx.room.AutoMigration
import androidx.room.Database
import androidx.room.RoomDatabase
import androidx.room.TypeConverters
@@ -26,8 +27,11 @@ import com.x8bit.bitwarden.data.vault.datasource.disk.entity.SendEntity
FolderEntity::class,
SendEntity::class,
],
version = 6,
version = 7,
exportSchema = true,
autoMigrations = [
AutoMigration(from = 6, to = 7),
],
)
@TypeConverters(ZonedDateTimeTypeConverter::class)
abstract class VaultDatabase : RoomDatabase() {

View File

@@ -16,6 +16,11 @@ data class CipherEntity(
@ColumnInfo(name = "user_id", index = true)
val userId: String,
// Default to true for initial migration.
// Subsequent syncs will populate with correct values for optimizations.
@ColumnInfo(name = "has_totp", defaultValue = "1")
val hasTotp: Boolean,
@ColumnInfo(name = "cipher_type")
val cipherType: String,

View File

@@ -0,0 +1,11 @@
package com.x8bit.bitwarden.data.vault.datasource.sdk
/**
* This is a non-singleton instance of the [VaultSdkSource] that is intentionally separate; this
* allows you to temporarily unlock vaults for a given user within its own scope without affecting
* the foreground behavior of the app.
*
* Users of this class must always call [ScopedVaultSdkSource.clearCrypto] when they are done using
* the unlocked vault in order to ensure that this instance of the vault is re-locked.
*/
interface ScopedVaultSdkSource : VaultSdkSource

View File

@@ -0,0 +1,32 @@
package com.x8bit.bitwarden.data.vault.datasource.sdk
import com.bitwarden.annotation.OmitFromCoverage
import com.bitwarden.core.data.util.asSuccess
import com.bitwarden.data.manager.DispatcherManager
import com.x8bit.bitwarden.data.platform.manager.FeatureFlagManager
import com.x8bit.bitwarden.data.platform.manager.NativeLibraryManager
import com.x8bit.bitwarden.data.platform.manager.SdkClientManagerImpl
import com.x8bit.bitwarden.data.platform.manager.sdk.SdkRepositoryFactory
/**
* The default instance of the [ScopedVaultSdkSource]. This uses its own instance of the
* [SdkClientManagerImpl] to keep it separate from the rest of the app.
*/
@OmitFromCoverage
class ScopedVaultSdkSourceImpl(
dispatcherManager: DispatcherManager,
featureFlagManager: FeatureFlagManager,
sdkRepositoryFactory: SdkRepositoryFactory,
vaultSdkSource: VaultSdkSource = VaultSdkSourceImpl(
sdkClientManager = SdkClientManagerImpl(
// We do not want to have the real NativeLibraryManager used here to avoid
// initializing the library twice.
nativeLibraryManager = object : NativeLibraryManager {
override fun loadLibrary(libraryName: String): Result<Unit> = Unit.asSuccess()
},
sdkRepoFactory = sdkRepositoryFactory,
featureFlagManager = featureFlagManager,
),
dispatcherManager = dispatcherManager,
),
) : ScopedVaultSdkSource, VaultSdkSource by vaultSdkSource

View File

@@ -3,6 +3,7 @@ package com.x8bit.bitwarden.data.vault.datasource.sdk
import com.bitwarden.core.DateTime
import com.bitwarden.core.DerivePinKeyResponse
import com.bitwarden.core.InitOrgCryptoRequest
import com.bitwarden.core.InitUserCryptoMethod
import com.bitwarden.core.InitUserCryptoRequest
import com.bitwarden.core.UpdatePasswordResponse
import com.bitwarden.crypto.Kdf
@@ -22,6 +23,7 @@ import com.bitwarden.vault.CipherListView
import com.bitwarden.vault.CipherView
import com.bitwarden.vault.Collection
import com.bitwarden.vault.CollectionView
import com.bitwarden.vault.DecryptCipherListResult
import com.bitwarden.vault.EncryptionContext
import com.bitwarden.vault.Folder
import com.bitwarden.vault.FolderView
@@ -226,6 +228,17 @@ interface VaultSdkSource {
cipherList: List<Cipher>,
): Result<List<CipherView>>
/**
* Decrypts a list of [Cipher]s for the user with the given [userId].
*
* @return A [DecryptCipherListResult] containing the decrypted [CipherListView]s and references
* to [Cipher]s that cannot be decrypted.
*/
suspend fun decryptCipherListWithFailures(
userId: String,
cipherList: List<Cipher>,
): Result<DecryptCipherListResult>
/**
* Decrypts a [Collection] for the user with the given [userId], returning a [CollectionView]
* wrapped in a [Result].

View File

@@ -25,6 +25,7 @@ import com.bitwarden.vault.CipherListView
import com.bitwarden.vault.CipherView
import com.bitwarden.vault.Collection
import com.bitwarden.vault.CollectionView
import com.bitwarden.vault.DecryptCipherListResult
import com.bitwarden.vault.EncryptionContext
import com.bitwarden.vault.Folder
import com.bitwarden.vault.FolderView
@@ -311,6 +312,17 @@ class VaultSdkSourceImpl(
}
}
override suspend fun decryptCipherListWithFailures(
userId: String,
cipherList: List<Cipher>,
): Result<DecryptCipherListResult> =
runCatchingWithLogs {
getClient(userId = userId)
.vault()
.ciphers()
.decryptListWithFailures(cipherList)
}
override suspend fun decryptCollection(
userId: String,
collection: Collection,

View File

@@ -3,7 +3,11 @@ package com.x8bit.bitwarden.data.vault.datasource.sdk.di
import com.bitwarden.data.manager.DispatcherManager
import com.bitwarden.sdk.Fido2CredentialStore
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
import com.x8bit.bitwarden.data.platform.manager.FeatureFlagManager
import com.x8bit.bitwarden.data.platform.manager.SdkClientManager
import com.x8bit.bitwarden.data.platform.manager.sdk.SdkRepositoryFactory
import com.x8bit.bitwarden.data.vault.datasource.sdk.ScopedVaultSdkSource
import com.x8bit.bitwarden.data.vault.datasource.sdk.ScopedVaultSdkSourceImpl
import com.x8bit.bitwarden.data.vault.datasource.sdk.VaultSdkSource
import com.x8bit.bitwarden.data.vault.datasource.sdk.VaultSdkSourceImpl
import com.x8bit.bitwarden.data.vault.datasource.sdk.model.Fido2CredentialStoreImpl
@@ -32,6 +36,18 @@ object VaultSdkModule {
dispatcherManager = dispatcherManager,
)
@Provides
fun providesScopedVaultSdkSource(
dispatcherManager: DispatcherManager,
featureFlagManager: FeatureFlagManager,
sdkRepositoryFactory: SdkRepositoryFactory,
): ScopedVaultSdkSource =
ScopedVaultSdkSourceImpl(
dispatcherManager = dispatcherManager,
featureFlagManager = featureFlagManager,
sdkRepositoryFactory = sdkRepositoryFactory,
)
@Provides
@Singleton
fun providesFido2CredentialStore(

View File

@@ -21,7 +21,7 @@ class Fido2CredentialAuthenticationUserInterfaceImpl(
override suspend fun checkUser(
options: CheckUserOptions,
hint: UiHint,
): CheckUserResult = CheckUserResult(true, true)
): CheckUserResult = CheckUserResult(userPresent = true, userVerified = true)
override suspend fun checkUserAndPickCredentialForCreation(
options: CheckUserOptions,

View File

@@ -22,14 +22,14 @@ class Fido2CredentialRegistrationUserInterfaceImpl(
override suspend fun checkUser(
options: CheckUserOptions,
hint: UiHint,
): CheckUserResult = CheckUserResult(true, true)
): CheckUserResult = CheckUserResult(userPresent = true, userVerified = true)
override suspend fun checkUserAndPickCredentialForCreation(
options: CheckUserOptions,
newCredential: Fido2CredentialNewView,
): CheckUserAndPickCredentialForCreationResult = CheckUserAndPickCredentialForCreationResult(
cipher = CipherViewWrapper(selectedCipherView),
checkUserResult = CheckUserResult(true, true),
checkUserResult = CheckUserResult(userPresent = true, userVerified = true),
)
override suspend fun isVerificationEnabled(): Boolean = isVerificationSupported

View File

@@ -2,6 +2,7 @@ package com.x8bit.bitwarden.data.vault.manager
import android.net.Uri
import com.bitwarden.vault.CipherView
import com.x8bit.bitwarden.data.vault.manager.model.GetCipherResult
import com.x8bit.bitwarden.data.vault.repository.model.CreateAttachmentResult
import com.x8bit.bitwarden.data.vault.repository.model.CreateCipherResult
import com.x8bit.bitwarden.data.vault.repository.model.DeleteAttachmentResult
@@ -51,6 +52,11 @@ interface CipherManager {
attachmentId: String,
): DownloadAttachmentResult
/**
* Attempt to retrieve a decrypted cipher based on the [cipherId].
*/
suspend fun getCipher(cipherId: String): GetCipherResult
/**
* Attempt to delete a cipher.
*/

View File

@@ -7,6 +7,7 @@ import com.bitwarden.core.data.util.asSuccess
import com.bitwarden.core.data.util.flatMap
import com.bitwarden.network.model.AttachmentJsonResponse
import com.bitwarden.network.model.CreateCipherInOrganizationJsonRequest
import com.bitwarden.network.model.CreateCipherResponseJson
import com.bitwarden.network.model.ShareCipherJsonRequest
import com.bitwarden.network.model.UpdateCipherCollectionsJsonRequest
import com.bitwarden.network.model.UpdateCipherResponseJson
@@ -20,6 +21,7 @@ import com.x8bit.bitwarden.data.platform.manager.ReviewPromptManager
import com.x8bit.bitwarden.data.vault.datasource.disk.VaultDiskSource
import com.x8bit.bitwarden.data.vault.datasource.sdk.VaultSdkSource
import com.x8bit.bitwarden.data.vault.manager.model.DownloadResult
import com.x8bit.bitwarden.data.vault.manager.model.GetCipherResult
import com.x8bit.bitwarden.data.vault.repository.model.CreateAttachmentResult
import com.x8bit.bitwarden.data.vault.repository.model.CreateCipherResult
import com.x8bit.bitwarden.data.vault.repository.model.DeleteAttachmentResult
@@ -52,19 +54,33 @@ class CipherManagerImpl(
override suspend fun createCipher(cipherView: CipherView): CreateCipherResult {
val userId = activeUserId
?: return CreateCipherResult.Error(error = NoActiveUserException())
?: return CreateCipherResult.Error(
error = NoActiveUserException(),
errorMessage = null,
)
return vaultSdkSource
.encryptCipher(
userId = userId,
cipherView = cipherView,
)
.flatMap { ciphersService.createCipher(body = it.toEncryptedNetworkCipher()) }
.onSuccess { vaultDiskSource.saveCipher(userId = userId, cipher = it) }
.map { response ->
when (response) {
is CreateCipherResponseJson.Invalid -> {
CreateCipherResult.Error(errorMessage = response.message, error = null)
}
is CreateCipherResponseJson.Success -> {
vaultDiskSource.saveCipher(userId = userId, cipher = response.cipher)
CreateCipherResult.Success
}
}
}
.fold(
onFailure = { CreateCipherResult.Error(error = it) },
onFailure = { CreateCipherResult.Error(errorMessage = null, error = it) },
onSuccess = {
reviewPromptManager.registerAddCipherAction()
CreateCipherResult.Success
it
},
)
}
@@ -74,7 +90,7 @@ class CipherManagerImpl(
collectionIds: List<String>,
): CreateCipherResult {
val userId = activeUserId
?: return CreateCipherResult.Error(error = NoActiveUserException())
?: return CreateCipherResult.Error(errorMessage = null, error = NoActiveUserException())
return vaultSdkSource
.encryptCipher(
userId = userId,
@@ -88,17 +104,26 @@ class CipherManagerImpl(
),
)
}
.onSuccess {
vaultDiskSource.saveCipher(
userId = userId,
cipher = it.copy(collectionIds = collectionIds),
)
.map { response ->
when (response) {
is CreateCipherResponseJson.Invalid -> {
CreateCipherResult.Error(errorMessage = response.message, error = null)
}
is CreateCipherResponseJson.Success -> {
vaultDiskSource.saveCipher(
userId = userId,
cipher = response.cipher.copy(collectionIds = collectionIds),
)
CreateCipherResult.Success
}
}
}
.fold(
onFailure = { CreateCipherResult.Error(error = it) },
onFailure = { CreateCipherResult.Error(errorMessage = null, error = it) },
onSuccess = {
reviewPromptManager.registerAddCipherAction()
CreateCipherResult.Success
it
},
)
}
@@ -194,6 +219,24 @@ class CipherManagerImpl(
}
}
override suspend fun getCipher(cipherId: String): GetCipherResult {
val userId = activeUserId ?: return GetCipherResult.Failure(NoActiveUserException())
return vaultDiskSource
.getCipher(userId = userId, cipherId = cipherId)
?.let { syncResponseCipher ->
vaultSdkSource
.decryptCipher(
userId = userId,
cipher = syncResponseCipher.toEncryptedSdkCipher(),
)
.fold(
onSuccess = { GetCipherResult.Success(it) },
onFailure = { GetCipherResult.Failure(it) },
)
}
?: GetCipherResult.CipherNotFound
}
override suspend fun restoreCipher(
cipherId: String,
cipherView: CipherView,

View File

@@ -182,6 +182,7 @@ class VaultLockManagerImpl(
privateKey = privateKey,
method = initUserCryptoMethod,
userId = userId,
signingKey = null,
),
)
.flatMap { result ->

View File

@@ -0,0 +1,29 @@
package com.x8bit.bitwarden.data.vault.manager.model
import com.bitwarden.vault.CipherView
/**
* Models result of getting a cipher.
*/
sealed class GetCipherResult {
/**
* Cipher retrieved successfully.
*
* @param cipherView The cipher retrieved.
*/
data class Success(
val cipherView: CipherView,
) : GetCipherResult()
/**
* Cipher not found.
*/
data object CipherNotFound : GetCipherResult()
/**
* Generic error while retrieving cipher.
*/
data class Failure(
val error: Throwable,
) : GetCipherResult()
}

View File

@@ -147,7 +147,7 @@ class VaultRepositoryImpl(
private val vaultLockManager: VaultLockManager,
private val totpCodeManager: TotpCodeManager,
private val userLogoutManager: UserLogoutManager,
private val databaseSchemeManager: DatabaseSchemeManager,
databaseSchemeManager: DatabaseSchemeManager,
pushManager: PushManager,
private val clock: Clock,
dispatcherManager: DispatcherManager,
@@ -498,23 +498,20 @@ class VaultRepositoryImpl(
)
return getVaultItemStateFlow(cipherId)
.flatMapLatest { cipherDataState ->
val cipher = cipherDataState.data
?: return@flatMapLatest flowOf(DataState.Loaded(null))
totpCodeManager
.getTotpCodeStateFlow(
userId = userId,
cipher = cipher,
)
.map { totpCodeDataState ->
combineDataStates(
totpCodeDataState,
cipherDataState,
) { _, _ ->
// We are only combining the DataStates to know the overall state,
// we map it to the appropriate value below.
}
.mapNullable { totpCodeDataState.data }
cipherDataState
.data
?.let {
totpCodeManager
.getTotpCodeStateFlow(userId = userId, cipher = it)
.map { totpCodeDataState ->
combineDataStates(totpCodeDataState, cipherDataState) { _, _ ->
// We are only combining the DataStates to know the overall
// state, we map it to the appropriate value below.
}
.mapNullable { totpCodeDataState.data }
}
}
?: flowOf(DataState.Loaded(null))
}
.stateIn(
scope = unconfinedScope,
@@ -608,12 +605,12 @@ class VaultRepositoryImpl(
)
.also {
if (it is VaultUnlockResult.Success) {
encryptedBiometricsKey?.let {
encryptedBiometricsKey?.let { key ->
// If this key is present, we store it and the associated IV for future use
// since we want to migrate the user to a more secure form of biometrics.
authDiskSource.storeUserBiometricUnlockKey(
userId = userId,
biometricsKey = it,
biometricsKey = key,
)
authDiskSource.storeUserBiometricInitVector(userId = userId, iv = cipher.iv)
}
@@ -925,7 +922,7 @@ class VaultRepositoryImpl(
}
private suspend fun clearFolderIdFromCiphers(folderId: String, userId: String) {
vaultDiskSource.getCiphers(userId).firstOrNull()?.forEach {
vaultDiskSource.getCiphersFlow(userId).firstOrNull()?.forEach {
if (it.folderId == folderId) {
vaultDiskSource.saveCipher(
userId, it.copy(folderId = null),
@@ -944,7 +941,7 @@ class VaultRepositoryImpl(
.map { it.toEncryptedSdkFolder() }
val ciphers = vaultDiskSource
.getCiphers(userId)
.getCiphersFlow(userId)
.firstOrNull()
.orEmpty()
.map { it.toEncryptedSdkCipher() }
@@ -1070,7 +1067,7 @@ class VaultRepositoryImpl(
userId: String,
): Flow<DataState<List<CipherView>>> =
vaultDiskSource
.getCiphers(userId = userId)
.getCiphersFlow(userId = userId)
.onStart { mutableCiphersStateFlow.updateToPendingOrLoading() }
.map {
waitUntilUnlocked(userId = userId)
@@ -1091,7 +1088,7 @@ class VaultRepositoryImpl(
userId: String,
): Flow<DataState<List<CipherListView>>> =
vaultDiskSource
.getCiphers(userId = userId)
.getCiphersFlow(userId = userId)
.onStart { mutableCiphersListViewStateFlow.updateToPendingOrLoading() }
.map {
waitUntilUnlocked(userId = userId)
@@ -1491,7 +1488,7 @@ class VaultRepositoryImpl(
)
vaultDiskSource.resyncVaultData(userId = userId)
val itemsAvailable = vaultDiskSource
.getCiphers(userId)
.getCiphersFlow(userId)
.firstOrNull()
?.isNotEmpty() == true
return SyncVaultDataResult.Success(itemsAvailable = itemsAvailable)

View File

@@ -11,7 +11,8 @@ sealed class CreateCipherResult {
data object Success : CreateCipherResult()
/**
* Generic error while creating cipher.
* Generic error while creating cipher. The optional [errorMessage] may be displayed directly in
* the UI when present.
*/
data class Error(val error: Throwable) : CreateCipherResult()
data class Error(val errorMessage: String?, val error: Throwable?) : CreateCipherResult()
}

View File

@@ -1,37 +1,59 @@
package com.x8bit.bitwarden.ui.auth.feature.accountsetup
import android.os.Parcelable
import androidx.lifecycle.SavedStateHandle
import androidx.navigation.NavController
import androidx.navigation.NavGraphBuilder
import androidx.navigation.NavOptions
import androidx.navigation.toRoute
import com.bitwarden.ui.platform.base.util.composableWithPushTransitions
import com.bitwarden.ui.platform.base.util.composableWithSlideTransitions
import com.x8bit.bitwarden.ui.platform.util.toObjectRoute
import com.bitwarden.ui.platform.util.ParcelableRouteSerializer
import kotlinx.parcelize.Parcelize
import kotlinx.serialization.Serializable
/**
* The type-safe route for the setup autofill screen.
*/
sealed class SetupAutofillRoute {
@Parcelize
@Serializable(with = SetupAutofillRoute.Serializer::class)
sealed class SetupAutofillRoute : Parcelable {
/**
* The [isInitialSetup] value used in the setup autofill screen.
*/
abstract val isInitialSetup: Boolean
/**
* Custom serializer to support polymorphic routes.
*/
class Serializer : ParcelableRouteSerializer<SetupAutofillRoute>(SetupAutofillRoute::class)
/**
* The type-safe route for the standard setup autofill screen.
*/
@Serializable
@Parcelize
@Serializable(with = Standard.Serializer::class)
data object Standard : SetupAutofillRoute() {
override val isInitialSetup: Boolean get() = false
/**
* Custom serializer to support polymorphic routes.
*/
class Serializer : ParcelableRouteSerializer<Standard>(Standard::class)
}
/**
* The type-safe route for the root setup autofill screen.
*/
@Serializable
@Parcelize
@Serializable(with = AsRoot.Serializer::class)
data object AsRoot : SetupAutofillRoute() {
override val isInitialSetup: Boolean get() = true
/**
* Custom serializer to support polymorphic routes.
*/
class Serializer : ParcelableRouteSerializer<AsRoot>(AsRoot::class)
}
}
@@ -44,11 +66,8 @@ data class SetupAutoFillScreenArgs(val isInitialSetup: Boolean)
* Constructs a [SetupAutoFillScreenArgs] from the [SavedStateHandle] and internal route data.
*/
fun SavedStateHandle.toSetupAutoFillArgs(): SetupAutoFillScreenArgs {
val route = (this.toObjectRoute<SetupAutofillRoute.AsRoot>()
?: this.toObjectRoute<SetupAutofillRoute.Standard>())
return route
?.let { SetupAutoFillScreenArgs(isInitialSetup = it.isInitialSetup) }
?: throw IllegalStateException("Missing correct route for SetupAutofillScreen")
val route = this.toRoute<SetupAutofillRoute>()
return SetupAutoFillScreenArgs(isInitialSetup = route.isInitialSetup)
}
/**

View File

@@ -34,7 +34,9 @@ import com.bitwarden.ui.platform.base.util.standardHorizontalMargin
import com.bitwarden.ui.platform.components.appbar.BitwardenTopAppBar
import com.bitwarden.ui.platform.components.appbar.NavigationIcon
import com.bitwarden.ui.platform.components.button.BitwardenFilledButton
import com.bitwarden.ui.platform.components.button.BitwardenTextButton
import com.bitwarden.ui.platform.components.model.CardStyle
import com.bitwarden.ui.platform.components.toggle.BitwardenSwitch
import com.bitwarden.ui.platform.components.util.rememberVectorPainter
import com.bitwarden.ui.platform.model.WindowSize
import com.bitwarden.ui.platform.resource.BitwardenDrawable
@@ -42,12 +44,10 @@ import com.bitwarden.ui.platform.theme.BitwardenTheme
import com.bitwarden.ui.platform.util.rememberWindowSize
import com.x8bit.bitwarden.R
import com.x8bit.bitwarden.ui.auth.feature.accountsetup.handlers.rememberSetupAutoFillHandler
import com.x8bit.bitwarden.ui.platform.components.button.BitwardenTextButton
import com.x8bit.bitwarden.ui.platform.components.dialog.BitwardenBasicDialog
import com.x8bit.bitwarden.ui.platform.components.dialog.BitwardenTwoButtonDialog
import com.x8bit.bitwarden.ui.platform.components.image.BitwardenGifImage
import com.x8bit.bitwarden.ui.platform.components.scaffold.BitwardenScaffold
import com.x8bit.bitwarden.ui.platform.components.toggle.BitwardenSwitch
import com.x8bit.bitwarden.ui.platform.composition.LocalIntentManager
import com.x8bit.bitwarden.ui.platform.manager.intent.IntentManager
@@ -223,7 +223,7 @@ private fun SetupAutoFillContentHeader(
@Composable
private fun OrderedHeaderContent() {
BitwardenGifImage(
resId = R.drawable.img_setup_autofill,
resId = BitwardenDrawable.img_setup_autofill,
modifier = Modifier
.clip(
RoundedCornerShape(

View File

@@ -28,6 +28,7 @@ import com.bitwarden.ui.platform.base.util.standardHorizontalMargin
import com.bitwarden.ui.platform.components.appbar.BitwardenTopAppBar
import com.bitwarden.ui.platform.components.button.BitwardenFilledButton
import com.bitwarden.ui.platform.components.util.rememberVectorPainter
import com.bitwarden.ui.platform.resource.BitwardenDrawable
import com.bitwarden.ui.platform.theme.BitwardenTheme
import com.x8bit.bitwarden.R
import com.x8bit.bitwarden.ui.platform.components.scaffold.BitwardenScaffold
@@ -79,7 +80,7 @@ private fun SetupCompleteContent(
) {
Spacer(Modifier.height(32.dp))
Image(
painter = rememberVectorPainter(R.drawable.img_setup_complete),
painter = rememberVectorPainter(BitwardenDrawable.img_setup_complete),
contentDescription = null,
modifier = Modifier
.align(CenterHorizontally)

View File

@@ -1,37 +1,59 @@
package com.x8bit.bitwarden.ui.auth.feature.accountsetup
import android.os.Parcelable
import androidx.lifecycle.SavedStateHandle
import androidx.navigation.NavController
import androidx.navigation.NavGraphBuilder
import androidx.navigation.NavOptions
import androidx.navigation.toRoute
import com.bitwarden.ui.platform.base.util.composableWithPushTransitions
import com.bitwarden.ui.platform.base.util.composableWithSlideTransitions
import com.x8bit.bitwarden.ui.platform.util.toObjectRoute
import com.bitwarden.ui.platform.util.ParcelableRouteSerializer
import kotlinx.parcelize.Parcelize
import kotlinx.serialization.Serializable
/**
* The type-safe route for the setup unlock screen.
*/
sealed class SetupUnlockRoute {
@Parcelize
@Serializable(with = SetupUnlockRoute.Serializer::class)
sealed class SetupUnlockRoute : Parcelable {
/**
* The [isInitialSetup] value used in the setup unlock screen.
*/
abstract val isInitialSetup: Boolean
/**
* Custom serializer to support polymorphic routes.
*/
class Serializer : ParcelableRouteSerializer<SetupUnlockRoute>(SetupUnlockRoute::class)
/**
* The type-safe route for the standard setup unlock screen.
*/
@Serializable
@Parcelize
@Serializable(with = Standard.Serializer::class)
data object Standard : SetupUnlockRoute() {
override val isInitialSetup: Boolean get() = false
/**
* Custom serializer to support polymorphic routes.
*/
class Serializer : ParcelableRouteSerializer<Standard>(Standard::class)
}
/**
* The type-safe route for the root setup unlock screen.
*/
@Serializable
@Parcelize
@Serializable(with = AsRoot.Serializer::class)
data object AsRoot : SetupUnlockRoute() {
override val isInitialSetup: Boolean get() = true
/**
* Custom serializer to support polymorphic routes.
*/
class Serializer : ParcelableRouteSerializer<AsRoot>(AsRoot::class)
}
}
@@ -46,11 +68,8 @@ data class SetupUnlockArgs(
* Constructs a [SetupUnlockArgs] from the [SavedStateHandle] and internal route data.
*/
fun SavedStateHandle.toSetupUnlockArgs(): SetupUnlockArgs {
val route = this.toObjectRoute<SetupUnlockRoute.AsRoot>()
?: this.toObjectRoute<SetupUnlockRoute.Standard>()
return route
?.let { SetupUnlockArgs(isInitialSetup = it.isInitialSetup) }
?: throw IllegalStateException("Missing correct route for SetupUnlockScreen")
val route = this.toRoute<SetupUnlockRoute>()
return SetupUnlockArgs(isInitialSetup = route.isInitialSetup)
}
/**

View File

@@ -38,6 +38,7 @@ import com.bitwarden.ui.platform.base.util.standardHorizontalMargin
import com.bitwarden.ui.platform.components.appbar.BitwardenTopAppBar
import com.bitwarden.ui.platform.components.appbar.NavigationIcon
import com.bitwarden.ui.platform.components.button.BitwardenFilledButton
import com.bitwarden.ui.platform.components.button.BitwardenTextButton
import com.bitwarden.ui.platform.components.model.CardStyle
import com.bitwarden.ui.platform.components.util.rememberVectorPainter
import com.bitwarden.ui.platform.model.WindowSize
@@ -46,7 +47,6 @@ import com.bitwarden.ui.platform.theme.BitwardenTheme
import com.bitwarden.ui.platform.util.rememberWindowSize
import com.x8bit.bitwarden.R
import com.x8bit.bitwarden.ui.auth.feature.accountsetup.handlers.SetupUnlockHandler
import com.x8bit.bitwarden.ui.platform.components.button.BitwardenTextButton
import com.x8bit.bitwarden.ui.platform.components.dialog.BitwardenBasicDialog
import com.x8bit.bitwarden.ui.platform.components.dialog.BitwardenLoadingDialog
import com.x8bit.bitwarden.ui.platform.components.dialog.BitwardenTwoButtonDialog
@@ -244,7 +244,7 @@ private fun SetUpLaterButton(
private fun ColumnScope.SetupUnlockHeaderCompact() {
Spacer(modifier = Modifier.height(height = 32.dp))
Image(
painter = rememberVectorPainter(id = R.drawable.account_setup),
painter = rememberVectorPainter(id = BitwardenDrawable.account_setup),
contentDescription = null,
modifier = Modifier
.standardHorizontalMargin()
@@ -288,7 +288,7 @@ private fun SetupUnlockHeaderMedium(
.standardHorizontalMargin(),
) {
Image(
painter = rememberVectorPainter(id = R.drawable.account_setup),
painter = rememberVectorPainter(id = BitwardenDrawable.account_setup),
contentDescription = null,
modifier = Modifier
.size(size = 100.dp)

View File

@@ -37,6 +37,7 @@ import com.bitwarden.ui.platform.components.appbar.BitwardenTopAppBar
import com.bitwarden.ui.platform.components.button.BitwardenFilledButton
import com.bitwarden.ui.platform.components.button.BitwardenOutlinedButton
import com.bitwarden.ui.platform.components.util.rememberVectorPainter
import com.bitwarden.ui.platform.resource.BitwardenDrawable
import com.bitwarden.ui.platform.theme.BitwardenTheme
import com.x8bit.bitwarden.R
import com.x8bit.bitwarden.ui.auth.feature.checkemail.handlers.rememberCheckEmailHandler
@@ -78,7 +79,7 @@ fun CheckEmailScreen(
BitwardenTopAppBar(
title = stringResource(id = R.string.create_account),
scrollBehavior = scrollBehavior,
navigationIcon = rememberVectorPainter(id = R.drawable.ic_back),
navigationIcon = rememberVectorPainter(id = BitwardenDrawable.ic_back),
navigationIconContentDescription = stringResource(id = R.string.back),
onNavigationIconClick = handler.onBackClick,
)
@@ -115,7 +116,7 @@ private fun CheckEmailContent(
) {
Spacer(modifier = Modifier.height(32.dp))
Image(
painter = rememberVectorPainter(id = R.drawable.open_email),
painter = rememberVectorPainter(id = BitwardenDrawable.open_email),
contentDescription = null,
contentScale = ContentScale.FillHeight,
modifier = Modifier

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