1
0
mirror of https://github.com/bitwarden/mobile synced 2025-12-10 13:23:39 +00:00

Compare commits

..

134 Commits

Author SHA1 Message Date
Federico Maccaroni
c0de54d69e PM-7746 Added specific validation messages for (non) privileged apps validation on Fido2 flows. Also fixed typo on "privileged" and updated UT 2024-04-26 13:10:26 -03:00
Federico Maccaroni
299899f952 [PM-7576] Implemented digital asset links verification on Fido2 flows (#3191)
* PM-7553 Fix native apps passkeys autofill and creation

* PM-7658 Implemented Fido2 priviliged apps verification

* PM-7576 Implemented digital asset links verification on Fido2 flows for native apps.

* PM-7576 Renamed to ValidateAssetLinksAndGetOriginAsync to go along with Google naming and also changed method to private given that public is not necessary

* PM-7576 Moved digital asset links verification to a Core service AssetLinksService and added unit tests for it.
2024-04-25 15:00:01 -03:00
Federico Maccaroni
ea098c92d3 [PM-7658] Implement Fido2 privileged apps verification (#3190)
* PM-7553 Fix native apps passkeys autofill and creation

* PM-7658 Implemented Fido2 priviliged apps verification
2024-04-25 13:19:07 -03:00
Federico Maccaroni
a652c9d3e0 PM-7553 Fix native apps passkeys autofill and creation (#3188) 2024-04-25 13:06:38 -03:00
Federico Maccaroni
1bfe894181 PM-7623 Fix proper implementation of IFido2GetAssertionUserInterface now that the Fido2ClientService is being used for passkey autofill (#3174) 2024-04-22 13:36:18 -03:00
Federico Maccaroni
bfa57ad888 PM-7585 Show error message when Origin is null, given no support for passkeys from native apps yet (#3175) 2024-04-19 17:41:35 -03:00
Federico Maccaroni
96a7d5e089 [PM-7365] Fix UV not being performed on Fido2 credential creation Android (#3171)
* PM-7365 Fix UV not being performed on Fido2 credential creation on Android

* PM-7365 Fix PublicKeyCredentialCreationOptions mapping from json on AuthenticatorSelection so mainly userVerification has correct value
2024-04-19 12:42:46 -03:00
Andreas Coroiu
c1522e249d [PM-7257] android add support for web authn resident key credential property in our net mobile app 2 (#3170)
* [PM-7257] feat: add ability to override `clientDataHash`

* [PM-7257] feat: add support for clientDataHash and extensions

* PM-7257 Updated the origin to be the correct one and not the android one to be passed to the Fido2Client

---------

Co-authored-by: Federico Maccaroni <fedemkr@gmail.com>
2024-04-19 10:52:19 -03:00
Federico Maccaroni
76e0f7e1a4 Merge branch 'main' into feature/maui-migration-passkeys
# Conflicts:
#	src/Core/Abstractions/ICipherService.cs
#	src/Core/Abstractions/IStateService.cs
#	src/Core/Models/AppOptions.cs
#	src/Core/Resources/Localization/AppResources.resx
#	src/Core/Utilities/ServiceContainer.cs
#	src/iOS.Autofill/iOS.Autofill.csproj
2024-04-18 13:28:22 -03:00
Federico Maccaroni
6e41731dcb PM-6971 Added PrivacyInfo file to iOS (#3163) 2024-04-17 19:11:13 -03:00
Federico Maccaroni
c88287ec64 PM-7258 Updated Android Credential creation details on description to be localized and passed the user email for further details. (#3162) 2024-04-17 19:10:53 -03:00
Federico Maccaroni
350a22d20e PM-5154 Avoid logging Fido2AuthenticatorExceptions (#3169) 2024-04-17 17:42:36 -03:00
Federico Maccaroni
8a0501f0f9 PM-7365 Fix UserVerification on Fido2 credential creation on Android by updating the HasUnlockedInThisTransaction flag when a new transaction starts. (#3168) 2024-04-17 17:41:49 -03:00
Federico Maccaroni
1defd68d26 PM-7518 Updated favicon placeholder color on iOS Autofill extension. (#3165) 2024-04-17 10:00:36 -03:00
Federico Maccaroni
69ba16ed9e PM-7385 Fix unit tests for Fido2 service (#3167) 2024-04-17 09:58:28 -03:00
Federico Maccaroni
4eb608ec11 PM-7385 Fix IFido2MakeCredentialConfirmationUserInterface resolve and usage to be constrained to Android. (#3164) 2024-04-17 09:05:46 -03:00
Dinis Vieira
5a4a54f4af [PM-7385] Fix for allowing switching accounts while creating a passkey of Android (#3155)
* PM-7385 Fixed for allowing switching accounts while creating a passkey on Android.
This fixes also include scenarios where we need to unlock the vault after switching
Also fixed the issue where tapping on cipher won't do anything after switching.

* PM-7385 ensure the Options.Fido2CredentialAction and FromFido2Framework are reset when the Credential flow is started to avoid erratic behaviors when switching accounts, app is in background or other edge case scenarios.
These properties where replaced by calls to _fido2MakeCredentialConfirmationUserInterface.IsConfirmingNewCredential instead.

* Minor changes and added comments

* [PM-7385] Implemented several changes suggested in PR for better/cleaner code.

* PM-7385 Added several minor code improvemments.
2024-04-16 21:52:52 +01:00
Bitwarden DevOps
7c90b35592 Bumped version to 2024.4.1 (#3161) 2024-04-15 21:31:08 +00:00
renovate[bot]
93f9dc4498 [deps]: Update gh minor (#3124)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-04-15 16:34:05 -04:00
Federico Maccaroni
f596f31ffa [PM-7366] Select cipher on search on Fido2 creation (#3154)
* PM-7366 Implemented cipher selection on search on passkey creation

* PM-7366 Fix typo
2024-04-15 17:16:51 -03:00
Federico Maccaroni
40f036742f PM-7367 Fix empty items state placeholder on Android cipher selection page (#3160) 2024-04-15 17:13:41 -03:00
Federico Maccaroni
4b7f8074f3 PM-7365 Fix setting HasUnlockedInThisTransaction on passkey creation on android (#3153) 2024-04-15 17:13:26 -03:00
renovate[bot]
e033832261 [deps]: Update actions/setup-dotnet action to v4 (#3139)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-04-15 10:04:36 -06:00
Álison Fernandes
fa5d92fbf7 [PM-7407] Updates the self-host release date in the unassigned items alert message (#3158) 2024-04-15 15:58:56 +02:00
github-actions[bot]
e672cb132f Autosync the updated translations (#3151)
Co-authored-by: bitwarden-devops-bot <106330231+bitwarden-devops-bot@users.noreply.github.com>
2024-04-12 19:51:04 +00:00
Federico Maccaroni
e7a7eed7e8 [PM-7407] Implemented check for organizations with unassigned items (#3150) 2024-04-12 15:52:39 -03:00
Federico Maccaroni
7413c43d49 PM-7379 Fix creating the PendingIntent for a passkey credential on Android so it has different request codes amongst each other so the extras are not overriden by the last credential entry created. (#3149) 2024-04-11 23:07:47 +01:00
Dinis Vieira
08f371b0db [PM-7369] Show passkey icon on android when the item has a Fido2 credential (#3148)
* PM-7369 Show passkey icon on android when the item has a Fido2 credential

* PM-7369 alternative way to show passkey icon only in scenarios where we are trying to create a passkey

* PM-7369 moved logic to show passkey icon to CipherItemViewModel

* Update src/Core/Utilities/IconGlyphConverter.cs

Co-authored-by: Federico Maccaroni <fedemkr@gmail.com>

---------

Co-authored-by: Federico Maccaroni <fedemkr@gmail.com>
2024-04-11 13:14:45 +01:00
Álison Fernandes
ab5e72ef83 Merge branch 'main' into feature/maui-migration-passkeys 2024-04-09 21:59:31 +01:00
Dinis Vieira
ca944025d7 [PM-5153] Android Passkey Implementation (#3020)
* Initial WIP implementation for the app unlock flow when called from Passkey. Still needs code organization and to be finished.
Also added a new Window workaround in App.xaml.cs to allow CredentialProviderSelectionActivity to launch separately.

* Added missing IDeviceActionService.cs implementation for iOS to build.

* Added Async to ReturnToPasskeyAfterUnlockMethod
Changed i18n to AppResource.Unlock
Removed unecessary cast

* minor code change (added comment)

* Added back the case for loading a specific Window for CredentialProviverSelectionActivity

* Added fix for Intent not passing properly to CredentialProviderSelectionActivity
Added Activity cancellation on error during execution of ReturnToPasskeyAfterUnlockAsync()

* Added WIP code for Android passkey implementation. Currently returns a mostly complete response that is missing the ClientDataJson

* Added WIP code for creating passkeys on Android. Still missing unlock flow and response of passkey creation is still not correct.
Removed unused throw NotImplementedException from Fido2ClientService
Added CredentialCreationActivity for passkey creation
Added alternative code on CredentialProviderSelectionActivity to try to debug issue with response not being valid

* Started working on logic to adding unlock flow. It's already handling the unlock but not passing the PendingIntentHandler info for CredentialCreation to CredentialCreationActivity

* Changed "cross-platform" to "platform"

* Created CredentialHelpers.cs class to share code used for Populating Passkeys in Android.

* Added Passkey Credential Creation shared code to CredentialHelpers.
Unlock flow for Passkey creation should now be working also.

* Updated code for checking if the CredentialProviderService has been enabled by the user or not. Still WIP, somes notes in code due to Credential API not being complete.
Also changed the disable code to open the Credential Settings.

* Replaced the AndroidX.Credential helpers with custom JSON creation to fix the response for Credential Creation

* minor code cleanup on CredentialProviderSelectionActivity

* added todo comment

* Feature/maui migraton passkeys android unlock fix andreas (#3077)

* fix: bitwarden providing too many/wrong credentials

* feat: use authenticator instead of client

---------

Co-authored-by: Dinis Vieira <dinisvieira@outlook.com>

* Removed / commented some older Passkey Proof of concept code.
Auth and creation of passkey should still work both when device is unlocked (and not)
Added some initial code in AutofillCiphersPageViewModel and CipherAddEditPageViewModel for handling Passkey creation

* PM-6829 Implemented Fido2...UserInterfaces on Android and necessary logic to get/make a credential with those

* Added IFido2MediatorService registrations
Inverted two IsLockedAsync checks

* Added navigation to autofillCipher when creating passkey

* Updated LockPage to avoid multiple executions of SubmitAsync

* Added new flow for creating new passkey on Android with the Cipher page for editing details

* Changed the Credential Provider Switch to an external link control

* Added i18n for Passkey Settings

* Cleanup of older Credentials code used for Android Fido2 POC.
Removed CredentialCreationActivity as it's no longer needed

* fixed merge conflict/error and added error check to Fido2 navigation in App.xaml.cs

* Removed from MainActivity casts from DeviceActionService
Changed CredentialProviderServiceActivity to handle Fido errors and exceptions gracefully and show the user an error. Still not with the correct messages.

* Added some error messages. Still need to confirm the Text Resource to use and change.

* Changed some messages to use AppResources

* Cleanup of Credential Android code and added exception result if the clientCreateCredentialResult is null

* Updated Add new item button text when creating a new passkey

* Added AccountSwitchedException for the Fido Mediator Service

* Removed TODO that is no longer needed

* Updated some todo messages in Android AutofillHandler

* When authenticating a passkey on Android the "showDialog" callback can be called and there's no MainPage available so it was changed for that specific scenario to use _deviceActionService instead of MainPage.

---------

Co-authored-by: Andreas Coroiu <andreas.coroiu@gmail.com>
Co-authored-by: Federico Maccaroni <fedemkr@gmail.com>
2024-04-09 21:57:31 +01:00
Bitwarden DevOps
43a4915323 Bumped version to 2024.4.0 (#3143) 2024-04-08 14:50:08 +00:00
Álison Fernandes
b1ae3cc325 iOS Beta variants now have their own Encryption Export Compliance Code (#3136) 2024-04-08 12:16:48 +01:00
JohanGallardo
b9dada07ea Fixed broken mobile documentation link in README (#3142) 2024-04-08 09:14:30 +00:00
github-actions[bot]
58442389df Autosync the updated translations (#3122)
Co-authored-by: bitwarden-devops-bot <106330231+bitwarden-devops-bot@users.noreply.github.com>
2024-04-08 06:02:32 +00:00
Álison Fernandes
a3378d33ae Removed cake from Platform team ownership (#3137) 2024-04-05 23:37:21 +01:00
Dinis Vieira
8644fe598e PM-7186 Remove error message when showing password list as a fallback with user interaction (#3133) 2024-04-03 21:33:38 +01:00
Andreas Coroiu
ceca142c65 feat: add support for credProps.rk extension (#3132) 2024-04-03 16:52:39 +01:00
Dinis Vieira
86368c57ef Merge branch 'main' into feature/maui-migration-passkeys 2024-04-03 16:21:52 +01:00
renovate[bot]
2e1982b08e [deps]: Update actions/labeler action to v5 (#2895)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-04-03 08:51:28 -06:00
renovate[bot]
e9e9b6f7bc [deps]: Update actions/checkout action to v4 (#2756)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-04-03 08:48:11 -06:00
Opeyemi
9be8fec219 [DEVOPS-1822] - Upload Mobile Beta Native Build (#3015)
* Upload  mobile beta native build

---------

Co-authored-by: Vince Grassia <593223+vgrassia@users.noreply.github.com>
Co-authored-by: Federico Maccaroni <fedemkr@gmail.com>
Co-authored-by: Álison Fernandes <vvolkgang@users.noreply.github.com>
Co-authored-by: MtnBurrit0 <77340197+mimartin12@users.noreply.github.com>
2024-04-03 15:09:04 +01:00
Dinis Vieira
856a084a47 [PM-7186] Fallback to password list on exception (#3127)
* PM-7186 Added fallback in case of exception that loads password list

* PM-7186 Added back the error message removed in last commit.
2024-04-02 20:52:21 +01:00
Federico Maccaroni
4633fea41e PM-6209 Removed MAUI label from environment and about pages (#2990) 2024-03-29 20:54:41 +00:00
Federico Maccaroni
8fd9e0203d PM-6798 Fix account switch on iOS Autofill extension and also changed to Try... actions for TaskCompletionSource to avoid exceptions on some occasions. (#3121)
Co-authored-by: Álison Fernandes <vvolkgang@users.noreply.github.com>
2024-03-27 22:11:26 +00:00
Federico Maccaroni
310d8b363f [PM-6655] Add null fallback cipher name on passkeys (#3116)
* PM-6655 Fixed fallback value on passkeys to take into account CipherView.Name. Also removed non-discoverable passkey filter on adding credentials to the ASStore and also added the fallback consideration on the passkeys list iOS extension

* PM-6655 Restored non-discoverable filter on credentials set for ASStore on this PR
2024-03-27 22:02:28 +00:00
Federico Maccaroni
bdf2ea879d PM-6538 Removed non-discoverable passkeys filter for credentials that go to the ASStore (#3117) 2024-03-27 22:02:17 +00:00
Federico Maccaroni
ae3cebb266 PM-6850 Removed duplicate MP Reprompt on passkey creation item selection (#3118) 2024-03-27 21:52:38 +00:00
Federico Maccaroni
1b3d5e5eb2 Merge branch 'main' into feature/maui-migration-passkeys 2024-03-25 15:37:46 -03:00
Federico Maccaroni
81fbb91c76 PM-6475 Fix dark theme on iOS Autofill extension (#3114) 2024-03-25 12:18:00 -03:00
Federico Maccaroni
45641aadfe [PM-6798] Fix account switch on autofill (#3106)
* PM-6798 Force state update when opening the Autofill extension

* PM-6798 Fix InitAppIfNeededAsync to be awaited and also ignored Fido2AuthenticatorException from logging them to AppCenter since they don't add much information and we're logging in other places what we need
2024-03-25 12:17:40 -03:00
Federico Maccaroni
27380abd89 PM-6844 Fix passkey creation cipher list empty label on small devices (#3104) 2024-03-25 12:17:27 -03:00
Bitwarden DevOps
9db32ca019 Bumped version to 2024.3.3 (#3113) 2024-03-25 14:20:35 +00:00
Federico Maccaroni
1fd7dd462e Merge branch 'main' into feature/maui-migration-passkeys 2024-03-22 15:42:51 -03:00
Dinis Vieira
f04ff7777a Added specific try catch in Android launchApp to avoid the app crashing when trying to launch app package name that are not installed on the device. (#3092) 2024-03-22 16:31:15 +00:00
github-actions[bot]
64775694e0 Autosync the updated translations (#3105)
Co-authored-by: bitwarden-devops-bot <106330231+bitwarden-devops-bot@users.noreply.github.com>
2024-03-22 16:07:23 +00:00
Dinis Vieira
3c0007a21a [PM-7009] Improved exception messages for the Broadcast Service message callback function (#3091)
* Improved exception messages for the Broadcast Service message callback function

* Update src/Core/App.xaml.cs

Co-authored-by: Federico Maccaroni <fedemkr@gmail.com>

* Update src/Core/App.xaml.cs

Co-authored-by: Federico Maccaroni <fedemkr@gmail.com>

* Update src/Core/App.xaml.cs

Additional Null Check

Co-authored-by: Federico Maccaroni <fedemkr@gmail.com>

* Update src/Core/App.xaml.cs

Additional Null Check

Co-authored-by: Federico Maccaroni <fedemkr@gmail.com>

* Update src/Core/App.xaml.cs

Additional Null Check

Co-authored-by: Federico Maccaroni <fedemkr@gmail.com>

* Update src/Core/App.xaml.cs

Additional Null Check

Co-authored-by: Federico Maccaroni <fedemkr@gmail.com>

* Update src/Core/App.xaml.cs

Additional Null Check

Co-authored-by: Federico Maccaroni <fedemkr@gmail.com>

---------

Co-authored-by: Federico Maccaroni <fedemkr@gmail.com>
2024-03-22 12:29:12 -03:00
Federico Maccaroni
ff49d041be [PM-6655] Add username empty fallback on passkey (#3101)
* PM-6655 Added fallback "Unknown account" to passkey username and moved it so it can be shared with Android

* PM-6655 Improved code lines formatting
2024-03-21 13:56:37 -03:00
Federico Maccaroni
b931263662 PM-6793 Updated autofill settings copy (#3102) 2024-03-21 13:28:54 -03:00
Federico Maccaroni
3a10e09469 PM-6706 Fixed UV attempts to be maximum 5 attempts and not 6 (#3103) 2024-03-21 13:28:38 -03:00
Federico Maccaroni
ebc068d820 [PM-6848] Improved User verification on passkeys creation (#3099)
* PM-6848 Updated cancellation flow on passkey user verification and improved UV enforcement on creation

* PM-6848 Added null checks to help diagnosing if NRE is presented
2024-03-21 13:28:14 -03:00
Federico Maccaroni
6bec0ede05 Merge branch 'main' into feature/maui-migration-passkeys 2024-03-20 09:02:57 -03:00
Bitwarden DevOps
35ff235010 Bumped version to 2024.3.2 (#3096) 2024-03-19 15:15:29 +00:00
Bitwarden DevOps
01bd5a7b8d Bumped version to 2024.3.1 (#3095) 2024-03-19 14:43:30 +00:00
Vince Grassia
3fce8c76bc Add Cleanup RC Branch workflow (#3093) 2024-03-18 11:36:21 -06:00
github-actions[bot]
3b64d7b979 Autosync the updated translations (#3083)
Co-authored-by: bitwarden-devops-bot <106330231+bitwarden-devops-bot@users.noreply.github.com>
2024-03-15 23:31:54 +00:00
Federico Maccaroni
f343a2cdbb [PM-6852 ] Fix F-Droid build constant (#3085)
* Fix FDROID trackers removal by changing publish to build to see if doing this it adds the corresponding CustomConstants

* Changed parameters in new line delimiter to the one used in bash to see if that fixes passing the corresponding parameters to the build

* Revert "Changed parameters in new line delimiter to the one used in bash to see if that fixes passing the corresponding parameters to the build"

This reverts commit 608b23d115.

* Enable FDROID constant by replacing the content of Directory.Build.props in the clean stage of F-Droid
2024-03-15 17:01:15 -03:00
Federico Maccaroni
39da2a82c6 PM-6706 Add maximum attempts to UV with MP and with PIN (#3079) 2024-03-14 18:22:38 -03:00
Federico Maccaroni
970d3c2621 PM-6468 Implemented copy TOTP if needed after using a Fido2 credential. Also added the Fido2MediatorService to have one point to interact with the authentication and also to add any new logic we need. (#3082) 2024-03-14 18:12:50 -03:00
Federico Maccaroni
faa515b415 Merge branch 'main' into feature/maui-migration-passkeys 2024-03-14 18:08:42 -03:00
Federico Maccaroni
74085689d3 PM-6685 Fix race condition issue where the biometrics check is being done before the iOS extension is being shown. So when we need the UI, we wait until ViewDidAppear happens. (#3078) 2024-03-14 18:07:52 -03:00
Vince Grassia
9a9fb85ad8 Add version codes to GitHub step summary (#3081) 2024-03-14 10:06:51 -06:00
Bitwarden DevOps
e7f9d64edb Bumped version to 2024.3.0 (#3080) 2024-03-14 11:10:21 -04:00
Federico Maccaroni
144fc7c727 [PM-5154] Implement combined view for passwords and passkeys on iOS Autofill extension (#3075)
* PM-5154 Implemented combined view of passwords and passkeys and improved search and items UI

* PM-5154 Code improvement from PR feedback

* PM-5154 Code improvement to log unknown exceptions
2024-03-13 12:06:08 -03:00
Federico Maccaroni
53aedea93a Merge branch 'main' into feature/maui-migration-passkeys 2024-03-12 18:13:08 -03:00
Vince Grassia
459d20c019 DEVOPS-1840 - Update for automatic version bump calculation (#3043) 2024-03-12 14:10:11 -06:00
Federico Maccaroni
dd997aaa47 Merge branch 'main' into feature/maui-migration-passkeys 2024-03-12 12:38:52 -03:00
github-actions[bot]
a8529fa4b7 Autosync the updated translations (#3064)
Co-authored-by: bitwarden-devops-bot <106330231+bitwarden-devops-bot@users.noreply.github.com>
2024-03-12 08:41:52 +00:00
Dinis Vieira
d1e82c9f1d [PM-6751]Added missing query intent for CustomTabs that might be responsible for the Exception in WebAuthenticator (#3071)
* Added missing query intent for CustomTabs that might be responsible for the crashes in WebAuthenticator

* minor formatting issue on AndroidManifest.xml

* Fix formatting in AndroidManifest
2024-03-11 19:36:07 -03:00
Federico Maccaroni
46c1d72b3c Merge branch 'main' into feature/maui-migration-passkeys 2024-03-11 18:12:27 -03:00
Dinis Vieira
9bc2901255 [PM-6726] Fix for Android 14 devices crashing when using the Tiles (#3069)
* Fix for Android 14 devices crashing when using the TileService.
Also added fix for an "hidden" crash in accessibility autofill

* Shared StartActivityAndCollapseFromTileService in AndroidHelpers

* Update src/App/Platforms/Android/Utilities/AndroidHelpers.cs

Co-authored-by: Federico Maccaroni <fedemkr@gmail.com>

* Updated name of StartActivityAndCollapseWithIntent method name used by TileService

---------

Co-authored-by: Federico Maccaroni <fedemkr@gmail.com>
2024-03-11 18:06:37 -03:00
Federico Maccaroni
01fe329f3b Merge branch 'main' into feature/maui-migration-passkeys 2024-03-08 14:03:05 -03:00
Federico Maccaroni
67f7b3156e [PM-6496] Improved iOS extensions cipher cell UI (#3058)
* PM-6496 Improved iOS extensions cipher list to have an updated UI for each cell

* PM-6496 Improved UI on iOS extensions list cells
2024-03-08 13:59:15 -03:00
Vince Grassia
e3441845cd DEVOPS-1866 - Fix F-Droid Signing (#3063) 2024-03-07 23:45:15 +00:00
Vince Grassia
3f463647a0 Add login step to be able to download secrets (#3061) 2024-03-07 07:18:32 -08:00
Bitwarden DevOps
4f169a6fe3 Bumped version to 2024.2.2 (#3060) 2024-03-07 15:07:46 +00:00
Vince Grassia
82c2e91446 Update release workflow with proper paths (#3059) 2024-03-07 15:53:27 +01:00
Federico Maccaroni
39187732c0 [PM-6474] Remove header on Save passkey as new login (#3054)
* PM-6474 Removed header on empty list view on iOS Autofill create passkey flow

* PM-6474 Fix TableView being hidden on Logins scene
2024-03-07 11:04:00 -03:00
renovate[bot]
7482808857 [deps]: Update chrnorm/deployment-status action to v2.0.3 (#3050)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-03-06 13:52:31 -05:00
Federico Maccaroni
4292542155 [PM-6466] Implement passkeys User Verification (#3044)
* PM-6441 Implement passkeys User Verification

* PM-6441 Reorganized UserVerificationMediatorService so everything is not in the same file

* PM-6441 Fix Unit tests

* PM-6441 Refactor UserVerification on Fido2Authenticator and Client services to be of an enum type so we can see which specific preference the RP sent and to be passed into the user verification mediator service to perform the correct flow depending on that. Also updated Unit tests.

* PM-6441 Changed user verification logic a bit so if preference is Preferred and the app has the ability to verify the user then enforce required UV and fix issue on on Discouraged to take into account MP reprompt
2024-03-06 12:32:39 -03:00
Federico Maccaroni
e41abf5003 Merge branch 'main' into feature/maui-migration-passkeys 2024-03-06 11:19:18 -03:00
Opeyemi
fd233fa27f Update Failure Job (#3055) 2024-03-06 13:58:41 +01:00
Andreas Coroiu
4c2932f4d0 Fix FIDO2 client bugs (#3056)
* fix: blockedUris null issue

* fix: trailing slash in origin breaking check
2024-03-06 10:58:48 +00:00
Federico Maccaroni
a10481603d Merge branch 'main' into feature/maui-migration-passkeys
# Conflicts:
#	src/iOS.Core/Controllers/LoginAddViewController.cs
2024-03-05 18:23:23 -03:00
Federico Maccaroni
19f238d9bb [PM-6539] Fix Autofill Extension TDE without MP flow (#3049)
* PM-6539 Fix Autofill Extension TDE without MP updating PromptSSO to work in MAUI and also Generator view. WebAuthenticator copied with UIWindow gotten as it was in Xamarin forms to work. Also fix one NRE on state migration.

* PM-6539 Remove unnecessary using
2024-03-05 18:09:20 -03:00
Vince Grassia
6f6487ccc9 Fix GoogleServices file location (#3053) 2024-03-04 08:11:54 -07:00
Federico Maccaroni
b8ff0e0244 Merge branch 'main' into feature/maui-migration-passkeys 2024-03-04 11:03:30 -03:00
Dinis Vieira
dd3dc82595 PM-6552 Added missing using (#3052) 2024-03-04 13:29:22 +00:00
Dinis Vieira
40c80f082d [PM-6552] Fix for Android Window issues when opening Autofill/Accessibility (#3051)
* PM-6552 Removed several of the Window Workarounds for Android. Now always relying on the AndroidNavigationRedirectPage.
AndroidNavigationRedirectPage now more resilient to failure and navigates to HomePage as fallback.

* Update src/Core/Pages/AndroidNavigationRedirectPage.xaml.cs

Co-authored-by: Federico Maccaroni <fedemkr@gmail.com>

---------

Co-authored-by: Federico Maccaroni <fedemkr@gmail.com>
2024-03-04 13:03:09 +00:00
Federico Maccaroni
85755902e1 Merge branch 'main' into feature/maui-migration-passkeys 2024-03-01 17:15:22 -03:00
André Bispo
bca5b95446 [PM-4760] Admin Recovery Permissions prompted to set MP. (#2912)
* [PM-4760] Add force password reset check on sync complete.

* [PM-4760] Log error on exception
2024-03-01 19:43:18 +00:00
Dinis Vieira
602627b5fa PM-6552 Fix for App only showing Home (Login) Page after closed after opening Accessibility Settings (#3047) 2024-03-01 19:17:45 +00:00
github-actions[bot]
6f32afb919 Autosync the updated translations (#3045)
Co-authored-by: bitwarden-devops-bot <106330231+bitwarden-devops-bot@users.noreply.github.com>
2024-03-01 12:26:42 +01:00
Daniel James Smith
2ca47a4da4 Update ownership of translations (#3046)
Co-authored-by: Daniel James Smith <djsmith85@users.noreply.github.com>
2024-03-01 11:10:15 +00:00
Federico Maccaroni
38d3a7ed41 [PM-6513] Omit creating CredentialIdentity if it throws an exception (#3040)
* PM-6513 Omit creating CredentialIdentity if that throws, so it doesn't affect other ciphers. E.g. if a Passkey doesn't have a UserName it will throw here and it shouldn't break replacing all the other identities.

* PM-6513 Added fallback values to passkey username not being set
2024-02-29 11:08:13 -03:00
Dinis Vieira
4ff56ba11e PM-5916 Fix for incorrect fonts in fingerprint phrases (#3042) 2024-02-29 09:57:25 +00:00
Vince Grassia
22d0cc681c Change version to proper value (#3041) 2024-02-28 11:49:30 -07:00
André Bispo
4e0a18cce5 [PM-6506] Fix double execution of command on returnType Go (#3039)
* [PM-6506] Fix double execution of command on returnType Go

* [PM-6506] Hide keyboard on environment page close

* [PM-6506] Task guard

Co-authored-by: Federico Maccaroni <fedemkr@gmail.com>

---------

Co-authored-by: Federico Maccaroni <fedemkr@gmail.com>
2024-02-28 17:49:46 +00:00
Vince Grassia
c9fdfa7a15 DEVOPS-1746 - Update iOS distribution certificate and profiles (#3018) 2024-02-28 09:48:16 -03:00
Vince Grassia
850a7e754a DEVOPS-1834 - Apply fix for signing issue (#3038) 2024-02-27 20:18:24 +00:00
Federico Maccaroni
18fae7ddd8 Merge branch 'main' into feature/maui-migration-passkeys
# Conflicts:
#	Directory.Build.props
2024-02-27 12:46:30 -03:00
Dinis Vieira
67c5f79625 [PM-5917] Fix for send arrow now being touch sensitive to expand collapse (#3036)
* PM-5917 fix for send arrow not being tappable

* Added min width to send icon button so that it has correct spacing like in Android and old Xamarin Forms app.

* Updated min width from previous commit to 25 instead of 20 for more equivalent look to xamarin forms app on iOS
2024-02-26 23:45:59 +00:00
Federico Maccaroni
04e7cfe06d [PM-6428] Implement Legacy Secure Storage (#3027) 2024-02-26 19:25:08 -03:00
Álison Fernandes
d6c2ebe4c2 [PM-6480] Update MAUI to 8.0.7 (#3035)
* Update MAUI to 8.0.7

Updates MAUI to the future SR2 release version.

* Using the released version instead of nightly
2024-02-26 22:24:44 +00:00
Dinis Vieira
2a28294f91 PM-5912 Added default min height and corner radius for iOS buttons. Also removed incorrect style class from one button. (#3031) 2024-02-26 19:23:06 -03:00
Federico Maccaroni
b83473ce3a Merge branch 'main' into feature/maui-migration-passkeys 2024-02-26 15:42:12 -03:00
Dinis Vieira
8584bbaecc PM-6301 Removed IsRefreshing=true in RefreshAsync as it can trigger the RefreshView to trigger the RefreshView command again (#3026) 2024-02-26 17:28:18 +00:00
Dinis Vieira
2f3cded9c5 PM-6309 Fix to ensure the Icon and Icon placeholder visibility states is updated correctly based on website icons visibility choice (#3033) 2024-02-26 17:16:54 +00:00
github-actions[bot]
eff0ea7ce7 Autosync the updated translations (#3025)
Co-authored-by: bitwarden-devops-bot <106330231+bitwarden-devops-bot@users.noreply.github.com>
2024-02-26 15:29:26 +00:00
renovate[bot]
6c3a53dd76 [deps]: Update dawidd6/action-download-artifact action to v3.1.2 (#3028)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-02-26 09:37:03 -05:00
Federico Maccaroni
e34a58e875 [PM-5154] Implement iOS Passkey -> Add login item (#3019)
* PM-5154 Implement iOS passkey add login

* PM-5154 Added Username to Create new login for passkey, for this the param was changed to the Fido2ConfirmNewCredentialParams object so we have access to the proper values. Also added back RpId to the params to have access to it when creating the vault item. Finally added loading to saving the passkey as new login
2024-02-26 09:33:39 -03:00
Federico Maccaroni
9f92fdeb29 Merge branch 'main' into feature/maui-migration-passkeys
# Conflicts:
#	src/Core/Resources/Localization/AppResources.resx
2024-02-23 20:11:27 -03:00
Vince Grassia
cf8d801c55 Add .NET 3.1 to fix Google Publisher project signing (#3024) 2024-02-22 13:45:42 -07:00
Federico Maccaroni
eaa6844742 Update build.yml to go back to net3.1 Publisher (#3023) 2024-02-22 17:01:33 -03:00
Federico Maccaroni
29e2f728e0 Update Publisher.csproj to go back to net 3.1 to see if that fixes the build (#3022) 2024-02-22 16:39:09 -03:00
Andreas Coroiu
c31444dc8b feat: optimize assertion network calls (#3021)
The server only needs to be updated if we have changed the counter. New passkeys that leave their counters at zero can therefore skip this step.
2024-02-22 14:34:10 +01:00
Federico Maccaroni
16e1b60a4d [PM-5154] Implement Passkeys on iOS (#3017)
* [PM-5731] feat: implement get assertion params object

* [PM-5731] feat: add first test

* [PM-5731] feat: add rp mismatch test

* [PM-5731] feat: ask for credentials when found

* [PM-5731] feat: find discoverable credentials

* [PM-5731] feat: add tests for successful UV requests

* [PM-5731] feat: add user does not consent test

* [PM-5731] feat: check for UV when reprompt is active

* [PM-5731] fix: tests a bit, needed some additional "arrange" steps

* [PM-5731] feat: add support for counter

* [PM-5731] feat: implement assertion without signature

* [PM-5732] feat: finish authenticator assertion implementation

note: CryptoFunctionService still needs Sign implemenation

* [PM-5731] chore: minor clean up

* [PM-5731] feat: scaffold make credential

* [PM-5731] feat: start implementing attestation

* [PM-5731] feat: implement credential exclusion

* [PM-5731] feat: add new credential confirmaiton

* [PM-5731] feat: implement credential creation

* [PM-5731] feat: add user verification checks

* [PM-5731] feat: add unknown error handling

* [PM-5731] chore: clean up unusued params

* [PM-5731] feat: partial attestation implementation

* [PM-5731] feat: implement key generation

* [PM-5731] feat: return public key in DER format

* [PM-5731] feat: implement signing

* [PM-5731] feat: remove logging

* [PM-5731] chore: use primary constructor

* [PM-5731] chore: add Async to method names

* [PM-5731] feat: add support for silent discoverability

* [PM-5731] feat: add support for specifying user presence requirement

* [PM-5731] feat: ensure unlocked vault

* [PM-5731] chore: clean up and refactor assertion tests

* [PM-5731] chore: clean up and refactor attestation tests

* [PM-5731] chore: add user presence todo comment

* [PM-5731] feat: scaffold fido2 client

* PM-5731 Fix build updating discoverable flag

* [PM-5731] fix: failing test

* [PM-5731] feat: add sameOriginWithAncestor and user id length checks

* [PM-5731] feat: add incomplete rpId verification

* [PM-5731] chore: document uri helpers

* [PM-5731] feat: implement fido2 client createCredential

* Added iOS passkeys integration, warning this branch has lots of logs to ease "debugging" extensions.

* [PM-5731] feat: implement credential assertion in client

* PM-5154 Fixed select passkey flow and started implementing create passkey on iOS

* fix wrong signature format

* PM-5154 [Passkeys iOS] Fix Credential ID handling on bytes and string formats. Fix Discoverable to be lowercase on set so it doesn't break parsing on clients. Added UserDisplayName on Fido2 entities. Extracted the Guid Standard/Raw format helpers to a extensions class.

* Fix incompatible GUID conversions

* PM-5154 [Passkeys iOS] Added custom UI flow for passkey creation

* PM-5154 [Passkeys iOS] Updated UI for passkey creation

* PM-5154 [Passkeys iOS] Refactored and added cipher selection for passkey creation on autofill search.

* PM-5154 [Passkeys iOS] Fixed empty top space on autofill password list

---------

Co-authored-by: Andreas Coroiu <andreas.coroiu@gmail.com>
Co-authored-by: mpbw2 <59324545+mpbw2@users.noreply.github.com>
2024-02-21 14:51:44 -03:00
Opeyemi
fe160a570f Add stub for DEVOPS-1822 (#3016) 2024-02-21 15:27:43 +00:00
Andreas Coroiu
71de3bedf4 [PM-5731] Create C# WebAuthn authenticator to support maui apps (#2951)
* [PM-5731] feat: implement get assertion params object

* [PM-5731] feat: add first test

* [PM-5731] feat: add rp mismatch test

* [PM-5731] feat: ask for credentials when found

* [PM-5731] feat: find discoverable credentials

* [PM-5731] feat: add tests for successful UV requests

* [PM-5731] feat: add user does not consent test

* [PM-5731] feat: check for UV when reprompt is active

* [PM-5731] fix: tests a bit, needed some additional "arrange" steps

* [PM-5731] feat: add support for counter

* [PM-5731] feat: implement assertion without signature

* [PM-5732] feat: finish authenticator assertion implementation

note: CryptoFunctionService still needs Sign implemenation

* [PM-5731] chore: minor clean up

* [PM-5731] feat: scaffold make credential

* [PM-5731] feat: start implementing attestation

* [PM-5731] feat: implement credential exclusion

* [PM-5731] feat: add new credential confirmaiton

* [PM-5731] feat: implement credential creation

* [PM-5731] feat: add user verification checks

* [PM-5731] feat: add unknown error handling

* [PM-5731] chore: clean up unusued params

* [PM-5731] feat: partial attestation implementation

* [PM-5731] feat: implement key generation

* [PM-5731] feat: return public key in DER format

* [PM-5731] feat: implement signing

* [PM-5731] feat: remove logging

* [PM-5731] chore: use primary constructor

* [PM-5731] chore: add Async to method names

* [PM-5731] feat: add support for silent discoverability

* [PM-5731] feat: add support for specifying user presence requirement

* [PM-5731] feat: ensure unlocked vault

* [PM-5731] chore: clean up and refactor assertion tests

* [PM-5731] chore: clean up and refactor attestation tests

* [PM-5731] chore: add user presence todo comment

* [PM-5731] feat: scaffold fido2 client

* PM-5731 Fix build updating discoverable flag

* [PM-5731] fix: failing test

* [PM-5731] feat: add sameOriginWithAncestor and user id length checks

* [PM-5731] feat: add incomplete rpId verification

* [PM-5731] chore: document uri helpers

* [PM-5731] feat: implement fido2 client createCredential

* [PM-5731] feat: implement credential assertion in client

* fix wrong signature format

(cherry picked from commit a1c9ebf01f)

* [PM-5731] fix: issues after cherry-pick

* Fix incompatible GUID conversions

(cherry picked from commit c801b2fc3a)

* [PM-5731] chore: remove default constructor

* [PM-5731] feat: refactor user interface to increase flexibility

* [PM-5731] feat: implement generic assertion user interface class

* [PM-5731] feat: remove ability to make user presence optional

* [PM-5731] chore: remove logging comments

* [PM-5731] feat: add native reprompt support to the authenticator

* [PM-5731] feat: allow pre and post UV

* [PM-5731] chore: add `Async` to method name. Remove `I` from struct

* [PM-5731] fix: discoverable string repr lowercase

* [PM-5731] chore: don't use C# 12 features

* [PM-5731] fix: replace magic strings and numbers with contants and enums

* [PM-5731] fix: use UTC creation date

* [PM-5731] fix: formatting

* [PM-5731] chore: use properties for public fields

* [PM-5731] chore: remove TODO

* [PM-5731] fix: IsValidRpId

---------

Co-authored-by: Federico Maccaroni <fedemkr@gmail.com>
Co-authored-by: mpbw2 <59324545+mpbw2@users.noreply.github.com>
2024-02-21 12:12:52 -03:00
Dinis Vieira
a508bea4b0 [PM-6291] Fix Device Login Pending Requests screen not displaying anything (#3012)
* PM-6291 Changed Passwordless Request Login Page Layout structure so that it can display properly

* PM-6291 Additional changes to allow iOS to show the correct with on the collectionview items
2024-02-21 10:55:26 +00:00
Dinis Vieira
a73923c4f7 [PM-5909] Fix Font MAUI Sizes (#3014)
* PM-5909 Set the default FontSize for Entry, Editor, SearchBar and Picker on Android so that the fonts have a similar size to the one in the Xamarin Forms app.

* PM-5909 Set the default FontSize for Entry, Editor, SearchBar and Picker on iOS so that the fonts have a similar size to the one in the Xamarin Forms app.

* PM-5909 Added spacing in specific scenario for Send Groups (between icon and text)
2024-02-20 22:06:47 +00:00
renovate[bot]
11465e8975 [deps]: Update gh minor (#3011)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-02-20 16:01:34 -05:00
André Bispo
4c88524f0e [PM-4615] [PM-6217] Add new DUO frameless 2fa flow (#2956)
* [PM-4615] Update DUO 2FA screen to support DUO frameless flow.
2024-02-20 18:46:47 +00:00
Opeyemi
f1c20e03bc Remove individual linter file (#3010) 2024-02-15 11:15:13 -05:00
github-actions[bot]
920a2273c5 Autosync Crowdin Translations (#3009)
* Autosync the updated translations

* Add whitespace to build.yml to trigger workflow linter

* Remove whitespace from build.yml

---------

Co-authored-by: bitwarden-devops-bot <106330231+bitwarden-devops-bot@users.noreply.github.com>
Co-authored-by: Daniel James Smith <djsmith85@users.noreply.github.com>
2024-02-15 12:52:13 -03:00
307 changed files with 10589 additions and 2869 deletions

7
.github/CODEOWNERS vendored
View File

@@ -21,7 +21,6 @@ src/App/Platforms/iOS/Info.plist
## Platform team files ## ## Platform team files ##
appIcons @bitwarden/team-platform-dev appIcons @bitwarden/team-platform-dev
build.cake @bitwarden/team-platform-dev
## Vault team files ## ## Vault team files ##
src/watchOS @bitwarden/team-vault-dev src/watchOS @bitwarden/team-vault-dev
@@ -30,14 +29,14 @@ src/watchOS @bitwarden/team-vault-dev
src/Core/Services/EmailForwarders @bitwarden/team-tools-dev src/Core/Services/EmailForwarders @bitwarden/team-tools-dev
## Crowdin Sync files ## ## Crowdin Sync files ##
src/App/Resources @bitwarden/team-tools-dev src/Core/Resources/Localization @bitwarden/team-tools-dev
src/watchOS/bitwarden/bitwarden\ WatchKit\ Extension/Localization @bitwarden/team-tools-dev src/watchOS/bitwarden/bitwarden\ WatchKit\ Extension/Localization @bitwarden/team-tools-dev
store/apple @bitwarden/team-tools-dev store/apple @bitwarden/team-tools-dev
store/google @bitwarden/team-tools-dev store/google @bitwarden/team-tools-dev
## Locales ## ## Locales ##
src/App/Resources/AppResources.Designer.cs src/Core/Resources/Localization/AppResources.Designer.cs
src/App/Resources/AppResources.resx src/Core/Resources/Localization/AppResources.resx
src/watchOS/bitwarden/bitwarden\ WatchKit\ Extension/Localization/en.lproj src/watchOS/bitwarden/bitwarden\ WatchKit\ Extension/Localization/en.lproj
store/apple/en store/apple/en
store/google/en store/google/en

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -1,3 +0,0 @@
<EFBFBD>
 K<>Y#<23>(<28><><EFBFBD><EFBFBD>EI֐߄T?)l<><6C><EFBFBD><18><><10>"=<3D>|<7C>'e<><0E>m<EFBFBD>/~<7E><>' F<><46>><3E><><EFBFBD><EFBFBD>l<EFBFBD>b<EFBFBD>[<5B>+R<><52>iL<69><4C>"<22><><EFBFBD>~V:<3A><>p<EFBFBD>a<17>ڵel%8t<38><74><EFBFBD>y<<3C>n<EFBFBD><6E><EFBFBD>aU<61>w<16>JD<4A><44><1F><>We<57>9<EFBFBD><39><EFBFBD><EFBFBD><x8d<38>O<EFBFBD>j\<14>ד<EFBFBD><D793><EFBFBD>Vq<56><71>֋
Ǻ<EFBFBD>-<2D>#<23><><11><>]$<24>(<28>l,<2C>Br<42><02><>d<><64><EFBFBD>•a-<2D><><EFBFBD>:<3A><>:<3A><04>9b,!Em<02><19><>Qf<>D<EFBFBD>g<EFBFBD><06><0E>x(P<>ȡ~<7E>͹<EFBFBD><CDB9> <09><>[<06><>!:<3A>;f<><66>

Binary file not shown.

Binary file not shown.

Binary file not shown.

350
.github/workflows/build-beta.yml vendored Normal file
View File

@@ -0,0 +1,350 @@
---
name: Build Beta
on:
workflow_dispatch:
inputs:
ref:
description: 'Branch or tag to build'
required: true
default: 'main'
type: string
env:
main_app_folder_path: src/App
main_app_project_path: src/App/App.csproj
target-net-version: net8.0
jobs:
setup:
name: Setup
runs-on: ubuntu-22.04
outputs:
rc_branch_exists: ${{ steps.branch-check.outputs.rc_branch_exists }}
hotfix_branch_exists: ${{ steps.branch-check.outputs.hotfix_branch_exists }}
steps:
- name: Checkout repo
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
with:
submodules: 'true'
- name: Check if special branches exist
id: branch-check
run: |
if [[ $(git ls-remote --heads origin rc) ]]; then
echo "rc_branch_exists=1" >> $GITHUB_OUTPUT
else
echo "rc_branch_exists=0" >> $GITHUB_OUTPUT
fi
if [[ $(git ls-remote --heads origin hotfix-rc) ]]; then
echo "hotfix_branch_exists=1" >> $GITHUB_OUTPUT
else
echo "hotfix_branch_exists=0" >> $GITHUB_OUTPUT
fi
ios:
name: Apple iOS
runs-on: macos-13
needs: setup
env:
ios_folder_path: src/App/Platforms/iOS
app_output_name: App
app_ci_output_filename: App_x64_Debug
steps:
- name: Set XCode version
uses: maxim-lobanov/setup-xcode@60606e260d2fc5762a71e64e74b2174e8ea3c8bd # v1.6.0
with:
xcode-version: 15.1
- name: Setup NuGet
uses: nuget/setup-nuget@296fd3ccf8528660c91106efefe2364482f86d6f # v1.2.0
with:
nuget-version: 6.4.0
- name: Set up .NET
uses: actions/setup-dotnet@4d6c8fcf3c8f7a60068d26b594648e99df24cee3 # v4.0.0
with:
dotnet-version: '8.0.x'
# This step might be obsolete at some point as .NET MAUI workloads
# are starting to come pre-installed on the GH Actions build agents.
- name: Install MAUI Workload
run: dotnet workload install maui --ignore-failed-sources
- name: Print environment
run: |
nuget help | grep Version
dotnet --info
echo "GitHub ref: $GITHUB_REF"
echo "GitHub event: $GITHUB_EVENT"
- name: Checkout repo
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
with:
fetch-depth: 0
ref: ${{ inputs.ref }}
submodules: 'true'
- name: Login to Azure - CI Subscription
uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0
with:
creds: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }}
- name: Retrieve secrets
id: retrieve-secrets
uses: bitwarden/gh-actions/get-keyvault-secrets@main
with:
keyvault: "bitwarden-ci"
secrets: "appcenter-ios-token"
- name: Download Provisioning Profiles secrets
env:
ACCOUNT_NAME: bitwardenci
CONTAINER_NAME: profiles
run: |
mkdir -p $HOME/secrets
profiles=(
"dist_beta_autofill.mobileprovision"
"dist_beta_bitwarden.mobileprovision"
"dist_beta_extension.mobileprovision"
"dist_beta_share_extension.mobileprovision"
"dist_beta_bitwarden_watch_app.mobileprovision"
"dist_beta_bitwarden_watch_app_extension.mobileprovision"
)
for FILE in "${profiles[@]}"
do
az storage blob download --account-name $ACCOUNT_NAME --container-name $CONTAINER_NAME --name $FILE \
--file $HOME/secrets/$FILE --output none
done
- name: Download Google Services secret
env:
ACCOUNT_NAME: bitwardenci
CONTAINER_NAME: mobile
FILE: GoogleService-Info.plist
run: |
mkdir -p $HOME/secrets
az storage blob download --account-name $ACCOUNT_NAME --container-name $CONTAINER_NAME --name $FILE \
--file $HOME/secrets/$FILE --output none
- name: Increment version
run: |
BUILD_NUMBER=$((100 + $GITHUB_RUN_NUMBER))
echo "##### Setting CFBundleVersion $BUILD_NUMBER"
echo "### CFBundleVersion $BUILD_NUMBER" >> $GITHUB_STEP_SUMMARY
perl -0777 -pi.bak -e 's/<key>CFBundleVersion<\/key>\s*<string>1<\/string>/<key>CFBundleVersion<\/key>\n\t<string>'"$BUILD_NUMBER"'<\/string>/' ./${{ env.ios_folder_path }}/Info.plist
perl -0777 -pi.bak -e 's/<key>CFBundleVersion<\/key>\s*<string>1<\/string>/<key>CFBundleVersion<\/key>\n\t<string>'"$BUILD_NUMBER"'<\/string>/' ./src/iOS.Extension/Info.plist
perl -0777 -pi.bak -e 's/<key>CFBundleVersion<\/key>\s*<string>1<\/string>/<key>CFBundleVersion<\/key>\n\t<string>'"$BUILD_NUMBER"'<\/string>/' ./src/iOS.Autofill/Info.plist
perl -0777 -pi.bak -e 's/<key>CFBundleVersion<\/key>\s*<string>1<\/string>/<key>CFBundleVersion<\/key>\n\t<string>'"$BUILD_NUMBER"'<\/string>/' ./src/iOS.ShareExtension/Info.plist
cd src/watchOS/bitwarden
agvtool new-version -all $BUILD_NUMBER
- name: Update Entitlements
run: |
echo "##### Updating Entitlements"
perl -0777 -pi.bak -e 's/<key>aps-environment<\/key>\s*<string>development<\/string>/<key>aps-environment<\/key>\n\t<string>beta<\/string>/' ./${{ env.ios_folder_path }}/Entitlements.plist
- name: Get certificates
run: |
mkdir -p $HOME/certificates
az keyvault secret show --id https://bitwarden-ci.vault.azure.net/certificates/ios-distribution |
jq -r .value | base64 -d > $HOME/certificates/ios-distribution.p12
- name: Set up Keychain
env:
KEYCHAIN_PASSWORD: ${{ secrets.IOS_KEYCHAIN_PASSWORD }}
MOBILE_KEY_PASSWORD: ${{ secrets.IOS_KEY_PASSWORD }}
DIST_CERT_PASSWORD: ${{ secrets.IOS_DIST_CERT_PASSWORD }}
run: |
security create-keychain -p $KEYCHAIN_PASSWORD build.keychain
security default-keychain -s build.keychain
security unlock-keychain -p $KEYCHAIN_PASSWORD build.keychain
security set-keychain-settings -lut 1200 build.keychain
security import $HOME/certificates/ios-distribution.p12 -k build.keychain -P "" -T /usr/bin/codesign \
-T /usr/bin/security
security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k $KEYCHAIN_PASSWORD build.keychain
- name: Set up provisioning profiles
run: |
AUTOFILL_PROFILE_PATH=$HOME/secrets/dist_beta_autofill.mobileprovision
BITWARDEN_PROFILE_PATH=$HOME/secrets/dist_beta_bitwarden.mobileprovision
EXTENSION_PROFILE_PATH=$HOME/secrets/dist_beta_extension.mobileprovision
SHARE_EXTENSION_PROFILE_PATH=$HOME/secrets/dist_beta_share_extension.mobileprovision
WATCH_APP_PROFILE_PATH=$HOME/secrets/dist_beta_bitwarden_watch_app.mobileprovision
WATCH_APP_EXTENSION_PROFILE_PATH=$HOME/secrets/dist_beta_bitwarden_watch_app_extension.mobileprovision
PROFILES_DIR_PATH=$HOME/Library/MobileDevice/Provisioning\ Profiles
mkdir -p "$PROFILES_DIR_PATH"
AUTOFILL_UUID=$(grep UUID -A1 -a $AUTOFILL_PROFILE_PATH | grep -io "[-A-F0-9]\{36\}")
cp $AUTOFILL_PROFILE_PATH "$PROFILES_DIR_PATH/$AUTOFILL_UUID.mobileprovision"
BITWARDEN_UUID=$(grep UUID -A1 -a $BITWARDEN_PROFILE_PATH | grep -io "[-A-F0-9]\{36\}")
cp $BITWARDEN_PROFILE_PATH "$PROFILES_DIR_PATH/$BITWARDEN_UUID.mobileprovision"
EXTENSION_UUID=$(grep UUID -A1 -a $EXTENSION_PROFILE_PATH | grep -io "[-A-F0-9]\{36\}")
cp $EXTENSION_PROFILE_PATH "$PROFILES_DIR_PATH/$EXTENSION_UUID.mobileprovision"
SHARE_EXTENSION_UUID=$(grep UUID -A1 -a $SHARE_EXTENSION_PROFILE_PATH | grep -io "[-A-F0-9]\{36\}")
cp $SHARE_EXTENSION_PROFILE_PATH "$PROFILES_DIR_PATH/$SHARE_EXTENSION_UUID.mobileprovision"
WATCH_APP_UUID=$(grep UUID -A1 -a $WATCH_APP_PROFILE_PATH | grep -io "[-A-F0-9]\{36\}")
cp $WATCH_APP_PROFILE_PATH "$PROFILES_DIR_PATH/$WATCH_APP_UUID.mobileprovision"
WATCH_APP_EXTENSION_UUID=$(grep UUID -A1 -a $WATCH_APP_EXTENSION_PROFILE_PATH | grep -io "[-A-F0-9]\{36\}")
cp $WATCH_APP_EXTENSION_PROFILE_PATH "$PROFILES_DIR_PATH/$WATCH_APP_EXTENSION_UUID.mobileprovision"
- name: Restore packages
run: |
dotnet restore
dotnet tool restore
- name: Setup iOS build CAKE (Testing)
run: dotnet cake build.cake --target iOS --variant beta
- name: Bulid WatchApp
run: |
echo "##### Build WatchApp with Release Configuration"
xcodebuild archive -workspace ./src/watchOS/bitwarden/bitwarden.xcodeproj/project.xcworkspace -configuration Release -scheme bitwarden\ WatchKit\ App -archivePath ./src/watchOS/bitwarden
echo "##### Done"
- name: Archive Build for App Store
shell: pwsh
run: |
Write-Output "##### Archive for Release ios-arm64"
dotnet publish ${{ env.main_app_project_path }} -c Release -f ${{ env.target-net-version }}-ios /p:RuntimeIdentifier=ios-arm64 /p:ArchiveOnBuild=true /p:MtouchUseLlvm=false
Write-Output "##### Done"
- name: Archive Build for Mobile Automation
shell: pwsh
run: |
Write-Output "##### Archive Debug for iossimulator-x64"
dotnet build ${{ env.main_app_project_path }} -c Debug -f ${{ env.target-net-version }}-ios /p:RuntimeIdentifier=iossimulator-x64 /p:ArchiveOnBuild=true /p:MtouchUseLlvm=false
Write-Output "##### Done"
ls ~/Library/Developer/Xcode/Archives
- name: Export .ipa for App Store
env:
EXPORT_OPTIONS_PATH: ./.github/resources/export-options-app-store.plist
EXPORT_PATH: ./bitwarden-export
run: |
ARCHIVE_PATH="$HOME/Library/Developer/Xcode/Archives/*/*.xcarchive"
xcodebuild -exportArchive -archivePath $ARCHIVE_PATH -exportPath $EXPORT_PATH \
-exportOptionsPlist $EXPORT_OPTIONS_PATH
- name: Export .app for Automation CI
env:
ARCHIVE_PATH: ./${{ env.main_app_folder_path }}/bin/Debug/${{ env.target-net-version }}-ios/iossimulator-x64
EXPORT_PATH: ./bitwarden-export
run: |
zip -r -q ${{ env.app_ci_output_filename }}.app.zip $ARCHIVE_PATH
mv ${{ env.app_ci_output_filename }}.app.zip $EXPORT_PATH
- name: Show Bitwarden Export
shell: bash
run: ls -a -R ./bitwarden-export
- name: Copy all dSYMs files to upload
env:
EXPORT_PATH: ./bitwarden-export
WATCH_ARCHIVE_DSYMS_PATH: ./src/watchOS/bitwarden.xcarchive/dSYMs/
WATCH_DSYMS_EXPORT_PATH: ./bitwarden-export/Watch_dSYMs
run: |
ARCHIVE_DSYMS_PATH="$HOME/Library/Developer/Xcode/Archives/*/*.xcarchive/dSYMs"
cp -r -v $ARCHIVE_DSYMS_PATH $EXPORT_PATH
mkdir $WATCH_DSYMS_EXPORT_PATH
cp -r -v $WATCH_ARCHIVE_DSYMS_PATH $WATCH_DSYMS_EXPORT_PATH
- name: Upload App Store .ipa & dSYMs artifacts
uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1
with:
name: Bitwarden iOS
path: |
./bitwarden-export/Bitwarden*.ipa
./bitwarden-export/dSYMs/*.*
if-no-files-found: error
- name: Upload .app file for Automation CI
uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1
with:
name: ${{ env.app_ci_output_filename }}.app.zip
path: ./bitwarden-export/${{ env.app_ci_output_filename }}.app.zip
if-no-files-found: error
- name: Install AppCenter CLI
run: npm install -g appcenter-cli
- name: Upload dSYMs to App Center
env:
APPCENTER_IOS_TOKEN: ${{ steps.retrieve-secrets.outputs.appcenter-ios-token }}
run: appcenter crashes upload-symbols -a bitwarden/bitwarden -s "./bitwarden-export/dSYMs" --token $APPCENTER_IOS_TOKEN
- name: Upload Watch dSYMs to Firebase Crashlytics
run: |
echo "##### Uploading Watch dSYMs to Firebase"
find "$HOME/Library/Developer/XCode/DerivedData" -name "upload-symbols" -exec chmod +x {} \; -exec {} -gsp "./src/watchOS/bitwarden/GoogleService-Info.plist" -p ios "./bitwarden-export/Watch_dSYMs" \;
- name: Validate app in App Store
env:
APPLE_ID_USERNAME: ${{ secrets.APPLE_ID_USERNAME }}
APPLE_ID_PASSWORD: ${{ secrets.APPLE_ID_PASSWORD }}
run: |
xcrun altool --validate-app --type ios --file "./bitwarden-export/Bitwarden Beta.ipa" \
--username "$APPLE_ID_USERNAME" --password "$APPLE_ID_PASSWORD"
shell: bash
- name: Deploy to App Store
env:
APPLE_ID_USERNAME: ${{ secrets.APPLE_ID_USERNAME }}
APPLE_ID_PASSWORD: ${{ secrets.APPLE_ID_PASSWORD }}
run: |
xcrun altool --upload-app --type ios --file "./bitwarden-export/Bitwarden Beta.ipa" \
--username "$APPLE_ID_USERNAME" --password "$APPLE_ID_PASSWORD"
check-failures:
name: Check for failures
if: always()
runs-on: ubuntu-22.04
needs:
- setup
- ios
steps:
- name: Check if any job failed
if: |
(github.ref == 'refs/heads/main'
|| github.ref == 'refs/heads/rc'
|| github.ref == 'refs/heads/hotfix-rc')
&& contains(needs.*.result, 'failure')
run: exit 1
- name: Login to Azure - CI Subscription
uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0
if: failure()
with:
creds: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }}
- name: Retrieve secrets
id: retrieve-secrets
uses: bitwarden/gh-actions/get-keyvault-secrets@main
if: failure()
with:
keyvault: "bitwarden-ci"
secrets: "devops-alerts-slack-webhook-url"
- name: Notify Slack on failure
uses: act10ns/slack@44541246747a30eb3102d87f7a4cc5471b0ffb7d # v2.1.0
if: failure()
env:
SLACK_WEBHOOK_URL: ${{ steps.retrieve-secrets.outputs.devops-alerts-slack-webhook-url }}
with:
status: ${{ job.status }}

View File

@@ -31,6 +31,7 @@ jobs:
- name: Print lines of code - name: Print lines of code
run: cloc --vcs git --exclude-dir Resources,store,test,Properties --include-lang C#,XAML run: cloc --vcs git --exclude-dir Resources,store,test,Properties --include-lang C#,XAML
setup: setup:
name: Setup name: Setup
runs-on: ubuntu-22.04 runs-on: ubuntu-22.04
@@ -58,6 +59,7 @@ jobs:
echo "hotfix_branch_exists=0" >> $GITHUB_OUTPUT echo "hotfix_branch_exists=0" >> $GITHUB_OUTPUT
fi fi
android: android:
name: Android name: Android
runs-on: windows-2022 runs-on: windows-2022
@@ -67,7 +69,8 @@ jobs:
matrix: matrix:
variant: ["prod", "qa"] variant: ["prod", "qa"]
env: env:
android_folder_path: src/App/Platforms/Android android_folder_path: src\App\Platforms\Android
android_folder_path_bash: src/App/Platforms/Android
steps: steps:
- name: Setup NuGet - name: Setup NuGet
uses: nuget/setup-nuget@296fd3ccf8528660c91106efefe2364482f86d6f # v1.2.0 uses: nuget/setup-nuget@296fd3ccf8528660c91106efefe2364482f86d6f # v1.2.0
@@ -93,7 +96,8 @@ jobs:
- name: Install Microsoft OpenJDK 11 - name: Install Microsoft OpenJDK 11
run: | run: |
choco install microsoft-openjdk11 --no-progress choco install microsoft-openjdk11 --no-progress
Write-Output "JAVA_HOME=$(Get-ChildItem -Path 'C:\Program Files\Microsoft\jdk*' | Select -First 1 -ExpandProperty FullName)" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append Write-Output "JAVA_HOME=$(Get-ChildItem -Path 'C:\Program Files\Microsoft\jdk*' | `
Select -First 1 -ExpandProperty FullName)" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append
Write-Output "Java Home: $env:JAVA_HOME" Write-Output "Java Home: $env:JAVA_HOME"
- name: Print environment - name: Print environment
@@ -109,39 +113,43 @@ jobs:
with: with:
fetch-depth: 0 fetch-depth: 0
- name: Decrypt secrets - name: Login to Azure - CI Subscription
env: uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0
DECRYPT_FILE_PASSWORD: ${{ secrets.DECRYPT_FILE_PASSWORD }} with:
run: | creds: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }}
mkdir -p ~/secrets
gpg --quiet --batch --yes --decrypt --passphrase="$DECRYPT_FILE_PASSWORD" \ - name: Download secrets
--output ./${{ env.main_app_folder_path }}/app_play-keystore.jks ./.github/secrets/app_play-keystore.jks.gpg env:
gpg --quiet --batch --yes --decrypt --passphrase="$DECRYPT_FILE_PASSWORD" \ ACCOUNT_NAME: bitwardenci
--output ./${{ env.main_app_folder_path }}/app_upload-keystore.jks ./.github/secrets/app_upload-keystore.jks.gpg CONTAINER_NAME: mobile
gpg --quiet --batch --yes --decrypt --passphrase="$DECRYPT_FILE_PASSWORD" \ run: |
--output $HOME/secrets/play_creds.json ./.github/secrets/play_creds.json.gpg mkdir -p $HOME/secrets
az storage blob download --account-name $ACCOUNT_NAME --container-name $CONTAINER_NAME \
--name app_play-keystore.jks --file ./${{ env.android_folder_path_bash }}/app_play-keystore.jks --output none
az storage blob download --account-name $ACCOUNT_NAME --container-name $CONTAINER_NAME \
--name app_upload-keystore.jks --file ./${{ env.android_folder_path_bash }}/app_upload-keystore.jks --output none
az storage blob download --account-name $ACCOUNT_NAME --container-name $CONTAINER_NAME \
--name play_creds.json --file $HOME/secrets/play_creds.json --output none
shell: bash shell: bash
- name: Decrypt secrets - Google Services - name: Download secrets - Google Services
if: ${{ matrix.variant == 'prod' }} if: ${{ matrix.variant == 'prod' }}
env: env:
DECRYPT_FILE_PASSWORD: ${{ secrets.DECRYPT_FILE_PASSWORD }} ACCOUNT_NAME: bitwardenci
CONTAINER_NAME: mobile
run: | run: |
gpg --quiet --batch --yes --decrypt --passphrase="$DECRYPT_FILE_PASSWORD" \ az storage blob download --account-name $ACCOUNT_NAME --container-name $CONTAINER_NAME \
--output ./${{ env.android_folder_path }}/google-services.json ./.github/secrets/google-services.json.gpg --name google-services.json --file ./${{ env.android_folder_path_bash }}/google-services.json --output none
shell: bash shell: bash
- name: Increment version - name: Increment version
run: | run: |
BUILD_NUMBER=$((3000 + $GITHUB_RUN_NUMBER)) BUILD_NUMBER=$((3000 + $GITHUB_RUN_NUMBER))
echo "##### Setting Android Version Code to $BUILD_NUMBER" | tee -a $GITHUB_STEP_SUMMARY
echo "########################################"
echo "##### Setting Version Code $BUILD_NUMBER"
echo "########################################"
sed -i "s/android:versionCode=\"1\"/android:versionCode=\"$BUILD_NUMBER\"/" \ sed -i "s/android:versionCode=\"1\"/android:versionCode=\"$BUILD_NUMBER\"/" \
./${{ env.android_folder_path }}/AndroidManifest.xml ./${{ env.android_folder_path_bash }}/AndroidManifest.xml
shell: bash shell: bash
- name: Restore packages - name: Restore packages
@@ -150,78 +158,70 @@ jobs:
- name: Restore tools - name: Restore tools
run: dotnet tool restore run: dotnet tool restore
# - name: Verify Format # - name: Run Core tests
# run: dotnet tool run dotnet-format --check # run: |
# dotnet test test/Core.Test/Core.Test.csproj --logger "trx;LogFileName=test-results.trx" `
# /p:CustomConstants=UT
- name: Run Core tests # - name: Report test results
run: dotnet test test/Core.Test/Core.Test.csproj --logger "trx;LogFileName=test-results.trx" /p:CustomConstants=UT # uses: dorny/test-reporter@eaa763f6ffc21c7a37837f56cd5f9737f27fc6c8 # v1.8.0
# if: always()
- name: Report test results # with:
uses: dorny/test-reporter@eaa763f6ffc21c7a37837f56cd5f9737f27fc6c8 # v1.8.0 # name: Test Results
if: always() # path: "**/test-results.trx"
with: # reporter: dotnet-trx
name: Test Results # fail-on-error: true
path: "**/test-results.trx"
reporter: dotnet-trx
fail-on-error: true
- name: Build Play Store publisher - name: Build Play Store publisher
if: ${{ matrix.variant == 'prod' }} if: ${{ matrix.variant == 'prod' }}
run: dotnet build ./store/google/Publisher/Publisher.csproj -p:Configuration=Release run: dotnet build .\store\google\Publisher\Publisher.csproj /p:Configuration=Release
- name: Setup Android build (${{ matrix.variant }}) - name: Setup Android build (${{ matrix.variant }})
run: dotnet cake build.cake --target Android --variant ${{ matrix.variant }} run: dotnet cake build.cake --target Android --variant ${{ matrix.variant }}
- name: Build Android - name: Build & Sign Android
run: |
$configuration = "Release";
$projToBuild = $($env:GITHUB_WORKSPACE + "/${{ env.main_app_project_path }}");
Write-Output "########################################"
Write-Output "##### Build $configuration Configuration"
Write-Output "########################################"
dotnet build $projToBuild -c $configuration -f ${{ env.target-net-version }}-android
- name: Sign Android Build
env: env:
PLAY_KEYSTORE_PASSWORD: ${{ secrets.PLAY_KEYSTORE_PASSWORD }} PLAY_KEYSTORE_PASSWORD: ${{ secrets.PLAY_KEYSTORE_PASSWORD }}
UPLOAD_KEYSTORE_PASSWORD: ${{ secrets.UPLOAD_KEYSTORE_PASSWORD }} UPLOAD_KEYSTORE_PASSWORD: ${{ secrets.UPLOAD_KEYSTORE_PASSWORD }}
run: | run: |
$projToBuild = $($env:GITHUB_WORKSPACE + "/${{ env.main_app_project_path }}"); $projToBuild = "$($env:GITHUB_WORKSPACE)/${{ env.main_app_project_path }}";
$packageName = "com.x8bit.bitwarden"; $packageName = "com.x8bit.bitwarden";
if ("${{ matrix.variant }}" -ne "prod") if ("${{ matrix.variant }}" -ne "prod")
{ {
$packageName = "com.x8bit.bitwarden.${{ matrix.variant }}"; $packageName = "com.x8bit.bitwarden.${{ matrix.variant }}";
} }
Write-Output "########################################"
Write-Output "##### Sign Google Play Bundle Release Configuration" Write-Output "##### Sign Google Play Bundle Release Configuration"
Write-Output "########################################"
dotnet publish $projToBuild -c Release -f ${{ env.target-net-version }}-android /p:AndroidPackageFormats=aab /p:AndroidKeyStore=true /p:AndroidSigningKeyStore=$("app_upload-keystore.jks") /p:AndroidSigningKeyAlias=upload /p:AndroidSigningKeyPass="$($env:UPLOAD_KEYSTORE_PASSWORD)" /p:AndroidSigningStorePass="$($env:UPLOAD_KEYSTORE_PASSWORD)" --no-restore $signingUploadKeyStore = "$($env:GITHUB_WORKSPACE)\${{ env.android_folder_path }}\app_upload-keystore.jks"
dotnet publish $projToBuild -c Release -f ${{ env.target-net-version }}-android `
/p:AndroidPackageFormats=aab `
/p:AndroidKeyStore=true `
/p:AndroidSigningKeyStore=$signingUploadKeyStore `
/p:AndroidSigningKeyAlias=upload `
/p:AndroidSigningKeyPass="$($env:UPLOAD_KEYSTORE_PASSWORD)" `
/p:AndroidSigningStorePass="$($env:UPLOAD_KEYSTORE_PASSWORD)" --no-restore
Write-Output "########################################"
Write-Output "##### Copy Google Play Bundle to project root" Write-Output "##### Copy Google Play Bundle to project root"
Write-Output "########################################"
$signedAabPath = $($env:GITHUB_WORKSPACE + "/${{ env.main_app_folder_path }}/bin/Release/${{ env.target-net-version }}-android/publish/$($packageName)-Signed.aab"); $signedAabPath = "$($env:GITHUB_WORKSPACE)\${{ env.main_app_folder_path }}\bin\Release\${{ env.target-net-version }}-android\publish\$($packageName)-Signed.aab";
$signedAabDestPath = $($env:GITHUB_WORKSPACE + "/$($packageName).aab"); $signedAabDestPath = "$($env:GITHUB_WORKSPACE)\$($packageName).aab";
Copy-Item $signedAabPath $signedAabDestPath Copy-Item $signedAabPath $signedAabDestPath
Write-Output "########################################"
Write-Output "##### Sign APK Release Configuration" Write-Output "##### Sign APK Release Configuration"
Write-Output "########################################"
dotnet publish $projToBuild -c Release -f ${{ env.target-net-version }}-android /p:AndroidKeyStore=true /p:AndroidSigningKeyStore=$("app_play-keystore.jks") /p:AndroidSigningKeyAlias=bitwarden /p:AndroidSigningKeyPass="$($env:PLAY_KEYSTORE_PASSWORD)" /p:AndroidSigningStorePass="$($env:PLAY_KEYSTORE_PASSWORD)" --no-restore $signingPlayKeyStore = "$($env:GITHUB_WORKSPACE)\${{ env.android_folder_path }}\app_play-keystore.jks"
dotnet publish $projToBuild -c Release -f ${{ env.target-net-version }}-android `
/p:AndroidKeyStore=true `
/p:AndroidSigningKeyStore=$signingPlayKeyStore `
/p:AndroidSigningKeyAlias=bitwarden `
/p:AndroidSigningKeyPass="$($env:PLAY_KEYSTORE_PASSWORD)" `
/p:AndroidSigningStorePass="$($env:PLAY_KEYSTORE_PASSWORD)" --no-restore
Write-Output "########################################"
Write-Output "##### Copy Release APK to project root" Write-Output "##### Copy Release APK to project root"
Write-Output "########################################"
$signedApkPath = $($env:GITHUB_WORKSPACE + "/${{ env.main_app_folder_path }}/bin/Release/${{ env.target-net-version }}-android/publish/$($packageName)-Signed.apk");
$signedApkDestPath = $($env:GITHUB_WORKSPACE + "/$($packageName).apk");
$signedApkPath = "$($env:GITHUB_WORKSPACE)\${{ env.main_app_folder_path }}\bin\Release\${{ env.target-net-version }}-android\publish\$($packageName)-Signed.apk";
$signedApkDestPath = "$($env:GITHUB_WORKSPACE)\$($packageName).apk";
Copy-Item $signedApkPath $signedApkDestPath Copy-Item $signedApkPath $signedApkDestPath
- name: Upload Prod .aab artifact - name: Upload Prod .aab artifact
@@ -283,20 +283,20 @@ jobs:
|| (github.ref == 'refs/heads/rc' && needs.setup.outputs.hotfix_branch_exists == 0) || (github.ref == 'refs/heads/rc' && needs.setup.outputs.hotfix_branch_exists == 0)
|| github.ref == 'refs/heads/hotfix-rc' ) }} || github.ref == 'refs/heads/hotfix-rc' ) }}
run: | run: |
PUBLISHER_PATH="$GITHUB_WORKSPACE/store/google/Publisher/bin/Release/net7.0/Publisher.dll" $publisherPath = "$($env:GITHUB_WORKSPACE)\store\google\Publisher\bin\Release\net8.0\Publisher.dll"
CREDS_PATH="$HOME/secrets/play_creds.json" $credsPath = "$($HOME)\secrets\play_creds.json"
AAB_PATH="$GITHUB_WORKSPACE/com.x8bit.bitwarden.aab" $aabPath = "$($env:GITHUB_WORKSPACE)\com.x8bit.bitwarden.aab"
TRACK="internal" $track = "internal"
dotnet $PUBLISHER_PATH $CREDS_PATH $AAB_PATH $TRACK dotnet $publisherPath $credsPath $aabPath $track
shell: bash
f-droid: f-droid:
name: F-Droid Build name: F-Droid Build
runs-on: windows-2022 runs-on: windows-2022
env: env:
android_folder_path: src/App/Platforms/Android android_folder_path: src\App\Platforms\Android
android_folder_path_bash: src/App/Platforms/Android
android_manifest_path: src/App/Platforms/Android/AndroidManifest.xml android_manifest_path: src/App/Platforms/Android/AndroidManifest.xml
steps: steps:
- name: Setup NuGet - name: Setup NuGet
@@ -305,7 +305,7 @@ jobs:
nuget-version: 6.4.0 nuget-version: 6.4.0
- name: Set up .NET - name: Set up .NET
uses: actions/setup-dotnet@3447fd6a9f9e57506b15f895c5b76d3b197dc7c2 # v3.2.0 uses: actions/setup-dotnet@4d6c8fcf3c8f7a60068d26b594648e99df24cee3 # v4.0.0
with: with:
dotnet-version: '8.0.x' dotnet-version: '8.0.x'
@@ -337,23 +337,25 @@ jobs:
- name: Checkout repo - name: Checkout repo
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
- name: Decrypt secrets - name: Login to Azure - CI Subscription
env: uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0
DECRYPT_FILE_PASSWORD: ${{ secrets.DECRYPT_FILE_PASSWORD }} with:
run: | creds: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }}
mkdir -p ~/secrets
gpg --quiet --batch --yes --decrypt --passphrase="$DECRYPT_FILE_PASSWORD" \ - name: Download secrets
--output ./${{ env.main_app_folder_path }}/app_fdroid-keystore.jks ./.github/secrets/app_fdroid-keystore.jks.gpg env:
ACCOUNT_NAME: bitwardenci
CONTAINER_NAME: mobile
FILE: app_fdroid-keystore.jks
run: |
az storage blob download --account-name $ACCOUNT_NAME --container-name $CONTAINER_NAME --name $FILE \
--file ${{ env.android_folder_path_bash }}/$FILE --output none
shell: bash shell: bash
- name: Increment version - name: Increment version
run: | run: |
BUILD_NUMBER=$((3000 + $GITHUB_RUN_NUMBER)) BUILD_NUMBER=$((3000 + $GITHUB_RUN_NUMBER))
echo "##### Setting F-Droid Version Code to $BUILD_NUMBER" | tee -a $GITHUB_STEP_SUMMARY
echo "########################################"
echo "##### Setting Version Code $BUILD_NUMBER"
echo "########################################"
sed -i "s/android:versionCode=\"1\"/android:versionCode=\"$BUILD_NUMBER\"/" \ sed -i "s/android:versionCode=\"1\"/android:versionCode=\"$BUILD_NUMBER\"/" \
./${{ env.android_manifest_path }} ./${{ env.android_manifest_path }}
@@ -361,21 +363,16 @@ jobs:
- name: Clean for F-Droid - name: Clean for F-Droid
run: | run: |
$appPath = $($env:GITHUB_WORKSPACE + "/${{ env.main_app_project_path }}"); $directoryBuildProps = $($env:GITHUB_WORKSPACE + "/Directory.Build.props");
$corePath = $($env:GITHUB_WORKSPACE + "/src/Core/Core.csproj");
$androidManifest = $($env:GITHUB_WORKSPACE + "/${{ env.android_manifest_path }}"); $androidManifest = $($env:GITHUB_WORKSPACE + "/${{ env.android_manifest_path }}");
Write-Output "########################################" Write-Output "##### Back up project files"
Write-Output "##### Backup project files"
Write-Output "########################################"
Copy-Item $androidManifest $($androidManifest + ".original"); Copy-Item $androidManifest $($androidManifest + ".original");
Copy-Item $appPath $($appPath + ".original"); Copy-Item $directoryBuildProps $($directoryBuildProps + ".original");
Write-Output "########################################"
Write-Output "##### Cleanup Android Manifest" Write-Output "##### Cleanup Android Manifest"
Write-Output "########################################"
$xml=New-Object XML; $xml=New-Object XML;
$xml.Load($androidManifest); $xml.Load($androidManifest);
@@ -385,39 +382,34 @@ jobs:
$xml.Save($androidManifest); $xml.Save($androidManifest);
Write-Output "##### Enabling FDROID constant"
(Get-Content $directoryBuildProps).Replace('<!-- <CustomConstants>FDROID</CustomConstants> -->', '<CustomConstants>FDROID</CustomConstants>') | Set-Content $directoryBuildProps
- name: Restore packages - name: Restore packages
run: dotnet restore run: dotnet restore
- name: Build for F-Droid - name: Build & Sign F-Droid
run: |
$configuration = "Release";
$projToBuild = $($env:GITHUB_WORKSPACE + "/${{ env.main_app_project_path }}");
Write-Output "########################################"
Write-Output "##### Build $configuration FDROID
Write-Output "########################################"
dotnet build $projToBuild -c $configuration -f ${{ env.target-net-version }}-android /p:CustomConstants="FDROID"
- name: Sign for F-Droid
env: env:
FDROID_KEYSTORE_PASSWORD: ${{ secrets.FDROID_KEYSTORE_PASSWORD }} FDROID_KEYSTORE_PASSWORD: ${{ secrets.FDROID_KEYSTORE_PASSWORD }}
run: | run: |
$projToBuild = $($env:GITHUB_WORKSPACE + "/${{ env.main_app_project_path }}"); $projToBuild = "$($env:GITHUB_WORKSPACE)\${{ env.main_app_project_path }}";
$packageName = "com.x8bit.bitwarden"; $packageName = "com.x8bit.bitwarden";
Write-Output "########################################"
Write-Output "##### Sign FDroid" Write-Output "##### Sign FDroid"
Write-Output "########################################"
dotnet publish $projToBuild -c Release -f ${{ env.target-net-version }}-android /p:AndroidKeyStore=true /p:AndroidSigningKeyStore=$("app_fdroid-keystore.jks") /p:AndroidSigningKeyAlias=bitwarden /p:AndroidSigningKeyPass="$($env:FDROID_KEYSTORE_PASSWORD)" /p:AndroidSigningStorePass="$($env:FDROID_KEYSTORE_PASSWORD)" /p:CustomConstants="FDROID" --no-restore $signingFdroidKeyStore = "$($env:GITHUB_WORKSPACE)\${{ env.android_folder_path }}\app_fdroid-keystore.jks"
dotnet build $projToBuild -c Release -f ${{ env.target-net-version }}-android `
/p:AndroidKeyStore=true `
/p:AndroidSigningKeyStore=$signingFdroidKeyStore `
/p:AndroidSigningKeyAlias=bitwarden `
/p:AndroidSigningKeyPass="$($env:FDROID_KEYSTORE_PASSWORD)" `
/p:AndroidSigningStorePass="$($env:FDROID_KEYSTORE_PASSWORD)" ` --no-restore
Write-Output "########################################"
Write-Output "##### Copy FDroid apk to project root" Write-Output "##### Copy FDroid apk to project root"
Write-Output "########################################"
$signedApkPath = $($env:GITHUB_WORKSPACE + "/${{ env.main_app_folder_path }}/bin/Release/${{ env.target-net-version }}-android/publish/$($packageName)-Signed.apk"); $signedApkPath = "$($env:GITHUB_WORKSPACE)\${{ env.main_app_folder_path }}\bin\Release\${{ env.target-net-version }}-android\$($packageName)-Signed.apk";
$signedApkDestPath = $($env:GITHUB_WORKSPACE + "/com.x8bit.bitwarden-fdroid.apk"); $signedApkDestPath = "$($env:GITHUB_WORKSPACE)\com.x8bit.bitwarden-fdroid.apk";
Copy-Item $signedApkPath $signedApkDestPath Copy-Item $signedApkPath $signedApkDestPath
@@ -440,6 +432,7 @@ jobs:
path: ./bw-fdroid-apk-sha256.txt path: ./bw-fdroid-apk-sha256.txt
if-no-files-found: error if-no-files-found: error
ios: ios:
name: Apple iOS name: Apple iOS
runs-on: macos-13 runs-on: macos-13
@@ -460,7 +453,7 @@ jobs:
nuget-version: 6.4.0 nuget-version: 6.4.0
- name: Set up .NET - name: Set up .NET
uses: actions/setup-dotnet@3447fd6a9f9e57506b15f895c5b76d3b197dc7c2 # v3.2.0 uses: actions/setup-dotnet@4d6c8fcf3c8f7a60068d26b594648e99df24cee3 # v4.0.0
with: with:
dotnet-version: '8.0.x' dotnet-version: '8.0.x'
@@ -493,43 +486,41 @@ jobs:
keyvault: "bitwarden-ci" keyvault: "bitwarden-ci"
secrets: "appcenter-ios-token" secrets: "appcenter-ios-token"
- name: Decrypt secrets - name: Download Provisioning Profiles secrets
env: env:
DECRYPT_FILE_PASSWORD: ${{ secrets.DECRYPT_FILE_PASSWORD }} ACCOUNT_NAME: bitwardenci
CONTAINER_NAME: profiles
run: | run: |
mkdir -p ~/secrets mkdir -p $HOME/secrets
profiles=(
"dist_autofill.mobileprovision"
"dist_bitwarden.mobileprovision"
"dist_extension.mobileprovision"
"dist_share_extension.mobileprovision"
"dist_bitwarden_watch_app.mobileprovision"
"dist_bitwarden_watch_app_extension.mobileprovision"
)
gpg --quiet --batch --yes --decrypt --passphrase="$DECRYPT_FILE_PASSWORD" \ for FILE in "${profiles[@]}"
--output $HOME/secrets/bitwarden-mobile-key.p12 ./.github/secrets/bitwarden-mobile-key.p12.gpg do
gpg --quiet --batch --yes --decrypt --passphrase="$DECRYPT_FILE_PASSWORD" \ az storage blob download --account-name $ACCOUNT_NAME --container-name $CONTAINER_NAME --name $FILE \
--output $HOME/secrets/iphone-distribution-cert.p12 ./.github/secrets/iphone-distribution-cert.p12.gpg --file $HOME/secrets/$FILE --output none
gpg --quiet --batch --yes --decrypt --passphrase="$DECRYPT_FILE_PASSWORD" \ done
--output $HOME/secrets/dist_autofill.mobileprovision ./.github/secrets/dist_autofill.mobileprovision.gpg
gpg --quiet --batch --yes --decrypt --passphrase="$DECRYPT_FILE_PASSWORD" \ - name: Download Google Services secret
--output $HOME/secrets/dist_bitwarden.mobileprovision ./.github/secrets/dist_bitwarden.mobileprovision.gpg env:
gpg --quiet --batch --yes --decrypt --passphrase="$DECRYPT_FILE_PASSWORD" \ ACCOUNT_NAME: bitwardenci
--output $HOME/secrets/dist_extension.mobileprovision ./.github/secrets/dist_extension.mobileprovision.gpg CONTAINER_NAME: mobile
gpg --quiet --batch --yes --decrypt --passphrase="$DECRYPT_FILE_PASSWORD" \ FILE: GoogleService-Info.plist
--output $HOME/secrets/dist_share_extension.mobileprovision \ run: |
./.github/secrets/dist_share_extension.mobileprovision.gpg mkdir -p $HOME/secrets
gpg --quiet --batch --yes --decrypt --passphrase="$DECRYPT_FILE_PASSWORD" \ az storage blob download --account-name $ACCOUNT_NAME --container-name $CONTAINER_NAME --name $FILE \
--output $HOME/secrets/dist_watch_app.mobileprovision \ --file src/watchOS/bitwarden/$FILE --output none
./.github/secrets/dist_watch_app.mobileprovision.gpg
gpg --quiet --batch --yes --decrypt --passphrase="$DECRYPT_FILE_PASSWORD" \
--output $HOME/secrets/dist_watch_app_extension.mobileprovision \
./.github/secrets/dist_watch_app_extension.mobileprovision.gpg
gpg --quiet --batch --yes --decrypt --passphrase="$DECRYPT_FILE_PASSWORD" \
--output ./src/watchOS/bitwarden/GoogleService-Info.plist ./.github/secrets/GoogleService-Info.plist.gpg
- name: Increment version - name: Increment version
run: | run: |
BUILD_NUMBER=$((100 + $GITHUB_RUN_NUMBER)) BUILD_NUMBER=$((100 + $GITHUB_RUN_NUMBER))
echo "##### Setting iOS CFBundleVersion to $BUILD_NUMBER" | tee -a $GITHUB_STEP_SUMMARY
echo "########################################"
echo "##### Setting CFBundleVersion $BUILD_NUMBER"
echo "########################################"
echo "### CFBundleVersion $BUILD_NUMBER" >> $GITHUB_STEP_SUMMARY
perl -0777 -pi.bak -e 's/<key>CFBundleVersion<\/key>\s*<string>1<\/string>/<key>CFBundleVersion<\/key>\n\t<string>'"$BUILD_NUMBER"'<\/string>/' ./${{ env.ios_folder_path }}/Info.plist perl -0777 -pi.bak -e 's/<key>CFBundleVersion<\/key>\s*<string>1<\/string>/<key>CFBundleVersion<\/key>\n\t<string>'"$BUILD_NUMBER"'<\/string>/' ./${{ env.ios_folder_path }}/Info.plist
perl -0777 -pi.bak -e 's/<key>CFBundleVersion<\/key>\s*<string>1<\/string>/<key>CFBundleVersion<\/key>\n\t<string>'"$BUILD_NUMBER"'<\/string>/' ./src/iOS.Extension/Info.plist perl -0777 -pi.bak -e 's/<key>CFBundleVersion<\/key>\s*<string>1<\/string>/<key>CFBundleVersion<\/key>\n\t<string>'"$BUILD_NUMBER"'<\/string>/' ./src/iOS.Extension/Info.plist
@@ -540,26 +531,26 @@ jobs:
- name: Update Entitlements - name: Update Entitlements
run: | run: |
echo "########################################"
echo "##### Updating Entitlements" echo "##### Updating Entitlements"
echo "########################################"
perl -0777 -pi.bak -e 's/<key>aps-environment<\/key>\s*<string>development<\/string>/<key>aps-environment<\/key>\n\t<string>production<\/string>/' ./${{ env.ios_folder_path }}/Entitlements.plist perl -0777 -pi.bak -e 's/<key>aps-environment<\/key>\s*<string>development<\/string>/<key>aps-environment<\/key>\n\t<string>production<\/string>/' ./${{ env.ios_folder_path }}/Entitlements.plist
- name: Get certificates
run: |
mkdir -p $HOME/certificates
az keyvault secret show --id https://bitwarden-ci.vault.azure.net/certificates/ios-distribution |
jq -r .value | base64 -d > $HOME/certificates/ios-distribution.p12
- name: Set up Keychain - name: Set up Keychain
env: env:
KEYCHAIN_PASSWORD: ${{ secrets.IOS_KEYCHAIN_PASSWORD }} KEYCHAIN_PASSWORD: ${{ secrets.IOS_KEYCHAIN_PASSWORD }}
MOBILE_KEY_PASSWORD: ${{ secrets.IOS_KEY_PASSWORD }}
DIST_CERT_PASSWORD: ${{ secrets.IOS_DIST_CERT_PASSWORD }}
run: | run: |
security create-keychain -p $KEYCHAIN_PASSWORD build.keychain security create-keychain -p $KEYCHAIN_PASSWORD build.keychain
security default-keychain -s build.keychain security default-keychain -s build.keychain
security unlock-keychain -p $KEYCHAIN_PASSWORD build.keychain security unlock-keychain -p $KEYCHAIN_PASSWORD build.keychain
security set-keychain-settings -lut 1200 build.keychain security set-keychain-settings -lut 1200 build.keychain
security import ~/secrets/bitwarden-mobile-key.p12 -k build.keychain -P $MOBILE_KEY_PASSWORD \
-T /usr/bin/codesign -T /usr/bin/security security import $HOME/certificates/ios-distribution.p12 -k build.keychain -P "" -T /usr/bin/codesign \
security import ~/secrets/iphone-distribution-cert.p12 -k build.keychain -P $DIST_CERT_PASSWORD \ -T /usr/bin/security
-T /usr/bin/codesign -T /usr/bin/security
security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k $KEYCHAIN_PASSWORD build.keychain security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k $KEYCHAIN_PASSWORD build.keychain
- name: Set up provisioning profiles - name: Set up provisioning profiles
@@ -568,8 +559,8 @@ jobs:
BITWARDEN_PROFILE_PATH=$HOME/secrets/dist_bitwarden.mobileprovision BITWARDEN_PROFILE_PATH=$HOME/secrets/dist_bitwarden.mobileprovision
EXTENSION_PROFILE_PATH=$HOME/secrets/dist_extension.mobileprovision EXTENSION_PROFILE_PATH=$HOME/secrets/dist_extension.mobileprovision
SHARE_EXTENSION_PROFILE_PATH=$HOME/secrets/dist_share_extension.mobileprovision SHARE_EXTENSION_PROFILE_PATH=$HOME/secrets/dist_share_extension.mobileprovision
WATCH_APP_PROFILE_PATH=$HOME/secrets/dist_watch_app.mobileprovision WATCH_APP_PROFILE_PATH=$HOME/secrets/dist_bitwarden_watch_app.mobileprovision
WATCH_APP_EXTENSION_PROFILE_PATH=$HOME/secrets/dist_watch_app_extension.mobileprovision WATCH_APP_EXTENSION_PROFILE_PATH=$HOME/secrets/dist_bitwarden_watch_app_extension.mobileprovision
PROFILES_DIR_PATH=$HOME/Library/MobileDevice/Provisioning\ Profiles PROFILES_DIR_PATH=$HOME/Library/MobileDevice/Provisioning\ Profiles
mkdir -p "$PROFILES_DIR_PATH" mkdir -p "$PROFILES_DIR_PATH"
@@ -597,68 +588,44 @@ jobs:
- name: Bulid WatchApp - name: Bulid WatchApp
run: | run: |
echo "########################################"
echo "##### Build WatchApp with Release Configuration" echo "##### Build WatchApp with Release Configuration"
echo "########################################"
xcodebuild archive -workspace ./src/watchOS/bitwarden/bitwarden.xcodeproj/project.xcworkspace -configuration Release -scheme bitwarden\ WatchKit\ App -archivePath ./src/watchOS/bitwarden xcodebuild archive -workspace ./src/watchOS/bitwarden/bitwarden.xcodeproj/project.xcworkspace -configuration Release -scheme bitwarden\ WatchKit\ App -archivePath ./src/watchOS/bitwarden
echo "########################################"
echo "##### Done"
echo "########################################"
- name: Archive Build for App Store - name: Archive Build for App Store
run: | run: |
Write-Output "########################################" echo "##### Archive for Release ios-arm64"
Write-Output "##### Archive for Release ios-arm64
Write-Output "########################################"
dotnet publish ${{ env.main_app_project_path }} -c Release -f ${{ env.target-net-version }}-ios /p:RuntimeIdentifier=ios-arm64 /p:ArchiveOnBuild=true /p:MtouchUseLlvm=false dotnet publish ${{ env.main_app_project_path }} -c Release -f ${{ env.target-net-version }}-ios /p:RuntimeIdentifier=ios-arm64 /p:ArchiveOnBuild=true /p:MtouchUseLlvm=false
Write-Output "########################################"
Write-Output "##### Done"
Write-Output "########################################"
shell: pwsh
- name: Archive Build for Mobile Automation - name: Archive Build for Mobile Automation
run: | run: |
Write-Output "########################################" echo "##### Archive Debug for iossimulator-x64"
Write-Output "##### Archive Debug for iossimulator-x64
Write-Output "########################################"
dotnet build ${{ env.main_app_project_path }} -c Debug -f ${{ env.target-net-version }}-ios /p:RuntimeIdentifier=iossimulator-x64 /p:ArchiveOnBuild=true /p:MtouchUseLlvm=false dotnet build ${{ env.main_app_project_path }} -c Debug -f ${{ env.target-net-version }}-ios /p:RuntimeIdentifier=iossimulator-x64 /p:ArchiveOnBuild=true /p:MtouchUseLlvm=false
ls $HOME/Library/Developer/Xcode/Archives
Write-Output "########################################"
Write-Output "##### Done"
Write-Output "########################################"
ls ~/Library/Developer/Xcode/Archives
shell: pwsh
- name: Export .ipa for App Store - name: Export .ipa for App Store
env:
EXPORT_OPTIONS_PATH: ./.github/resources/export-options-app-store.plist
EXPORT_PATH: ./bitwarden-export
run: | run: |
EXPORT_OPTIONS_PATH="./.github/resources/export-options-app-store.plist"
ARCHIVE_PATH="$HOME/Library/Developer/Xcode/Archives/*/*.xcarchive" ARCHIVE_PATH="$HOME/Library/Developer/Xcode/Archives/*/*.xcarchive"
EXPORT_PATH="./bitwarden-export"
xcodebuild -exportArchive -archivePath $ARCHIVE_PATH -exportPath $EXPORT_PATH \ xcodebuild -exportArchive -archivePath $ARCHIVE_PATH -exportPath $EXPORT_PATH \
-exportOptionsPlist $EXPORT_OPTIONS_PATH -exportOptionsPlist $EXPORT_OPTIONS_PATH
- name: Export .app for Automation CI - name: Export .app for Automation CI
env:
ARCHIVE_PATH: ./${{ env.main_app_folder_path }}/bin/Debug/${{ env.target-net-version }}-ios/iossimulator-x64
EXPORT_PATH: ./bitwarden-export
run: | run: |
ARCHIVE_PATH="./${{ env.main_app_folder_path }}/bin/Debug/${{ env.target-net-version }}-ios/iossimulator-x64"
EXPORT_PATH="./bitwarden-export"
zip -r -q ${{ env.app_ci_output_filename }}.app.zip $ARCHIVE_PATH zip -r -q ${{ env.app_ci_output_filename }}.app.zip $ARCHIVE_PATH
mv ${{ env.app_ci_output_filename }}.app.zip $EXPORT_PATH mv ${{ env.app_ci_output_filename }}.app.zip $EXPORT_PATH
- name: Copy all dSYMs files to upload - name: Copy all dSYMs files to upload
env:
EXPORT_PATH: ./bitwarden-export
WATCH_ARCHIVE_DSYMS_PATH: ./src/watchOS/bitwarden.xcarchive/dSYMs/
WATCH_DSYMS_EXPORT_PATH: ./bitwarden-export/Watch_dSYMs
run: | run: |
ARCHIVE_DSYMS_PATH="$HOME/Library/Developer/Xcode/Archives/*/*.xcarchive/dSYMs" ARCHIVE_DSYMS_PATH="$HOME/Library/Developer/Xcode/Archives/*/*.xcarchive/dSYMs"
EXPORT_PATH="./bitwarden-export"
WATCH_ARCHIVE_DSYMS_PATH="./src/watchOS/bitwarden.xcarchive/dSYMs/"
WATCH_DSYMS_EXPORT_PATH="$EXPORT_PATH/Watch_dSYMs"
cp -r -v $ARCHIVE_DSYMS_PATH $EXPORT_PATH cp -r -v $ARCHIVE_DSYMS_PATH $EXPORT_PATH
mkdir $WATCH_DSYMS_EXPORT_PATH mkdir $WATCH_DSYMS_EXPORT_PATH
cp -r -v $WATCH_ARCHIVE_DSYMS_PATH $WATCH_DSYMS_EXPORT_PATH cp -r -v $WATCH_ARCHIVE_DSYMS_PATH $WATCH_DSYMS_EXPORT_PATH
@@ -707,10 +674,7 @@ jobs:
|| (github.ref == 'refs/heads/rc' && needs.setup.outputs.hotfix_branch_exists == 0) || (github.ref == 'refs/heads/rc' && needs.setup.outputs.hotfix_branch_exists == 0)
|| github.ref == 'refs/heads/hotfix-rc' || github.ref == 'refs/heads/hotfix-rc'
run: | run: |
echo "########################################"
echo "##### Uploading Watch dSYMs to Firebase" echo "##### Uploading Watch dSYMs to Firebase"
echo "########################################"
find "$HOME/Library/Developer/XCode/DerivedData" -name "upload-symbols" -exec chmod +x {} \; -exec {} -gsp "./src/watchOS/bitwarden/GoogleService-Info.plist" -p ios "./bitwarden-export/Watch_dSYMs" \; find "$HOME/Library/Developer/XCode/DerivedData" -name "upload-symbols" -exec chmod +x {} \; -exec {} -gsp "./src/watchOS/bitwarden/GoogleService-Info.plist" -p ios "./bitwarden-export/Watch_dSYMs" \;
- name: Validate app in App Store - name: Validate app in App Store
@@ -726,7 +690,6 @@ jobs:
run: | run: |
xcrun altool --validate-app --type ios --file "./bitwarden-export/Bitwarden.ipa" \ xcrun altool --validate-app --type ios --file "./bitwarden-export/Bitwarden.ipa" \
--username "$APPLE_ID_USERNAME" --password "$APPLE_ID_PASSWORD" --username "$APPLE_ID_USERNAME" --password "$APPLE_ID_PASSWORD"
shell: bash
- name: Deploy to App Store - name: Deploy to App Store
if: | if: |
@@ -770,7 +733,7 @@ jobs:
secrets: "crowdin-api-token" secrets: "crowdin-api-token"
- name: Upload Sources - name: Upload Sources
uses: crowdin/github-action@198daeb2d30636c4608d6a6bb96c009dbefc02a2 # v1.18.0 uses: crowdin/github-action@67705afb6985401459cd143d5f5f00c9dc212f23 # v1.20.2
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
CROWDIN_API_TOKEN: ${{ steps.retrieve-secrets.outputs.crowdin-api-token }} CROWDIN_API_TOKEN: ${{ steps.retrieve-secrets.outputs.crowdin-api-token }}
@@ -794,27 +757,11 @@ jobs:
steps: steps:
- name: Check if any job failed - name: Check if any job failed
if: | if: |
(github.ref == 'refs/heads/main') (github.ref == 'refs/heads/main'
|| (github.ref == 'refs/heads/rc') || github.ref == 'refs/heads/rc'
|| (github.ref == 'refs/heads/hotfix-rc') || github.ref == 'refs/heads/hotfix-rc')
env: && contains(needs.*.result, 'failure')
CLOC_STATUS: ${{ needs.cloc.result }} run: exit 1
ANDROID_STATUS: ${{ needs.android.result }}
F_DROID_STATUS: ${{ needs.f-droid.result }}
IOS_STATUS: ${{ needs.ios.result }}
CROWDIN_PUSH_STATUS: ${{ needs.crowdin-push.result }}
run: |
if [ "$CLOC_STATUS" = "failure" ]; then
exit 1
elif [ "$ANDROID_STATUS" = "failure" ]; then
exit 1
elif [ "$F_DROID_STATUS" = "failure" ]; then
exit 1
elif [ "$IOS_STATUS" = "failure" ]; then
exit 1
elif [ "$CROWDIN_PUSH_STATUS" = "failure" ]; then
exit 1
fi
- name: Login to Azure - CI Subscription - name: Login to Azure - CI Subscription
uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0 uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0
@@ -831,7 +778,7 @@ jobs:
secrets: "devops-alerts-slack-webhook-url" secrets: "devops-alerts-slack-webhook-url"
- name: Notify Slack on failure - name: Notify Slack on failure
uses: act10ns/slack@ed1309ab9862e57e9e583e51c7889486b9a00b0f # v2.0.0 uses: act10ns/slack@44541246747a30eb3102d87f7a4cc5471b0ffb7d # v2.1.0
if: failure() if: failure()
env: env:
SLACK_WEBHOOK_URL: ${{ steps.retrieve-secrets.outputs.devops-alerts-slack-webhook-url }} SLACK_WEBHOOK_URL: ${{ steps.retrieve-secrets.outputs.devops-alerts-slack-webhook-url }}

53
.github/workflows/cleanup-rc-branch.yml vendored Normal file
View File

@@ -0,0 +1,53 @@
---
name: Cleanup RC Branch
on:
push:
tags:
- v**
jobs:
delete-rc:
name: Delete RC Branch
runs-on: ubuntu-22.04
steps:
- name: Login to Azure - CI Subscription
uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0
with:
creds: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }}
- name: Retrieve bot secrets
id: retrieve-bot-secrets
uses: bitwarden/gh-actions/get-keyvault-secrets@main
with:
keyvault: bitwarden-ci
secrets: "github-pat-bitwarden-devops-bot-repo-scope"
- name: Checkout main
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
with:
ref: main
token: ${{ steps.retrieve-bot-secrets.outputs.github-pat-bitwarden-devops-bot-repo-scope }}
- name: Check if a RC branch exists
id: branch-check
run: |
hotfix_rc_branch_check=$(git ls-remote --heads origin hotfix-rc | wc -l)
rc_branch_check=$(git ls-remote --heads origin rc | wc -l)
if [[ "${hotfix_rc_branch_check}" -gt 0 ]]; then
echo "hotfix-rc branch exists." | tee -a $GITHUB_STEP_SUMMARY
echo "name=hotfix-rc" >> $GITHUB_OUTPUT
elif [[ "${rc_branch_check}" -gt 0 ]]; then
echo "rc branch exists." | tee -a $GITHUB_STEP_SUMMARY
echo "name=rc" >> $GITHUB_OUTPUT
fi
- name: Delete RC branch
env:
BRANCH_NAME: ${{ steps.branch-check.outputs.name }}
run: |
if ! [[ -z "$BRANCH_NAME" ]]; then
git push --quiet origin --delete $BRANCH_NAME
echo "Deleted $BRANCH_NAME branch." | tee -a $GITHUB_STEP_SUMMARY
fi

View File

@@ -15,7 +15,7 @@ jobs:
_CROWDIN_PROJECT_ID: "269690" _CROWDIN_PROJECT_ID: "269690"
steps: steps:
- name: Checkout repo - name: Checkout repo
uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3.6.0 uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
- name: Login to Azure - CI Subscription - name: Login to Azure - CI Subscription
uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0 uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0
@@ -30,7 +30,7 @@ jobs:
secrets: "crowdin-api-token, github-gpg-private-key, github-gpg-private-key-passphrase" secrets: "crowdin-api-token, github-gpg-private-key, github-gpg-private-key-passphrase"
- name: Download translations - name: Download translations
uses: crowdin/github-action@198daeb2d30636c4608d6a6bb96c009dbefc02a2 # v1.18.0 uses: crowdin/github-action@67705afb6985401459cd143d5f5f00c9dc212f23 # v1.20.2
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
CROWDIN_API_TOKEN: ${{ steps.retrieve-secrets.outputs.crowdin-api-token }} CROWDIN_API_TOKEN: ${{ steps.retrieve-secrets.outputs.crowdin-api-token }}

View File

@@ -12,6 +12,6 @@ jobs:
pull-requests: write pull-requests: write
runs-on: ubuntu-20.04 runs-on: ubuntu-20.04
steps: steps:
- uses: actions/labeler@ac9175f8a1f3625fd0d4fb234536d26811351594 # v4.3.0 - uses: actions/labeler@8558fd74291d67161a8a78ce36a881fa63b766a9 # v5.0.0
with: with:
sync-labels: true sync-labels: true

View File

@@ -28,7 +28,7 @@ jobs:
branch-name: ${{ steps.branch.outputs.branch-name }} branch-name: ${{ steps.branch.outputs.branch-name }}
steps: steps:
- name: Branch check - name: Branch check
if: github.event.inputs.release_type != 'Dry Run' if: inputs.release_type != 'Dry Run'
run: | run: |
if [[ "$GITHUB_REF" != "refs/heads/rc" ]] && [[ "$GITHUB_REF" != "refs/heads/hotfix-rc" ]]; then if [[ "$GITHUB_REF" != "refs/heads/rc" ]] && [[ "$GITHUB_REF" != "refs/heads/hotfix-rc" ]]; then
echo "===================================" echo "==================================="
@@ -38,15 +38,15 @@ jobs:
fi fi
- name: Checkout repo - name: Checkout repo
uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3.6.0 uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
- name: Check Release Version - name: Check Release Version
id: version id: version
uses: bitwarden/gh-actions/release-version-check@main uses: bitwarden/gh-actions/release-version-check@main
with: with:
release-type: ${{ github.event.inputs.release_type }} release-type: ${{ inputs.release_type }}
project-type: xamarin project-type: xamarin
file: src/Android/Properties/AndroidManifest.xml file: src/App/Platforms/Android/AndroidManifest.xml
- name: Get branch name - name: Get branch name
id: branch id: branch
@@ -55,7 +55,7 @@ jobs:
echo "branch-name=$BRANCH_NAME" >> $GITHUB_OUTPUT echo "branch-name=$BRANCH_NAME" >> $GITHUB_OUTPUT
- name: Create GitHub deployment - name: Create GitHub deployment
if: ${{ github.event.inputs.release_type != 'Dry Run' }} if: ${{ inputs.release_type != 'Dry Run' }}
uses: chrnorm/deployment-action@55729fcebec3d284f60f5bcabbd8376437d696b1 # v2.0.7 uses: chrnorm/deployment-action@55729fcebec3d284f60f5bcabbd8376437d696b1 # v2.0.7
id: deployment id: deployment
with: with:
@@ -67,16 +67,16 @@ jobs:
- name: Download all artifacts - name: Download all artifacts
if: ${{ github.event.inputs.release_type != 'Dry Run' }} if: ${{ inputs.release_type != 'Dry Run' }}
uses: dawidd6/action-download-artifact@e7466d1a7587ed14867642c2ca74b5bcc1e19a2d # v3.0.0 uses: dawidd6/action-download-artifact@09f2f74827fd3a8607589e5ad7f9398816f540fe # v3.1.4
with: with:
workflow: build.yml workflow: build.yml
workflow_conclusion: success workflow_conclusion: success
branch: ${{ steps.branch.outputs.branch-name }} branch: ${{ steps.branch.outputs.branch-name }}
- name: Dry Run - Download all artifacts - name: Dry Run - Download all artifacts
if: ${{ github.event.inputs.release_type == 'Dry Run' }} if: ${{ inputs.release_type == 'Dry Run' }}
uses: dawidd6/action-download-artifact@e7466d1a7587ed14867642c2ca74b5bcc1e19a2d # v3.0.0 uses: dawidd6/action-download-artifact@09f2f74827fd3a8607589e5ad7f9398816f540fe # v3.1.4
with: with:
workflow: build.yml workflow: build.yml
workflow_conclusion: success workflow_conclusion: success
@@ -86,7 +86,7 @@ jobs:
run: zip -r Bitwarden\ iOS.zip Bitwarden\ iOS run: zip -r Bitwarden\ iOS.zip Bitwarden\ iOS
- name: Create release - name: Create release
if: ${{ github.event.inputs.release_type != 'Dry Run' }} if: ${{ inputs.release_type != 'Dry Run' }}
uses: ncipollo/release-action@2c591bcc8ecdcd2db72b97d6147f871fcd833ba5 # v1.14.0 uses: ncipollo/release-action@2c591bcc8ecdcd2db72b97d6147f871fcd833ba5 # v1.14.0
with: with:
artifacts: "./com.x8bit.bitwarden.aab/com.x8bit.bitwarden.aab, artifacts: "./com.x8bit.bitwarden.aab/com.x8bit.bitwarden.aab,
@@ -103,16 +103,16 @@ jobs:
draft: true draft: true
- name: Update deployment status to Success - name: Update deployment status to Success
if: ${{ github.event.inputs.release_type != 'Dry Run' && success() }} if: ${{ inputs.release_type != 'Dry Run' && success() }}
uses: chrnorm/deployment-status@2afb7d27101260f4a764219439564d954d10b5b0 # v2.0.1 uses: chrnorm/deployment-status@9a72af4586197112e0491ea843682b5dc280d806 # v2.0.3
with: with:
token: '${{ secrets.GITHUB_TOKEN }}' token: '${{ secrets.GITHUB_TOKEN }}'
state: 'success' state: 'success'
deployment-id: ${{ steps.deployment.outputs.deployment_id }} deployment-id: ${{ steps.deployment.outputs.deployment_id }}
- name: Update deployment status to Failure - name: Update deployment status to Failure
if: ${{ github.event.inputs.release_type != 'Dry Run' && failure() }} if: ${{ inputs.release_type != 'Dry Run' && failure() }}
uses: chrnorm/deployment-status@2afb7d27101260f4a764219439564d954d10b5b0 # v2.0.1 uses: chrnorm/deployment-status@9a72af4586197112e0491ea843682b5dc280d806 # v2.0.3
with: with:
token: '${{ secrets.GITHUB_TOKEN }}' token: '${{ secrets.GITHUB_TOKEN }}'
state: 'failure' state: 'failure'
@@ -126,11 +126,11 @@ jobs:
if: inputs.fdroid_publish if: inputs.fdroid_publish
steps: steps:
- name: Checkout repo - name: Checkout repo
uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3.6.0 uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
- name: Download F-Droid .apk artifact - name: Download F-Droid .apk artifact
if: ${{ github.event.inputs.release_type != 'Dry Run' }} if: ${{ inputs.release_type != 'Dry Run' }}
uses: dawidd6/action-download-artifact@e7466d1a7587ed14867642c2ca74b5bcc1e19a2d # v3.0.0 uses: dawidd6/action-download-artifact@09f2f74827fd3a8607589e5ad7f9398816f540fe # v3.1.4
with: with:
workflow: build.yml workflow: build.yml
workflow_conclusion: success workflow_conclusion: success
@@ -138,8 +138,8 @@ jobs:
name: com.x8bit.bitwarden-fdroid.apk name: com.x8bit.bitwarden-fdroid.apk
- name: Dry Run - Download F-Droid .apk artifact - name: Dry Run - Download F-Droid .apk artifact
if: ${{ github.event.inputs.release_type == 'Dry Run' }} if: ${{ inputs.release_type == 'Dry Run' }}
uses: dawidd6/action-download-artifact@e7466d1a7587ed14867642c2ca74b5bcc1e19a2d # v3.0.0 uses: dawidd6/action-download-artifact@09f2f74827fd3a8607589e5ad7f9398816f540fe # v3.1.4
with: with:
workflow: build.yml workflow: build.yml
workflow_conclusion: success workflow_conclusion: success
@@ -176,13 +176,19 @@ jobs:
- name: Install Node dependencies - name: Install Node dependencies
run: npm install run: npm install
- name: Decrypt secrets - name: Login to Azure - CI Subscription
uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0
with:
creds: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }}
- name: Download secrets
env: env:
DECRYPT_FILE_PASSWORD: ${{ secrets.DECRYPT_FILE_PASSWORD }} ACCOUNT_NAME: bitwardenci
CONTAINER_NAME: mobile
run: | run: |
mkdir -p ~/secrets mkdir -p $HOME/secrets
gpg --quiet --batch --yes --decrypt --passphrase="$DECRYPT_FILE_PASSWORD" \ az storage blob download --account-name $ACCOUNT_NAME --container-name $CONTAINER_NAME \
--output ./store/fdroid/keystore.jks ./.github/secrets/store_fdroid-keystore.jks.gpg --name store_fdroid-keystore.jks --file ./store/fdroid/keystore.jks --output none
- name: Compile for F-Droid Store - name: Compile for F-Droid Store
env: env:
@@ -211,5 +217,5 @@ jobs:
cd $GITHUB_WORKSPACE cd $GITHUB_WORKSPACE
- name: Deploy to gh-pages - name: Deploy to gh-pages
if: ${{ github.event.inputs.release_type != 'Dry Run' }} if: ${{ inputs.release_type != 'Dry Run' }}
run: npm run deploy run: npm run deploy

View File

@@ -11,24 +11,6 @@ jobs:
name: Bump Mobile Version name: Bump Mobile Version
runs-on: ubuntu-22.04 runs-on: ubuntu-22.04
steps: steps:
- name: Checkout Branch
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
- name: Calculate bumped version
id: version
env:
RELEASE_TAG: ${{ github.ref }}
run: |
CURR_MAJOR=$(echo $RELEASE_TAG | sed -r 's/refs\/tags\/v([0-9]{4}\.[0-9]{1,2})\.([0-9]{1,2})/\1/')
CURR_PATCH=$(echo $RELEASE_TAG | sed -r 's/refs\/tags\/v([0-9]{4}\.[0-9]{1,2})\.([0-9]{1,2})/\2/')
echo "Current Major: $CURR_MAJOR"
echo "Current Patch: $CURR_PATCH"
NEW_PATCH=$((CURR_PATCH+1))
NEW_VER=$CURR_MAJOR.$NEW_PATCH
echo "New Version: $NEW_VER"
echo "new_version=$NEW_VER" >> $GITHUB_OUTPUT
- name: Login to Azure - CI Subscription - name: Login to Azure - CI Subscription
uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0 uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0
with: with:
@@ -41,9 +23,9 @@ jobs:
keyvault: bitwarden-ci keyvault: bitwarden-ci
secrets: "github-pat-bitwarden-devops-bot-repo-scope" secrets: "github-pat-bitwarden-devops-bot-repo-scope"
- name: "Bump version to ${{ steps.version.outputs.new_version }}" - name: Trigger Version Bump workflow
env: env:
GH_TOKEN: ${{ steps.retrieve-bot-secrets.outputs.github-pat-bitwarden-devops-bot-repo-scope }} GH_TOKEN: ${{ steps.retrieve-bot-secrets.outputs.github-pat-bitwarden-devops-bot-repo-scope }}
run: | run: |
echo '{"cut_rc_branch": "false", "version_number": "${{ steps.version.outputs.new_version }}"}' | \ echo '{"cut_rc_branch": "false"}' | \
gh workflow run version-bump.yml --json --repo bitwarden/mobile gh workflow run version-bump.yml --json --repo bitwarden/mobile

View File

@@ -1,13 +1,13 @@
--- ---
name: Version Bump name: Version Bump
run-name: Version Bump - v${{ inputs.version_number }}
on: on:
workflow_dispatch: workflow_dispatch:
inputs: inputs:
version_number: version_number_override:
description: "New version (example: '2024.1.0')" description: "New version override (leave blank for automatic calculation, example: '2024.1.0')"
required: true required: false
type: string
cut_rc_branch: cut_rc_branch:
description: "Cut RC branch?" description: "Cut RC branch?"
default: true default: true
@@ -15,22 +15,16 @@ on:
jobs: jobs:
bump_version: bump_version:
name: "Bump Version to v${{ inputs.version_number }}" name: Bump Version
runs-on: ubuntu-22.04 runs-on: ubuntu-22.04
outputs:
version: ${{ steps.set-final-version-output.outputs.version }}
steps: steps:
- name: Login to Azure - CI Subscription - name: Validate version input
uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0 if: ${{ inputs.version_number_override != '' }}
uses: bitwarden/gh-actions/version-check@main
with: with:
creds: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }} version: ${{ inputs.version_number_override }}
- name: Retrieve secrets
id: retrieve-secrets
uses: bitwarden/gh-actions/get-keyvault-secrets@main
with:
keyvault: "bitwarden-ci"
secrets: "github-gpg-private-key,
github-gpg-private-key-passphrase,
github-pat-bitwarden-devops-bot-repo-scope"
- name: Checkout Branch - name: Checkout Branch
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
@@ -47,6 +41,20 @@ jobs:
exit 1 exit 1
fi fi
- name: Login to Azure - CI Subscription
uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0
with:
creds: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }}
- name: Retrieve secrets
id: retrieve-secrets
uses: bitwarden/gh-actions/get-keyvault-secrets@main
with:
keyvault: "bitwarden-ci"
secrets: "github-gpg-private-key,
github-gpg-private-key-passphrase,
github-pat-bitwarden-devops-bot-repo-scope"
- name: Import GPG key - name: Import GPG key
uses: crazy-max/ghaction-import-gpg@01dd5d3ca463c7f10f7f4f7b4f177225ac661ee4 # v6.1.0 uses: crazy-max/ghaction-import-gpg@01dd5d3ca463c7f10f7f4f7b4f177225ac661ee4 # v6.1.0
with: with:
@@ -55,25 +63,38 @@ jobs:
git_user_signingkey: true git_user_signingkey: true
git_commit_gpgsign: true git_commit_gpgsign: true
- name: Setup git
run: |
git config --local user.email "106330231+bitwarden-devops-bot@users.noreply.github.com"
git config --local user.name "bitwarden-devops-bot"
- name: Create Version Branch - name: Create Version Branch
id: create-branch id: create-branch
run: | run: |
NAME=version_bump_${{ github.ref_name }}_${{ inputs.version_number }} NAME=version_bump_${{ github.ref_name }}_$(date +"%Y-%m-%d")
git switch -c $NAME git switch -c $NAME
echo "name=$NAME" >> $GITHUB_OUTPUT echo "name=$NAME" >> $GITHUB_OUTPUT
- name: Install xmllint - name: Install xmllint
run: sudo apt install -y libxml2-utils run: |
sudo apt-get update
sudo apt-get install -y libxml2-utils
- name: Verify input version - name: Get current version
env: id: current-version
NEW_VERSION: ${{ inputs.version_number }}
run: | run: |
CURRENT_VERSION=$(xmllint --xpath ' CURRENT_VERSION=$(xmllint --xpath '
string(/manifest/@*[local-name()="versionName" string(/manifest/@*[local-name()="versionName"
and namespace-uri()="http://schemas.android.com/apk/res/android"]) and namespace-uri()="http://schemas.android.com/apk/res/android"])
' src/App/Platforms/Android/AndroidManifest.xml) ' src/App/Platforms/Android/AndroidManifest.xml)
echo "version=$CURRENT_VERSION" >> $GITHUB_OUTPUT
- name: Verify input version
if: ${{ inputs.version_number_override != '' }}
env:
CURRENT_VERSION: ${{ steps.current-version.outputs.version }}
NEW_VERSION: ${{ inputs.version_number_override }}
run: |
# Error if version has not changed. # Error if version has not changed.
if [[ "$NEW_VERSION" == "$CURRENT_VERSION" ]]; then if [[ "$NEW_VERSION" == "$CURRENT_VERSION" ]]; then
echo "Version has not changed." echo "Version has not changed."
@@ -89,40 +110,93 @@ jobs:
exit 1 exit 1
fi fi
- name: Bump Version - Android XML - name: Calculate next release version
if: ${{ inputs.version_number_override == '' }}
id: calculate-next-version
uses: bitwarden/gh-actions/version-next@main
with:
version: ${{ steps.current-version.outputs.version }}
- name: Bump Version - Android XML - Version Override
if: ${{ inputs.version_number_override != '' }}
id: bump-version-override
uses: bitwarden/gh-actions/version-bump@main uses: bitwarden/gh-actions/version-bump@main
with: with:
version: ${{ inputs.version_number }}
file_path: "src/App/Platforms/Android/AndroidManifest.xml" file_path: "src/App/Platforms/Android/AndroidManifest.xml"
version: ${{ inputs.version_number_override }}
- name: Bump Version - iOS.Autofill - name: Bump Version - Android XML - Automatic Calculation
if: ${{ inputs.version_number_override == '' }}
id: bump-version-automatic
uses: bitwarden/gh-actions/version-bump@main
with:
file_path: "src/App/Platforms/Android/AndroidManifest.xml"
version: ${{ steps.calculate-next-version.outputs.version }}
- name: Bump Version - iOS.Autofill - Version Override
if: ${{ inputs.version_number_override != '' }}
uses: bitwarden/gh-actions/version-bump@main uses: bitwarden/gh-actions/version-bump@main
with: with:
version: ${{ inputs.version_number }}
file_path: "src/iOS.Autofill/Info.plist" file_path: "src/iOS.Autofill/Info.plist"
version: ${{ inputs.version_number_override }}
- name: Bump Version - iOS.Extension - name: Bump Version - iOS.Autofill - Automatic Calculation
if: ${{ inputs.version_number_override == '' }}
uses: bitwarden/gh-actions/version-bump@main
with:
file_path: "src/iOS.Autofill/Info.plist"
version: ${{ steps.calculate-next-version.outputs.version }}
- name: Bump Version - iOS.Extension - Version Override
if: ${{ inputs.version_number_override != '' }}
uses: bitwarden/gh-actions/version-bump@main uses: bitwarden/gh-actions/version-bump@main
with: with:
version: ${{ inputs.version_number }}
file_path: "src/iOS.Extension/Info.plist" file_path: "src/iOS.Extension/Info.plist"
version: ${{ inputs.version_number_override }}
- name: Bump Version - iOS.ShareExtension - name: Bump Version - iOS.Extension - Automatic Calculation
if: ${{ inputs.version_number_override == '' }}
uses: bitwarden/gh-actions/version-bump@main
with:
file_path: "src/iOS.Extension/Info.plist"
version: ${{ steps.calculate-next-version.outputs.version }}
- name: Bump Version - iOS.ShareExtension - Version Override
if: ${{ inputs.version_number_override != '' }}
uses: bitwarden/gh-actions/version-bump@main uses: bitwarden/gh-actions/version-bump@main
with: with:
version: ${{ inputs.version_number }}
file_path: "src/iOS.ShareExtension/Info.plist" file_path: "src/iOS.ShareExtension/Info.plist"
version: ${{ inputs.version_number_override }}
- name: Bump Version - iOS - name: Bump Version - iOS.ShareExtension - Automatic Calculation
if: ${{ inputs.version_number_override == '' }}
uses: bitwarden/gh-actions/version-bump@main uses: bitwarden/gh-actions/version-bump@main
with: with:
version: ${{ inputs.version_number }} file_path: "src/iOS.ShareExtension/Info.plist"
file_path: "src/App/Platforms/iOS/Info.plist" version: ${{ steps.calculate-next-version.outputs.version }}
- name: Setup git - name: Bump Version - iOS - Version Override
if: ${{ inputs.version_number_override != '' }}
uses: bitwarden/gh-actions/version-bump@main
with:
file_path: "src/App/Platforms/iOS/Info.plist"
version: ${{ inputs.version_number_override }}
- name: Bump Version - iOS - Automatic Calculation
if: ${{ inputs.version_number_override == '' }}
uses: bitwarden/gh-actions/version-bump@main
with:
file_path: "src/App/Platforms/iOS/Info.plist"
version: ${{ steps.calculate-next-version.outputs.version }}
- name: Set Job output
id: set-final-version-output
run: | run: |
git config --local user.email "106330231+bitwarden-devops-bot@users.noreply.github.com" if [[ "${{ steps.bump-version-override.outcome }}" == "success" ]]; then
git config --local user.name "bitwarden-devops-bot" echo "version=${{ inputs.version_number_override }}" >> $GITHUB_OUTPUT
elif [[ "${{ steps.bump-version-automatic.outcome }}" == "success" ]]; then
echo "version=${{ steps.calculate-next-version.outputs.version }}" >> $GITHUB_OUTPUT
fi
- name: Check if version changed - name: Check if version changed
id: version-changed id: version-changed
@@ -136,7 +210,7 @@ jobs:
- name: Commit files - name: Commit files
if: ${{ steps.version-changed.outputs.changes_to_commit == 'TRUE' }} if: ${{ steps.version-changed.outputs.changes_to_commit == 'TRUE' }}
run: git commit -m "Bumped version to ${{ inputs.version_number }}" -a run: git commit -m "Bumped version to ${{ steps.set-final-version-output.outputs.version }}" -a
- name: Push changes - name: Push changes
if: ${{ steps.version-changed.outputs.changes_to_commit == 'TRUE' }} if: ${{ steps.version-changed.outputs.changes_to_commit == 'TRUE' }}
@@ -150,7 +224,7 @@ jobs:
env: env:
GH_TOKEN: ${{ steps.retrieve-secrets.outputs.github-pat-bitwarden-devops-bot-repo-scope }} GH_TOKEN: ${{ steps.retrieve-secrets.outputs.github-pat-bitwarden-devops-bot-repo-scope }}
PR_BRANCH: ${{ steps.create-branch.outputs.name }} PR_BRANCH: ${{ steps.create-branch.outputs.name }}
TITLE: "Bump version to ${{ inputs.version_number }}" TITLE: "Bump version to ${{ steps.set-final-version-output.outputs.version }}"
run: | run: |
PR_URL=$(gh pr create --title "$TITLE" \ PR_URL=$(gh pr create --title "$TITLE" \
--base "main" \ --base "main" \
@@ -166,16 +240,18 @@ jobs:
- [X] Other - [X] Other
## Objective ## Objective
Automated version bump to ${{ inputs.version_number }}") Automated version bump to ${{ steps.set-final-version-output.outputs.version }}")
echo "pr_number=${PR_URL##*/}" >> $GITHUB_OUTPUT echo "pr_number=${PR_URL##*/}" >> $GITHUB_OUTPUT
- name: Approve PR - name: Approve PR
if: ${{ steps.version-changed.outputs.changes_to_commit == 'TRUE' }}
env: env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
PR_NUMBER: ${{ steps.create-pr.outputs.pr_number }} PR_NUMBER: ${{ steps.create-pr.outputs.pr_number }}
run: gh pr review $PR_NUMBER --approve run: gh pr review $PR_NUMBER --approve
- name: Merge PR - name: Merge PR
if: ${{ steps.version-changed.outputs.changes_to_commit == 'TRUE' }}
env: env:
GH_TOKEN: ${{ steps.retrieve-secrets.outputs.github-pat-bitwarden-devops-bot-repo-scope }} GH_TOKEN: ${{ steps.retrieve-secrets.outputs.github-pat-bitwarden-devops-bot-repo-scope }}
PR_NUMBER: ${{ steps.create-pr.outputs.pr_number }} PR_NUMBER: ${{ steps.create-pr.outputs.pr_number }}
@@ -183,8 +259,8 @@ jobs:
cut_rc: cut_rc:
name: Cut RC branch name: Cut RC branch
needs: bump_version
if: ${{ inputs.cut_rc_branch == true }} if: ${{ inputs.cut_rc_branch == true }}
needs: bump_version
runs-on: ubuntu-22.04 runs-on: ubuntu-22.04
steps: steps:
- name: Checkout Branch - name: Checkout Branch
@@ -193,11 +269,13 @@ jobs:
ref: main ref: main
- name: Install xmllint - name: Install xmllint
run: sudo apt install -y libxml2-utils run: |
sudo apt-get update
sudo apt-get install -y libxml2-utils
- name: Verify version has been updated - name: Verify version has been updated
env: env:
NEW_VERSION: ${{ inputs.version_number }} NEW_VERSION: ${{ needs.bump_version.outputs.version }}
run: | run: |
# Wait for version to change. # Wait for version to change.
while : ; do while : ; do

View File

@@ -1,11 +0,0 @@
---
name: Workflow Linter
on:
pull_request:
paths:
- .github/workflows/**
jobs:
call-workflow:
uses: bitwarden/gh-actions/.github/workflows/workflow-linter.yml@main

View File

@@ -1,6 +1,6 @@
<Project> <Project>
<PropertyGroup> <PropertyGroup>
<MauiVersion>8.0.7-nightly.*</MauiVersion> <MauiVersion>8.0.7</MauiVersion>
<ReleaseCodesignProvision>Automatic:AppStore</ReleaseCodesignProvision> <ReleaseCodesignProvision>Automatic:AppStore</ReleaseCodesignProvision>
<ReleaseCodesignKey>iPhone Distribution</ReleaseCodesignKey> <ReleaseCodesignKey>iPhone Distribution</ReleaseCodesignKey>
<IncludeBitwardeniOSExtensions>True</IncludeBitwardeniOSExtensions> <IncludeBitwardeniOSExtensions>True</IncludeBitwardeniOSExtensions>
@@ -9,5 +9,8 @@
<!-- Uncomment this when Unit Testing--> <!-- Uncomment this when Unit Testing-->
<!-- <CustomConstants>UT</CustomConstants> --> <!-- <CustomConstants>UT</CustomConstants> -->
<!-- Uncomment this when building FDROID-->
<!-- <CustomConstants>FDROID</CustomConstants> -->
</PropertyGroup> </PropertyGroup>
</Project> </Project>

View File

@@ -12,7 +12,7 @@ The Bitwarden mobile application is written in C# using .NET MAUI.
# Build/Run # Build/Run
Please refer to the [Mobile section](https://contributing.bitwarden.com/getting-started/clients/mobile/) of the [Contributing Documentation](https://contributing.bitwarden.com/) for build instructions, recommended tooling, code style tips, and lots of other great information to get you started. Please refer to the [Mobile section](https://contributing.bitwarden.com/getting-started/mobile/) of the [Contributing Documentation](https://contributing.bitwarden.com/) for build instructions, recommended tooling, code style tips, and lots of other great information to get you started.
# We're Hiring! # We're Hiring!

View File

@@ -15,16 +15,18 @@ abstract record VariantConfig(
string AppName, string AppName,
string AndroidPackageName, string AndroidPackageName,
string iOSBundleId, string iOSBundleId,
string ApsEnvironment string ApsEnvironment,
string DistProvisioningProfilePrefix
); );
const string BASE_BUNDLE_ID_DROID = "com.x8bit.bitwarden"; const string BASE_BUNDLE_ID_DROID = "com.x8bit.bitwarden";
const string BASE_BUNDLE_ID_IOS = "com.8bit.bitwarden"; const string BASE_BUNDLE_ID_IOS = "com.8bit.bitwarden";
record Dev(): VariantConfig("Bitwarden Dev", $"{BASE_BUNDLE_ID_DROID}.dev", $"{BASE_BUNDLE_ID_IOS}.dev", "development"); //NOTE: Beta iOS variants have a different ITSEncryptionExportComplianceCode
record QA(): VariantConfig("Bitwarden QA", $"{BASE_BUNDLE_ID_DROID}.qa", $"{BASE_BUNDLE_ID_IOS}.qa", "development"); record Dev(): VariantConfig("Bitwarden Dev", $"{BASE_BUNDLE_ID_DROID}.dev", $"{BASE_BUNDLE_ID_IOS}.dev", "development", "Dist:");
record Beta(): VariantConfig("Bitwarden Beta", $"{BASE_BUNDLE_ID_DROID}.beta", $"{BASE_BUNDLE_ID_IOS}.beta", "production"); record QA(): VariantConfig("Bitwarden QA", $"{BASE_BUNDLE_ID_DROID}.qa", $"{BASE_BUNDLE_ID_IOS}.qa", "development", "Dist:");
record Prod(): VariantConfig("Bitwarden", $"{BASE_BUNDLE_ID_DROID}", $"{BASE_BUNDLE_ID_IOS}", "production"); record Beta(): VariantConfig("Bitwarden Beta", $"{BASE_BUNDLE_ID_DROID}.beta", $"{BASE_BUNDLE_ID_IOS}.beta", "production", "Dist: Beta");
record Prod(): VariantConfig("Bitwarden", $"{BASE_BUNDLE_ID_DROID}", $"{BASE_BUNDLE_ID_IOS}", "production", "Dist:");
VariantConfig GetVariant() => variant.ToLower() switch{ VariantConfig GetVariant() => variant.ToLower() switch{
"qa" => new QA(), "qa" => new QA(),
@@ -197,7 +199,8 @@ private void UpdateiOSInfoPlist(string plistPath, VariantConfig buildVariant, Gi
var prevBundleId = plist["CFBundleIdentifier"]; var prevBundleId = plist["CFBundleIdentifier"];
var prevBundleName = plist["CFBundleName"]; var prevBundleName = plist["CFBundleName"];
//var newVersion = CreateBuildNumber(prevVersion).ToString(); //var newVersion = CreateBuildNumber(prevVersion).ToString();
var newVersionName = GetVersionName(prevVersionName, buildVariant, git); // we need to maintain version formatting here composed of one to three period-separated integers, so we cannot use the GetVersionName method as in Android for non-Prod.
var newVersionName = prevVersionName;
var newBundleId = GetiOSBundleId(buildVariant, projectType); var newBundleId = GetiOSBundleId(buildVariant, projectType);
var newBundleName = GetiOSBundleName(buildVariant, projectType); var newBundleName = GetiOSBundleName(buildVariant, projectType);
@@ -219,6 +222,11 @@ private void UpdateiOSInfoPlist(string plistPath, VariantConfig buildVariant, Gi
plist["NSExtension"]["NSExtensionAttributes"]["NSExtensionActivationRule"] = keyText.Replace("com.8bit.bitwarden", buildVariant.iOSBundleId); plist["NSExtension"]["NSExtensionAttributes"]["NSExtensionActivationRule"] = keyText.Replace("com.8bit.bitwarden", buildVariant.iOSBundleId);
} }
if(buildVariant is Beta)
{
plist["ITSEncryptionExportComplianceCode"] = "3dd3e32f-efa6-4d99-b410-28aa28b1cb77";
}
SerializePlist(plistFile, plist); SerializePlist(plistFile, plist);
Information($"Changed app name from {prevBundleName} to {newBundleName}"); Information($"Changed app name from {prevBundleName} to {newBundleName}");
@@ -228,12 +236,15 @@ private void UpdateiOSInfoPlist(string plistPath, VariantConfig buildVariant, Gi
Information($"{plistPath} updated with success!"); Information($"{plistPath} updated with success!");
} }
private void UpdateiOSEntitlementsPlist(string entitlementsPath, VariantConfig buildVariant) private void UpdateiOSEntitlementsPlist(string entitlementsPath, VariantConfig buildVariant, bool updateApsEnv)
{ {
var EntitlementlistFile = File(entitlementsPath); var EntitlementlistFile = File(entitlementsPath);
dynamic Entitlements = DeserializePlist(EntitlementlistFile); dynamic Entitlements = DeserializePlist(EntitlementlistFile);
if (updateApsEnv)
{
Entitlements["aps-environment"] = buildVariant.ApsEnvironment; Entitlements["aps-environment"] = buildVariant.ApsEnvironment;
}
Entitlements["keychain-access-groups"] = new List<string>() { "$(AppIdentifierPrefix)" + buildVariant.iOSBundleId }; Entitlements["keychain-access-groups"] = new List<string>() { "$(AppIdentifierPrefix)" + buildVariant.iOSBundleId };
Entitlements["com.apple.security.application-groups"] = new List<string>() { $"group.{buildVariant.iOSBundleId}" };; Entitlements["com.apple.security.application-groups"] = new List<string>() { $"group.{buildVariant.iOSBundleId}" };;
@@ -274,7 +285,8 @@ private void UpdateWatchPbxproj(string pbxprojPath, string newVersion)
fileText = Regex.Replace(fileText, pattern, $"MARKETING_VERSION = {newVersion};"); fileText = Regex.Replace(fileText, pattern, $"MARKETING_VERSION = {newVersion};");
FileWriteText(pbxprojPath, fileText); FileWriteText(pbxprojPath, fileText);
Information($"{pbxprojPath} modified successfully.");
Information($"{pbxprojPath} modified Marketing Version successfully.");
} }
/// <summary> /// <summary>
@@ -327,7 +339,7 @@ Task("UpdateiOSPlist")
var infoPath = Path.Combine(_slnPath, "src", "App", "Platforms", "iOS", "Info.plist"); var infoPath = Path.Combine(_slnPath, "src", "App", "Platforms", "iOS", "Info.plist");
var entitlementsPath = Path.Combine(_slnPath, "src", "App", "Platforms", "iOS", "Entitlements.plist"); var entitlementsPath = Path.Combine(_slnPath, "src", "App", "Platforms", "iOS", "Entitlements.plist");
UpdateiOSInfoPlist(infoPath, buildVariant, _gitVersion, iOSProjectType.MainApp); UpdateiOSInfoPlist(infoPath, buildVariant, _gitVersion, iOSProjectType.MainApp);
UpdateiOSEntitlementsPlist(entitlementsPath, buildVariant); UpdateiOSEntitlementsPlist(entitlementsPath, buildVariant, true);
}); });
Task("UpdateiOSAutofillPlist") Task("UpdateiOSAutofillPlist")
@@ -338,7 +350,7 @@ Task("UpdateiOSAutofillPlist")
var infoPath = Path.Combine(_slnPath, "src", "iOS.Autofill", "Info.plist"); var infoPath = Path.Combine(_slnPath, "src", "iOS.Autofill", "Info.plist");
var entitlementsPath = Path.Combine(_slnPath, "src", "iOS.Autofill", "Entitlements.plist"); var entitlementsPath = Path.Combine(_slnPath, "src", "iOS.Autofill", "Entitlements.plist");
UpdateiOSInfoPlist(infoPath, buildVariant, _gitVersion, iOSProjectType.Autofill); UpdateiOSInfoPlist(infoPath, buildVariant, _gitVersion, iOSProjectType.Autofill);
UpdateiOSEntitlementsPlist(entitlementsPath, buildVariant); UpdateiOSEntitlementsPlist(entitlementsPath, buildVariant, false);
}); });
Task("UpdateiOSExtensionPlist") Task("UpdateiOSExtensionPlist")
@@ -349,7 +361,7 @@ Task("UpdateiOSExtensionPlist")
var infoPath = Path.Combine(_slnPath, "src", "iOS.Extension", "Info.plist"); var infoPath = Path.Combine(_slnPath, "src", "iOS.Extension", "Info.plist");
var entitlementsPath = Path.Combine(_slnPath, "src", "iOS.Extension", "Entitlements.plist"); var entitlementsPath = Path.Combine(_slnPath, "src", "iOS.Extension", "Entitlements.plist");
UpdateiOSInfoPlist(infoPath, buildVariant, _gitVersion, iOSProjectType.Extension); UpdateiOSInfoPlist(infoPath, buildVariant, _gitVersion, iOSProjectType.Extension);
UpdateiOSEntitlementsPlist(entitlementsPath, buildVariant); UpdateiOSEntitlementsPlist(entitlementsPath, buildVariant, false);
}); });
Task("UpdateiOSShareExtensionPlist") Task("UpdateiOSShareExtensionPlist")
@@ -360,7 +372,7 @@ Task("UpdateiOSShareExtensionPlist")
var infoPath = Path.Combine(_slnPath, "src", "iOS.ShareExtension", "Info.plist"); var infoPath = Path.Combine(_slnPath, "src", "iOS.ShareExtension", "Info.plist");
var entitlementsPath = Path.Combine(_slnPath, "src", "iOS.ShareExtension", "Entitlements.plist"); var entitlementsPath = Path.Combine(_slnPath, "src", "iOS.ShareExtension", "Entitlements.plist");
UpdateiOSInfoPlist(infoPath, buildVariant, _gitVersion, iOSProjectType.ShareExtension); UpdateiOSInfoPlist(infoPath, buildVariant, _gitVersion, iOSProjectType.ShareExtension);
UpdateiOSEntitlementsPlist(entitlementsPath, buildVariant); UpdateiOSEntitlementsPlist(entitlementsPath, buildVariant, false);
}); });
Task("UpdateiOSCodeFiles") Task("UpdateiOSCodeFiles")
@@ -397,6 +409,22 @@ Task("UpdateWatchKitAppInfoPlist")
UpdateWatchKitAppInfoPlist(infoPath, buildVariant); UpdateWatchKitAppInfoPlist(infoPath, buildVariant);
}); });
Task("UpdateDistProfiles")
.IsDependentOn("UpdateiOSCodeFiles")
.Does(()=> {
var buildVariant = GetVariant();
var filesToReplace = new string[] {
Path.Combine(".github", "resources", "export-options-app-store.plist"),
Path.Combine(_slnPath, "src", "watchOS", "bitwarden", "bitwarden.xcodeproj", "project.pbxproj")
};
foreach(string path in filesToReplace)
{
ReplaceInFile(path, "Dist:", buildVariant.DistProvisioningProfilePrefix);
}
});
#endregion iOS #endregion iOS
#region Main Tasks #region Main Tasks
@@ -418,6 +446,7 @@ Task("iOS")
.IsDependentOn("UpdateiOSCodeFiles") .IsDependentOn("UpdateiOSCodeFiles")
.IsDependentOn("UpdateWatchProject") .IsDependentOn("UpdateWatchProject")
.IsDependentOn("UpdateWatchKitAppInfoPlist") .IsDependentOn("UpdateWatchKitAppInfoPlist")
.IsDependentOn("UpdateDistProfiles")
.Does(()=> .Does(()=>
{ {
Information("iOS app updated"); Information("iOS app updated");

View File

@@ -117,6 +117,8 @@
<Folder Include="Platforms\Android\Services\" /> <Folder Include="Platforms\Android\Services\" />
<Folder Include="Platforms\Android\Tiles\" /> <Folder Include="Platforms\Android\Tiles\" />
<Folder Include="Platforms\Android\Utilities\" /> <Folder Include="Platforms\Android\Utilities\" />
<Folder Include="Platforms\Android\Resources\drawable-xxxhdpi\" />
<Folder Include="Resources\Raw\" />
</ItemGroup> </ItemGroup>
<ItemGroup Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'android'"> <ItemGroup Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'android'">
<PackageReference Include="Xamarin.AndroidX.AutoFill" Version="1.1.0.18" /> <PackageReference Include="Xamarin.AndroidX.AutoFill" Version="1.1.0.18" />
@@ -257,5 +259,13 @@
<None Remove="Platforms\iOS\Resources\more_vert.png" /> <None Remove="Platforms\iOS\Resources\more_vert.png" />
<None Remove="Platforms\iOS\Resources\logo_white.png" /> <None Remove="Platforms\iOS\Resources\logo_white.png" />
<None Remove="Platforms\iOS\Resources\logo%402x.png" /> <None Remove="Platforms\iOS\Resources\logo%402x.png" />
<None Remove="Platforms\Android\Resources\drawable-xxxhdpi\" />
<None Remove="Resources\Raw\" />
</ItemGroup>
<ItemGroup Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'ios'">
<BundleResource Include="Platforms\iOS\PrivacyInfo.xcprivacy" LogicalName="PrivacyInfo.xcprivacy" />
</ItemGroup>
<ItemGroup Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'android'">
<MauiAsset Include="Resources\Raw\fido2_privileged_allow_list.json" LogicalName="fido2_privileged_allow_list.json" />
</ItemGroup> </ItemGroup>
</Project> </Project>

View File

@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:versionCode="1" android:versionName="2024.2.2" android:installLocation="internalOnly" package="com.x8bit.bitwarden"> <manifest xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:versionCode="1" android:versionName="2024.4.1" android:installLocation="internalOnly" package="com.x8bit.bitwarden">
<uses-sdk android:minSdkVersion="21" android:targetSdkVersion="34" /> <uses-sdk android:minSdkVersion="21" android:targetSdkVersion="34" />
<uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.NFC" /> <uses-permission android:name="android.permission.NFC" />
@@ -43,6 +43,9 @@
<!-- Support for Xamarin.Essentials.Browser.OpenAsync (for Android > 11) --> <!-- Support for Xamarin.Essentials.Browser.OpenAsync (for Android > 11) -->
<!-- Related docs: https://learn.microsoft.com/en-us/xamarin/essentials/open-browser?tabs=android --> <!-- Related docs: https://learn.microsoft.com/en-us/xamarin/essentials/open-browser?tabs=android -->
<queries> <queries>
<intent>
<action android:name="android.support.customtabs.action.CustomTabsService" />
</intent>
<intent> <intent>
<action android:name="android.intent.action.VIEW" /> <action android:name="android.intent.action.VIEW" />
<data android:scheme="http" /> <data android:scheme="http" />

View File

@@ -347,7 +347,7 @@ namespace Bit.Droid.Autofill
// InlinePresentation requires nonNull pending intent (even though we only utilize one for the // InlinePresentation requires nonNull pending intent (even though we only utilize one for the
// "my vault" presentation) so we're including an empty one here // "my vault" presentation) so we're including an empty one here
pendingIntent = PendingIntent.GetService(context, 0, new Intent(), pendingIntent = PendingIntent.GetService(context, 0, new Intent(),
AndroidHelpers.AddPendingIntentMutabilityFlag(PendingIntentFlags.OneShot | PendingIntentFlags.UpdateCurrent, true)); AndroidHelpers.AddPendingIntentMutabilityFlag(PendingIntentFlags.OneShot | PendingIntentFlags.UpdateCurrent, false));
} }
var slice = CreateInlinePresentationSlice( var slice = CreateInlinePresentationSlice(
inlinePresentationSpec, inlinePresentationSpec,

View File

@@ -0,0 +1,321 @@
using System.ComponentModel.DataAnnotations;
using System.Text.Json.Nodes;
using Android.App;
using Android.Content;
using Android.OS;
using AndroidX.Credentials;
using AndroidX.Credentials.Exceptions;
using AndroidX.Credentials.Provider;
using AndroidX.Credentials.WebAuthn;
using Bit.App.Abstractions;
using Bit.App.Droid.Utilities;
using Bit.Core.Abstractions;
using Bit.Core.Resources.Localization;
using Bit.Core.Services;
using Bit.Core.Utilities;
using Bit.Core.Utilities.Fido2;
using Bit.Core.Utilities.Fido2.Extensions;
using Bit.Droid;
using Org.Json;
using Activity = Android.App.Activity;
using Drawables = Android.Graphics.Drawables;
namespace Bit.App.Platforms.Android.Autofill
{
public static class CredentialHelpers
{
public static async Task<List<CredentialEntry>> PopulatePasskeyDataAsync(CallingAppInfo callingAppInfo,
BeginGetPublicKeyCredentialOption option, Context context, bool hasVaultBeenUnlockedInThisTransaction)
{
var passkeyEntries = new List<CredentialEntry>();
var requestOptions = new PublicKeyCredentialRequestOptions(option.RequestJson);
var authenticator = Bit.Core.Utilities.ServiceContainer.Resolve<IFido2AuthenticatorService>();
var credentials = await authenticator.SilentCredentialDiscoveryAsync(requestOptions.RpId);
// We need to change the request code for every pending intent on mapping the credential so the extras are not overriten by the last
// credential entry created.
int requestCodeAddition = 0;
passkeyEntries = credentials.Select(credential => MapCredential(credential, option, context, hasVaultBeenUnlockedInThisTransaction, Bit.Droid.Autofill.CredentialProviderService.UniqueGetRequestCode + requestCodeAddition++) as CredentialEntry).ToList();
return passkeyEntries;
}
private static PublicKeyCredentialEntry MapCredential(Fido2AuthenticatorDiscoverableCredentialMetadata credential, BeginGetPublicKeyCredentialOption option, Context context, bool hasVaultBeenUnlockedInThisTransaction, int requestCode)
{
var credDataBundle = new Bundle();
credDataBundle.PutByteArray(Bit.Core.Utilities.Fido2.CredentialProviderConstants.CredentialIdIntentExtra, credential.Id);
var intent = new Intent(context, typeof(Bit.Droid.Autofill.CredentialProviderSelectionActivity))
.SetAction(Bit.Droid.Autofill.CredentialProviderService.GetFido2IntentAction).SetPackage(Constants.PACKAGE_NAME);
intent.PutExtra(Bit.Core.Utilities.Fido2.CredentialProviderConstants.CredentialDataIntentExtra, credDataBundle);
intent.PutExtra(Bit.Core.Utilities.Fido2.CredentialProviderConstants.CredentialProviderCipherId, credential.CipherId);
intent.PutExtra(Bit.Core.Utilities.Fido2.CredentialProviderConstants.CredentialHasVaultBeenUnlockedInThisTransactionExtra, hasVaultBeenUnlockedInThisTransaction);
var pendingIntent = PendingIntent.GetActivity(context, requestCode, intent,
PendingIntentFlags.Mutable | PendingIntentFlags.UpdateCurrent);
return new PublicKeyCredentialEntry.Builder(
context,
credential.UserName ?? "No username",
pendingIntent,
option)
.SetDisplayName(credential.UserName ?? "No username")
.SetIcon(Drawables.Icon.CreateWithResource(context, Microsoft.Maui.Resource.Drawable.icon))
.Build();
}
private static PublicKeyCredentialCreationOptions GetPublicKeyCredentialCreationOptionsFromJson(string json)
{
var request = new PublicKeyCredentialCreationOptions(json);
var jsonObj = new JSONObject(json);
var authenticatorSelection = jsonObj.GetJSONObject("authenticatorSelection");
request.AuthenticatorSelection = new AndroidX.Credentials.WebAuthn.AuthenticatorSelectionCriteria(
authenticatorSelection.OptString("authenticatorAttachment", "platform"),
authenticatorSelection.OptString("residentKey", null),
authenticatorSelection.OptBoolean("requireResidentKey", false),
authenticatorSelection.OptString("userVerification", "preferred"));
return request;
}
public static async Task CreateCipherPasskeyAsync(ProviderCreateCredentialRequest getRequest, Activity activity)
{
var callingRequest = getRequest?.CallingRequest as CreatePublicKeyCredentialRequest;
if (callingRequest is null)
{
await DisplayAlertAsync(AppResources.AnErrorHasOccurred, string.Empty);
FailAndFinish();
return;
}
var credentialCreationOptions = GetPublicKeyCredentialCreationOptionsFromJson(callingRequest.RequestJson);
string origin;
try
{
origin = await ValidateCallingAppInfoAndGetOriginAsync(getRequest.CallingAppInfo, credentialCreationOptions.Rp.Id);
}
catch (Core.Exceptions.ValidationException valEx)
{
await DisplayAlertAsync(AppResources.AnErrorHasOccurred, valEx.Message);
FailAndFinish();
return;
}
if (origin is null)
{
await DisplayAlertAsync(AppResources.ErrorCreatingPasskey, AppResources.PasskeysNotSupportedForThisApp);
FailAndFinish();
return;
}
var rp = new Core.Utilities.Fido2.PublicKeyCredentialRpEntity()
{
Id = credentialCreationOptions.Rp.Id,
Name = credentialCreationOptions.Rp.Name
};
var user = new Core.Utilities.Fido2.PublicKeyCredentialUserEntity()
{
Id = credentialCreationOptions.User.GetId(),
Name = credentialCreationOptions.User.Name,
DisplayName = credentialCreationOptions.User.DisplayName
};
var pubKeyCredParams = new List<Core.Utilities.Fido2.PublicKeyCredentialParameters>();
foreach (var pubKeyCredParam in credentialCreationOptions.PubKeyCredParams)
{
pubKeyCredParams.Add(new Core.Utilities.Fido2.PublicKeyCredentialParameters() { Alg = Convert.ToInt32(pubKeyCredParam.Alg), Type = pubKeyCredParam.Type });
}
var excludeCredentials = new List<Core.Utilities.Fido2.PublicKeyCredentialDescriptor>();
foreach (var excludeCred in credentialCreationOptions.ExcludeCredentials)
{
excludeCredentials.Add(new Core.Utilities.Fido2.PublicKeyCredentialDescriptor() { Id = excludeCred.GetId(), Type = excludeCred.Type, Transports = excludeCred.Transports.ToArray() });
}
var authenticatorSelection = new Core.Utilities.Fido2.AuthenticatorSelectionCriteria()
{
UserVerification = credentialCreationOptions.AuthenticatorSelection.UserVerification,
ResidentKey = credentialCreationOptions.AuthenticatorSelection.ResidentKey,
RequireResidentKey = credentialCreationOptions.AuthenticatorSelection.RequireResidentKey
};
var timeout = Convert.ToInt32(credentialCreationOptions.Timeout);
var credentialCreateParams = new Fido2ClientCreateCredentialParams()
{
Challenge = credentialCreationOptions.GetChallenge(),
Origin = origin,
PubKeyCredParams = pubKeyCredParams.ToArray(),
Rp = rp,
User = user,
Timeout = timeout,
Attestation = credentialCreationOptions.Attestation,
AuthenticatorSelection = authenticatorSelection,
ExcludeCredentials = excludeCredentials.ToArray(),
Extensions = MapExtensionsFromJson(credentialCreationOptions),
SameOriginWithAncestors = true
};
var credentialExtraCreateParams = new Fido2ExtraCreateCredentialParams
(
callingRequest.GetClientDataHash(),
getRequest.CallingAppInfo?.PackageName
);
var fido2MediatorService = ServiceContainer.Resolve<IFido2MediatorService>();
var clientCreateCredentialResult = await fido2MediatorService.CreateCredentialAsync(credentialCreateParams, credentialExtraCreateParams);
if (clientCreateCredentialResult == null)
{
FailAndFinish();
return;
}
var transportsArray = new JSONArray();
if (clientCreateCredentialResult.Transports != null)
{
foreach (var transport in clientCreateCredentialResult.Transports)
{
transportsArray.Put(transport);
}
}
var responseInnerAndroidJson = new JSONObject();
if (clientCreateCredentialResult.ClientDataJSON != null)
{
responseInnerAndroidJson.Put("clientDataJSON", CoreHelpers.Base64UrlEncode(clientCreateCredentialResult.ClientDataJSON));
}
responseInnerAndroidJson.Put("authenticatorData", CoreHelpers.Base64UrlEncode(clientCreateCredentialResult.AuthData));
responseInnerAndroidJson.Put("attestationObject", CoreHelpers.Base64UrlEncode(clientCreateCredentialResult.AttestationObject));
responseInnerAndroidJson.Put("transports", transportsArray);
responseInnerAndroidJson.Put("publicKeyAlgorithm", clientCreateCredentialResult.PublicKeyAlgorithm);
responseInnerAndroidJson.Put("publicKey", CoreHelpers.Base64UrlEncode(clientCreateCredentialResult.PublicKey));
var rootAndroidJson = new JSONObject();
rootAndroidJson.Put("id", CoreHelpers.Base64UrlEncode(clientCreateCredentialResult.CredentialId));
rootAndroidJson.Put("rawId", CoreHelpers.Base64UrlEncode(clientCreateCredentialResult.CredentialId));
rootAndroidJson.Put("authenticatorAttachment", "platform");
rootAndroidJson.Put("type", "public-key");
rootAndroidJson.Put("clientExtensionResults", MapExtensionsToJson(clientCreateCredentialResult.Extensions));
rootAndroidJson.Put("response", responseInnerAndroidJson);
var result = new Intent();
var publicKeyResponse = new CreatePublicKeyCredentialResponse(rootAndroidJson.ToString());
PendingIntentHandler.SetCreateCredentialResponse(result, publicKeyResponse);
activity.SetResult(Result.Ok, result);
activity.Finish();
async Task DisplayAlertAsync(string title, string message)
{
if (ServiceContainer.TryResolve<IDeviceActionService>(out var deviceActionService))
{
await deviceActionService.DisplayAlertAsync(title, message, AppResources.Ok);
}
}
void FailAndFinish()
{
var result = new Intent();
PendingIntentHandler.SetCreateCredentialException(result, new CreateCredentialUnknownException());
activity.SetResult(Result.Ok, result);
activity.Finish();
}
}
private static Fido2CreateCredentialExtensionsParams MapExtensionsFromJson(PublicKeyCredentialCreationOptions options)
{
if (options == null || !options.Json.Has("extensions"))
{
return null;
}
var extensions = options.Json.GetJSONObject("extensions");
return new Fido2CreateCredentialExtensionsParams
{
CredProps = extensions.Has("credProps") && extensions.GetBoolean("credProps")
};
}
private static JSONObject MapExtensionsToJson(Fido2CreateCredentialExtensionsResult extensions)
{
if (extensions == null)
{
return null;
}
var extensionsJson = new JSONObject();
if (extensions.CredProps != null)
{
var credPropsJson = new JSONObject();
credPropsJson.Put("rk", extensions.CredProps.Rk);
extensionsJson.Put("credProps", credPropsJson);
}
return extensionsJson;
}
public static async Task<string> LoadFido2PrivilegedAllowedListAsync()
{
try
{
using var stream = await FileSystem.OpenAppPackageFileAsync("fido2_privileged_allow_list.json");
using var reader = new StreamReader(stream);
return reader.ReadToEnd();
}
catch
{
return null;
}
}
public static async Task<string> ValidateCallingAppInfoAndGetOriginAsync(CallingAppInfo callingAppInfo, string rpId)
{
if (callingAppInfo.Origin is null)
{
return await ValidateAssetLinksAndGetOriginAsync(callingAppInfo, rpId);
}
var privilegedAllowedList = await LoadFido2PrivilegedAllowedListAsync();
if (privilegedAllowedList is null)
{
throw new InvalidOperationException("Could not load Fido2 privileged allowed list");
}
if (!privilegedAllowedList.Contains($"\"package_name\": \"{callingAppInfo.PackageName}\""))
{
throw new Core.Exceptions.ValidationException(AppResources.PasskeyOperationFailedBecauseBrowserIsNotPrivileged);
}
try
{
return callingAppInfo.GetOrigin(privilegedAllowedList);
}
catch (Java.Lang.IllegalStateException)
{
throw new Core.Exceptions.ValidationException(AppResources.PasskeyOperationFailedBecauseBrowserSignatureDoesNotMatch);
}
catch (Java.Lang.IllegalArgumentException)
{
return null; // wrong list format
}
}
private static async Task<string> ValidateAssetLinksAndGetOriginAsync(CallingAppInfo callingAppInfo, string rpId)
{
if (!ServiceContainer.TryResolve<IAssetLinksService>(out var assetLinksService))
{
throw new InvalidOperationException("Can't resolve IAssetLinksService");
}
var normalizedFingerprint = callingAppInfo.GetLatestCertificationFingerprint();
var isValid = await assetLinksService.ValidateAssetLinksAsync(rpId, callingAppInfo.PackageName, normalizedFingerprint);
return isValid ? callingAppInfo.GetAndroidOrigin() : null;
}
}
}

View File

@@ -1,9 +0,0 @@
namespace Bit.Droid.Autofill
{
public class CredentialProviderConstants
{
public const string CredentialProviderCipherId = "credentialProviderCipherId";
public const string CredentialDataIntentExtra = "CREDENTIAL_DATA";
public const string CredentialIdIntentExtra = "credId";
}
}

View File

@@ -1,12 +1,20 @@
using System.Threading.Tasks; using Android.App;
using Android.App; using Android.Content;
using Android.Content.PM; using Android.Content.PM;
using Android.OS; using Android.OS;
using AndroidX.Credentials;
using AndroidX.Credentials.Provider; using AndroidX.Credentials.Provider;
using AndroidX.Credentials.WebAuthn; using AndroidX.Credentials.WebAuthn;
using Bit.App.Droid.Utilities;
using Bit.App.Abstractions;
using Bit.Core.Abstractions; using Bit.Core.Abstractions;
using Bit.Core.Utilities; using Bit.Core.Utilities;
using Bit.App.Droid.Utilities; using Bit.Core.Resources.Localization;
using Bit.Core.Utilities.Fido2;
using Bit.Core.Services;
using Bit.App.Platforms.Android.Autofill;
using AndroidX.Credentials.Exceptions;
using Org.Json;
namespace Bit.Droid.Autofill namespace Bit.Droid.Autofill
{ {
@@ -15,6 +23,14 @@ namespace Bit.Droid.Autofill
LaunchMode = LaunchMode.SingleTop)] LaunchMode = LaunchMode.SingleTop)]
public class CredentialProviderSelectionActivity : MauiAppCompatActivity public class CredentialProviderSelectionActivity : MauiAppCompatActivity
{ {
private LazyResolve<IFido2MediatorService> _fido2MediatorService = new LazyResolve<IFido2MediatorService>();
private LazyResolve<IFido2AndroidGetAssertionUserInterface> _fido2GetAssertionUserInterface = new LazyResolve<IFido2AndroidGetAssertionUserInterface>();
private LazyResolve<IVaultTimeoutService> _vaultTimeoutService = new LazyResolve<IVaultTimeoutService>();
private LazyResolve<IStateService> _stateService = new LazyResolve<IStateService>();
private LazyResolve<ICipherService> _cipherService = new LazyResolve<ICipherService>();
private LazyResolve<IUserVerificationMediatorService> _userVerificationMediatorService = new LazyResolve<IUserVerificationMediatorService>();
private LazyResolve<IDeviceActionService> _deviceActionService = new LazyResolve<IDeviceActionService>();
protected override void OnCreate(Bundle bundle) protected override void OnCreate(Bundle bundle)
{ {
Intent?.Validate(); Intent?.Validate();
@@ -23,43 +39,145 @@ namespace Bit.Droid.Autofill
var cipherId = Intent?.GetStringExtra(CredentialProviderConstants.CredentialProviderCipherId); var cipherId = Intent?.GetStringExtra(CredentialProviderConstants.CredentialProviderCipherId);
if (string.IsNullOrEmpty(cipherId)) if (string.IsNullOrEmpty(cipherId))
{ {
SetResult(Result.Canceled);
Finish(); Finish();
return; return;
} }
GetCipherAndPerformPasskeyAuthAsync(cipherId).FireAndForget(); GetCipherAndPerformFido2AuthAsync(cipherId).FireAndForget();
} }
private async Task GetCipherAndPerformPasskeyAuthAsync(string cipherId) //Used to avoid crash on MAUI when doing back
public override void OnBackPressed()
{ {
// TODO this is a work in progress Finish();
// https://developer.android.com/training/sign-in/credential-provider#passkeys-implement }
private async Task GetCipherAndPerformFido2AuthAsync(string cipherId)
{
string RpId = string.Empty;
try
{
var getRequest = PendingIntentHandler.RetrieveProviderGetCredentialRequest(Intent); var getRequest = PendingIntentHandler.RetrieveProviderGetCredentialRequest(Intent);
// var publicKeyRequest = getRequest?.CredentialOptions as PublicKeyCredentialRequestOptions;
if (getRequest is null)
{
FailAndFinish();
return;
}
var credentialOption = getRequest.CredentialOptions.FirstOrDefault();
var credentialPublic = credentialOption as GetPublicKeyCredentialOption;
var requestOptions = new PublicKeyCredentialRequestOptions(credentialPublic.RequestJson);
RpId = requestOptions.RpId;
var requestInfo = Intent.GetBundleExtra(CredentialProviderConstants.CredentialDataIntentExtra); var requestInfo = Intent.GetBundleExtra(CredentialProviderConstants.CredentialDataIntentExtra);
var credIdEnc = requestInfo?.GetString(CredentialProviderConstants.CredentialIdIntentExtra); var credentialId = requestInfo?.GetByteArray(CredentialProviderConstants.CredentialIdIntentExtra);
var hasVaultBeenUnlockedInThisTransaction = Intent.GetBooleanExtra(CredentialProviderConstants.CredentialHasVaultBeenUnlockedInThisTransactionExtra, false);
var cipherService = ServiceContainer.Resolve<ICipherService>(); var packageName = getRequest.CallingAppInfo.PackageName;
var cipher = await cipherService.GetAsync(cipherId);
var decCipher = await cipher.DecryptAsync();
var passkey = decCipher.Login.Fido2Credentials.Find(f => f.CredentialId == credIdEnc); string origin;
try
{
origin = await CredentialHelpers.ValidateCallingAppInfoAndGetOriginAsync(getRequest.CallingAppInfo, RpId);
}
catch (Core.Exceptions.ValidationException valEx)
{
await _deviceActionService.Value.DisplayAlertAsync(AppResources.AnErrorHasOccurred, valEx.Message, AppResources.Ok);
FailAndFinish();
return;
}
var credId = Convert.FromBase64String(credIdEnc); if (origin is null)
// var privateKey = Convert.FromBase64String(passkey.PrivateKey); {
// var uid = Convert.FromBase64String(passkey.uid); await _deviceActionService.Value.DisplayAlertAsync(AppResources.ErrorReadingPasskey, AppResources.PasskeysNotSupportedForThisApp, AppResources.Ok);
FailAndFinish();
return;
}
var origin = getRequest?.CallingAppInfo.Origin; _fido2GetAssertionUserInterface.Value.Init(
var packageName = getRequest?.CallingAppInfo.PackageName; cipherId,
false,
() => hasVaultBeenUnlockedInThisTransaction,
RpId
);
// --- continue WIP here (save TOTP copy as last step) --- var clientAssertParams = new Fido2ClientAssertCredentialParams
{
Challenge = requestOptions.GetChallenge(),
RpId = RpId,
AllowCredentials = new Core.Utilities.Fido2.PublicKeyCredentialDescriptor[] { new Core.Utilities.Fido2.PublicKeyCredentialDescriptor { Id = credentialId } },
Origin = origin,
SameOriginWithAncestors = true,
UserVerification = requestOptions.UserVerification
};
// Copy TOTP if needed var extraAssertParams = new Fido2ExtraAssertCredentialParams
var autofillHandler = ServiceContainer.Resolve<IAutofillHandler>(); (
autofillHandler.Autofill(decCipher); getRequest.CallingAppInfo.Origin != null ? credentialPublic.GetClientDataHash() : null,
packageName
);
var assertResult = await _fido2MediatorService.Value.AssertCredentialAsync(clientAssertParams, extraAssertParams);
var result = new Intent();
var responseInnerAndroidJson = new JSONObject();
if (assertResult.ClientDataJSON != null)
{
responseInnerAndroidJson.Put("clientDataJSON", CoreHelpers.Base64UrlEncode(assertResult.ClientDataJSON));
}
responseInnerAndroidJson.Put("authenticatorData", CoreHelpers.Base64UrlEncode(assertResult.AuthenticatorData));
responseInnerAndroidJson.Put("signature", CoreHelpers.Base64UrlEncode(assertResult.Signature));
responseInnerAndroidJson.Put("userHandle", CoreHelpers.Base64UrlEncode(assertResult.SelectedCredential.UserHandle));
var rootAndroidJson = new JSONObject();
rootAndroidJson.Put("id", CoreHelpers.Base64UrlEncode(assertResult.SelectedCredential.Id));
rootAndroidJson.Put("rawId", CoreHelpers.Base64UrlEncode(assertResult.SelectedCredential.Id));
rootAndroidJson.Put("authenticatorAttachment", "platform");
rootAndroidJson.Put("type", "public-key");
rootAndroidJson.Put("clientExtensionResults", new JSONObject());
rootAndroidJson.Put("response", responseInnerAndroidJson);
var json = rootAndroidJson.ToString();
var cred = new PublicKeyCredential(json);
var credResponse = new GetCredentialResponse(cred);
PendingIntentHandler.SetGetCredentialResponse(result, credResponse);
await MainThread.InvokeOnMainThreadAsync(() =>
{
SetResult(Result.Ok, result);
Finish();
});
}
catch (NotAllowedError)
{
await MainThread.InvokeOnMainThreadAsync(async () =>
{
await _deviceActionService.Value.DisplayAlertAsync(AppResources.ErrorReadingPasskey, string.Format(AppResources.ThereWasAProblemReadingAPasskeyForXTryAgainLater, RpId), AppResources.Ok);
FailAndFinish();
});
}
catch (Exception ex)
{
LoggerHelper.LogEvenIfCantBeResolved(ex);
await MainThread.InvokeOnMainThreadAsync(async () =>
{
await _deviceActionService.Value.DisplayAlertAsync(AppResources.ErrorReadingPasskey, string.Format(AppResources.ThereWasAProblemReadingAPasskeyForXTryAgainLater, RpId), AppResources.Ok);
FailAndFinish();
});
}
}
private void FailAndFinish()
{
var result = new Intent();
PendingIntentHandler.SetGetCredentialException(result, new GetCredentialUnknownException());
SetResult(Result.Ok, result);
Finish();
} }
} }
} }

View File

@@ -1,16 +1,15 @@
using Android; using Android;
using Android.App; using Android.App;
using Android.Content; using Android.Content;
using Android.Graphics.Drawables;
using Android.OS; using Android.OS;
using Android.Runtime; using Android.Runtime;
using AndroidX.Credentials.Provider; using AndroidX.Credentials.Provider;
using Bit.Core.Abstractions; using Bit.Core.Abstractions;
using Bit.Core.Utilities; using Bit.Core.Utilities;
using AndroidX.Credentials.Exceptions; using AndroidX.Credentials.Exceptions;
using AndroidX.Credentials.WebAuthn; using Bit.App.Droid.Utilities;
using Bit.Core.Models.View; using Bit.Core.Resources.Localization;
using Resource = Microsoft.Maui.Resource; using Bit.Core.Utilities.Fido2;
namespace Bit.Droid.Autofill namespace Bit.Droid.Autofill
{ {
@@ -20,62 +19,123 @@ namespace Bit.Droid.Autofill
[Register("com.x8bit.bitwarden.Autofill.CredentialProviderService")] [Register("com.x8bit.bitwarden.Autofill.CredentialProviderService")]
public class CredentialProviderService : AndroidX.Credentials.Provider.CredentialProviderService public class CredentialProviderService : AndroidX.Credentials.Provider.CredentialProviderService
{ {
private const string GetPasskeyIntentAction = "PACKAGE_NAME.GET_PASSKEY"; public const string GetFido2IntentAction = "PACKAGE_NAME.GET_PASSKEY";
private const int UniqueRequestCode = 94556023; public const string CreateFido2IntentAction = "PACKAGE_NAME.CREATE_PASSKEY";
public const int UniqueGetRequestCode = 94556023;
public const int UniqueCreateRequestCode = 94556024;
private ICipherService _cipherService; private readonly LazyResolve<IVaultTimeoutService> _vaultTimeoutService = new LazyResolve<IVaultTimeoutService>();
private IUserVerificationService _userVerificationService; private readonly LazyResolve<IStateService> _stateService = new LazyResolve<IStateService>();
private IVaultTimeoutService _vaultTimeoutService; private readonly LazyResolve<ILogger> _logger = new LazyResolve<ILogger>();
private LazyResolve<ILogger> _logger = new LazyResolve<ILogger>("logger");
public override async void OnBeginCreateCredentialRequest(BeginCreateCredentialRequest request, public override async void OnBeginCreateCredentialRequest(BeginCreateCredentialRequest request,
CancellationSignal cancellationSignal, IOutcomeReceiver callback) => throw new NotImplementedException(); CancellationSignal cancellationSignal, IOutcomeReceiver callback)
{
try
{
var response = await ProcessCreateCredentialsRequestAsync(request);
if (response != null)
{
await MainThread.InvokeOnMainThreadAsync(() => callback.OnResult(response));
return;
}
}
catch (Exception ex)
{
_logger.Value.Exception(ex);
}
MainThread.BeginInvokeOnMainThread(() => callback.OnError(AppResources.ErrorCreatingPasskey));
}
public override async void OnBeginGetCredentialRequest(BeginGetCredentialRequest request, public override async void OnBeginGetCredentialRequest(BeginGetCredentialRequest request,
CancellationSignal cancellationSignal, IOutcomeReceiver callback) CancellationSignal cancellationSignal, IOutcomeReceiver callback)
{ {
try try
{ {
_vaultTimeoutService ??= ServiceContainer.Resolve<IVaultTimeoutService>(); await _vaultTimeoutService.Value.CheckVaultTimeoutAsync();
var locked = await _vaultTimeoutService.Value.IsLockedAsync();
await _vaultTimeoutService.CheckVaultTimeoutAsync();
var locked = await _vaultTimeoutService.IsLockedAsync();
if (!locked) if (!locked)
{ {
var response = await ProcessGetCredentialsRequestAsync(request); var response = await ProcessGetCredentialsRequestAsync(request);
callback.OnResult(response); callback.OnResult(response);
return;
} }
// TODO handle auth/unlock account flow
var intent = new Intent(ApplicationContext, typeof(MainActivity));
intent.PutExtra(CredentialProviderConstants.Fido2CredentialAction, CredentialProviderConstants.Fido2CredentialGet);
var pendingIntent = PendingIntent.GetActivity(ApplicationContext, UniqueGetRequestCode, intent,
AndroidHelpers.AddPendingIntentMutabilityFlag(PendingIntentFlags.UpdateCurrent, true));
var unlockAction = new AuthenticationAction(AppResources.Unlock, pendingIntent);
var unlockResponse = new BeginGetCredentialResponse.Builder()
.SetAuthenticationActions(new List<AuthenticationAction>() { unlockAction } )
.Build();
callback.OnResult(unlockResponse);
} }
catch (GetCredentialException e) catch (GetCredentialException e)
{ {
_logger.Value.Exception(e); _logger.Value.Exception(e);
callback.OnError(e.ErrorMessage ?? "Error getting credentials"); callback.OnError(e.ErrorMessage ?? AppResources.ErrorReadingPasskey);
} }
catch (Exception e) catch (Exception e)
{ {
_logger.Value.Exception(e); _logger.Value.Exception(e);
throw; callback.OnError(AppResources.ErrorReadingPasskey);
} }
} }
private async Task<BeginCreateCredentialResponse> ProcessCreateCredentialsRequestAsync(
BeginCreateCredentialRequest request)
{
if (request == null) { return null; }
if (request is BeginCreatePasswordCredentialRequest beginCreatePasswordCredentialRequest)
{
//This flow can be used if Password flow needs to be implemented
throw new NotImplementedException();
//return HandleCreatePasswordQuery(beginCreatePasswordCredentialRequest);
}
else if (request is BeginCreatePublicKeyCredentialRequest beginCreatePublicKeyCredentialRequest)
{
return await HandleCreatePasskeyQueryAsync(beginCreatePublicKeyCredentialRequest);
}
return null;
}
private async Task<BeginCreateCredentialResponse> HandleCreatePasskeyQueryAsync(BeginCreatePublicKeyCredentialRequest optionRequest)
{
var intent = new Intent(ApplicationContext, typeof(MainActivity));
intent.PutExtra(CredentialProviderConstants.Fido2CredentialAction, CredentialProviderConstants.Fido2CredentialCreate);
var pendingIntent = PendingIntent.GetActivity(ApplicationContext, UniqueCreateRequestCode, intent,
AndroidHelpers.AddPendingIntentMutabilityFlag(PendingIntentFlags.UpdateCurrent, true));
var userEmail = await GetSafeActiveAccountEmailAsync();
var createEntryBuilder = new CreateEntry.Builder(userEmail ?? AppResources.Bitwarden, pendingIntent)
.SetDescription(userEmail != null
? string.Format(AppResources.YourPasskeyWillBeSavedToYourBitwardenVaultForX, userEmail)
: AppResources.YourPasskeyWillBeSavedToYourBitwardenVault)
.Build();
var createCredentialResponse = new BeginCreateCredentialResponse.Builder()
.AddCreateEntry(createEntryBuilder);
return createCredentialResponse.Build();
}
private async Task<BeginGetCredentialResponse> ProcessGetCredentialsRequestAsync( private async Task<BeginGetCredentialResponse> ProcessGetCredentialsRequestAsync(
BeginGetCredentialRequest request) BeginGetCredentialRequest request)
{ {
IList<CredentialEntry> credentialEntries = null; var credentialEntries = new List<CredentialEntry>();
foreach (var option in request.BeginGetCredentialOptions) foreach (var option in request.BeginGetCredentialOptions.OfType<BeginGetPublicKeyCredentialOption>())
{ {
var credentialOption = option as BeginGetPublicKeyCredentialOption; credentialEntries.AddRange(await Bit.App.Platforms.Android.Autofill.CredentialHelpers.PopulatePasskeyDataAsync(request.CallingAppInfo, option, ApplicationContext, false));
if (credentialOption != null)
{
credentialEntries ??= new List<CredentialEntry>();
((List<CredentialEntry>)credentialEntries).AddRange(
await PopulatePasskeyDataAsync(request.CallingAppInfo, credentialOption));
}
} }
if (credentialEntries == null) if (!credentialEntries.Any())
{ {
return new BeginGetCredentialResponse(); return new BeginGetCredentialResponse();
} }
@@ -85,63 +145,24 @@ namespace Bit.Droid.Autofill
.Build(); .Build();
} }
private async Task<List<CredentialEntry>> PopulatePasskeyDataAsync(CallingAppInfo callingAppInfo,
BeginGetPublicKeyCredentialOption option)
{
var packageName = callingAppInfo.PackageName;
var origin = callingAppInfo.Origin;
var signingInfo = callingAppInfo.SigningInfo;
var request = new PublicKeyCredentialRequestOptions(option.RequestJson);
var passkeyEntries = new List<CredentialEntry>();
_cipherService ??= ServiceContainer.Resolve<ICipherService>();
var ciphers = await _cipherService.GetAllDecryptedForUrlAsync(origin);
if (ciphers == null)
{
return passkeyEntries;
}
var passkeyCiphers = ciphers.Where(cipher => cipher.HasFido2Credential).ToList();
if (!passkeyCiphers.Any())
{
return passkeyEntries;
}
foreach (var cipher in passkeyCiphers)
{
var passkeyEntry = GetPasskey(cipher, option);
passkeyEntries.Add(passkeyEntry);
}
return passkeyEntries;
}
private PublicKeyCredentialEntry GetPasskey(CipherView cipher, BeginGetPublicKeyCredentialOption option)
{
var credDataBundle = new Bundle();
credDataBundle.PutString(CredentialProviderConstants.CredentialIdIntentExtra,
cipher.Login.MainFido2Credential.CredentialId);
var intent = new Intent(ApplicationContext, typeof(CredentialProviderSelectionActivity))
.SetAction(GetPasskeyIntentAction).SetPackage(Constants.PACKAGE_NAME);
intent.PutExtra(CredentialProviderConstants.CredentialDataIntentExtra, credDataBundle);
intent.PutExtra(CredentialProviderConstants.CredentialProviderCipherId, cipher.Id);
var pendingIntent = PendingIntent.GetActivity(ApplicationContext, UniqueRequestCode, intent,
PendingIntentFlags.Mutable | PendingIntentFlags.UpdateCurrent);
return new PublicKeyCredentialEntry.Builder(
ApplicationContext,
cipher.Login.Username ?? "No username",
pendingIntent,
option)
.SetDisplayName(cipher.Name)
.SetIcon(Icon.CreateWithResource(ApplicationContext, Resource.Drawable.icon))
.Build();
}
public override void OnClearCredentialStateRequest(ProviderClearCredentialStateRequest request, public override void OnClearCredentialStateRequest(ProviderClearCredentialStateRequest request,
CancellationSignal cancellationSignal, IOutcomeReceiver callback) => throw new NotImplementedException(); CancellationSignal cancellationSignal, IOutcomeReceiver callback)
{
callback.OnResult(null);
}
private async Task<string> GetSafeActiveAccountEmailAsync()
{
try
{
return await _stateService.Value.GetEmailAsync();
}
catch (Exception ex)
{
// if it throws to get the user's email then we log and continue showing a more generic message
_logger.Value.Exception(ex);
return null;
}
}
} }
} }

View File

@@ -0,0 +1,77 @@
using Bit.Core.Abstractions;
using Bit.Core.Services;
using Bit.Core.Utilities.Fido2;
namespace Bit.App.Platforms.Android.Autofill
{
public interface IFido2AndroidGetAssertionUserInterface : IFido2GetAssertionUserInterface
{
void Init(string cipherId,
bool userVerified,
Func<bool> hasVaultBeenUnlockedInThisTransaction,
string rpId);
}
public class Fido2GetAssertionUserInterface : Core.Utilities.Fido2.Fido2GetAssertionUserInterface, IFido2AndroidGetAssertionUserInterface
{
private readonly IStateService _stateService;
private readonly IVaultTimeoutService _vaultTimeoutService;
private readonly ICipherService _cipherService;
private readonly IUserVerificationMediatorService _userVerificationMediatorService;
public Fido2GetAssertionUserInterface(IStateService stateService,
IVaultTimeoutService vaultTimeoutService,
ICipherService cipherService,
IUserVerificationMediatorService userVerificationMediatorService)
{
_stateService = stateService;
_vaultTimeoutService = vaultTimeoutService;
_cipherService = cipherService;
_userVerificationMediatorService = userVerificationMediatorService;
}
public void Init(string cipherId,
bool userVerified,
Func<bool> hasVaultBeenUnlockedInThisTransaction,
string rpId)
{
Init(cipherId,
userVerified,
EnsureAuthenAndVaultUnlockedAsync,
hasVaultBeenUnlockedInThisTransaction,
(cipherId, userVerificationPreference) => VerifyUserAsync(cipherId, userVerificationPreference, rpId, hasVaultBeenUnlockedInThisTransaction()));
}
public async Task EnsureAuthenAndVaultUnlockedAsync()
{
if (!await _stateService.IsAuthenticatedAsync() || await _vaultTimeoutService.IsLockedAsync())
{
// this should never happen but just in case.
throw new InvalidOperationException("Not authed or vault locked");
}
}
private async Task<bool> VerifyUserAsync(string selectedCipherId, Fido2UserVerificationPreference userVerificationPreference, string rpId, bool vaultUnlockedDuringThisTransaction)
{
try
{
var encrypted = await _cipherService.GetAsync(selectedCipherId);
var cipher = await encrypted.DecryptAsync();
var userVerification = await _userVerificationMediatorService.VerifyUserForFido2Async(
new Fido2UserVerificationOptions(
cipher?.Reprompt == Core.Enums.CipherRepromptType.Password,
userVerificationPreference,
vaultUnlockedDuringThisTransaction,
rpId)
);
return !userVerification.IsCancelled && userVerification.Result;
}
catch (Exception ex)
{
LoggerHelper.LogEvenIfCantBeResolved(ex);
return false;
}
}
}
}

View File

@@ -0,0 +1,202 @@
using Bit.App.Abstractions;
using Bit.Core.Abstractions;
using Bit.Core.Resources.Localization;
using Bit.Core.Services;
using Bit.Core.Utilities;
using Bit.Core.Utilities.Fido2;
namespace Bit.App.Platforms.Android.Autofill
{
public class Fido2MakeCredentialUserInterface : IFido2MakeCredentialConfirmationUserInterface
{
private readonly IStateService _stateService;
private readonly IVaultTimeoutService _vaultTimeoutService;
private readonly ICipherService _cipherService;
private readonly IUserVerificationMediatorService _userVerificationMediatorService;
private readonly IDeviceActionService _deviceActionService;
private readonly IPlatformUtilsService _platformUtilsService;
private LazyResolve<IMessagingService> _messagingService = new LazyResolve<IMessagingService>();
private TaskCompletionSource<(string cipherId, bool? userVerified)> _confirmCredentialTcs;
private TaskCompletionSource<bool> _unlockVaultTcs;
private Fido2UserVerificationOptions? _currentDefaultUserVerificationOptions;
private Func<bool> _checkHasVaultBeenUnlockedInThisTransaction;
public Fido2MakeCredentialUserInterface(IStateService stateService,
IVaultTimeoutService vaultTimeoutService,
ICipherService cipherService,
IUserVerificationMediatorService userVerificationMediatorService,
IDeviceActionService deviceActionService,
IPlatformUtilsService platformUtilsService)
{
_stateService = stateService;
_vaultTimeoutService = vaultTimeoutService;
_cipherService = cipherService;
_userVerificationMediatorService = userVerificationMediatorService;
_deviceActionService = deviceActionService;
_platformUtilsService = platformUtilsService;
}
public bool HasVaultBeenUnlockedInThisTransaction => _checkHasVaultBeenUnlockedInThisTransaction?.Invoke() == true;
public bool IsConfirmingNewCredential => _confirmCredentialTcs?.Task != null && !_confirmCredentialTcs.Task.IsCompleted;
public bool IsWaitingUnlockVault => _unlockVaultTcs?.Task != null && !_unlockVaultTcs.Task.IsCompleted;
public async Task<(string CipherId, bool UserVerified)> ConfirmNewCredentialAsync(Fido2ConfirmNewCredentialParams confirmNewCredentialParams)
{
_confirmCredentialTcs?.TrySetCanceled();
_confirmCredentialTcs = null;
_confirmCredentialTcs = new TaskCompletionSource<(string cipherId, bool? userVerified)>();
_currentDefaultUserVerificationOptions = new Fido2UserVerificationOptions(false, confirmNewCredentialParams.UserVerificationPreference, HasVaultBeenUnlockedInThisTransaction, confirmNewCredentialParams.RpId);
_messagingService.Value.Send(Bit.Core.Constants.CredentialNavigateToAutofillCipherMessageCommand, confirmNewCredentialParams);
var (cipherId, isUserVerified) = await _confirmCredentialTcs.Task;
var verified = isUserVerified;
if (verified is null)
{
var userVerification = await VerifyUserAsync(cipherId, confirmNewCredentialParams.UserVerificationPreference, confirmNewCredentialParams.RpId);
// TODO: If cancelled then let the user choose another cipher.
// I think this can be done by showing a message to the uesr and recursive calling of this method ConfirmNewCredentialAsync
verified = !userVerification.IsCancelled && userVerification.Result;
}
if (cipherId is null)
{
return await CreateNewLoginForFido2CredentialAsync(confirmNewCredentialParams, verified.Value);
}
return (cipherId, verified.Value);
}
private async Task<(string CipherId, bool UserVerified)> CreateNewLoginForFido2CredentialAsync(Fido2ConfirmNewCredentialParams confirmNewCredentialParams, bool userVerified)
{
if (!userVerified && await _userVerificationMediatorService.ShouldEnforceFido2RequiredUserVerificationAsync(new Fido2UserVerificationOptions
(
false,
confirmNewCredentialParams.UserVerificationPreference,
true,
confirmNewCredentialParams.RpId
)))
{
return (null, false);
}
try
{
await _deviceActionService.ShowLoadingAsync(AppResources.Loading);
var cipherId = await _cipherService.CreateNewLoginForPasskeyAsync(confirmNewCredentialParams);
await _deviceActionService.HideLoadingAsync();
return (cipherId, userVerified);
}
catch
{
await _deviceActionService.HideLoadingAsync();
throw;
}
}
public async Task EnsureUnlockedVaultAsync()
{
if (!await _stateService.IsAuthenticatedAsync()
||
await _vaultTimeoutService.IsLoggedOutByTimeoutAsync()
||
await _vaultTimeoutService.ShouldLogOutByTimeoutAsync())
{
await NavigateAndWaitForUnlockAsync(Bit.Core.Enums.NavigationTarget.HomeLogin);
return;
}
if (!await _vaultTimeoutService.IsLockedAsync())
{
return;
}
await NavigateAndWaitForUnlockAsync(Bit.Core.Enums.NavigationTarget.Lock);
}
private async Task NavigateAndWaitForUnlockAsync(Bit.Core.Enums.NavigationTarget navTarget)
{
_unlockVaultTcs?.TrySetCanceled();
_unlockVaultTcs = new TaskCompletionSource<bool>();
_messagingService.Value.Send(Bit.Core.Constants.NavigateToMessageCommand, navTarget);
await _unlockVaultTcs.Task;
}
public Task InformExcludedCredentialAsync(string[] existingCipherIds)
{
// TODO: Show excluded credential to the user in some screen.
return Task.FromResult(true);
}
public void SetCheckHasVaultBeenUnlockedInThisTransaction(Func<bool> checkHasVaultBeenUnlockedInThisTransaction)
{
_checkHasVaultBeenUnlockedInThisTransaction = checkHasVaultBeenUnlockedInThisTransaction;
}
public void Confirm(string cipherId, bool? userVerified) => _confirmCredentialTcs?.TrySetResult((cipherId, userVerified));
public void ConfirmVaultUnlocked() => _unlockVaultTcs?.TrySetResult(true);
public async Task ConfirmAsync(string cipherId, bool alreadyHasFido2Credential, bool? userVerified)
{
if (alreadyHasFido2Credential
&&
!await _platformUtilsService.ShowDialogAsync(
AppResources.ThisItemAlreadyContainsAPasskeyAreYouSureYouWantToOverwriteTheCurrentPasskey,
AppResources.OverwritePasskey,
AppResources.Yes,
AppResources.No))
{
return;
}
Confirm(cipherId, userVerified);
}
public void Cancel() => _confirmCredentialTcs?.TrySetCanceled();
public void OnConfirmationException(Exception ex) => _confirmCredentialTcs?.TrySetException(ex);
private async Task<CancellableResult<bool>> VerifyUserAsync(string selectedCipherId, Fido2UserVerificationPreference userVerificationPreference, string rpId)
{
try
{
if (selectedCipherId is null && userVerificationPreference == Fido2UserVerificationPreference.Discouraged)
{
return new CancellableResult<bool>(false);
}
var shouldCheckMasterPasswordReprompt = false;
if (selectedCipherId != null)
{
var encrypted = await _cipherService.GetAsync(selectedCipherId);
var cipher = await encrypted.DecryptAsync();
shouldCheckMasterPasswordReprompt = cipher?.Reprompt == Core.Enums.CipherRepromptType.Password;
}
return await _userVerificationMediatorService.VerifyUserForFido2Async(
new Fido2UserVerificationOptions(
shouldCheckMasterPasswordReprompt,
userVerificationPreference,
HasVaultBeenUnlockedInThisTransaction,
rpId)
);
}
catch (Exception ex)
{
LoggerHelper.LogEvenIfCantBeResolved(ex);
return new CancellableResult<bool>(false);
}
}
public Fido2UserVerificationOptions? GetCurrentUserVerificationOptions() => _currentDefaultUserVerificationOptions;
}
}

View File

@@ -24,6 +24,7 @@ using Bit.App.Droid.Utilities;
using Newtonsoft.Json; using Newtonsoft.Json;
using Newtonsoft.Json.Linq; using Newtonsoft.Json.Linq;
using FileProvider = AndroidX.Core.Content.FileProvider; using FileProvider = AndroidX.Core.Content.FileProvider;
using Bit.Core.Utilities.Fido2;
namespace Bit.Droid namespace Bit.Droid
{ {
@@ -167,6 +168,13 @@ namespace Bit.Droid
base.OnNewIntent(intent); base.OnNewIntent(intent);
try try
{ {
if (intent?.GetStringExtra(CredentialProviderConstants.Fido2CredentialAction) == CredentialProviderConstants.Fido2CredentialCreate
&&
_appOptions != null)
{
_appOptions.HasUnlockedInThisTransaction = false;
}
if (intent?.GetStringExtra("uri") is string uri) if (intent?.GetStringExtra("uri") is string uri)
{ {
_messagingService.Send(App.App.POP_ALL_AND_GO_TO_AUTOFILL_CIPHERS_MESSAGE); _messagingService.Send(App.App.POP_ALL_AND_GO_TO_AUTOFILL_CIPHERS_MESSAGE);
@@ -325,12 +333,15 @@ namespace Bit.Droid
private AppOptions GetOptions() private AppOptions GetOptions()
{ {
var fido2CredentialAction = Intent.GetStringExtra(CredentialProviderConstants.Fido2CredentialAction);
var options = new AppOptions var options = new AppOptions
{ {
Uri = Intent.GetStringExtra("uri") ?? Intent.GetStringExtra(AutofillConstants.AutofillFrameworkUri), Uri = Intent.GetStringExtra("uri") ?? Intent.GetStringExtra(AutofillConstants.AutofillFrameworkUri),
MyVaultTile = Intent.GetBooleanExtra("myVaultTile", false), MyVaultTile = Intent.GetBooleanExtra("myVaultTile", false),
GeneratorTile = Intent.GetBooleanExtra("generatorTile", false), GeneratorTile = Intent.GetBooleanExtra("generatorTile", false),
FromAutofillFramework = Intent.GetBooleanExtra(AutofillConstants.AutofillFramework, false), FromAutofillFramework = Intent.GetBooleanExtra(AutofillConstants.AutofillFramework, false),
Fido2CredentialAction = fido2CredentialAction,
FromFido2Framework = !string.IsNullOrWhiteSpace(fido2CredentialAction),
CreateSend = GetCreateSendRequest(Intent) CreateSend = GetCreateSendRequest(Intent)
}; };
var fillType = Intent.GetIntExtra(AutofillConstants.AutofillFrameworkFillType, 0); var fillType = Intent.GetIntExtra(AutofillConstants.AutofillFrameworkFillType, 0);

View File

@@ -20,7 +20,10 @@ using Bit.App.Utilities;
using Bit.App.Pages; using Bit.App.Pages;
using Bit.App.Utilities.AccountManagement; using Bit.App.Utilities.AccountManagement;
using Bit.App.Controls; using Bit.App.Controls;
using Bit.App.Platforms.Android.Autofill;
using Bit.Core.Enums; using Bit.Core.Enums;
using Bit.Core.Services.UserVerification;
#if !FDROID #if !FDROID
using Android.Gms.Security; using Android.Gms.Security;
#endif #endif
@@ -85,6 +88,57 @@ namespace Bit.Droid
ServiceContainer.Resolve<IWatchDeviceService>(), ServiceContainer.Resolve<IWatchDeviceService>(),
ServiceContainer.Resolve<IConditionedAwaiterManager>()); ServiceContainer.Resolve<IConditionedAwaiterManager>());
ServiceContainer.Register<IAccountsManager>("accountsManager", accountsManager); ServiceContainer.Register<IAccountsManager>("accountsManager", accountsManager);
var userPinService = new UserPinService(
ServiceContainer.Resolve<IStateService>(),
ServiceContainer.Resolve<ICryptoService>(),
ServiceContainer.Resolve<IVaultTimeoutService>());
ServiceContainer.Register<IUserPinService>(userPinService);
var userVerificationMediatorService = new UserVerificationMediatorService(
ServiceContainer.Resolve<IPlatformUtilsService>("platformUtilsService"),
ServiceContainer.Resolve<IPasswordRepromptService>("passwordRepromptService"),
userPinService,
deviceActionService,
ServiceContainer.Resolve<IUserVerificationService>());
ServiceContainer.Register<IUserVerificationMediatorService>(userVerificationMediatorService);
var fido2AuthenticatorService = new Fido2AuthenticatorService(
ServiceContainer.Resolve<ICipherService>(),
ServiceContainer.Resolve<ISyncService>(),
ServiceContainer.Resolve<ICryptoFunctionService>(),
userVerificationMediatorService);
ServiceContainer.Register<IFido2AuthenticatorService>(fido2AuthenticatorService);
var fido2GetAssertionUserInterface = new Fido2GetAssertionUserInterface(
ServiceContainer.Resolve<IStateService>(),
ServiceContainer.Resolve<IVaultTimeoutService>(),
ServiceContainer.Resolve<ICipherService>(),
ServiceContainer.Resolve<IUserVerificationMediatorService>());
ServiceContainer.Register<IFido2AndroidGetAssertionUserInterface>(fido2GetAssertionUserInterface);
var fido2MakeCredentialUserInterface = new Fido2MakeCredentialUserInterface(
ServiceContainer.Resolve<IStateService>(),
ServiceContainer.Resolve<IVaultTimeoutService>(),
ServiceContainer.Resolve<ICipherService>(),
ServiceContainer.Resolve<IUserVerificationMediatorService>(),
ServiceContainer.Resolve<IDeviceActionService>(),
ServiceContainer.Resolve<IPlatformUtilsService>());
ServiceContainer.Register<IFido2MakeCredentialConfirmationUserInterface>(fido2MakeCredentialUserInterface);
var fido2ClientService = new Fido2ClientService(
ServiceContainer.Resolve<IStateService>(),
ServiceContainer.Resolve<IEnvironmentService>(),
ServiceContainer.Resolve<ICryptoFunctionService>(),
ServiceContainer.Resolve<IFido2AuthenticatorService>(),
fido2GetAssertionUserInterface,
fido2MakeCredentialUserInterface);
ServiceContainer.Register<IFido2ClientService>(fido2ClientService);
ServiceContainer.Register<IFido2MediatorService>(new Fido2MediatorService(
fido2AuthenticatorService,
fido2ClientService,
ServiceContainer.Resolve<ICipherService>()));
} }
#if !FDROID #if !FDROID
if (Build.VERSION.SdkInt <= BuildVersionCodes.Kitkat) if (Build.VERSION.SdkInt <= BuildVersionCodes.Kitkat)
@@ -160,7 +214,6 @@ namespace Bit.Droid
var cryptoFunctionService = new PclCryptoFunctionService(cryptoPrimitiveService); var cryptoFunctionService = new PclCryptoFunctionService(cryptoPrimitiveService);
var cryptoService = new CryptoService(stateService, cryptoFunctionService, logger); var cryptoService = new CryptoService(stateService, cryptoFunctionService, logger);
var biometricService = new BiometricService(stateService, cryptoService); var biometricService = new BiometricService(stateService, cryptoService);
var userPinService = new UserPinService(stateService, cryptoService);
var passwordRepromptService = new MobilePasswordRepromptService(platformUtilsService, cryptoService, stateService); var passwordRepromptService = new MobilePasswordRepromptService(platformUtilsService, cryptoService, stateService);
ServiceContainer.Register<ISynchronousStorageService>(preferencesStorage); ServiceContainer.Register<ISynchronousStorageService>(preferencesStorage);
@@ -184,7 +237,6 @@ namespace Bit.Droid
ServiceContainer.Register<ICryptoService>("cryptoService", cryptoService); ServiceContainer.Register<ICryptoService>("cryptoService", cryptoService);
ServiceContainer.Register<IPasswordRepromptService>("passwordRepromptService", passwordRepromptService); ServiceContainer.Register<IPasswordRepromptService>("passwordRepromptService", passwordRepromptService);
ServiceContainer.Register<IAvatarImageSourcePool>("avatarImageSourcePool", new AvatarImageSourcePool()); ServiceContainer.Register<IAvatarImageSourcePool>("avatarImageSourcePool", new AvatarImageSourcePool());
ServiceContainer.Register<IUserPinService>(userPinService);
// Push // Push
#if FDROID #if FDROID

View File

@@ -1,11 +1,11 @@
using System.Linq; using Android.App;
using System.Threading.Tasks;
using Android.App;
using Android.App.Assist; using Android.App.Assist;
using Android.Content; using Android.Content;
using Android.Credentials;
using Android.OS; using Android.OS;
using Android.Provider; using Android.Provider;
using Android.Views.Autofill; using Android.Views.Autofill;
using Bit.App.Abstractions;
using Bit.Core.Resources.Localization; using Bit.Core.Resources.Localization;
using Bit.Core.Abstractions; using Bit.Core.Abstractions;
using Bit.Core.Enums; using Bit.Core.Enums;
@@ -43,9 +43,28 @@ namespace Bit.Droid.Services
{ {
return false; return false;
} }
try try
{ {
// TODO - find a way to programmatically check if the credential provider service is enabled var activity = (MainActivity)Platform.CurrentActivity;
if (activity == null)
{
return false;
}
var credManager = activity.GetSystemService(Java.Lang.Class.FromType(typeof(CredentialManager))) as CredentialManager;
if (credManager == null)
{
return false;
}
var credentialProviderServiceComponentName = new ComponentName(activity, Java.Lang.Class.FromType(typeof(CredentialProviderService)));
return credManager.IsEnabledCredentialProviderService(credentialProviderServiceComponentName);
}
catch (Java.Lang.NullPointerException)
{
// CredentialManager API is not working fully and may return a NullPointerException even if the CredentialProviderService is working and enabled
// Info Here: https://developer.android.com/reference/android/credentials/CredentialManager#isEnabledCredentialProviderService(android.content.ComponentName)
return false; return false;
} }
catch catch
@@ -184,7 +203,10 @@ namespace Bit.Droid.Services
{ {
try try
{ {
// TODO - find a way to programmatically disable the provider service, or take the user to the settings page where they can do it // We should try to find a way to programmatically disable the provider service when the API allows for it.
// For now we'll take the user to Credential Settings so they can manually disable it
var deviceActionService = ServiceContainer.Resolve<IDeviceActionService>();
deviceActionService.OpenCredentialProviderSettings();
} }
catch { } catch { }
} }

View File

@@ -1,6 +1,4 @@
using System; using Android.App;
using System.Threading.Tasks;
using Android.App;
using Android.Content; using Android.Content;
using Android.Content.PM; using Android.Content.PM;
using Android.Nfc; using Android.Nfc;
@@ -17,11 +15,14 @@ using Bit.Core.Resources.Localization;
using Bit.App.Utilities; using Bit.App.Utilities;
using Bit.App.Utilities.Prompts; using Bit.App.Utilities.Prompts;
using Bit.Core.Abstractions; using Bit.Core.Abstractions;
using Bit.Core.Enums;
using Bit.App.Droid.Utilities; using Bit.App.Droid.Utilities;
using Bit.App.Models;
using Bit.Droid.Autofill;
using Microsoft.Maui.Controls.Compatibility.Platform.Android; using Microsoft.Maui.Controls.Compatibility.Platform.Android;
using Resource = Bit.Core.Resource; using Resource = Bit.Core.Resource;
using Application = Android.App.Application; using Application = Android.App.Application;
using Bit.Core.Services;
using Bit.Core.Utilities.Fido2;
namespace Bit.Droid.Services namespace Bit.Droid.Services
{ {
@@ -72,6 +73,8 @@ namespace Bit.Droid.Services
} }
public bool LaunchApp(string appName) public bool LaunchApp(string appName)
{
try
{ {
if ((int)Build.VERSION.SdkInt < 33) if ((int)Build.VERSION.SdkInt < 33)
{ {
@@ -85,6 +88,15 @@ namespace Bit.Droid.Services
launchIntentSender?.SendIntent(activity, Result.Ok, null, null, null); launchIntentSender?.SendIntent(activity, Result.Ok, null, null, null);
return launchIntentSender != null; return launchIntentSender != null;
} }
catch (IntentSender.SendIntentException)
{
return false;
}
catch (Android.Util.AndroidException)
{
return false;
}
}
public async Task ShowLoadingAsync(string text) public async Task ShowLoadingAsync(string text)
{ {
@@ -193,7 +205,7 @@ namespace Bit.Droid.Services
string text = null, string okButtonText = null, string cancelButtonText = null, string text = null, string okButtonText = null, string cancelButtonText = null,
bool numericKeyboard = false, bool autofocus = true, bool password = false) bool numericKeyboard = false, bool autofocus = true, bool password = false)
{ {
var activity = (MainActivity)Microsoft.Maui.ApplicationModel.Platform.CurrentActivity; var activity = Microsoft.Maui.ApplicationModel.Platform.CurrentActivity;
if (activity == null) if (activity == null)
{ {
return Task.FromResult<string>(null); return Task.FromResult<string>(null);
@@ -250,7 +262,7 @@ namespace Bit.Droid.Services
public Task<ValidatablePromptResponse?> DisplayValidatablePromptAsync(ValidatablePromptConfig config) public Task<ValidatablePromptResponse?> DisplayValidatablePromptAsync(ValidatablePromptConfig config)
{ {
var activity = (MainActivity)Microsoft.Maui.ApplicationModel.Platform.CurrentActivity; var activity = Microsoft.Maui.ApplicationModel.Platform.CurrentActivity;
if (activity == null) if (activity == null)
{ {
return Task.FromResult<ValidatablePromptResponse?>(null); return Task.FromResult<ValidatablePromptResponse?>(null);
@@ -327,7 +339,7 @@ namespace Bit.Droid.Services
public void RateApp() public void RateApp()
{ {
var activity = (MainActivity)Microsoft.Maui.ApplicationModel.Platform.CurrentActivity; var activity = Microsoft.Maui.ApplicationModel.Platform.CurrentActivity;
try try
{ {
var rateIntent = RateIntentForUrl("market://details", activity); var rateIntent = RateIntentForUrl("market://details", activity);
@@ -360,14 +372,14 @@ namespace Bit.Droid.Services
public bool SupportsNfc() public bool SupportsNfc()
{ {
var activity = (MainActivity)Microsoft.Maui.ApplicationModel.Platform.CurrentActivity; var activity = Microsoft.Maui.ApplicationModel.Platform.CurrentActivity;
var manager = activity.GetSystemService(Context.NfcService) as NfcManager; var manager = activity.GetSystemService(Context.NfcService) as NfcManager;
return manager.DefaultAdapter?.IsEnabled ?? false; return manager.DefaultAdapter?.IsEnabled ?? false;
} }
public bool SupportsCamera() public bool SupportsCamera()
{ {
var activity = (MainActivity)Microsoft.Maui.ApplicationModel.Platform.CurrentActivity; var activity = Microsoft.Maui.ApplicationModel.Platform.CurrentActivity;
return activity.PackageManager.HasSystemFeature(PackageManager.FeatureCamera); return activity.PackageManager.HasSystemFeature(PackageManager.FeatureCamera);
} }
@@ -383,7 +395,7 @@ namespace Bit.Droid.Services
public Task<string> DisplayAlertAsync(string title, string message, string cancel, params string[] buttons) public Task<string> DisplayAlertAsync(string title, string message, string cancel, params string[] buttons)
{ {
var activity = (MainActivity)Microsoft.Maui.ApplicationModel.Platform.CurrentActivity; var activity = Microsoft.Maui.ApplicationModel.Platform.CurrentActivity;
if (activity == null) if (activity == null)
{ {
return Task.FromResult<string>(null); return Task.FromResult<string>(null);
@@ -464,7 +476,7 @@ namespace Bit.Droid.Services
public void OpenAccessibilityOverlayPermissionSettings() public void OpenAccessibilityOverlayPermissionSettings()
{ {
var activity = (MainActivity)Microsoft.Maui.ApplicationModel.Platform.CurrentActivity; var activity = Microsoft.Maui.ApplicationModel.Platform.CurrentActivity;
try try
{ {
var intent = new Intent(Settings.ActionManageOverlayPermission); var intent = new Intent(Settings.ActionManageOverlayPermission);
@@ -493,10 +505,10 @@ namespace Bit.Droid.Services
public void OpenCredentialProviderSettings() public void OpenCredentialProviderSettings()
{ {
var activity = (MainActivity)Microsoft.Maui.ApplicationModel.Platform.CurrentActivity; var activity = Microsoft.Maui.ApplicationModel.Platform.CurrentActivity;
try try
{ {
var pendingIntent = CredentialManager.Create(activity).CreateSettingsPendingIntent(); var pendingIntent = ICredentialManager.Create(activity).CreateSettingsPendingIntent();
pendingIntent.Send(); pendingIntent.Send();
} }
catch (ActivityNotFoundException) catch (ActivityNotFoundException)
@@ -516,7 +528,7 @@ namespace Bit.Droid.Services
{ {
try try
{ {
var activity = (MainActivity)Microsoft.Maui.ApplicationModel.Platform.CurrentActivity; var activity = Microsoft.Maui.ApplicationModel.Platform.CurrentActivity;
var intent = new Intent(Settings.ActionAccessibilitySettings); var intent = new Intent(Settings.ActionAccessibilitySettings);
activity.StartActivity(intent); activity.StartActivity(intent);
} }
@@ -525,7 +537,7 @@ namespace Bit.Droid.Services
public void OpenAutofillSettings() public void OpenAutofillSettings()
{ {
var activity = (MainActivity)Microsoft.Maui.ApplicationModel.Platform.CurrentActivity; var activity = Microsoft.Maui.ApplicationModel.Platform.CurrentActivity;
try try
{ {
var intent = new Intent(Settings.ActionRequestSetAutofillService); var intent = new Intent(Settings.ActionRequestSetAutofillService);
@@ -554,9 +566,91 @@ namespace Bit.Droid.Services
return SystemClock.ElapsedRealtime(); return SystemClock.ElapsedRealtime();
} }
public async Task ExecuteFido2CredentialActionAsync(AppOptions appOptions)
{
var activity = Microsoft.Maui.ApplicationModel.Platform.CurrentActivity;
if (activity == null || string.IsNullOrWhiteSpace(appOptions.Fido2CredentialAction))
{
return;
}
if (appOptions.Fido2CredentialAction == CredentialProviderConstants.Fido2CredentialGet)
{
await ExecuteFido2GetCredentialAsync(appOptions);
}
else if (appOptions.Fido2CredentialAction == CredentialProviderConstants.Fido2CredentialCreate)
{
await ExecuteFido2CreateCredentialAsync();
}
// Clear CredentialAction and FromFido2Framework values to avoid erratic behaviors in subsequent navigation/flows
// For Fido2CredentialGet these are no longer needed as a new Activity will be initiated.
// For Fido2CredentialCreate the app will rely on IFido2MakeCredentialConfirmationUserInterface.IsConfirmingNewCredential
appOptions.Fido2CredentialAction = null;
appOptions.FromFido2Framework = false;
}
private async Task ExecuteFido2GetCredentialAsync(AppOptions appOptions)
{
var activity = Microsoft.Maui.ApplicationModel.Platform.CurrentActivity;
if (activity == null)
{
return;
}
try
{
var request = AndroidX.Credentials.Provider.PendingIntentHandler.RetrieveBeginGetCredentialRequest(activity.Intent);
var response = new AndroidX.Credentials.Provider.BeginGetCredentialResponse();;
var credentialEntries = new List<AndroidX.Credentials.Provider.CredentialEntry>();
foreach (var option in request.BeginGetCredentialOptions.OfType<AndroidX.Credentials.Provider.BeginGetPublicKeyCredentialOption>())
{
credentialEntries.AddRange(await Bit.App.Platforms.Android.Autofill.CredentialHelpers.PopulatePasskeyDataAsync(request.CallingAppInfo, option, activity, appOptions.HasUnlockedInThisTransaction));
}
if (credentialEntries.Any())
{
response = new AndroidX.Credentials.Provider.BeginGetCredentialResponse.Builder()
.SetCredentialEntries(credentialEntries)
.Build();
}
var result = new Android.Content.Intent();
AndroidX.Credentials.Provider.PendingIntentHandler.SetBeginGetCredentialResponse(result, response);
activity.SetResult(Result.Ok, result);
activity.Finish();
}
catch (Exception ex)
{
Bit.Core.Services.LoggerHelper.LogEvenIfCantBeResolved(ex);
activity.SetResult(Result.Canceled);
activity.Finish();
}
}
private async Task ExecuteFido2CreateCredentialAsync()
{
var activity = Microsoft.Maui.ApplicationModel.Platform.CurrentActivity;
if (activity == null) { return; }
try
{
var getRequest = AndroidX.Credentials.Provider.PendingIntentHandler.RetrieveProviderCreateCredentialRequest(activity.Intent);
await Bit.App.Platforms.Android.Autofill.CredentialHelpers.CreateCipherPasskeyAsync(getRequest, activity);
}
catch (Exception ex)
{
Bit.Core.Services.LoggerHelper.LogEvenIfCantBeResolved(ex);
activity.SetResult(Result.Canceled);
activity.Finish();
}
}
public void CloseMainApp() public void CloseMainApp()
{ {
var activity = (MainActivity)Microsoft.Maui.ApplicationModel.Platform.CurrentActivity; var activity = Microsoft.Maui.ApplicationModel.Platform.CurrentActivity;
if (activity == null) if (activity == null)
{ {
return; return;
@@ -597,7 +691,7 @@ namespace Bit.Droid.Services
public float GetSystemFontSizeScale() public float GetSystemFontSizeScale()
{ {
var activity = Microsoft.Maui.ApplicationModel.Platform.CurrentActivity as MainActivity; var activity = Microsoft.Maui.ApplicationModel.Platform.CurrentActivity;
return activity?.Resources?.Configuration?.FontScale ?? 1; return activity?.Resources?.Configuration?.FontScale ?? 1;
} }

View File

@@ -8,6 +8,7 @@ using Bit.Core.Abstractions;
using Bit.Core.Utilities; using Bit.Core.Utilities;
using Bit.Droid.Accessibility; using Bit.Droid.Accessibility;
using Java.Lang; using Java.Lang;
using Bit.App.Droid.Utilities;
namespace Bit.Droid.Tile namespace Bit.Droid.Tile
{ {
@@ -76,7 +77,7 @@ namespace Bit.Droid.Tile
var intent = new Intent(this, typeof(AccessibilityActivity)); var intent = new Intent(this, typeof(AccessibilityActivity));
intent.SetFlags(ActivityFlags.NewTask | ActivityFlags.SingleTop | ActivityFlags.ClearTop); intent.SetFlags(ActivityFlags.NewTask | ActivityFlags.SingleTop | ActivityFlags.ClearTop);
intent.PutExtra("autofillTileClicked", true); intent.PutExtra("autofillTileClicked", true);
StartActivityAndCollapse(intent); this.StartActivityAndCollapseWithIntent(intent, isMutable: true);
} }
private void ShowConfigErrorDialog() private void ShowConfigErrorDialog()

View File

@@ -1,15 +1,8 @@
using System; using Android.App;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Android.App;
using Android.Content; using Android.Content;
using Android.OS;
using Android.Runtime; using Android.Runtime;
using Android.Service.QuickSettings; using Android.Service.QuickSettings;
using Android.Views; using Bit.App.Droid.Utilities;
using Android.Widget;
using Java.Lang; using Java.Lang;
namespace Bit.Droid.Tile namespace Bit.Droid.Tile
@@ -62,7 +55,7 @@ namespace Bit.Droid.Tile
var intent = new Intent(this, typeof(MainActivity)); var intent = new Intent(this, typeof(MainActivity));
intent.SetFlags(ActivityFlags.NewTask | ActivityFlags.SingleTop | ActivityFlags.ClearTop); intent.SetFlags(ActivityFlags.NewTask | ActivityFlags.SingleTop | ActivityFlags.ClearTop);
intent.PutExtra("generatorTile", true); intent.PutExtra("generatorTile", true);
StartActivityAndCollapse(intent); this.StartActivityAndCollapseWithIntent(intent, isMutable: false);
} }
} }
} }

View File

@@ -1,15 +1,8 @@
using System; using Android.App;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Android.App;
using Android.Content; using Android.Content;
using Android.OS;
using Android.Runtime; using Android.Runtime;
using Android.Service.QuickSettings; using Android.Service.QuickSettings;
using Android.Views; using Bit.App.Droid.Utilities;
using Android.Widget;
using Java.Lang; using Java.Lang;
namespace Bit.Droid.Tile namespace Bit.Droid.Tile
@@ -63,7 +56,7 @@ namespace Bit.Droid.Tile
var intent = new Intent(this, typeof(MainActivity)); var intent = new Intent(this, typeof(MainActivity));
intent.SetFlags(ActivityFlags.NewTask | ActivityFlags.SingleTop | ActivityFlags.ClearTop); intent.SetFlags(ActivityFlags.NewTask | ActivityFlags.SingleTop | ActivityFlags.ClearTop);
intent.PutExtra("myVaultTile", true); intent.PutExtra("myVaultTile", true);
StartActivityAndCollapse(intent); this.StartActivityAndCollapseWithIntent(intent, isMutable: false);
} }
} }
} }

View File

@@ -2,6 +2,7 @@
using Android.Content; using Android.Content;
using Android.OS; using Android.OS;
using Android.Provider; using Android.Provider;
using Android.Service.QuickSettings;
using Bit.App.Utilities; using Bit.App.Utilities;
namespace Bit.App.Droid.Utilities namespace Bit.App.Droid.Utilities
@@ -64,5 +65,26 @@ namespace Bit.App.Droid.Utilities
return pendingIntentFlags; return pendingIntentFlags;
} }
public static void StartActivityAndCollapseWithIntent(this TileService service, Intent intent, bool isMutable)
{
//For Android 14+ We need to use PendingIntent instead of Intent directly. Older versions still need to use Intent.
if (Build.VERSION.SdkInt < BuildVersionCodes.UpsideDownCake)
{
service.StartActivityAndCollapse(intent);
return;
}
var pendingIntent = PendingIntent.GetActivity(
service.ApplicationContext,
0,
intent,
AddPendingIntentMutabilityFlag(PendingIntentFlags.UpdateCurrent, isMutable)
);
if (pendingIntent == null)
{
return;
}
service.StartActivityAndCollapse(pendingIntent);
}
} }
} }

View File

@@ -0,0 +1,37 @@
using Android.OS;
using AndroidX.Credentials.Provider;
using Bit.Core.Utilities;
using Java.Security;
namespace Bit.App.Droid.Utilities
{
public static class CallingAppInfoExtensions
{
public static string GetAndroidOrigin(this CallingAppInfo callingAppInfo)
{
if (Build.VERSION.SdkInt < BuildVersionCodes.P || callingAppInfo?.SigningInfo?.GetApkContentsSigners().Any() != true)
{
return null;
}
var cert = callingAppInfo.SigningInfo.GetApkContentsSigners()[0].ToByteArray();
var md = MessageDigest.GetInstance("SHA-256");
var certHash = md.Digest(cert);
return $"android:apk-key-hash:{CoreHelpers.Base64UrlEncode(certHash)}";
}
public static string GetLatestCertificationFingerprint(this CallingAppInfo callingAppInfo)
{
if (callingAppInfo.SigningInfo.HasMultipleSigners)
{
return null;
}
var signature = callingAppInfo.SigningInfo.GetSigningCertificateHistory()[0].ToByteArray();
var md = MessageDigest.GetInstance("SHA-256");
var digestedSignature = md.Digest(signature);
var normalizedFingerprint = string.Join(":", digestedSignature.Select(b => b.ToString("X2")));
return normalizedFingerprint;
}
}
}

View File

@@ -11,7 +11,7 @@
<key>CFBundleIdentifier</key> <key>CFBundleIdentifier</key>
<string>com.8bit.bitwarden</string> <string>com.8bit.bitwarden</string>
<key>CFBundleShortVersionString</key> <key>CFBundleShortVersionString</key>
<string>2024.2.2</string> <string>2024.4.1</string>
<key>CFBundleVersion</key> <key>CFBundleVersion</key>
<string>1</string> <string>1</string>
<key>CFBundleIconName</key> <key>CFBundleIconName</key>

View File

@@ -0,0 +1,41 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>NSPrivacyAccessedAPITypes</key>
<array>
<dict>
<key>NSPrivacyAccessedAPIType</key>
<string>NSPrivacyAccessedAPICategoryFileTimestamp</string>
<key>NSPrivacyAccessedAPITypeReasons</key>
<array>
<string>C617.1</string>
</array>
</dict>
<dict>
<key>NSPrivacyAccessedAPIType</key>
<string>NSPrivacyAccessedAPICategorySystemBootTime</string>
<key>NSPrivacyAccessedAPITypeReasons</key>
<array>
<string>35F9.1</string>
</array>
</dict>
<dict>
<key>NSPrivacyAccessedAPIType</key>
<string>NSPrivacyAccessedAPICategoryDiskSpace</string>
<key>NSPrivacyAccessedAPITypeReasons</key>
<array>
<string>E174.1</string>
</array>
</dict>
<dict>
<key>NSPrivacyAccessedAPIType</key>
<string>NSPrivacyAccessedAPICategoryUserDefaults</string>
<key>NSPrivacyAccessedAPITypeReasons</key>
<array>
<string>1C8F.1</string>
</array>
</dict>
</array>
</dict>
</plist>

View File

@@ -0,0 +1,481 @@
{
"apps": [
{
"type": "android",
"info": {
"package_name": "com.android.chrome",
"signatures": [
{
"build": "release",
"cert_fingerprint_sha256": "F0:FD:6C:5B:41:0F:25:CB:25:C3:B5:33:46:C8:97:2F:AE:30:F8:EE:74:11:DF:91:04:80:AD:6B:2D:60:DB:83"
},
{
"build": "userdebug",
"cert_fingerprint_sha256": "19:75:B2:F1:71:77:BC:89:A5:DF:F3:1F:9E:64:A6:CA:E2:81:A5:3D:C1:D1:D5:9B:1D:14:7F:E1:C8:2A:FA:00"
}
]
}
},
{
"type": "android",
"info": {
"package_name": "com.chrome.beta",
"signatures": [
{
"build": "release",
"cert_fingerprint_sha256": "DA:63:3D:34:B6:9E:63:AE:21:03:B4:9D:53:CE:05:2F:C5:F7:F3:C5:3A:AB:94:FD:C2:A2:08:BD:FD:14:24:9C"
},
{
"build": "release",
"cert_fingerprint_sha256": "3D:7A:12:23:01:9A:A3:9D:9E:A0:E3:43:6A:B7:C0:89:6B:FB:4F:B6:79:F4:DE:5F:E7:C2:3F:32:6C:8F:99:4A"
}
]
}
},
{
"type": "android",
"info": {
"package_name": "com.chrome.dev",
"signatures": [
{
"build": "release",
"cert_fingerprint_sha256": "90:44:EE:5F:EE:4B:BC:5E:21:DD:44:66:54:31:C4:EB:1F:1F:71:A3:27:16:A0:BC:92:7B:CB:B3:92:33:CA:BF"
},
{
"build": "release",
"cert_fingerprint_sha256": "3D:7A:12:23:01:9A:A3:9D:9E:A0:E3:43:6A:B7:C0:89:6B:FB:4F:B6:79:F4:DE:5F:E7:C2:3F:32:6C:8F:99:4A"
}
]
}
},
{
"type": "android",
"info": {
"package_name": "com.chrome.canary",
"signatures": [
{
"build": "release",
"cert_fingerprint_sha256": "20:19:DF:A1:FB:23:EF:BF:70:C5:BC:D1:44:3C:5B:EA:B0:4F:3F:2F:F4:36:6E:9A:C1:E3:45:76:39:A2:4C:FC"
}
]
}
},
{
"type": "android",
"info": {
"package_name": "org.chromium.chrome",
"signatures": [
{
"build": "release",
"cert_fingerprint_sha256": "C6:AD:B8:B8:3C:6D:4C:17:D2:92:AF:DE:56:FD:48:8A:51:D3:16:FF:8F:2C:11:C5:41:02:23:BF:F8:A7:DB:B3"
},
{
"build": "userdebug",
"cert_fingerprint_sha256": "19:75:B2:F1:71:77:BC:89:A5:DF:F3:1F:9E:64:A6:CA:E2:81:A5:3D:C1:D1:D5:9B:1D:14:7F:E1:C8:2A:FA:00"
}
]
}
},
{
"type": "android",
"info": {
"package_name": "com.google.android.apps.chrome",
"signatures": [
{
"build": "userdebug",
"cert_fingerprint_sha256": "19:75:B2:F1:71:77:BC:89:A5:DF:F3:1F:9E:64:A6:CA:E2:81:A5:3D:C1:D1:D5:9B:1D:14:7F:E1:C8:2A:FA:00"
}
]
}
},
{
"type": "android",
"info": {
"package_name": "org.mozilla.fennec_webauthndebug",
"signatures": [
{
"build": "userdebug",
"cert_fingerprint_sha256": "BD:AE:82:02:80:D2:AF:B7:74:94:EF:22:58:AA:78:A9:AE:A1:36:41:7E:8B:C2:3D:C9:87:75:2E:6F:48:E8:48"
}
]
}
},
{
"type": "android",
"info": {
"package_name": "org.mozilla.firefox",
"signatures": [
{
"build": "release",
"cert_fingerprint_sha256": "A7:8B:62:A5:16:5B:44:94:B2:FE:AD:9E:76:A2:80:D2:2D:93:7F:EE:62:51:AE:CE:59:94:46:B2:EA:31:9B:04"
}
]
}
},
{
"type": "android",
"info": {
"package_name": "org.mozilla.firefox_beta",
"signatures": [
{
"build": "release",
"cert_fingerprint_sha256": "A7:8B:62:A5:16:5B:44:94:B2:FE:AD:9E:76:A2:80:D2:2D:93:7F:EE:62:51:AE:CE:59:94:46:B2:EA:31:9B:04"
}
]
}
},
{
"type": "android",
"info": {
"package_name": "org.mozilla.focus",
"signatures": [
{
"build": "release",
"cert_fingerprint_sha256": "62:03:A4:73:BE:36:D6:4E:E3:7F:87:FA:50:0E:DB:C7:9E:AB:93:06:10:AB:9B:9F:A4:CA:7D:5C:1F:1B:4F:FC"
}
]
}
},
{
"type": "android",
"info": {
"package_name": "org.mozilla.fennec_aurora",
"signatures": [
{
"build": "release",
"cert_fingerprint_sha256": "BC:04:88:83:8D:06:F4:CA:6B:F3:23:86:DA:AB:0D:D8:EB:CF:3E:77:30:78:74:59:F6:2F:B3:CD:14:A1:BA:AA"
}
]
}
},
{
"type": "android",
"info": {
"package_name": "org.mozilla.rocket",
"signatures": [
{
"build": "release",
"cert_fingerprint_sha256": "86:3A:46:F0:97:39:32:B7:D0:19:9B:54:91:12:74:1C:2D:27:31:AC:72:EA:11:B7:52:3A:A9:0A:11:BF:56:91"
}
]
}
},
{
"type": "android",
"info": {
"package_name": "com.microsoft.emmx.canary",
"signatures": [
{
"build": "release",
"cert_fingerprint_sha256": "01:E1:99:97:10:A8:2C:27:49:B4:D5:0C:44:5D:C8:5D:67:0B:61:36:08:9D:0A:76:6A:73:82:7C:82:A1:EA:C9"
}
]
}
},
{
"type": "android",
"info": {
"package_name": "com.microsoft.emmx.dev",
"signatures": [
{
"build": "release",
"cert_fingerprint_sha256": "01:E1:99:97:10:A8:2C:27:49:B4:D5:0C:44:5D:C8:5D:67:0B:61:36:08:9D:0A:76:6A:73:82:7C:82:A1:EA:C9"
}
]
}
},
{
"type": "android",
"info": {
"package_name": "com.microsoft.emmx.beta",
"signatures": [
{
"build": "release",
"cert_fingerprint_sha256": "01:E1:99:97:10:A8:2C:27:49:B4:D5:0C:44:5D:C8:5D:67:0B:61:36:08:9D:0A:76:6A:73:82:7C:82:A1:EA:C9"
}
]
}
},
{
"type": "android",
"info": {
"package_name": "com.microsoft.emmx",
"signatures": [
{
"build": "release",
"cert_fingerprint_sha256": "01:E1:99:97:10:A8:2C:27:49:B4:D5:0C:44:5D:C8:5D:67:0B:61:36:08:9D:0A:76:6A:73:82:7C:82:A1:EA:C9"
}
]
}
},
{
"type": "android",
"info": {
"package_name": "com.microsoft.emmx.rolling",
"signatures": [
{
"build": "userdebug",
"cert_fingerprint_sha256": "32:A2:FC:74:D7:31:10:58:59:E5:A8:5D:F1:6D:95:F1:02:D8:5B:22:09:9B:80:64:C5:D8:91:5C:61:DA:D1:E0"
}
]
}
},
{
"type": "android",
"info": {
"package_name": "com.microsoft.emmx.local",
"signatures": [
{
"build": "userdebug",
"cert_fingerprint_sha256": "32:A2:FC:74:D7:31:10:58:59:E5:A8:5D:F1:6D:95:F1:02:D8:5B:22:09:9B:80:64:C5:D8:91:5C:61:DA:D1:E0"
}
]
}
},
{
"type": "android",
"info": {
"package_name": "com.brave.browser",
"signatures": [
{
"build": "release",
"cert_fingerprint_sha256": "9C:2D:B7:05:13:51:5F:DB:FB:BC:58:5B:3E:DF:3D:71:23:D4:DC:67:C9:4F:FD:30:63:61:C1:D7:9B:BF:18:AC"
}
]
}
},
{
"type": "android",
"info": {
"package_name": "com.brave.browser_beta",
"signatures": [
{
"build": "release",
"cert_fingerprint_sha256": "9C:2D:B7:05:13:51:5F:DB:FB:BC:58:5B:3E:DF:3D:71:23:D4:DC:67:C9:4F:FD:30:63:61:C1:D7:9B:BF:18:AC"
}
]
}
},
{
"type": "android",
"info": {
"package_name": "com.brave.browser_nightly",
"signatures": [
{
"build": "release",
"cert_fingerprint_sha256": "9C:2D:B7:05:13:51:5F:DB:FB:BC:58:5B:3E:DF:3D:71:23:D4:DC:67:C9:4F:FD:30:63:61:C1:D7:9B:BF:18:AC"
}
]
}
},
{
"type": "android",
"info": {
"package_name": "app.vanadium.browser",
"signatures": [
{
"build": "release",
"cert_fingerprint_sha256": "C6:AD:B8:B8:3C:6D:4C:17:D2:92:AF:DE:56:FD:48:8A:51:D3:16:FF:8F:2C:11:C5:41:02:23:BF:F8:A7:DB:B3"
}
]
}
},
{
"type": "android",
"info": {
"package_name": "com.vivaldi.browser",
"signatures": [
{
"build": "release",
"cert_fingerprint_sha256": "E8:A7:85:44:65:5B:A8:C0:98:17:F7:32:76:8F:56:89:B1:66:2E:C4:B2:BC:5A:0B:C0:EC:13:8D:33:CA:3D:1E"
}
]
}
},
{
"type": "android",
"info": {
"package_name": "com.vivaldi.browser.snapshot",
"signatures": [
{
"build": "release",
"cert_fingerprint_sha256": "E8:A7:85:44:65:5B:A8:C0:98:17:F7:32:76:8F:56:89:B1:66:2E:C4:B2:BC:5A:0B:C0:EC:13:8D:33:CA:3D:1E"
}
]
}
},
{
"type": "android",
"info": {
"package_name": "com.vivaldi.browser.sopranos",
"signatures": [
{
"build": "release",
"cert_fingerprint_sha256": "E8:A7:85:44:65:5B:A8:C0:98:17:F7:32:76:8F:56:89:B1:66:2E:C4:B2:BC:5A:0B:C0:EC:13:8D:33:CA:3D:1E"
}
]
}
},
{
"type": "android",
"info": {
"package_name": "com.citrix.Receiver",
"signatures": [
{
"build": "release",
"cert_fingerprint_sha256": "3D:D1:12:67:10:69:AB:36:4E:F9:BE:73:9A:B7:B5:EE:15:E1:CD:E9:D8:75:7B:1B:F0:64:F5:0C:55:68:9A:49"
},
{
"build": "release",
"cert_fingerprint_sha256": "CE:B2:23:D7:77:09:F2:B6:BC:0B:3A:78:36:F5:A5:AF:4C:E1:D3:55:F4:A7:28:86:F7:9D:F8:0D:C9:D6:12:2E"
},
{
"build": "release",
"cert_fingerprint_sha256": "AA:D0:D4:57:E6:33:C3:78:25:77:30:5B:C1:B2:D9:E3:81:41:C7:21:DF:0D:AA:6E:29:07:2F:C4:1D:34:F0:AB"
}
]
}
},
{
"type": "android",
"info": {
"package_name": "com.android.browser",
"signatures": [
{
"build": "release",
"cert_fingerprint_sha256": "C9:00:9D:01:EB:F9:F5:D0:30:2B:C7:1B:2F:E9:AA:9A:47:A4:32:BB:A1:73:08:A3:11:1B:75:D7:B2:14:90:25"
}
]
}
},
{
"type": "android",
"info": {
"package_name": "com.sec.android.app.sbrowser",
"signatures": [
{
"build": "release",
"cert_fingerprint_sha256": "C8:A2:E9:BC:CF:59:7C:2F:B6:DC:66:BE:E2:93:FC:13:F2:FC:47:EC:77:BC:6B:2B:0D:52:C1:1F:51:19:2A:B8"
},
{
"build": "release",
"cert_fingerprint_sha256": "34:DF:0E:7A:9F:1C:F1:89:2E:45:C0:56:B4:97:3C:D8:1C:CF:14:8A:40:50:D1:1A:EA:4A:C5:A6:5F:90:0A:42"
}
]
}
},
{
"type": "android",
"info": {
"package_name": "com.sec.android.app.sbrowser.beta",
"signatures": [
{
"build": "release",
"cert_fingerprint_sha256": "C8:A2:E9:BC:CF:59:7C:2F:B6:DC:66:BE:E2:93:FC:13:F2:FC:47:EC:77:BC:6B:2B:0D:52:C1:1F:51:19:2A:B8"
},
{
"build": "release",
"cert_fingerprint_sha256": "34:DF:0E:7A:9F:1C:F1:89:2E:45:C0:56:B4:97:3C:D8:1C:CF:14:8A:40:50:D1:1A:EA:4A:C5:A6:5F:90:0A:42"
}
]
}
},
{
"type": "android",
"info": {
"package_name": "com.google.android.gms",
"signatures": [
{
"build": "release",
"cert_fingerprint_sha256": "7C:E8:3C:1B:71:F3:D5:72:FE:D0:4C:8D:40:C5:CB:10:FF:75:E6:D8:7D:9D:F6:FB:D5:3F:04:68:C2:90:50:53"
},
{
"build": "release",
"cert_fingerprint_sha256": "D2:2C:C5:00:29:9F:B2:28:73:A0:1A:01:0D:E1:C8:2F:BE:4D:06:11:19:B9:48:14:DD:30:1D:AB:50:CB:76:78"
},
{
"build": "release",
"cert_fingerprint_sha256": "F0:FD:6C:5B:41:0F:25:CB:25:C3:B5:33:46:C8:97:2F:AE:30:F8:EE:74:11:DF:91:04:80:AD:6B:2D:60:DB:83"
},
{
"build": "release",
"cert_fingerprint_sha256": "19:75:B2:F1:71:77:BC:89:A5:DF:F3:1F:9E:64:A6:CA:E2:81:A5:3D:C1:D1:D5:9B:1D:14:7F:E1:C8:2A:FA:00"
}
]
}
},
{
"type": "android",
"info": {
"package_name": "com.yandex.browser",
"signatures": [
{
"build": "release",
"cert_fingerprint_sha256": "AC:A4:05:DE:D8:B2:5C:B2:E8:C6:DA:69:42:5D:2B:43:07:D0:87:C1:27:6F:C0:6A:D5:94:27:31:CC:C5:1D:BA"
}
]
}
},
{
"type": "android",
"info": {
"package_name": "com.yandex.browser.beta",
"signatures": [
{
"build": "release",
"cert_fingerprint_sha256": "AC:A4:05:DE:D8:B2:5C:B2:E8:C6:DA:69:42:5D:2B:43:07:D0:87:C1:27:6F:C0:6A:D5:94:27:31:CC:C5:1D:BA"
}
]
}
},
{
"type": "android",
"info": {
"package_name": "com.yandex.browser.alpha",
"signatures": [
{
"build": "release",
"cert_fingerprint_sha256": "AC:A4:05:DE:D8:B2:5C:B2:E8:C6:DA:69:42:5D:2B:43:07:D0:87:C1:27:6F:C0:6A:D5:94:27:31:CC:C5:1D:BA"
}
]
}
},
{
"type": "android",
"info": {
"package_name": "com.yandex.browser.corp",
"signatures": [
{
"build": "release",
"cert_fingerprint_sha256": "AC:A4:05:DE:D8:B2:5C:B2:E8:C6:DA:69:42:5D:2B:43:07:D0:87:C1:27:6F:C0:6A:D5:94:27:31:CC:C5:1D:BA"
}
]
}
},
{
"type": "android",
"info": {
"package_name": "com.yandex.browser.canary",
"signatures": [
{
"build": "release",
"cert_fingerprint_sha256": "1D:A9:CB:AE:2D:CC:C6:A5:8D:6C:94:7B:E9:4C:DB:B7:33:D6:5D:A4:D1:77:0F:A1:4A:53:64:CB:4A:28:EB:49"
}
]
}
},
{
"type": "android",
"info": {
"package_name": "com.yandex.browser.broteam",
"signatures": [
{
"build": "release",
"cert_fingerprint_sha256": "1D:A9:CB:AE:2D:CC:C6:A5:8D:6C:94:7B:E9:4C:DB:B7:33:D6:5D:A4:D1:77:0F:A1:4A:53:64:CB:4A:28:EB:49"
}
]
}
}
]
}

View File

@@ -1,9 +1,4 @@
using System; using Bit.Core.Enums;
using System.Collections.Generic;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using Bit.Core.Enums;
using Bit.Core.Models.Data; using Bit.Core.Models.Data;
using Bit.Core.Models.Request; using Bit.Core.Models.Request;
using Bit.Core.Models.Response; using Bit.Core.Models.Response;
@@ -46,6 +41,7 @@ namespace Bit.Core.Abstractions
Task<CipherResponse> PutShareCipherAsync(string id, CipherShareRequest request); Task<CipherResponse> PutShareCipherAsync(string id, CipherShareRequest request);
Task PutDeleteCipherAsync(string id); Task PutDeleteCipherAsync(string id);
Task<CipherResponse> PutRestoreCipherAsync(string id); Task<CipherResponse> PutRestoreCipherAsync(string id);
Task<bool> HasUnassignedCiphersAsync();
Task RefreshIdentityTokenAsync(); Task RefreshIdentityTokenAsync();
Task<SsoPrevalidateResponse> PreValidateSsoAsync(string identifier); Task<SsoPrevalidateResponse> PreValidateSsoAsync(string identifier);
Task<TResponse> SendAsync<TRequest, TResponse>(HttpMethod method, string path, Task<TResponse> SendAsync<TRequest, TResponse>(HttpMethod method, string path,
@@ -99,5 +95,6 @@ namespace Bit.Core.Abstractions
Task<bool> GetDevicesExistenceByTypes(DeviceType[] deviceTypes); Task<bool> GetDevicesExistenceByTypes(DeviceType[] deviceTypes);
Task<ConfigResponse> GetConfigsAsync(); Task<ConfigResponse> GetConfigsAsync();
Task<string> GetFastmailAccountIdAsync(string apiKey); Task<string> GetFastmailAccountIdAsync(string apiKey);
Task<List<Utilities.DigitalAssetLinks.Statement>> GetDigitalAssetLinksForRpAsync(string rpId);
} }
} }

View File

@@ -0,0 +1,7 @@
namespace Bit.Core.Services
{
public interface IAssetLinksService
{
Task<bool> ValidateAssetLinksAsync(string rpId, string packageName, string normalizedFingerprint);
}
}

View File

@@ -34,6 +34,8 @@ namespace Bit.Core.Abstractions
Task<byte[]> DownloadAndDecryptAttachmentAsync(string cipherId, AttachmentView attachment, string organizationId); Task<byte[]> DownloadAndDecryptAttachmentAsync(string cipherId, AttachmentView attachment, string organizationId);
Task SoftDeleteWithServerAsync(string id); Task SoftDeleteWithServerAsync(string id);
Task RestoreWithServerAsync(string id); Task RestoreWithServerAsync(string id);
Task<string> CreateNewLoginForPasskeyAsync(string rpId); Task<string> CreateNewLoginForPasskeyAsync(Fido2ConfirmNewCredentialParams newPasskeyParams);
Task CopyTotpCodeIfNeededAsync(CipherView cipher);
Task<bool> VerifyOrganizationHasUnassignedItemsAsync();
} }
} }

View File

@@ -1,11 +1,10 @@
using System; namespace Bit.Core.Abstractions
using System.Threading.Tasks;
namespace Bit.Core.Abstractions
{ {
public enum AwaiterPrecondition public enum AwaiterPrecondition
{ {
EnvironmentUrlsInited EnvironmentUrlsInited,
AndroidWindowCreated,
AutofillIOSExtensionViewDidAppear
} }
public interface IConditionedAwaiterManager public interface IConditionedAwaiterManager
@@ -13,5 +12,6 @@ namespace Bit.Core.Abstractions
Task GetAwaiterForPrecondition(AwaiterPrecondition awaiterPrecondition); Task GetAwaiterForPrecondition(AwaiterPrecondition awaiterPrecondition);
void SetAsCompleted(AwaiterPrecondition awaiterPrecondition); void SetAsCompleted(AwaiterPrecondition awaiterPrecondition);
void SetException(AwaiterPrecondition awaiterPrecondition, Exception ex); void SetException(AwaiterPrecondition awaiterPrecondition, Exception ex);
void Recreate(AwaiterPrecondition awaiterPrecondition);
} }
} }

View File

@@ -1,4 +1,5 @@
using System.Threading.Tasks; using System.Threading.Tasks;
using Bit.App.Models;
using Bit.App.Utilities.Prompts; using Bit.App.Utilities.Prompts;
using Bit.Core.Enums; using Bit.Core.Enums;
using Bit.Core.Models; using Bit.Core.Models;
@@ -40,6 +41,7 @@ namespace Bit.App.Abstractions
void OpenCredentialProviderSettings(); void OpenCredentialProviderSettings();
void OpenAutofillSettings(); void OpenAutofillSettings();
long GetActiveTime(); long GetActiveTime();
Task ExecuteFido2CredentialActionAsync(AppOptions appOptions);
void CloseMainApp(); void CloseMainApp();
float GetSystemFontSizeScale(); float GetSystemFontSizeScale();
Task OnAccountSwitchCompleteAsync(); Task OnAccountSwitchCompleteAsync();

View File

@@ -1,9 +0,0 @@
using Bit.Core.Utilities.Fido2;
namespace Bit.Core.Abstractions
{
public interface IFido2AuthenticationService
{
Task<Fido2AuthenticatorGetAssertionResult> GetAssertionAsync(Fido2AuthenticatorGetAssertionParams assertionParams);
}
}

View File

@@ -4,9 +4,8 @@ namespace Bit.Core.Abstractions
{ {
public interface IFido2AuthenticatorService public interface IFido2AuthenticatorService
{ {
void Init(IFido2UserInterface userInterface); Task<Fido2AuthenticatorMakeCredentialResult> MakeCredentialAsync(Fido2AuthenticatorMakeCredentialParams makeCredentialParams, IFido2MakeCredentialUserInterface userInterface);
Task<Fido2AuthenticatorMakeCredentialResult> MakeCredentialAsync(Fido2AuthenticatorMakeCredentialParams makeCredentialParams); Task<Fido2AuthenticatorGetAssertionResult> GetAssertionAsync(Fido2AuthenticatorGetAssertionParams assertionParams, IFido2GetAssertionUserInterface userInterface);
Task<Fido2AuthenticatorGetAssertionResult> GetAssertionAsync(Fido2AuthenticatorGetAssertionParams assertionParams);
// TODO: Should this return a List? Or maybe IEnumerable? // TODO: Should this return a List? Or maybe IEnumerable?
Task<Fido2AuthenticatorDiscoverableCredentialMetadata[]> SilentCredentialDiscoveryAsync(string rpId); Task<Fido2AuthenticatorDiscoverableCredentialMetadata[]> SilentCredentialDiscoveryAsync(string rpId);
} }

View File

@@ -20,8 +20,9 @@ namespace Bit.Core.Abstractions
/// For more information please see: https://www.w3.org/TR/webauthn-3/#sctn-createCredential /// For more information please see: https://www.w3.org/TR/webauthn-3/#sctn-createCredential
/// </summary> /// </summary>
/// <param name="createCredentialParams">The parameters for the credential creation operation</param> /// <param name="createCredentialParams">The parameters for the credential creation operation</param>
/// <param name="extraParams">Extra parameters for the credential creation operation</param>
/// <returns>The new credential</returns> /// <returns>The new credential</returns>
Task<Fido2ClientCreateCredentialResult> CreateCredentialAsync(Fido2ClientCreateCredentialParams createCredentialParams); Task<Fido2ClientCreateCredentialResult> CreateCredentialAsync(Fido2ClientCreateCredentialParams createCredentialParams, Fido2ExtraCreateCredentialParams extraParams);
/// <summary> /// <summary>
/// Allows WebAuthn Relying Party scripts to discover and use an existing public key credential, with the users consent. /// Allows WebAuthn Relying Party scripts to discover and use an existing public key credential, with the users consent.
@@ -29,7 +30,8 @@ namespace Bit.Core.Abstractions
/// For more information please see: https://www.w3.org/TR/webauthn-3/#sctn-getAssertion /// For more information please see: https://www.w3.org/TR/webauthn-3/#sctn-getAssertion
/// </summary> /// </summary>
/// <param name="assertCredentialParams">The parameters for the credential assertion operation</param> /// <param name="assertCredentialParams">The parameters for the credential assertion operation</param>
/// <param name="extraParams">Extra parameters for the credential assertion operation</param>
/// <returns>The asserted credential</returns> /// <returns>The asserted credential</returns>
Task<Fido2ClientAssertCredentialResult> AssertCredentialAsync(Fido2ClientAssertCredentialParams assertCredentialParams); Task<Fido2ClientAssertCredentialResult> AssertCredentialAsync(Fido2ClientAssertCredentialParams assertCredentialParams, Fido2ExtraAssertCredentialParams extraParams);
} }
} }

View File

@@ -0,0 +1,20 @@
using Bit.Core.Utilities.Fido2;
namespace Bit.Core.Abstractions
{
public struct Fido2GetAssertionUserInterfaceCredential
{
public string CipherId { get; set; }
public Fido2UserVerificationPreference UserVerificationPreference { get; set; }
}
public interface IFido2GetAssertionUserInterface : IFido2UserInterface
{
/// <summary>
/// Ask the user to pick a credential from a list of existing credentials.
/// </summary>
/// <param name="credentials">The credentials that the user can pick from, and if the user must be verified before completing the operation</param>
/// <returns>The ID of the cipher that contains the credentials the user picked, and if the user was verified before completing the operation</returns>
Task<(string CipherId, bool UserVerified)> PickCredentialAsync(Fido2GetAssertionUserInterfaceCredential[] credentials);
}
}

View File

@@ -0,0 +1,66 @@
using Bit.Core.Utilities.Fido2;
namespace Bit.Core.Abstractions
{
public interface IFido2MakeCredentialConfirmationUserInterface : IFido2MakeCredentialUserInterface
{
/// <summary>
/// Call this method after the user chose where to save the new Fido2 credential.
/// </summary>
/// <param name="cipherId">
/// Cipher ID where to save the new credential.
/// If <c>null</c> a new default passkey cipher item will be created
/// </param>
/// <param name="userVerified">
/// Whether the user has been verified or not.
/// If <c>null</c> verification has not taken place yet.
/// </param>
void Confirm(string cipherId, bool? userVerified);
/// <summary>
/// Call this method after the user chose where to save the new Fido2 credential.
/// </summary>
/// <param name="cipherId">
/// Cipher ID where to save the new credential.
/// If <c>null</c> a new default passkey cipher item will be created
/// </param>
/// <param name="alreadyHasFido2Credential">
/// If the cipher corresponding to the <paramref name="cipherId"/> already has a Fido2 credential.
/// </param>
/// <param name="userVerified">
/// Whether the user has been verified or not.
/// If <c>null</c> verification has not taken place yet.
/// </param>
Task ConfirmAsync(string cipherId, bool alreadyHasFido2Credential, bool? userVerified);
/// <summary>
/// Cancels the current flow to make a credential
/// </summary>
void Cancel();
/// <summary>
/// Call this if an exception needs to happen on the credential making process
/// </summary>
void OnConfirmationException(Exception ex);
/// <summary>
/// True if we are already confirming a new credential.
/// </summary>
bool IsConfirmingNewCredential { get; }
/// <summary>
/// Call this after the vault was unlocked so that Fido2 credential creation can proceed.
/// </summary>
void ConfirmVaultUnlocked();
/// <summary>
/// True if we are waiting for the vault to be unlocked.
/// </summary>
bool IsWaitingUnlockVault { get; }
Fido2UserVerificationOptions? GetCurrentUserVerificationOptions();
void SetCheckHasVaultBeenUnlockedInThisTransaction(Func<bool> checkHasVaultBeenUnlockedInThisTransaction);
}
}

View File

@@ -0,0 +1,44 @@
using Bit.Core.Utilities.Fido2;
namespace Bit.Core.Abstractions
{
public struct Fido2ConfirmNewCredentialParams
{
///<summary>
/// The name of the credential.
///</summary>
public string CredentialName { get; set; }
///<summary>
/// The name of the user.
///</summary>
public string UserName { get; set; }
/// <summary>
/// The preference to whether or not the user must be verified before completing the operation.
/// </summary>
public Fido2UserVerificationPreference UserVerificationPreference { get; set; }
/// <summary>
/// The relying party identifier
/// </summary>
public string RpId { get; set; }
}
public interface IFido2MakeCredentialUserInterface : IFido2UserInterface
{
/// <summary>
/// Inform the user that the operation was cancelled because their vault contains excluded credentials.
/// </summary>
/// <param name="existingCipherIds">The IDs of the excluded credentials.</param>
/// <returns>When user has confirmed the message</returns>
Task InformExcludedCredentialAsync(string[] existingCipherIds);
/// <summary>
/// Ask the user to confirm the creation of a new credential.
/// </summary>
/// <param name="confirmNewCredentialParams">The parameters to use when asking the user to confirm the creation of a new credential.</param>
/// <returns>The ID of the cipher where the new credential should be saved, and if the user was verified before completing the operation</returns>
Task<(string CipherId, bool UserVerified)> ConfirmNewCredentialAsync(Fido2ConfirmNewCredentialParams confirmNewCredentialParams);
}
}

View File

@@ -0,0 +1,14 @@
using Bit.Core.Utilities.Fido2;
namespace Bit.Core.Abstractions
{
public interface IFido2MediatorService
{
Task<Fido2ClientCreateCredentialResult> CreateCredentialAsync(Fido2ClientCreateCredentialParams createCredentialParams, Fido2ExtraCreateCredentialParams extraParams);
Task<Fido2ClientAssertCredentialResult> AssertCredentialAsync(Fido2ClientAssertCredentialParams assertCredentialParams, Fido2ExtraAssertCredentialParams extraParams);
Task<Fido2AuthenticatorMakeCredentialResult> MakeCredentialAsync(Fido2AuthenticatorMakeCredentialParams makeCredentialParams, IFido2MakeCredentialUserInterface userInterface);
Task<Fido2AuthenticatorGetAssertionResult> GetAssertionAsync(Fido2AuthenticatorGetAssertionParams assertionParams, IFido2GetAssertionUserInterface userInterface);
Task<Fido2AuthenticatorDiscoverableCredentialMetadata[]> SilentCredentialDiscoveryAsync(string rpId);
}
}

View File

@@ -1,94 +1,11 @@
using Bit.Core.Utilities.Fido2;
namespace Bit.Core.Abstractions namespace Bit.Core.Abstractions
{ {
/// <summary>
/// Parameters used to ask the user to pick a credential from a list of existing credentials.
/// </summary>
public struct Fido2PickCredentialParams
{
/// <summary>
/// The IDs of the credentials that the user can pick from.
/// </summary>
public string[] CipherIds { get; set; }
/// <summary>
/// Whether or not the user must be verified before completing the operation.
/// </summary>
public bool UserVerification { get; set; }
}
/// <summary>
/// The result of asking the user to pick a credential from a list of existing credentials.
/// </summary>
public struct Fido2PickCredentialResult
{
/// <summary>
/// The ID of the cipher that contains the credentials the user picked.
/// </summary>
public string CipherId { get; set; }
/// <summary>
/// Whether or not the user was verified before completing the operation.
/// </summary>
public bool UserVerified { get; set; }
}
public struct Fido2ConfirmNewCredentialParams
{
///<summary>
/// The name of the credential.
///</summary>
public string CredentialName { get; set; }
///<summary>
/// The name of the user.
///</summary>
public string UserName { get; set; }
/// <summary>
/// Whether or not the user must be verified before completing the operation.
/// </summary>
public bool UserVerification { get; set; }
public string RpId { get; set; }
}
public struct Fido2ConfirmNewCredentialResult
{
/// <summary>
/// The name of the user.
/// </summary>
public string CipherId { get; set; }
/// <summary>
/// Whether or not the user was verified.
/// </summary>
public bool UserVerified { get; set; }
}
public interface IFido2UserInterface public interface IFido2UserInterface
{ {
/// <summary> /// <summary>
/// Ask the user to pick a credential from a list of existing credentials. /// Whether the vault has been unlocked during this transaction
/// </summary> /// </summary>
/// <param name="pickCredentialParams">The parameters to use when asking the user to pick a credential.</param> bool HasVaultBeenUnlockedInThisTransaction { get; }
/// <returns>The ID of the cipher that contains the credentials the user picked.</returns>
Task<Fido2PickCredentialResult> PickCredentialAsync(Fido2PickCredentialParams pickCredentialParams);
/// <summary>
/// Inform the user that the operation was cancelled because their vault contains excluded credentials.
/// </summary>
/// <param name="existingCipherIds">The IDs of the excluded credentials.</param>
/// <returns>When user has confirmed the message</returns>
Task InformExcludedCredential(string[] existingCipherIds);
/// <summary>
/// Ask the user to confirm the creation of a new credential.
/// </summary>
/// <param name="confirmNewCredentialParams">The parameters to use when asking the user to confirm the creation of a new credential.</param>
/// <returns>The ID of the cipher where the new credential should be saved.</returns>
Task<Fido2ConfirmNewCredentialResult> ConfirmNewCredentialAsync(Fido2ConfirmNewCredentialParams confirmNewCredentialParams);
/// <summary> /// <summary>
/// Make sure that the vault is unlocked. /// Make sure that the vault is unlocked.

View File

@@ -1,5 +1,4 @@
using System.Threading.Tasks; using Bit.Core.Enums;
using Bit.Core.Enums;
namespace Bit.App.Abstractions namespace Bit.App.Abstractions
{ {
@@ -10,5 +9,7 @@ namespace Bit.App.Abstractions
Task<bool> PromptAndCheckPasswordIfNeededAsync(CipherRepromptType repromptType = CipherRepromptType.Password); Task<bool> PromptAndCheckPasswordIfNeededAsync(CipherRepromptType repromptType = CipherRepromptType.Password);
Task<(string password, bool valid)> ShowPasswordPromptAndGetItAsync(); Task<(string password, bool valid)> ShowPasswordPromptAndGetItAsync();
Task<bool> ShouldByPassMasterPasswordRepromptAsync();
} }
} }

View File

@@ -1,7 +1,4 @@
using System; using Bit.Core.Enums;
using System.Collections.Generic;
using System.Threading.Tasks;
using Bit.Core.Enums;
namespace Bit.Core.Abstractions namespace Bit.Core.Abstractions
{ {
@@ -29,7 +26,7 @@ namespace Bit.Core.Abstractions
bool SupportsDuo(); bool SupportsDuo();
Task<bool> SupportsBiometricAsync(); Task<bool> SupportsBiometricAsync();
Task<bool> IsBiometricIntegrityValidAsync(string bioIntegritySrcKey = null); Task<bool> IsBiometricIntegrityValidAsync(string bioIntegritySrcKey = null);
Task<bool> AuthenticateBiometricAsync(string text = null, string fallbackText = null, Action fallback = null, bool logOutOnTooManyAttempts = false); Task<bool?> AuthenticateBiometricAsync(string text = null, string fallbackText = null, Action fallback = null, bool logOutOnTooManyAttempts = false, bool allowAlternativeAuthentication = false);
long GetActiveTime(); long GetActiveTime();
} }
} }

View File

@@ -186,6 +186,9 @@ namespace Bit.Core.Abstractions
Task<BwRegion?> GetActiveUserRegionAsync(); Task<BwRegion?> GetActiveUserRegionAsync();
Task<BwRegion?> GetPreAuthRegionAsync(); Task<BwRegion?> GetPreAuthRegionAsync();
Task SetPreAuthRegionAsync(BwRegion value); Task SetPreAuthRegionAsync(BwRegion value);
Task ReloadStateAsync();
Task<bool> GetShouldCheckOrganizationUnassignedItemsAsync(string userId = null);
Task SetShouldCheckOrganizationUnassignedItemsAsync(bool shouldCheck, string userId = null);
[Obsolete("Use GetPinKeyEncryptedUserKeyAsync instead, left for migration purposes")] [Obsolete("Use GetPinKeyEncryptedUserKeyAsync instead, left for migration purposes")]
Task<string> GetPinProtectedAsync(string userId = null); Task<string> GetPinProtectedAsync(string userId = null);
[Obsolete("Use SetPinKeyEncryptedUserKeyAsync instead, left for migration purposes")] [Obsolete("Use SetPinKeyEncryptedUserKeyAsync instead, left for migration purposes")]

View File

@@ -1,9 +1,12 @@
using System.Threading.Tasks; using Bit.Core.Services;
namespace Bit.Core.Abstractions namespace Bit.Core.Abstractions
{ {
public interface IUserPinService public interface IUserPinService
{ {
Task<bool> IsPinLockEnabledAsync();
Task SetupPinAsync(string pin, bool requireMasterPasswordOnRestart); Task SetupPinAsync(string pin, bool requireMasterPasswordOnRestart);
Task<bool> VerifyPinAsync(string inputPin);
Task<bool> VerifyPinAsync(string inputPin, string email, KdfConfig kdfConfig, PinLockType pinLockType);
} }
} }

View File

@@ -0,0 +1,28 @@
using Bit.Core.Utilities;
using Bit.Core.Utilities.Fido2;
namespace Bit.Core.Abstractions
{
public interface IUserVerificationMediatorService
{
Task<CancellableResult<bool>> VerifyUserForFido2Async(Fido2UserVerificationOptions options);
Task<bool> CanPerformUserVerificationPreferredAsync(Fido2UserVerificationOptions options);
Task<bool> ShouldPerformMasterPasswordRepromptAsync(Fido2UserVerificationOptions options);
Task<bool> ShouldEnforceFido2RequiredUserVerificationAsync(Fido2UserVerificationOptions options);
Task<CancellableResult<UVResult>> PerformOSUnlockAsync();
Task<CancellableResult<UVResult>> VerifyPinCodeAsync();
Task<CancellableResult<UVResult>> VerifyMasterPasswordAsync(bool isMasterPasswordReprompt);
public struct UVResult
{
public UVResult(bool canPerform, bool isVerified)
{
CanPerform = canPerform;
IsVerified = isVerified;
}
public bool CanPerform { get; set; }
public bool IsVerified { get; set; }
}
}
}

View File

@@ -1,11 +1,11 @@
using System.Threading.Tasks; using Bit.Core.Enums;
using Bit.Core.Enums;
namespace Bit.Core.Abstractions namespace Bit.Core.Abstractions
{ {
public interface IUserVerificationService public interface IUserVerificationService
{ {
Task<bool> VerifyUser(string secret, VerificationType verificationType); Task<bool> VerifyUser(string secret, VerificationType verificationType);
Task<bool> VerifyMasterPasswordAsync(string masterPassword);
Task<bool> HasMasterPasswordAsync(bool checkMasterKeyHash = false); Task<bool> HasMasterPasswordAsync(bool checkMasterKeyHash = false);
} }
} }

View File

@@ -9,10 +9,12 @@ using Bit.Core;
using Bit.Core.Abstractions; using Bit.Core.Abstractions;
using Bit.Core.Enums; using Bit.Core.Enums;
using Bit.Core.Models.Data; using Bit.Core.Models.Data;
using Bit.Core.Models.Domain;
using Bit.Core.Models.Response; using Bit.Core.Models.Response;
using Bit.Core.Pages; using Bit.Core.Pages;
using Bit.Core.Services; using Bit.Core.Services;
using Bit.Core.Utilities; using Bit.Core.Utilities;
using Bit.Core.Utilities.Fido2;
[assembly: XamlCompilation(XamlCompilationOptions.Compile)] [assembly: XamlCompilation(XamlCompilationOptions.Compile)]
namespace Bit.App namespace Bit.App
@@ -36,6 +38,9 @@ namespace Bit.App
private readonly IPushNotificationService _pushNotificationService; private readonly IPushNotificationService _pushNotificationService;
private readonly IConfigService _configService; private readonly IConfigService _configService;
private readonly ILogger _logger; private readonly ILogger _logger;
#if ANDROID
private LazyResolve<IFido2MakeCredentialConfirmationUserInterface> _fido2MakeCredentialConfirmationUserInterface = new LazyResolve<IFido2MakeCredentialConfirmationUserInterface>();
#endif
private static bool _isResumed; private static bool _isResumed;
// these variables are static because the app is launching new activities on notification click, creating new instances of App. // these variables are static because the app is launching new activities on notification click, creating new instances of App.
@@ -46,7 +51,6 @@ namespace Bit.App
// This queue keeps those actions so that when the app has resumed they can still be executed. // This queue keeps those actions so that when the app has resumed they can still be executed.
// Links: https://github.com/dotnet/maui/issues/11501 and https://bitwarden.atlassian.net/wiki/spaces/NMME/pages/664862722/MainPage+Assignments+not+working+on+Android+on+Background+or+App+resume // Links: https://github.com/dotnet/maui/issues/11501 and https://bitwarden.atlassian.net/wiki/spaces/NMME/pages/664862722/MainPage+Assignments+not+working+on+Android+on+Background+or+App+resume
private readonly Queue<Action> _onResumeActions = new Queue<Action>(); private readonly Queue<Action> _onResumeActions = new Queue<Action>();
private bool _hasNavigatedToAutofillWindow;
#if ANDROID #if ANDROID
@@ -104,7 +108,10 @@ namespace Bit.App
Options.MyVaultTile = appOptions.MyVaultTile; Options.MyVaultTile = appOptions.MyVaultTile;
Options.GeneratorTile = appOptions.GeneratorTile; Options.GeneratorTile = appOptions.GeneratorTile;
Options.FromAutofillFramework = appOptions.FromAutofillFramework; Options.FromAutofillFramework = appOptions.FromAutofillFramework;
Options.FromFido2Framework = appOptions.FromFido2Framework;
Options.Fido2CredentialAction = appOptions.Fido2CredentialAction;
Options.CreateSend = appOptions.CreateSend; Options.CreateSend = appOptions.CreateSend;
Options.HasUnlockedInThisTransaction = appOptions.HasUnlockedInThisTransaction;
} }
} }
@@ -120,41 +127,17 @@ namespace Bit.App
return new Window(new NavigationPage()); //No actual page needed. Only used for auto-filling the fields directly (externally) return new Window(new NavigationPage()); //No actual page needed. Only used for auto-filling the fields directly (externally)
} }
//"Internal" Autofill and Uri/Otp/CreateSend. This is where we create the autofill specific Window //When executing from CredentialProviderSelectionActivity we don't have "Options" so we need to filter "manually"
if (Options != null && (Options.FromAutofillFramework || Options.Uri != null || Options.OtpData != null || Options.CreateSend != null)) //In the CredentialProviderSelectionActivity we don't need to show any Page, so we just create a "dummy" Window with a NavigationPage to avoid crashing.
if (activationState != null
&& activationState.State.ContainsKey("CREDENTIAL_DATA")
&& activationState.State.ContainsKey("credentialProviderCipherId"))
{ {
_isResumed = true; //Specifically for the Autofill scenario we need to manually set the _isResumed here return new Window(new NavigationPage()); //No actual page needed. Only used for auto-filling the fields directly (externally)
_hasNavigatedToAutofillWindow = true;
return new AutoFillWindow(new NavigationPage(new AndroidNavigationRedirectPage()));
} }
var homePage = new HomePage(Options);
// WORKAROUND: If the user autofills with Accessibility Services enabled and goes back to the application then there is currently an issue
// where this method is called again
// thus it goes through here and the user goes to HomePage as we see here.
// So to solve this, the next flag check has been added which then turns on a flag on the home page
// that will trigger a navigation on the accounts manager when it loads; workarounding this behavior and navigating the user
// to the proper page depending on its state.
// WARNING: this doens't navigate the user to where they were but it acts as if the user had changed their account.
if(_hasNavigatedToAutofillWindow)
{
homePage.PerformNavigationOnAccountChangedOnLoad = true;
// this is needed because when coming back from AutofillWindow OnResume won't be called and we need this flag
// so that void Navigate(NavigationTarget navTarget, INavigationParams navParams) doesn't enqueue the navigation
// and it performs it directly.
_isResumed = true; _isResumed = true;
_hasNavigatedToAutofillWindow = false; return new ResumeWindow(new NavigationPage(new AndroidNavigationRedirectPage(Options)));
}
//If we have an existing MainAppWindow we can use that one
var mainAppWindow = Windows.OfType<MainAppWindow>().FirstOrDefault();
if (mainAppWindow != null)
{
mainAppWindow.PendingPage = new NavigationPage(homePage);
}
//Create new main window
return new MainAppWindow(new NavigationPage(homePage));
} }
#else #else
//iOS doesn't use the CreateWindow override used in Android so we just set the Application.Current.MainPage directly //iOS doesn't use the CreateWindow override used in Android so we just set the Application.Current.MainPage directly
@@ -201,30 +184,40 @@ namespace Bit.App
_accountsManager.Init(() => Options, this); _accountsManager.Init(() => Options, this);
_broadcasterService.Subscribe(nameof(App), BroadcastServiceMessageCallbackAsync);
Bootstrap(); Bootstrap();
_broadcasterService.Subscribe(nameof(App), async (message) => }
private async void BroadcastServiceMessageCallbackAsync(Message message)
{ {
try try
{ {
ArgumentNullException.ThrowIfNull(message);
if (message.Command == "showDialog") if (message.Command == "showDialog")
{ {
var details = message.Data as DialogDetails; var details = message.Data as DialogDetails;
ArgumentNullException.ThrowIfNull(details);
var confirmed = true; var confirmed = true;
var confirmText = string.IsNullOrWhiteSpace(details.ConfirmText) ? var confirmText = string.IsNullOrWhiteSpace(details.ConfirmText) ?
AppResources.Ok : details.ConfirmText; AppResources.Ok : details.ConfirmText;
await MainThread.InvokeOnMainThreadAsync(async () => await MainThread.InvokeOnMainThreadAsync(ShowDialogAction);
async Task ShowDialogAction()
{ {
if (!string.IsNullOrWhiteSpace(details.CancelText)) if (!string.IsNullOrWhiteSpace(details.CancelText))
{ {
ArgumentNullException.ThrowIfNull(MainPage);
confirmed = await MainPage.DisplayAlert(details.Title, details.Text, confirmText, confirmed = await MainPage.DisplayAlert(details.Title, details.Text, confirmText,
details.CancelText); details.CancelText);
} }
else else
{ {
await MainPage.DisplayAlert(details.Title, details.Text, confirmText); await _deviceActionService.DisplayAlertAsync(details.Title, details.Text, confirmText);
} }
_messagingService.Send("showDialogResolve", new Tuple<int, bool>(details.DialogId, confirmed)); _messagingService.Send("showDialogResolve", new Tuple<int, bool>(details.DialogId, confirmed));
}); }
} }
#if IOS #if IOS
else if (message.Command == AppHelpers.RESUMED_MESSAGE_COMMAND) else if (message.Command == AppHelpers.RESUMED_MESSAGE_COMMAND)
@@ -252,14 +245,18 @@ namespace Bit.App
Options.OtpData = new OtpData((string)message.Data); Options.OtpData = new OtpData((string)message.Data);
} }
await MainThread.InvokeOnMainThreadAsync(async () => await MainThread.InvokeOnMainThreadAsync(ExecuteNavigationAction);
async Task ExecuteNavigationAction()
{ {
if (MainPage is TabsPage tabsPage) if (MainPage is TabsPage tabsPage)
{ {
ArgumentNullException.ThrowIfNull(tabsPage.Navigation);
ArgumentNullException.ThrowIfNull(tabsPage.Navigation.ModalStack);
while (tabsPage.Navigation.ModalStack.Count > 0) while (tabsPage.Navigation.ModalStack.Count > 0)
{ {
await tabsPage.Navigation.PopModalAsync(false); await tabsPage.Navigation.PopModalAsync(false);
} }
if (message.Command == POP_ALL_AND_GO_TO_AUTOFILL_CIPHERS_MESSAGE) if (message.Command == POP_ALL_AND_GO_TO_AUTOFILL_CIPHERS_MESSAGE)
{ {
MainPage = new NavigationPage(new CipherSelectionPage(Options)); MainPage = new NavigationPage(new CipherSelectionPage(Options));
@@ -281,31 +278,54 @@ namespace Bit.App
else if (message.Command == DeepLinkContext.NEW_OTP_MESSAGE) else if (message.Command == DeepLinkContext.NEW_OTP_MESSAGE)
{ {
tabsPage.ResetToVaultPage(); tabsPage.ResetToVaultPage();
ArgumentNullException.ThrowIfNull(tabsPage.Navigation);
await tabsPage.Navigation.PushModalAsync(new NavigationPage(new CipherSelectionPage(Options))); await tabsPage.Navigation.PushModalAsync(new NavigationPage(new CipherSelectionPage(Options)));
} }
} }
}); }
}
else if (message.Command == Constants.CredentialNavigateToAutofillCipherMessageCommand && message.Data is Fido2ConfirmNewCredentialParams createParams)
{
ArgumentNullException.ThrowIfNull(MainPage);
ArgumentNullException.ThrowIfNull(Options);
await MainThread.InvokeOnMainThreadAsync(NavigateToCipherSelectionPageAction);
void NavigateToCipherSelectionPageAction()
{
Options.Uri = createParams.RpId;
Options.SaveUsername = createParams.UserName;
Options.SaveName = createParams.CredentialName;
MainPage = new NavigationPage(new CipherSelectionPage(Options));
}
} }
else if (message.Command == "convertAccountToKeyConnector") else if (message.Command == "convertAccountToKeyConnector")
{ {
await MainThread.InvokeOnMainThreadAsync(async () => ArgumentNullException.ThrowIfNull(MainPage);
await MainThread.InvokeOnMainThreadAsync(NavigateToRemoveMasterPasswordPageAction);
async Task NavigateToRemoveMasterPasswordPageAction()
{ {
await MainPage.Navigation.PushModalAsync( await MainPage.Navigation.PushModalAsync(
new NavigationPage(new RemoveMasterPasswordPage())); new NavigationPage(new RemoveMasterPasswordPage()));
}); }
} }
else if (message.Command == Constants.ForceUpdatePassword) else if (message.Command == Constants.ForceUpdatePassword)
{ {
await MainThread.InvokeOnMainThreadAsync(async () => ArgumentNullException.ThrowIfNull(MainPage);
await MainThread.InvokeOnMainThreadAsync(NavigateToUpdateTempPasswordPageAction);
async Task NavigateToUpdateTempPasswordPageAction()
{ {
await MainPage.Navigation.PushModalAsync( await MainPage.Navigation.PushModalAsync(
new NavigationPage(new UpdateTempPasswordPage())); new NavigationPage(new UpdateTempPasswordPage()));
}); }
} }
else if (message.Command == Constants.ForceSetPassword) else if (message.Command == Constants.ForceSetPassword)
{ {
await MainThread.InvokeOnMainThreadAsync(() => MainPage.Navigation.PushModalAsync( ArgumentNullException.ThrowIfNull(MainPage);
new NavigationPage(new SetPasswordPage(orgIdentifier: (string)message.Data)))); await MainThread.InvokeOnMainThreadAsync(NavigateToSetPasswordPageAction);
void NavigateToSetPasswordPageAction()
{
MainPage.Navigation.PushModalAsync(
new NavigationPage(new SetPasswordPage(orgIdentifier: (string)message.Data)));
}
} }
else if (message.Command == "syncCompleted") else if (message.Command == "syncCompleted")
{ {
@@ -315,18 +335,31 @@ namespace Bit.App
|| message.Command == "unlocked" || message.Command == "unlocked"
|| message.Command == AccountsManagerMessageCommands.ACCOUNT_SWITCH_COMPLETED) || message.Command == AccountsManagerMessageCommands.ACCOUNT_SWITCH_COMPLETED)
{ {
#if ANDROID
if (message.Command == AccountsManagerMessageCommands.ACCOUNT_SWITCH_COMPLETED && _fido2MakeCredentialConfirmationUserInterface.Value.IsConfirmingNewCredential)
{
_fido2MakeCredentialConfirmationUserInterface.Value.OnConfirmationException(new AccountSwitchedException());
}
#endif
lock (_processingLoginRequestLock) lock (_processingLoginRequestLock)
{ {
// lock doesn't allow for async execution // lock doesn't allow for async execution
CheckPasswordlessLoginRequestsAsync().Wait(); CheckPasswordlessLoginRequestsAsync().Wait();
} }
} }
else if (message.Command == Constants.NavigateToMessageCommand && message.Data is NavigationTarget navigationTarget)
{
await MainThread.InvokeOnMainThreadAsync(() =>
{
Navigate(navigationTarget, null);
});
}
} }
catch (Exception ex) catch (Exception ex)
{ {
LoggerHelper.LogEvenIfCantBeResolved(ex); LoggerHelper.LogEvenIfCantBeResolved(ex);
} }
});
} }
private async Task CheckPasswordlessLoginRequestsAsync() private async Task CheckPasswordlessLoginRequestsAsync()
@@ -341,7 +374,6 @@ namespace Bit.App
{ {
return; return;
} }
var notification = await _stateService.GetPasswordlessLoginNotificationAsync(); var notification = await _stateService.GetPasswordlessLoginNotificationAsync();
if (notification == null) if (notification == null)
{ {
@@ -693,6 +725,15 @@ namespace Bit.App
// If we are in background we add the Navigation Actions to a queue to execute when the app resumes. // If we are in background we add the Navigation Actions to a queue to execute when the app resumes.
// Links: https://github.com/dotnet/maui/issues/11501 and https://bitwarden.atlassian.net/wiki/spaces/NMME/pages/664862722/MainPage+Assignments+not+working+on+Android+on+Background+or+App+resume // Links: https://github.com/dotnet/maui/issues/11501 and https://bitwarden.atlassian.net/wiki/spaces/NMME/pages/664862722/MainPage+Assignments+not+working+on+Android+on+Background+or+App+resume
#if ANDROID #if ANDROID
if (_fido2MakeCredentialConfirmationUserInterface != null && _fido2MakeCredentialConfirmationUserInterface.Value.IsConfirmingNewCredential)
{
// if it's creating passkey
// and we have an active pending TaskCompletionSource
// then we let the Fido2 Authenticator flow manage the navigation to avoid issues
// like duplicated navigation to lock page.
return;
}
if (!_isResumed) if (!_isResumed)
{ {
_onResumeActions.Enqueue(() => NavigateImpl(navTarget, navParams)); _onResumeActions.Enqueue(() => NavigateImpl(navTarget, navParams));

View File

@@ -46,7 +46,11 @@ namespace Bit.Core
public const string PreLoginEmailKey = "preLoginEmailKey"; public const string PreLoginEmailKey = "preLoginEmailKey";
public const string ConfigsKey = "configsKey"; public const string ConfigsKey = "configsKey";
public const string DisplayEuEnvironmentFlag = "display-eu-environment"; public const string DisplayEuEnvironmentFlag = "display-eu-environment";
public const string UnassignedItemsBannerFlag = "unassigned-items-banner";
public const string RegionEnvironment = "regionEnvironment"; public const string RegionEnvironment = "regionEnvironment";
public const string DuoCallback = "bitwarden://duo-callback";
public const string NavigateToMessageCommand = "navigateTo";
public const string CredentialNavigateToAutofillCipherMessageCommand = "credentialNavigateToAutofillCipher";
/// <summary> /// <summary>
/// This key is used to store the value of "ShouldConnectToWatch" of the last user that had logged in /// This key is used to store the value of "ShouldConnectToWatch" of the last user that had logged in
@@ -135,6 +139,7 @@ namespace Bit.Core
public static string ShouldConnectToWatchKey(string userId) => $"shouldConnectToWatch_{userId}"; public static string ShouldConnectToWatchKey(string userId) => $"shouldConnectToWatch_{userId}";
public static string ScreenCaptureAllowedKey(string userId) => $"screenCaptureAllowed_{userId}"; public static string ScreenCaptureAllowedKey(string userId) => $"screenCaptureAllowed_{userId}";
public static string PendingAdminAuthRequest(string userId) => $"pendingAdminAuthRequest_{userId}"; public static string PendingAdminAuthRequest(string userId) => $"pendingAdminAuthRequest_{userId}";
public static string ShouldCheckOrganizationUnassignedItemsKey(string userId) => $"shouldCheckOrganizationUnassignedItems_{userId}";
[Obsolete] [Obsolete]
public static string KeyKey(string userId) => $"key_{userId}"; public static string KeyKey(string userId) => $"key_{userId}";
[Obsolete] [Obsolete]

View File

@@ -53,13 +53,14 @@ namespace Bit.App.Controls
if (BindingContext is CipherItemViewModel cipherItemVM) if (BindingContext is CipherItemViewModel cipherItemVM)
{ {
cipherItemVM.IconImageSuccesfullyLoaded = true; cipherItemVM.IconImageSuccesfullyLoaded = true;
}
MainThread.BeginInvokeOnMainThread(() => MainThread.BeginInvokeOnMainThread(() =>
{ {
Icon.IsVisible = true; Icon.IsVisible = cipherItemVM.ShowIconImage;
IconPlaceholder.IsVisible = false; IconPlaceholder.IsVisible = !cipherItemVM.ShowIconImage;
}); });
} }
}
public void Icon_Error(object sender, FFImageLoading.Maui.CachedImageEvents.ErrorEventArgs e) public void Icon_Error(object sender, FFImageLoading.Maui.CachedImageEvents.ErrorEventArgs e)
{ {

View File

@@ -50,7 +50,7 @@
HorizontalOptions="Center" HorizontalOptions="Center"
VerticalOptions="Center" VerticalOptions="Center"
StyleClass="list-icon, list-icon-platform" StyleClass="list-icon, list-icon-platform"
Text="{Binding Cipher, Converter={StaticResource iconGlyphConverter}}" Text="{Binding ., Converter={StaticResource iconGlyphConverter}}"
ShouldUpdateFontSizeDynamicallyForAccesibility="True" ShouldUpdateFontSizeDynamicallyForAccesibility="True"
AutomationProperties.IsInAccessibleTree="False" AutomationProperties.IsInAccessibleTree="False"
AutomationId="CipherTypeIcon" /> AutomationId="CipherTypeIcon" />

View File

@@ -0,0 +1,22 @@
<?xml version="1.0" encoding="UTF-8" ?>
<controls:BaseSettingItemView
xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:controls="clr-namespace:Bit.App.Controls"
xmlns:core="clr-namespace:Bit.Core"
x:Class="Bit.App.Controls.ExternalLinkSubtitleItemView"
x:Name="_contentView"
ControlTemplate="{StaticResource SettingControlTemplate}">
<controls:BaseSettingItemView.GestureRecognizers>
<TapGestureRecognizer Tapped="ContentView_Tapped" />
</controls:BaseSettingItemView.GestureRecognizers>
<controls:IconLabel
Text="{Binding Source={x:Static core:BitwardenIcons.ExternalLink}}"
TextColor="{DynamicResource TextColor}"
FontSize="25"
Margin="6,0,7,0"
HorizontalOptions="End"
VerticalOptions="Center"
SemanticProperties.Description="{Binding Title, Mode=OneWay, Source={x:Reference _contentView}}" />
</controls:BaseSettingItemView>

View File

@@ -0,0 +1,26 @@
using System.Windows.Input;
namespace Bit.App.Controls
{
public partial class ExternalLinkSubtitleItemView : BaseSettingItemView
{
public static readonly BindableProperty GoToLinkCommandProperty = BindableProperty.Create(
nameof(GoToLinkCommand), typeof(ICommand), typeof(ExternalLinkSubtitleItemView));
public ExternalLinkSubtitleItemView()
{
InitializeComponent();
}
public ICommand GoToLinkCommand
{
get => GetValue(GoToLinkCommandProperty) as ICommand;
set => SetValue(GoToLinkCommandProperty, value);
}
void ContentView_Tapped(System.Object sender, System.EventArgs e)
{
GoToLinkCommand?.Execute(null);
}
}
}

View File

@@ -80,11 +80,11 @@
<Folder Include="Utilities\Fido2\" /> <Folder Include="Utilities\Fido2\" />
<Folder Include="Controls\Picker\" /> <Folder Include="Controls\Picker\" />
<Folder Include="Controls\Avatar\" /> <Folder Include="Controls\Avatar\" />
<Folder Include="Services\UserVerification\" />
<Folder Include="Utilities\WebAuthenticatorMAUI\" />
<Folder Include="Resources\Images\" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<MauiImage Include="Resources\Images\dotnet_bot.svg">
<BaseSize>168,208</BaseSize>
</MauiImage>
<MauiAsset Include="Resources\Raw\**" LogicalName="%(RecursiveDir)%(Filename)%(Extension)" /> <MauiAsset Include="Resources\Raw\**" LogicalName="%(RecursiveDir)%(Filename)%(Extension)" />
<MauiFont Include="Resources\Fonts\*" /> <MauiFont Include="Resources\Fonts\*" />
</ItemGroup> </ItemGroup>
@@ -93,6 +93,9 @@
<LastGenOutput>AppResources.Designer.cs</LastGenOutput> <LastGenOutput>AppResources.Designer.cs</LastGenOutput>
<Generator>PublicResXFileCodeGenerator</Generator> <Generator>PublicResXFileCodeGenerator</Generator>
</EmbeddedResource> </EmbeddedResource>
<Compile Update="Controls\Settings\ExternalLinkSubtitleItemView.xaml.cs">
<DependentUpon>ExternalLinkSubtitleItemView.xaml</DependentUpon>
</Compile>
<Compile Update="Pages\AndroidNavigationRedirectPage.xaml.cs"> <Compile Update="Pages\AndroidNavigationRedirectPage.xaml.cs">
<DependentUpon>AndroidNavigationRedirectPage.xaml</DependentUpon> <DependentUpon>AndroidNavigationRedirectPage.xaml</DependentUpon>
</Compile> </Compile>
@@ -103,6 +106,9 @@
</Compile> </Compile>
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<MauiXaml Update="Controls\Settings\ExternalLinkSubtitleItemView.xaml">
<Generator>MSBuild:Compile</Generator>
</MauiXaml>
<MauiXaml Update="Pages\AndroidNavigationRedirectPage.xaml"> <MauiXaml Update="Pages\AndroidNavigationRedirectPage.xaml">
<Generator>MSBuild:Compile</Generator> <Generator>MSBuild:Compile</Generator>
</MauiXaml> </MauiXaml>
@@ -111,5 +117,14 @@
<None Remove="Utilities\Fido2\" /> <None Remove="Utilities\Fido2\" />
<None Remove="Controls\Picker\" /> <None Remove="Controls\Picker\" />
<None Remove="Controls\Avatar\" /> <None Remove="Controls\Avatar\" />
<None Remove="Services\UserVerification\" />
<None Remove="Utilities\WebAuthenticatorMAUI\" />
<None Remove="Resources\Images\" />
<None Remove="Resources\Images\empty_items_state_dark.svg" />
<None Remove="Resources\Images\empty_items_state.svg" />
</ItemGroup>
<ItemGroup Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'android'">
<MauiImage Include="Resources\Images\empty_items_state.svg" />
<MauiImage Include="Resources\Images\empty_items_state_dark.svg" />
</ItemGroup> </ItemGroup>
</Project> </Project>

View File

@@ -0,0 +1,10 @@
namespace Bit.Core.Exceptions
{
public class ValidationException : Exception
{
public ValidationException(string localizedMessage)
: base(localizedMessage)
{
}
}
}

View File

@@ -1,5 +1,4 @@
using System; using Bit.Core.Enums;
using Bit.Core.Enums;
using Bit.Core.Utilities; using Bit.Core.Utilities;
namespace Bit.App.Models namespace Bit.App.Models
@@ -9,6 +8,8 @@ namespace Bit.App.Models
public bool MyVaultTile { get; set; } public bool MyVaultTile { get; set; }
public bool GeneratorTile { get; set; } public bool GeneratorTile { get; set; }
public bool FromAutofillFramework { get; set; } public bool FromAutofillFramework { get; set; }
public bool FromFido2Framework { get; set; }
public string Fido2CredentialAction { get; set; }
public CipherType? FillType { get; set; } public CipherType? FillType { get; set; }
public string Uri { get; set; } public string Uri { get; set; }
public CipherType? SaveType { get; set; } public CipherType? SaveType { get; set; }
@@ -25,6 +26,8 @@ namespace Bit.App.Models
public bool CopyInsteadOfShareAfterSaving { get; set; } public bool CopyInsteadOfShareAfterSaving { get; set; }
public bool HideAccountSwitcher { get; set; } public bool HideAccountSwitcher { get; set; }
public OtpData? OtpData { get; set; } public OtpData? OtpData { get; set; }
public bool HasUnlockedInThisTransaction { get; set; }
public bool HasJustLoggedInOrUnlocked { get; set; }
public void SetAllFrom(AppOptions o) public void SetAllFrom(AppOptions o)
{ {
@@ -35,6 +38,7 @@ namespace Bit.App.Models
MyVaultTile = o.MyVaultTile; MyVaultTile = o.MyVaultTile;
GeneratorTile = o.GeneratorTile; GeneratorTile = o.GeneratorTile;
FromAutofillFramework = o.FromAutofillFramework; FromAutofillFramework = o.FromAutofillFramework;
Fido2CredentialAction = o.Fido2CredentialAction;
FillType = o.FillType; FillType = o.FillType;
Uri = o.Uri; Uri = o.Uri;
SaveType = o.SaveType; SaveType = o.SaveType;
@@ -51,6 +55,7 @@ namespace Bit.App.Models
CopyInsteadOfShareAfterSaving = o.CopyInsteadOfShareAfterSaving; CopyInsteadOfShareAfterSaving = o.CopyInsteadOfShareAfterSaving;
HideAccountSwitcher = o.HideAccountSwitcher; HideAccountSwitcher = o.HideAccountSwitcher;
OtpData = o.OtpData; OtpData = o.OtpData;
HasUnlockedInThisTransaction = o.HasUnlockedInThisTransaction;
} }
} }
} }

View File

@@ -1,5 +1,4 @@
using System; using Bit.Core.Enums;
using Bit.Core.Enums;
namespace Bit.Core.Models.Domain namespace Bit.Core.Models.Domain
{ {
@@ -9,7 +8,7 @@ namespace Bit.Core.Models.Domain
{ {
if (key == null) if (key == null)
{ {
throw new Exception("Must provide key."); throw new ArgumentKeyNullException(nameof(key));
} }
if (encType == null) if (encType == null)
@@ -24,7 +23,7 @@ namespace Bit.Core.Models.Domain
} }
else else
{ {
throw new Exception("Unable to determine encType."); throw new InvalidKeyOperationException("Unable to determine encType.");
} }
} }
@@ -48,7 +47,7 @@ namespace Bit.Core.Models.Domain
} }
else else
{ {
throw new Exception("Unsupported encType/key length."); throw new InvalidKeyOperationException("Unsupported encType/key length.");
} }
if (Key != null) if (Key != null)
@@ -72,6 +71,32 @@ namespace Bit.Core.Models.Domain
public string KeyB64 { get; set; } public string KeyB64 { get; set; }
public string EncKeyB64 { get; set; } public string EncKeyB64 { get; set; }
public string MacKeyB64 { get; set; } public string MacKeyB64 { get; set; }
public class ArgumentKeyNullException : ArgumentNullException
{
public ArgumentKeyNullException(string paramName) : base(paramName)
{
}
public ArgumentKeyNullException(string message, Exception innerException) : base(message, innerException)
{
}
public ArgumentKeyNullException(string paramName, string message) : base(paramName, message)
{
}
}
public class InvalidKeyOperationException : InvalidOperationException
{
public InvalidKeyOperationException(string message) : base(message)
{
}
public InvalidKeyOperationException(string message, Exception innerException) : base(message, innerException)
{
}
}
} }
public class UserKey : SymmetricCryptoKey public class UserKey : SymmetricCryptoKey

View File

@@ -1,5 +1,7 @@
using Bit.Core.Enums; using Bit.Core.Enums;
using Bit.Core.Models.Domain; using Bit.Core.Models.Domain;
using Bit.Core.Resources.Localization;
using Bit.Core.Utilities;
namespace Bit.Core.Models.View namespace Bit.Core.Models.View
{ {
@@ -119,5 +121,14 @@ namespace Bit.Core.Models.View
public bool IsClonable => OrganizationId is null; public bool IsClonable => OrganizationId is null;
public bool HasFido2Credential => Type == CipherType.Login && Login?.HasFido2Credentials == true; public bool HasFido2Credential => Type == CipherType.Login && Login?.HasFido2Credentials == true;
public string GetMainFido2CredentialUsername()
{
return Login?.MainFido2Credential?.UserName
.FallbackOnNullOrWhiteSpace(Login?.MainFido2Credential?.UserDisplayName)
.FallbackOnNullOrWhiteSpace(Login?.Username)
.FallbackOnNullOrWhiteSpace(Name)
.FallbackOnNullOrWhiteSpace(AppResources.UnknownAccount);
}
} }
} }

View File

@@ -51,12 +51,14 @@ namespace Bit.Core.Models.View
[JsonIgnore] [JsonIgnore]
public bool DiscoverableValue { public bool DiscoverableValue {
get => bool.TryParse(Discoverable, out var discoverable) && discoverable; get => bool.TryParse(Discoverable, out var discoverable) && discoverable;
set => Discoverable = value.ToString().ToLower(); // must be lowercase so it can be parsed in the current version of clients set => Discoverable = value.ToString().ToLower();
} }
[JsonIgnore] [JsonIgnore]
public override string SubTitle => UserName; public override string SubTitle => UserName;
public override List<KeyValuePair<string, LinkedIdType>> LinkedFieldOptions => new List<KeyValuePair<string, LinkedIdType>>(); public override List<KeyValuePair<string, LinkedIdType>> LinkedFieldOptions => new List<KeyValuePair<string, LinkedIdType>>();
[JsonIgnore] [JsonIgnore]
public bool CanLaunch => !string.IsNullOrEmpty(RpId); public bool CanLaunch => !string.IsNullOrEmpty(RpId);
[JsonIgnore] [JsonIgnore]

View File

@@ -1,7 +1,4 @@
using System; using Bit.Core.Enums;
using System.Collections.Generic;
using System.Linq;
using Bit.Core.Enums;
using Bit.Core.Models.Domain; using Bit.Core.Models.Domain;
namespace Bit.Core.Models.View namespace Bit.Core.Models.View

View File

@@ -21,10 +21,6 @@
<ScrollView> <ScrollView>
<StackLayout Spacing="20"> <StackLayout Spacing="20">
<StackLayout StyleClass="box"> <StackLayout StyleClass="box">
<StackLayout StyleClass="box-row-header">
<Label Text="MAUI APP"
StyleClass="box-header, box-header-platform" />
</StackLayout>
<StackLayout StyleClass="box-row-header"> <StackLayout StyleClass="box-row-header">
<Label Text="{u:I18n SelfHostedEnvironment, Header=True}" <Label Text="{u:I18n SelfHostedEnvironment, Header=True}"
StyleClass="box-header, box-header-platform" /> StyleClass="box-header, box-header-platform" />

View File

@@ -1,6 +1,7 @@
using Bit.Core.Abstractions; using Bit.Core.Abstractions;
using Bit.Core.Resources.Localization; using Bit.Core.Resources.Localization;
using Bit.Core.Utilities; using Bit.Core.Utilities;
using Microsoft.Maui.Platform;
namespace Bit.App.Pages namespace Bit.App.Pages
{ {
@@ -26,7 +27,7 @@ namespace Bit.App.Pages
_apiEntry.ReturnCommand = new Command(() => _identityEntry.Focus()); _apiEntry.ReturnCommand = new Command(() => _identityEntry.Focus());
_identityEntry.ReturnType = ReturnType.Next; _identityEntry.ReturnType = ReturnType.Next;
_identityEntry.ReturnCommand = new Command(() => _iconsEntry.Focus()); _identityEntry.ReturnCommand = new Command(() => _iconsEntry.Focus());
_vm.SubmitSuccessAction = () => MainThread.BeginInvokeOnMainThread(async () => await SubmitSuccessAsync()); _vm.SubmitSuccessTask = () => MainThread.InvokeOnMainThreadAsync(SubmitSuccessAsync);
_vm.CloseAction = async () => _vm.CloseAction = async () =>
{ {
await Navigation.PopModalAsync(); await Navigation.PopModalAsync();
@@ -37,6 +38,12 @@ namespace Bit.App.Pages
{ {
_platformUtilsService.ShowToast("success", null, AppResources.EnvironmentSaved); _platformUtilsService.ShowToast("success", null, AppResources.EnvironmentSaved);
await Navigation.PopModalAsync(); await Navigation.PopModalAsync();
#if ANDROID
if (Platform.CurrentActivity.CurrentFocus != null)
{
Platform.CurrentActivity.HideKeyboard(Platform.CurrentActivity.CurrentFocus);
}
#endif
} }
private void Close_Clicked(object sender, EventArgs e) private void Close_Clicked(object sender, EventArgs e)

View File

@@ -44,7 +44,7 @@ namespace Bit.App.Pages
public string WebVaultUrl { get; set; } public string WebVaultUrl { get; set; }
public string IconsUrl { get; set; } public string IconsUrl { get; set; }
public string NotificationsUrls { get; set; } public string NotificationsUrls { get; set; }
public Action SubmitSuccessAction { get; set; } public Func<Task> SubmitSuccessTask { get; set; }
public Action CloseAction { get; set; } public Action CloseAction { get; set; }
public async Task SubmitAsync() public async Task SubmitAsync()
@@ -73,7 +73,10 @@ namespace Bit.App.Pages
IconsUrl = resUrls.Icons; IconsUrl = resUrls.Icons;
NotificationsUrls = resUrls.Notifications; NotificationsUrls = resUrls.Notifications;
SubmitSuccessAction?.Invoke(); if (SubmitSuccessTask != null)
{
await SubmitSuccessTask();
}
} }
public bool ValidateUrls() public bool ValidateUrls()

View File

@@ -12,12 +12,14 @@ namespace Bit.App.Pages
private readonly HomeViewModel _vm; private readonly HomeViewModel _vm;
private readonly AppOptions _appOptions; private readonly AppOptions _appOptions;
private IBroadcasterService _broadcasterService; private IBroadcasterService _broadcasterService;
private IConditionedAwaiterManager _conditionedAwaiterManager;
readonly LazyResolve<ILogger> _logger = new LazyResolve<ILogger>(); readonly LazyResolve<ILogger> _logger = new LazyResolve<ILogger>();
public HomePage(AppOptions appOptions = null) public HomePage(AppOptions appOptions = null)
{ {
_broadcasterService = ServiceContainer.Resolve<IBroadcasterService>(); _broadcasterService = ServiceContainer.Resolve<IBroadcasterService>();
_conditionedAwaiterManager = ServiceContainer.Resolve<IConditionedAwaiterManager>();
_appOptions = appOptions; _appOptions = appOptions;
InitializeComponent(); InitializeComponent();
_vm = BindingContext as HomeViewModel; _vm = BindingContext as HomeViewModel;
@@ -56,6 +58,8 @@ namespace Bit.App.Pages
PerformNavigationOnAccountChangedOnLoad = false; PerformNavigationOnAccountChangedOnLoad = false;
accountsManager.NavigateOnAccountChangeAsync().FireAndForget(); accountsManager.NavigateOnAccountChangeAsync().FireAndForget();
} }
_conditionedAwaiterManager.SetAsCompleted(AwaiterPrecondition.AndroidWindowCreated);
#endif #endif
} }

View File

@@ -168,7 +168,7 @@ namespace Bit.App.Pages
var tasks = Task.Run(async () => var tasks = Task.Run(async () =>
{ {
await Task.Delay(50); await Task.Delay(50);
MainThread.BeginInvokeOnMainThread(async () => await _vm.SubmitAsync()); _vm.SubmitCommand.Execute(null);
}); });
} }
} }
@@ -233,6 +233,7 @@ namespace Bit.App.Pages
} }
var previousPage = await AppHelpers.ClearPreviousPage(); var previousPage = await AppHelpers.ClearPreviousPage();
_appOptions.HasJustLoggedInOrUnlocked = true;
App.MainPage = new TabsPage(_appOptions, previousPage); App.MainPage = new TabsPage(_appOptions, previousPage);
} }
} }

View File

@@ -1,5 +1,6 @@
using System; using System;
using System.Threading.Tasks; using System.Threading.Tasks;
using System.Windows.Input;
using Bit.App.Abstractions; using Bit.App.Abstractions;
using Bit.App.Controls; using Bit.App.Controls;
using Bit.Core.Resources.Localization; using Bit.Core.Resources.Localization;
@@ -73,7 +74,7 @@ namespace Bit.App.Pages
PageTitle = AppResources.VerifyMasterPassword; PageTitle = AppResources.VerifyMasterPassword;
TogglePasswordCommand = new Command(TogglePassword); TogglePasswordCommand = new Command(TogglePassword);
SubmitCommand = new Command(async () => await SubmitAsync()); SubmitCommand = CreateDefaultAsyncRelayCommand(SubmitAsync, onException: _logger.Exception, allowsMultipleExecutions: false);
AccountSwitchingOverlayViewModel = AccountSwitchingOverlayViewModel =
new AccountSwitchingOverlayViewModel(_stateService, _messagingService, _logger) new AccountSwitchingOverlayViewModel(_stateService, _messagingService, _logger)
@@ -157,7 +158,7 @@ namespace Bit.App.Pages
public AccountSwitchingOverlayViewModel AccountSwitchingOverlayViewModel { get; } public AccountSwitchingOverlayViewModel AccountSwitchingOverlayViewModel { get; }
public Command SubmitCommand { get; } public ICommand SubmitCommand { get; }
public Command TogglePasswordCommand { get; } public Command TogglePasswordCommand { get; }
public string ShowPasswordIcon => ShowPassword ? BitwardenIcons.EyeSlash : BitwardenIcons.Eye; public string ShowPasswordIcon => ShowPassword ? BitwardenIcons.EyeSlash : BitwardenIcons.Eye;
@@ -233,8 +234,8 @@ namespace Bit.App.Pages
} }
BiometricButtonVisible = true; BiometricButtonVisible = true;
BiometricButtonText = AppResources.UseBiometricsToUnlock; BiometricButtonText = AppResources.UseBiometricsToUnlock;
// TODO Xamarin.Forms.Device.RuntimePlatform is no longer supported. Use Microsoft.Maui.Devices.DeviceInfo.Platform instead. For more details see https://learn.microsoft.com/en-us/dotnet/maui/migration/forms-projects#device-changes
if (Device.RuntimePlatform == Device.iOS) if (DeviceInfo.Platform == DevicePlatform.iOS)
{ {
var supportsFace = await _deviceActionService.SupportsFaceBiometricAsync(); var supportsFace = await _deviceActionService.SupportsFaceBiometricAsync();
BiometricButtonText = supportsFace ? AppResources.UseFaceIDToUnlock : BiometricButtonText = supportsFace ? AppResources.UseFaceIDToUnlock :
@@ -330,6 +331,7 @@ namespace Bit.App.Pages
Pin = string.Empty; Pin = string.Empty;
await AppHelpers.ResetInvalidUnlockAttemptsAsync(); await AppHelpers.ResetInvalidUnlockAttemptsAsync();
await SetUserKeyAndContinueAsync(userKey); await SetUserKeyAndContinueAsync(userKey);
await Task.Delay(150); //Workaround Delay to avoid "duplicate" execution of SubmitAsync on Android when invoked from the ReturnCommand
} }
} }
catch (LegacyUserException) catch (LegacyUserException)
@@ -418,6 +420,7 @@ namespace Bit.App.Pages
var userKey = await _cryptoService.DecryptUserKeyWithMasterKeyAsync(masterKey); var userKey = await _cryptoService.DecryptUserKeyWithMasterKeyAsync(masterKey);
await _cryptoService.SetMasterKeyAsync(masterKey); await _cryptoService.SetMasterKeyAsync(masterKey);
await SetUserKeyAndContinueAsync(userKey); await SetUserKeyAndContinueAsync(userKey);
await Task.Delay(150); //Workaround Delay to avoid "duplicate" execution of SubmitAsync on Android when invoked from the ReturnCommand
// Re-enable biometrics // Re-enable biometrics
if (BiometricEnabled & !BiometricIntegrityValid) if (BiometricEnabled & !BiometricIntegrityValid)
@@ -515,7 +518,7 @@ namespace Bit.App.Pages
var success = await _platformUtilsService.AuthenticateBiometricAsync(null, var success = await _platformUtilsService.AuthenticateBiometricAsync(null,
PinEnabled ? AppResources.PIN : AppResources.MasterPassword, PinEnabled ? AppResources.PIN : AppResources.MasterPassword,
() => _secretEntryFocusWeakEventManager.RaiseEvent((int?)null, nameof(FocusSecretEntry)), () => _secretEntryFocusWeakEventManager.RaiseEvent((int?)null, nameof(FocusSecretEntry)),
!PinEnabled && !HasMasterPassword); !PinEnabled && !HasMasterPassword) ?? false;
await _stateService.SetBiometricLockedAsync(!success); await _stateService.SetBiometricLockedAsync(!success);
if (success) if (success)

View File

@@ -35,6 +35,8 @@ namespace Bit.App.Pages
{ {
return; return;
} }
_appOptions.HasJustLoggedInOrUnlocked = true;
var previousPage = await AppHelpers.ClearPreviousPage(); var previousPage = await AppHelpers.ClearPreviousPage();
App.MainPage = new TabsPage(_appOptions, previousPage); App.MainPage = new TabsPage(_appOptions, previousPage);
} }

View File

@@ -195,6 +195,8 @@ namespace Bit.App.Pages
{ {
return; return;
} }
_appOptions.HasJustLoggedInOrUnlocked = true;
var previousPage = await AppHelpers.ClearPreviousPage(); var previousPage = await AppHelpers.ClearPreviousPage();
App.MainPage = new TabsPage(_appOptions, previousPage); App.MainPage = new TabsPage(_appOptions, previousPage);
} }

View File

@@ -39,7 +39,7 @@
FontSize="Small" FontSize="Small"
FontAttributes="Bold"/> FontAttributes="Bold"/>
<controls:MonoLabel <controls:MonoLabel
FormattedText="{Binding LoginRequest.FingerprintPhrase}" Text="{Binding LoginRequest.FingerprintPhrase}"
FontSize="Medium" FontSize="Medium"
TextColor="{DynamicResource FingerprintPhrase}" TextColor="{DynamicResource FingerprintPhrase}"
Margin="0,0,0,27" Margin="0,0,0,27"
@@ -85,7 +85,6 @@
<Button <Button
Text="{u:I18n DenyLogIn}" Text="{u:I18n DenyLogIn}"
Command="{Binding RejectRequestCommand}" Command="{Binding RejectRequestCommand}"
StyleClass="btn-secundary"
AutomationId="DenyLoginButton" /> AutomationId="DenyLoginButton" />
</StackLayout> </StackLayout>

View File

@@ -41,7 +41,7 @@
FontSize="Small" FontSize="Small"
FontAttributes="Bold" /> FontAttributes="Bold" />
<controls:MonoLabel <controls:MonoLabel
FormattedText="{Binding FingerprintPhrase}" Text="{Binding FingerprintPhrase}"
FontSize="Small" FontSize="Small"
TextColor="{DynamicResource FingerprintPhrase}" TextColor="{DynamicResource FingerprintPhrase}"
AutomationId="FingerprintPhraseValue" /> AutomationId="FingerprintPhraseValue" />

View File

@@ -55,6 +55,8 @@ namespace Bit.App.Pages
{ {
return; return;
} }
_appOptions.HasJustLoggedInOrUnlocked = true;
var previousPage = await AppHelpers.ClearPreviousPage(); var previousPage = await AppHelpers.ClearPreviousPage();
App.MainPage = new TabsPage(_appOptions, previousPage); App.MainPage = new TabsPage(_appOptions, previousPage);
} }

View File

@@ -21,6 +21,7 @@ namespace Bit.App.Pages
InitializeComponent(); InitializeComponent();
_vm = BindingContext as LoginSsoPageViewModel; _vm = BindingContext as LoginSsoPageViewModel;
_vm.Page = this; _vm.Page = this;
_vm.FromIosExtension = _appOptions?.IosExtension ?? false;
_vm.StartTwoFactorAction = () => MainThread.BeginInvokeOnMainThread(async () => await StartTwoFactorAsync()); _vm.StartTwoFactorAction = () => MainThread.BeginInvokeOnMainThread(async () => await StartTwoFactorAsync());
_vm.StartSetPasswordAction = () => _vm.StartSetPasswordAction = () =>
MainThread.BeginInvokeOnMainThread(async () => await StartSetPasswordAsync()); MainThread.BeginInvokeOnMainThread(async () => await StartSetPasswordAsync());

View File

@@ -15,6 +15,16 @@ using Bit.Core.Utilities;
using Microsoft.Maui.Authentication; using Microsoft.Maui.Authentication;
using Microsoft.Maui.Networking; using Microsoft.Maui.Networking;
using NetworkAccess = Microsoft.Maui.Networking.NetworkAccess; using NetworkAccess = Microsoft.Maui.Networking.NetworkAccess;
using Org.BouncyCastle.Asn1.Ocsp;
#if IOS
using AuthenticationServices;
using Foundation;
using UIKit;
using WebAuthenticator = Bit.Core.Utilities.MAUI.WebAuthenticator;
using WebAuthenticatorResult = Bit.Core.Utilities.MAUI.WebAuthenticatorResult;
using WebAuthenticatorOptions = Bit.Core.Utilities.MAUI.WebAuthenticatorOptions;
#endif
namespace Bit.App.Pages namespace Bit.App.Pages
{ {
@@ -64,6 +74,8 @@ namespace Bit.App.Pages
set => SetProperty(ref _orgIdentifier, value); set => SetProperty(ref _orgIdentifier, value);
} }
public bool FromIosExtension { get; set; }
public ICommand LogInCommand { get; } public ICommand LogInCommand { get; }
public Action StartTwoFactorAction { get; set; } public Action StartTwoFactorAction { get; set; }
public Action StartSetPasswordAction { get; set; } public Action StartSetPasswordAction { get; set; }
@@ -153,6 +165,9 @@ namespace Bit.App.Pages
CallbackUrl = new Uri(REDIRECT_URI), CallbackUrl = new Uri(REDIRECT_URI),
Url = new Uri(url), Url = new Uri(url),
PrefersEphemeralWebBrowserSession = _useEphemeralWebBrowserSession, PrefersEphemeralWebBrowserSession = _useEphemeralWebBrowserSession,
#if IOS
ShouldUseSharedApplicationKeyWindow = FromIosExtension
#endif
}); });
var code = GetResultCode(authResult, state); var code = GetResultCode(authResult, state);

View File

@@ -71,6 +71,8 @@ namespace Bit.App.Pages
{ {
return; return;
} }
_appOptions.HasJustLoggedInOrUnlocked = true;
var previousPage = await AppHelpers.ClearPreviousPage(); var previousPage = await AppHelpers.ClearPreviousPage();
App.MainPage = new TabsPage(_appOptions, previousPage); App.MainPage = new TabsPage(_appOptions, previousPage);
} }

View File

@@ -132,14 +132,26 @@
</StackLayout> </StackLayout>
</StackLayout> </StackLayout>
</StackLayout> </StackLayout>
<StackLayout Spacing="0" Padding="0" IsVisible="{Binding DuoMethod, Mode=OneWay}" <StackLayout
VerticalOptions="StartAndExpand"> Spacing="0"
Padding="0"
IsVisible="{Binding DuoMethod, Mode=OneWay}"
VerticalOptions="FillAndExpand">
<Label
StyleClass="box"
Text="{Binding DuoFramelessLabel}"
HorizontalOptions="StartAndExpand"
Margin="10,21"
IsVisible="{Binding IsDuoFrameless}"/>
<controls:HybridWebView <controls:HybridWebView
x:Name="_duoWebView" x:Name="_duoWebView"
HorizontalOptions="FillAndExpand" HorizontalOptions="FillAndExpand"
VerticalOptions="FillAndExpand" VerticalOptions="FillAndExpand"
HeightRequest="{Binding DuoWebViewHeight, Mode=OneWay}" /> HeightRequest="{Binding DuoWebViewHeight, Mode=OneWay}"
<StackLayout StyleClass="box" VerticalOptions="End"> IsVisible="{Binding IsDuoFrameless, Converter={StaticResource inverseBool}}"/>
<StackLayout
StyleClass="box"
VerticalOptions="End">
<StackLayout StyleClass="box-row, box-row-switch"> <StackLayout StyleClass="box-row, box-row-switch">
<Label <Label
Text="{u:I18n RememberMe}" Text="{u:I18n RememberMe}"
@@ -151,6 +163,12 @@
HorizontalOptions="End" /> HorizontalOptions="End" />
</StackLayout> </StackLayout>
</StackLayout> </StackLayout>
<Button Text="{u:I18n LaunchDuo}"
Margin="10,21"
StyleClass="btn-primary"
Command="{Binding AuthenticateWithDuoFramelessCommand}"
AutomationId="DuoFramelessButton"
IsVisible="{Binding IsDuoFrameless}"/>
</StackLayout> </StackLayout>
<StackLayout <StackLayout
Spacing="0" Spacing="0"

View File

@@ -206,6 +206,8 @@ namespace Bit.App.Pages
{ {
return; return;
} }
_appOptions.HasJustLoggedInOrUnlocked = true;
var previousPage = await AppHelpers.ClearPreviousPage(); var previousPage = await AppHelpers.ClearPreviousPage();
App.MainPage = new TabsPage(_appOptions, previousPage); App.MainPage = new TabsPage(_appOptions, previousPage);
} }

View File

@@ -2,6 +2,7 @@
using System.Windows.Input; using System.Windows.Input;
using Bit.App.Abstractions; using Bit.App.Abstractions;
using Bit.App.Utilities; using Bit.App.Utilities;
using Bit.Core;
using Bit.Core.Abstractions; using Bit.Core.Abstractions;
using Bit.Core.Enums; using Bit.Core.Enums;
using Bit.Core.Exceptions; using Bit.Core.Exceptions;
@@ -34,6 +35,7 @@ namespace Bit.App.Pages
private string _webVaultUrl = "https://vault.bitwarden.com"; private string _webVaultUrl = "https://vault.bitwarden.com";
private bool _enableContinue = false; private bool _enableContinue = false;
private bool _showContinue = true; private bool _showContinue = true;
private bool _isDuoFrameless = false;
private double _duoWebViewHeight; private double _duoWebViewHeight;
public TwoFactorPageViewModel() public TwoFactorPageViewModel()
@@ -56,6 +58,7 @@ namespace Bit.App.Pages
PageTitle = AppResources.TwoStepLogin; PageTitle = AppResources.TwoStepLogin;
SubmitCommand = CreateDefaultAsyncRelayCommand(() => MainThread.InvokeOnMainThreadAsync(async () => await SubmitAsync()), allowsMultipleExecutions: false); SubmitCommand = CreateDefaultAsyncRelayCommand(() => MainThread.InvokeOnMainThreadAsync(async () => await SubmitAsync()), allowsMultipleExecutions: false);
MoreCommand = CreateDefaultAsyncRelayCommand(MoreAsync, onException: _logger.Exception, allowsMultipleExecutions: false); MoreCommand = CreateDefaultAsyncRelayCommand(MoreAsync, onException: _logger.Exception, allowsMultipleExecutions: false);
AuthenticateWithDuoFramelessCommand = CreateDefaultAsyncRelayCommand(DuoFramelessAuthenticateAsync, allowsMultipleExecutions: false);
} }
public string TotpInstruction public string TotpInstruction
@@ -103,6 +106,16 @@ namespace Bit.App.Pages
set => SetProperty(ref _enableContinue, value); set => SetProperty(ref _enableContinue, value);
} }
public bool IsDuoFrameless
{
get => _isDuoFrameless;
set => SetProperty(ref _isDuoFrameless, value, additionalPropertyNames: new string[] { nameof(DuoFramelessLabel) });
}
public string DuoFramelessLabel => SelectedProviderType == TwoFactorProviderType.OrganizationDuo ?
$"{AppResources.DuoTwoStepLoginIsRequiredForYourAccount} {AppResources.FollowTheStepsFromDuoToFinishLoggingIn}" :
AppResources.FollowTheStepsFromDuoToFinishLoggingIn;
#if IOS #if IOS
public string YubikeyInstruction => AppResources.YubiKeyInstructionIos; public string YubikeyInstruction => AppResources.YubiKeyInstructionIos;
#else #else
@@ -125,6 +138,7 @@ namespace Bit.App.Pages
} }
public ICommand SubmitCommand { get; } public ICommand SubmitCommand { get; }
public ICommand MoreCommand { get; } public ICommand MoreCommand { get; }
public ICommand AuthenticateWithDuoFramelessCommand { get; }
public Action TwoFactorAuthSuccessAction { get; set; } public Action TwoFactorAuthSuccessAction { get; set; }
public Action LockAction { get; set; } public Action LockAction { get; set; }
public Action StartDeviceApprovalOptionsAction { get; set; } public Action StartDeviceApprovalOptionsAction { get; set; }
@@ -179,6 +193,9 @@ namespace Bit.App.Pages
break; break;
case TwoFactorProviderType.Duo: case TwoFactorProviderType.Duo:
case TwoFactorProviderType.OrganizationDuo: case TwoFactorProviderType.OrganizationDuo:
IsDuoFrameless = providerData.ContainsKey("AuthUrl");
if (!IsDuoFrameless)
{
SetDuoWebViewHeight(); SetDuoWebViewHeight();
var host = WebUtility.UrlEncode(providerData["Host"] as string); var host = WebUtility.UrlEncode(providerData["Host"] as string);
var req = WebUtility.UrlEncode(providerData["Signature"] as string); var req = WebUtility.UrlEncode(providerData["Signature"] as string);
@@ -186,8 +203,19 @@ namespace Bit.App.Pages
page.DuoWebView.RegisterAction(sig => page.DuoWebView.RegisterAction(sig =>
{ {
Token = sig; Token = sig;
SubmitCommand.Execute(null); MainThread.BeginInvokeOnMainThread(async () =>
{
try
{
await SubmitAsync();
}
catch (Exception ex)
{
HandleException(ex);
}
}); });
});
}
break; break;
case TwoFactorProviderType.Email: case TwoFactorProviderType.Email:
TotpInstruction = string.Format(AppResources.EnterVerificationCodeEmail, TotpInstruction = string.Format(AppResources.EnterVerificationCodeEmail,
@@ -211,6 +239,77 @@ namespace Bit.App.Pages
ShowContinue = !(SelectedProviderType == null || DuoMethod || Fido2Method); ShowContinue = !(SelectedProviderType == null || DuoMethod || Fido2Method);
} }
private async Task DuoFramelessAuthenticateAsync()
{
await _deviceActionService.ShowLoadingAsync(AppResources.Validating);
if (!_authService.TwoFactorProvidersData.TryGetValue(SelectedProviderType.Value, out var providerData) ||
!providerData.TryGetValue("AuthUrl", out var urlObject))
{
throw new InvalidOperationException("Duo authentication error: Could not get ProviderData or AuthUrl");
}
var url = urlObject as string;
if (string.IsNullOrWhiteSpace(url))
{
throw new ArgumentNullException("Duo authentication error: Could not get valid auth url");
}
WebAuthenticatorResult authResult;
try
{
authResult = await WebAuthenticator.AuthenticateAsync(new WebAuthenticatorOptions
{
Url = new Uri(url),
CallbackUrl = new Uri(Constants.DuoCallback)
});
}
catch (TaskCanceledException)
{
// user canceled
await _deviceActionService.HideLoadingAsync();
return;
}
await _deviceActionService.HideLoadingAsync();
if (authResult == null || authResult.Properties == null)
{
throw new InvalidOperationException("Duo authentication error: Could not get result from authentication");
}
if (authResult.Properties.TryGetValue("error", out var resultError))
{
_logger.Error(resultError);
await _platformUtilsService.ShowDialogAsync(AppResources.AnErrorHasOccurred, AppResources.Ok);
return;
}
string code = null;
if (authResult.Properties.TryGetValue("code", out var resultCodeData))
{
code = Uri.UnescapeDataString(resultCodeData);
}
if (string.IsNullOrWhiteSpace(code))
{
throw new ArgumentException("Duo authentication error: response code is null or empty/whitespace");
}
string state = null;
if (authResult.Properties.TryGetValue("state", out var resultStateData))
{
state = Uri.UnescapeDataString(resultStateData);
}
if (string.IsNullOrWhiteSpace(state))
{
throw new ArgumentException("Duo authentication error: response state is null or empty/whitespace");
}
Token = $"{code}|{state}";
await SubmitAsync(true);
}
public void SetDuoWebViewHeight() public void SetDuoWebViewHeight()
{ {
var screenHeight = DeviceDisplay.MainDisplayInfo.Height / DeviceDisplay.MainDisplayInfo.Density; var screenHeight = DeviceDisplay.MainDisplayInfo.Height / DeviceDisplay.MainDisplayInfo.Density;

View File

@@ -1,20 +1,40 @@
using Bit.App.Abstractions; using Bit.App.Abstractions;
using Bit.App.Models;
using Bit.App.Pages;
using Bit.Core.Abstractions;
using Bit.Core.Services;
using Bit.Core.Utilities; using Bit.Core.Utilities;
namespace Bit.Core.Pages; namespace Bit.Core.Pages;
public partial class AndroidNavigationRedirectPage : ContentPage public partial class AndroidNavigationRedirectPage : ContentPage
{ {
private readonly IAccountsManager _accountsManager; private AppOptions _options;
public AndroidNavigationRedirectPage(AppOptions options)
public AndroidNavigationRedirectPage()
{ {
_accountsManager = ServiceContainer.Resolve<IAccountsManager>("accountsManager"); _options = options ?? new AppOptions();
InitializeComponent(); InitializeComponent();
} }
private void AndroidNavigationRedirectPage_OnLoaded(object sender, EventArgs e) private void AndroidNavigationRedirectPage_OnLoaded(object sender, EventArgs e)
{ {
_accountsManager.NavigateOnAccountChangeAsync().FireAndForget(); if (ServiceContainer.TryResolve<IAccountsManager>(out var accountsManager))
{
accountsManager.NavigateOnAccountChangeAsync().FireAndForget();
}
else
{
Bit.App.App.MainPage = new NavigationPage(new HomePage(_options)); //Fallback scenario to load HomePage just in case something goes wrong when resolving IAccountsManager
}
if (ServiceContainer.TryResolve<IConditionedAwaiterManager>(out var conditionedAwaiterManager))
{
conditionedAwaiterManager?.SetAsCompleted(AwaiterPrecondition.AndroidWindowCreated);
}
else
{
LoggerHelper.LogEvenIfCantBeResolved(new InvalidOperationException("ConditionedAwaiterManager can't be resolved on Android Navigation redirection"));
}
} }
} }

View File

@@ -266,6 +266,8 @@
AutomationId="SendShowHideOptionsButton" /> AutomationId="SendShowHideOptionsButton" />
<controls:IconButton <controls:IconButton
x:Name="_btnOptionsUp" x:Name="_btnOptionsUp"
InputTransparent="True"
MinimumWidthRequest="25"
Text="{Binding Source={x:Static core:BitwardenIcons.ChevronUp}}" Text="{Binding Source={x:Static core:BitwardenIcons.ChevronUp}}"
StyleClass="box-row-button" StyleClass="box-row-button"
TextColor="{DynamicResource PrimaryColor}" TextColor="{DynamicResource PrimaryColor}"
@@ -274,6 +276,8 @@
AutomationId="SendOptionsDisplayed" /> AutomationId="SendOptionsDisplayed" />
<controls:IconButton <controls:IconButton
x:Name="_btnOptionsDown" x:Name="_btnOptionsDown"
InputTransparent="True"
MinimumWidthRequest="25"
Text="{Binding Source={x:Static core:BitwardenIcons.AngleDown}}" Text="{Binding Source={x:Static core:BitwardenIcons.AngleDown}}"
StyleClass="box-row-button" StyleClass="box-row-button"
TextColor="{DynamicResource PrimaryColor}" TextColor="{DynamicResource PrimaryColor}"

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