1
0
mirror of https://github.com/bitwarden/mobile synced 2025-12-05 23:53:33 +00:00

Compare commits

...

268 Commits

Author SHA1 Message Date
Federico Maccaroni
f312e8c4d2 PM-7052 WIP fix theme change on iOS Autofill extension (with ClipLogger activated) 2024-03-26 10:58:09 -03: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
Federico Maccaroni
1fd7dd462e Merge branch 'main' into feature/maui-migration-passkeys 2024-03-22 15:42:51 -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
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
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
Federico Maccaroni
dd997aaa47 Merge branch 'main' into feature/maui-migration-passkeys 2024-03-12 12:38:52 -03:00
Federico Maccaroni
46c1d72b3c Merge branch 'main' into feature/maui-migration-passkeys 2024-03-11 18:12:27 -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
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
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
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
b8ff0e0244 Merge branch 'main' into feature/maui-migration-passkeys 2024-03-04 11:03:30 -03:00
Federico Maccaroni
85755902e1 Merge branch 'main' into feature/maui-migration-passkeys 2024-03-01 17:15:22 -03: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
Federico Maccaroni
18fae7ddd8 Merge branch 'main' into feature/maui-migration-passkeys
# Conflicts:
#	Directory.Build.props
2024-02-27 12:46:30 -03:00
Federico Maccaroni
b83473ce3a Merge branch 'main' into feature/maui-migration-passkeys 2024-02-26 15:42:12 -03: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
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
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
Federico Maccaroni
d339514d9a Merge branch 'main' into feature/maui-migration-passkeys
# Conflicts:
#	.github/workflows/build.yml
#	nuget.config
#	src/App/App.csproj
#	src/App/Platforms/Android/AndroidManifest.xml
#	src/App/Platforms/iOS/AppDelegate.cs
#	src/Core/Core.csproj
#	src/Core/Pages/Settings/AutofillSettingsPageViewModel.android.cs
#	src/Core/Utilities/ThemeManager.cs
#	src/iOS.Autofill/CredentialProviderViewController.cs
#	src/iOS.Autofill/iOS.Autofill.csproj
2024-02-14 14:19:51 -03:00
Federico Maccaroni
75ec96f282 PM-3349 Added exception on gitignore so the nupkg for the AndroidX Credentials is added 2024-02-14 13:58:35 -03:00
Federico Maccaroni
8f8a5795d3 Merge branch 'feature/maui-migration' into feature/maui-migration-passkeys 2024-02-08 12:33:16 -03:00
Federico Maccaroni
4631a9e62c PM-3349 PM-3350 Fixed Unit tests because of referencing FFImageLoading when it's not possible 2024-02-08 12:31:52 -03:00
Federico Maccaroni
51ee6a84b5 Merged main into maui-migration 2024-02-08 12:17:34 -03:00
Dinis Vieira
8d5006c0bd [PM-5906] Fix for incorrect Send MaxAccess white text color on label when using light theme on iOS (#2981)
* PM-5906 workaround for incorrect textcolor when programmatically changing text on Entry

* Update src/Core/Pages/Send/SendAddEditPage.xaml.cs

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

---------

Co-authored-by: Federico Maccaroni <fedemkr@gmail.com>
2024-02-07 19:09:18 +00:00
Dinis Vieira
37208571fe [PM-5907] Fix for incorrect TOTP white text color on label when using light theme on iOS (#2982)
* PM-5907 workaround for incorrect textcolor when programmatically changing text on Entry

* Update src/Core/Pages/Vault/CipherAddEditPage.xaml.cs

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

---------

Co-authored-by: Federico Maccaroni <fedemkr@gmail.com>
2024-02-07 19:08:51 +00:00
Dinis Vieira
759627b3c7 PM-6077 Separated Android and iOS HybridWebViewHandler so that it can be used on iOS.Core (#2983) 2024-02-07 17:50:07 +00:00
Federico Maccaroni
08fac4752f PM-3349 PM-3350 Fix crash on iOS AppGroup container URL because of sln config on AndroidX Credentials. Changed the project reference of Credentials to be a local NuGet. 2024-02-07 13:59:31 -03:00
Federico Maccaroni
9307e7e0d8 Merge branch 'feature/maui-migration' into feature/maui-migration-passkeys 2024-02-05 14:31:44 -03:00
Federico Maccaroni
b1a0801f9b PM-3349 Removed commented code from build.yml regarding FDroid that is not needed anymore. 2024-02-05 13:14:37 -03:00
Federico Maccaroni
04cc53b934 Merge branch 'main' into feature/maui-migration 2024-02-05 11:50:41 -03:00
Federico Maccaroni
c138658a31 [PM-5896] Fix MAUI iOS Background crash due to lock files on suspension (#2969)
* PM-5896 Fix background crash on iOS due to lock files when app gets suspended. Changed loading and error placeholders of the CachedImage to not be used and use default icon of IconLabel instead changing visibility.

* PM-5896 Changed methods to be protected so that they don't get removed by the linker.

* PM-5896 Added stub class and references to it so to have stronger references to Icon_Success and Icon_Error so the linker doesn't remove them.
2024-02-05 11:46:08 -03:00
Dinis Vieira
f1854f2c04 PM-5903 Changed App.xaml.cs SetOption to only update the needed properties instead of replacing the existing Options object which would cause the AccountSwitcher button bug (#2973) 2024-02-03 15:59:22 +00:00
Federico Maccaroni
e4056d9ee6 Merge branch 'feature/maui-migration' into feature/maui-migration-passkeys 2024-02-02 15:51:50 -03:00
Federico Maccaroni
eb95a54db2 PM-3350 Fix iossimulator-x64 argon2id load so we can test on simulators and also made easier to maintain loading the argon2id library on the iOS projects by setting a general Directory.Build.props that is shared. 2024-02-02 15:48:04 -03:00
Dinis Vieira
7ddea4c70b PM-5902 fix for account switcher not dismissing when tapping outside (#2974) 2024-02-01 21:44:13 +00:00
Federico Maccaroni
3804e86995 Merge branch 'feature/maui-migration' into feature/maui-migration-passkeys 2024-02-01 12:35:50 -03:00
Federico Maccaroni
b23bed182f Merge branch 'main' into feature/maui-migration 2024-01-31 12:14:02 -03:00
Federico Maccaroni
f8e421871b Merge branch 'feature/maui-migration' of https://github.com/bitwarden/mobile into feature/maui-migration 2024-01-31 12:13:31 -03:00
mpbw2
d0103496b9 [PM-5910] Workaround for for sliding elements in Duo 2FA flow (#2967)
* workaround for sliding elements in duo 2fa flow

* restrict workaround to Android

* restrict workaround to Android

* Revert "restrict workaround to Android"

This reverts commit c2753d5dc4.

* Revert "restrict workaround to Android"

This reverts commit 69688cfb98.
2024-01-29 17:26:14 -05:00
Federico Maccaroni
cd8952221e Merge branch 'main' into feature/maui-migration 2024-01-29 17:17:07 -03:00
Federico Maccaroni
155c7539bd Removing error/loading placeholders of icons on the cells to see if that is causing the background crash on iOS; so we can test this in TestFlight 2024-01-29 17:16:01 -03:00
Federico Maccaroni
5f43681fb1 PM-3350 Go back to include argon2id and interpreter 2024-01-29 14:30:42 -03:00
Federico Maccaroni
d2965e6e10 PM-3350 Enabled iOS extensions and WatchOS app to be included based on the Directory.Build.props 2024-01-29 14:01:18 -03:00
Federico Maccaroni
ec1ade7761 Merge branch 'main' into feature/maui-migration
# Conflicts:
#	src/App/Platforms/Android/AndroidManifest.xml
2024-01-29 13:58:30 -03:00
Federico Maccaroni
f35bef0d7b PM-3350 Go back to use Interpreter and added some Directory.Build.props to easily change Codesign properties and also include/exclude iOS extensions / WatchOS from the build. 2024-01-29 13:56:31 -03:00
Federico Maccaroni
138d37cf5e PM-5928 Fix circle animation to be shown on verification codes list on each item 2024-01-26 12:27:57 -03:00
Federico Maccaroni
fc2fed079f PM-3350 Try to disable Interpreter to have better crash knowledge. This time testing if avoiding loading the argon2id lib we're able to not use the interpreter. 2024-01-26 10:34:28 -03:00
Federico Maccaroni
9c441a98f4 Merge branch 'main' into feature/maui-migration 2024-01-26 10:17:21 -03:00
Federico Maccaroni
1491872b62 PM-3350 Added check for state migration version before trying to migrate LiteDB values into Prefs when there's no need to and that may be inducing crashes on backgrounded iOS apps. 2024-01-25 17:56:28 -03:00
Federico Maccaroni
c74636ffa5 PM-3350 Commented event collection upload on the timer and when sending the app to background to see if that prevents the app from crashing on release mode. 2024-01-25 13:22:42 -03:00
Federico Maccaroni
05677f93c5 Fix merge from main 2024-01-25 13:20:13 -03:00
Federico Maccaroni
0aef241df6 Merge branch 'main' into feature/maui-migration
# Conflicts:
#	src/Core/Pages/Settings/AboutSettingsPageViewModel.cs
2024-01-25 13:16:42 -03:00
Federico Maccaroni
e0b58461b5 Merge branch 'main' into feature/maui-migration 2024-01-24 11:22:28 -03:00
Federico Maccaroni
cd33c7f608 PM-3350 Added new style to prevent spell check and text prediction 2024-01-24 11:22:02 -03:00
Federico Maccaroni
9d29af36e5 Merge branch 'feature/maui-migration' into feature/maui-migration-passkeys 2024-01-23 18:00:53 -03:00
Federico Maccaroni
4472d7f9a8 PM-3349 PM-3350 Fix external link icon 2024-01-23 17:59:32 -03:00
Federico Maccaroni
999579915c Merge branch 'main' into feature/maui-migration 2024-01-23 17:34:55 -03:00
Federico Maccaroni
63904fd303 PM-3350 Fix Avatar toolbar icon on extensions to load properly and to take advantage of using directly SkiaSharp to do the native conversion to UIImage. Also improved the toolbar item so that size is set appropriately. 2024-01-23 17:34:27 -03:00
Federico Maccaroni
2cb6872e4e Merge branch 'feature/maui-migration' into feature/maui-migration-passkeys 2024-01-22 16:57:59 -03:00
Federico Maccaroni
f539bf051d Merge branch 'main' into feature/maui-migration 2024-01-22 16:57:45 -03:00
Federico Maccaroni
14f845d623 Merge branch 'feature/maui-migration' into feature/maui-migration-passkeys
# Conflicts:
#	src/App/Platforms/iOS/AppDelegate.cs
2024-01-22 16:57:07 -03:00
Federico Maccaroni
133a80acef PM-3350 Fixed CancellationTokenSource proper disposal 2024-01-22 12:30:29 -03:00
Federico Maccaroni
b43790de9a Merge branch 'main' into feature/maui-migration 2024-01-22 10:00:30 -03:00
Federico Maccaroni
0bdd63df06 Merge branch 'main' into feature/maui-migration 2024-01-19 15:14:50 -03:00
Federico Maccaroni
c6544b49e9 PM-3350 Removed TapGesture Window MAUI hack from iOS.Extension and iOS.ShareExtension 2024-01-19 15:14:22 -03:00
Federico Maccaroni
8e1a8b5f0e PM-3349 PM-3350 Updated XCode version on build.yml to 15.1 2024-01-19 15:02:53 -03:00
Federico Maccaroni
4717f5e230 PM-3349 PM-3350 Improved code safety with try...catch, better invoke on main thread and better null handling. 2024-01-19 15:01:31 -03:00
Federico Maccaroni
01ee1ff845 PM-3350 Enabled back UseInterpreter on iOS Release given that it crashes on startup without it. 2024-01-18 18:54:34 -03:00
Federico Maccaroni
75b4655f38 PM-3350 Testing UseInterpreter false on CI build 2024-01-18 16:32:00 -03:00
mpbw2
9b2f596d15 Move Android camera/scan changes to xaml 2024-01-18 11:31:16 -05:00
mpbw2
55fb71744d Improve TOTP scan performance on Android 2024-01-17 13:03:11 -05:00
Federico Maccaroni
ee252be634 Merge branch 'feature/maui-migration' into feature/maui-migration-passkeys 2024-01-17 13:22:47 -03:00
Federico Maccaroni
66f0471f2e Merge branch 'main' into feature/maui-migration 2024-01-17 13:22:26 -03:00
Federico Maccaroni
6b9eeba88d PM-3350 PM-3349 Added property to Directory.Build.props to enable Unit Testing globally so Test runners work 2024-01-17 13:22:00 -03:00
Federico Maccaroni
0a1fbfafb5 PM-3349 Fix navigation when coming from autofill with Accessibility Services enabled. The user was getting into Home page instead of where they were, with this workaround the app navigates as if the account has been switched, leaving the user as closely as possible to where they were, basically on the first screen for the current state of the user. 2024-01-17 09:34:31 -03:00
Federico Maccaroni
0a5d772886 PM-3350 Fix build.yml format 2024-01-16 11:32:27 -03:00
Federico Maccaroni
70c8a264d2 PM-3350 PM-3349 Enable running Core tests 2024-01-15 17:35:37 -03:00
Federico Maccaroni
b5fbb2cade Merge branch 'feature/maui-migration' into feature/maui-migration-passkeys
# Conflicts:
#	bitwarden-mobile.sln
2024-01-15 17:24:33 -03:00
Federico Maccaroni
9027755b71 PM-3350 PM-3349 Updated Readme with MAUI and main branch 2024-01-15 17:20:46 -03:00
Federico Maccaroni
6d625f285b PM-3350 PM-3349 Updated Unit Test projects to NET 8.0 and fixed it to work with Core project reference. Also fixed a test that was breaking due to CIpherKey creation being wrong. Added "UT" as a constant to add when building/running Core.Test project so we have something on the context that tells us that is for a UT. With this I had to remove FFImageLoading on UT context because it doesn't support NET 8.0 2024-01-15 17:18:51 -03:00
Federico Maccaroni
822ad7564e Merge branch 'feature/maui-migration' into feature/maui-migration-passkeys
# Conflicts:
#	src/iOS.Autofill/iOS.Autofill.csproj
2024-01-12 13:59:59 -03:00
Federico Maccaroni
1949a450fd PM-3350 Improved code safety adding a lot of try...catch and logging throughout the app. Also made the invoking on main thread safer on several places of the app. Additionally, on the GroupingsPageViewModel changed the code removing the old Xamarin hack and just using Replace directly instead of Clearing first to see if that fixes the crash we're having sometimes on the app. 2024-01-12 13:54:34 -03:00
Federico Maccaroni
27fa79e0bd Merge branch 'main' into feature/maui-migration 2024-01-12 09:16:10 -03:00
Federico Maccaroni
1e29eacc61 PM-3350 Removed "iOS" old folder project that has been moved into the MAUI Single app project. 2024-01-11 17:51:34 -03:00
Federico Maccaroni
b81d26d589 Bump main iOS version 2024-01-11 14:18:34 -03:00
Federico Maccaroni
cd107b6161 Merge branch 'main' into feature/maui-migration
# Conflicts:
#	src/App/Platforms/Android/AndroidManifest.xml
#	src/iOS.Core/Renderers/CollectionView/ExtendedGroupableItemsViewController.cs
2024-01-11 14:17:34 -03:00
Federico Maccaroni
7ac3646fb0 PM-3350 Added nuget.config so we add the nuget package source for MAUI Nightly builds 2024-01-11 10:31:36 -03:00
Federico Maccaroni
d1e4e8645a PM-3350 Updated MauiVersion to 8.0.4-nightly.* to have the TapGestureRecognizer fix applied. This is done on the Directory.Build.props so we don't have to change it on every csproj. Also removed the workaround of TapGestureHack and fix the Show environment picker to work on the extensions as well. 2024-01-10 19:53:53 -03:00
Federico Maccaroni
36a648e53e Merge branch 'feature/maui-migration' into feature/maui-migration-passkeys
# Conflicts:
#	src/Core/Core.csproj
2024-01-09 18:10:56 -03:00
Federico Maccaroni
6c04ac67b1 PM-3349 Fix Picker selection style by doing a custom PickerHandler for Android which uses SetSingleChoiceItems(...) to provide with the appropriate UI 2024-01-09 15:08:45 -03:00
Federico Maccaroni
dfb7a0621f PM-5154 Added Fido2AuthenticationService to provide us a wrapper for the actual implementation 2024-01-03 19:02:49 -03:00
mpbw2
1eb9e5f8ea add cred manager project to build config 2024-01-03 16:36:56 -05:00
mpbw2
b149e7549c initial commit of android credential provider service (wip) 2024-01-03 16:30:44 -05:00
Federico Maccaroni
e3877cc589 PM-5154 Continue Passkeys Autofill in iOS 2024-01-03 18:22:03 -03:00
Federico Maccaroni
275ae76761 PM-5154 Start implementing Passkeys Autofill in iOS 2024-01-03 18:21:02 -03:00
Federico Maccaroni
a1e4f0aaa2 PM-3350 Updated version to 2024.1.0 on iOS 2024-01-02 13:21:32 -03:00
Federico Maccaroni
adaef0d15b Merge branch 'main' into feature/maui-migration
# Conflicts:
#	src/App/Platforms/Android/AndroidManifest.xml
2024-01-02 13:20:15 -03:00
Dinis Vieira
fa4a2247e3 PM-3349 Changed the "track color" for the Android switch so that the color is different from the "thumb" 2023-12-23 23:09:11 +00:00
Dinis Vieira
5d2fc4530f PM-3349 Minor aligment improvemment for region selection in HomePage 2023-12-23 15:25:41 +00:00
Dinis Vieira
9b64af3423 PM-3349 Fix for icon and text spacing in some list items 2023-12-23 14:30:24 +00:00
Dinis Vieira
b6ff6e34f6 PM-3349 Added custom window so that we can always get the current Active Window. This is used to support the Android Autofil and multi-window scenarios. 2023-12-23 12:30:21 +00:00
Dinis Vieira
6d4c706026 Merge branch 'feature/maui-migration-android-autofill' into feature/maui-migration-android-autofill-redirect-page 2023-12-22 22:16:17 +00:00
Dinis Vieira
14fd026ea0 Merge branch 'feature/maui-migration' into feature/maui-migration-android-autofill 2023-12-22 22:15:58 +00:00
Dinis Vieira
a4392a8730 PM-3349 PM-3350 Changed search icon used in app to avoid issue with icon size on iOS 2023-12-22 22:15:01 +00:00
Dinis Vieira
1b885ea438 PM-3350 Fix for Delete Account buttons on iOS 2023-12-22 17:07:43 +00:00
mpbw2
fa022a1a4f Revert "initial commit of android credential provider service (wip)"
This reverts commit 6011b63958.
2023-12-20 18:23:08 -05:00
mpbw2
6011b63958 initial commit of android credential provider service (wip) 2023-12-20 18:04:21 -05:00
Dinis Vieira
7d79b98bf2 PM-3349 Minor ui fix (space between buttons in delete account page) 2023-12-20 19:04:26 +00:00
Dinis Vieira
d4e75e9de8 PM-3349 Added Window events unsubscription of events. Also changed code to avoid potentially having multiple autofillwindow 2023-12-18 22:56:21 +00:00
Dinis Vieira
c3370b58ec Added some comments and improvemments. 2023-12-18 00:24:38 +00:00
Dinis Vieira
3de13325c9 Merge branch 'feature/maui-migration-android-autofill' into feature/maui-migration-android-autofill-redirect-page 2023-12-17 22:26:46 +00:00
Dinis Vieira
c253c110c1 Merge branch 'feature/maui-migration' into feature/maui-migration-android-autofill 2023-12-17 22:26:33 +00:00
Dinis Vieira
bf35d1f2dc PM-3349 minor fix to add space in HomePage between the region picker labels. 2023-12-17 22:26:15 +00:00
Dinis Vieira
05b6aa90b6 Improved autofill workaround to better handle switching between windows. 2023-12-17 22:05:37 +00:00
Dinis Vieira
e39898bba6 Reusing App.xaml.cs Navigation for the Android RedirectPage
Some other cleanup and changes
2023-12-16 15:18:07 +00:00
Dinis Vieira
da0866cc85 Merge branch 'feature/maui-migration-android-autofill' into feature/maui-migration-android-autofill-redirect-page 2023-12-15 23:51:08 +00:00
Dinis Vieira
b3140381ab Merge branch 'feature/maui-migration' into feature/maui-migration-android-autofill
# Conflicts:
#	src/Core/App.xaml.cs
2023-12-15 23:50:48 +00:00
Dinis Vieira
c01a8f8d93 PM-3349 Ensure "_isResumed=true" is set on App.xaml.cs:Bootstrap 2023-12-15 23:48:11 +00:00
Federico Maccaroni
8484b4af30 PM-3349 Uncommented the Deploy step for Android job 2023-12-15 16:38:40 -03:00
Federico Maccaroni
6b9faed45f PM-3349 Commented the Deploy step for Android job given that we're using the hotfix-rc branch for testing iOS on TestFlight 2023-12-15 12:08:01 -03:00
Federico Maccaroni
770a1c5dfe Merge branch 'main' into feature/maui-migration 2023-12-15 11:53:57 -03:00
Dinis Vieira
3c87d4db1c minor improvemments on autofill-redirect 2023-12-15 13:48:34 +00:00
Dinis Vieira
8b3c6ab35f minor change (public to private fields) 2023-12-14 22:43:50 +00:00
Dinis Vieira
90912977c4 Merge branch 'feature/maui-migration-android-autofill' into feature/maui-migration-android-autofill-redirect-page 2023-12-14 22:02:39 +00:00
Dinis Vieira
740b368b8c Merge branch 'feature/maui-migration' into feature/maui-migration-android-autofill 2023-12-14 22:00:06 +00:00
Federico Maccaroni
3a40a4cda8 PM-3349 Upgraded Android targetSdkVersion to 34 2023-12-14 17:52:11 -03:00
Federico Maccaroni
9bcd2e51f7 Merge branch 'feature/maui-migration' of https://github.com/bitwarden/mobile into feature/maui-migration 2023-12-14 17:32:48 -03:00
Federico Maccaroni
741214a1cc PM-3349 PM-3350 Enabled argon2id and fixed one issue with the Uris when getting the icon image 2023-12-14 17:32:43 -03:00
Federico Maccaroni
aad87dfdce Update .github/workflows/build.yml setup-xcode commit hash
Co-authored-by: Vince Grassia <593223+vgrassia@users.noreply.github.com>
2023-12-14 17:28:10 -03:00
Federico Maccaroni
8fc1e9a3b9 PM-3350 Updated Plugin.Fingerprint so biometrics work 2023-12-14 16:32:04 -03:00
Federico Maccaroni
2a8e15146e PM-3350 Apply Cryptography TrimmerRootAssembly only to iOS 2023-12-14 14:28:20 -03:00
Federico Maccaroni
8559d5908e Merge branch 'feature/maui-migration' of https://github.com/bitwarden/mobile into feature/maui-migration 2023-12-14 13:16:41 -03:00
Federico Maccaroni
f60c4d94fe PM-3350 Fix crash on Release by adding Interpreter on iOS and also adding System.Security.Cryptography to be ignored by the linker 2023-12-14 13:16:32 -03:00
Dinis Vieira
05858bea48 Proof of concept for having multiple window in Android for autofill support and navigating with the help of an Extended splash page. 2023-12-13 23:42:58 +00:00
Dinis Vieira
5cbef47fd4 Merge branch 'feature/maui-migration' into feature/maui-migration-android-autofill 2023-12-13 22:05:52 +00:00
Dinis Vieira
4bf695d18c PM-3349 Fix for app crashing on Android when Dark mode is enabled
Removed unused button style for android
2023-12-13 19:20:43 +00:00
Dinis Vieira
c24e0dfa28 Merge branch 'feature/maui-migration' into feature/maui-migration-android-autofill 2023-12-13 17:30:27 +00:00
Federico Maccaroni
9ccd0834ff Merge branch 'main' into feature/maui-migration 2023-12-13 10:24:29 -03:00
Federico Maccaroni
a806f17d3b PM-3350 Changed linker to use default mode given that "Full" is presenting some problems as the linker is stripping things it shouldn't and we're trying to solve it. So for now we will use the mode "Link SDK assemblies only" so QA can test. 2023-12-12 21:10:51 -03:00
Federico Maccaroni
436a162df2 Merge branch 'main' into feature/maui-migration 2023-12-12 20:19:42 -03:00
Dinis Vieira
f2c298607e Merge branch 'feature/maui-migration' into feature/maui-migration-android-autofill 2023-12-11 22:19:49 +00:00
Federico Maccaroni
b5dbb9ae5e PM-3350 Bumped iOS version 2023-12-11 13:21:31 -03:00
Federico Maccaroni
7a5f7c0274 Merge branch 'master' into feature/maui-migration 2023-12-11 11:42:46 -03:00
Dinis Vieira
17acb57732 PM-3349 Added base structure for avoiding Android Autofill crash. This workaround works but it's not complete as it can't handle the entire workflow when showing CipherSelectionPAge (like checking if it should show LockPage) 2023-12-10 15:05:08 +00:00
Federico Maccaroni
5803635f44 PM-3349 PM-3350 Refactored cipher bindings to have a simpler approach reusing a new CipherItemViewModel to avoid unwanted issues in the app 2023-12-07 23:59:06 -03:00
Federico Maccaroni
19c393842f PM-3350 Cleared left ClipLogger from the iOS extensions debug logging. 2023-12-07 18:14:02 -03:00
Federico Maccaroni
15a306490d Merge branch 'feature/maui-migration' into feature/maui-migration-ios-ext-tap-workaround 2023-12-07 17:41:53 -03:00
Federico Maccaroni
a4a3d31c19 PM-3350 Fix iOS extensions navigation and Window/RootViewController handling for TapGestureRecognizer to work 2023-12-07 17:40:20 -03:00
Dinis Vieira
922dc683af PM-3349 Replaced CrossCurrentActivity plugin with MAUI internal CurrentActivity 2023-12-06 17:27:17 +00:00
Dinis Vieira
bae1b3e891 PM-3349 PM-3350 Minor change: removed unneeded HorizontalTextAlignment from Label. 2023-12-04 16:54:55 +00:00
Federico Maccaroni
a5888827c9 Merge branch 'feature/maui-migration' into feature/maui-migration-ios-ext-tap-workaround 2023-12-04 12:18:01 -03:00
Federico Maccaroni
0348940a12 Merge branch 'master' into feature/maui-migration 2023-12-04 12:17:48 -03:00
Federico Maccaroni
4c2998337d Merge branch 'feature/maui-migration' into feature/maui-migration-ios-ext-tap-workaround 2023-12-04 11:44:03 -03:00
Federico Maccaroni
7ea86380f4 PM-3349 PM-3350 build.yml uncommented jobs so we have a more complete workflow 2023-12-04 11:41:55 -03:00
Federico Maccaroni
406f4425c8 PM-3349 PM-3350 Added MAUI label on self-host settings and on about settings to differentiate from XF app 2023-12-04 11:35:35 -03:00
Federico Maccaroni
95ca911444 PM-3350 Improved MTouch linking and extra args on iOS related csprojs 2023-12-04 11:30:54 -03:00
Federico Maccaroni
fa62510e09 PM-3350 build.yml Added several fixes like not using MtouchUseLLVM on the iOS builds to fix they taking forever to build and some changes on the automation CI to do a debug build for the moment 2023-12-04 10:41:41 -03:00
Federico Maccaroni
65dc73495d PM-3350 build.yml added step to validate app for submitting into App Store and have better logs of it 2023-12-04 10:11:29 -03:00
Federico Maccaroni
02a2e41118 PM-3350 fix build.yml Bitwarden.ipa AppStore exported file 2023-12-04 10:10:18 -03:00
Federico Maccaroni
bd6f8295e7 PM-3350 iOS updated CFBundlerShortVersionString to latest one 2023.10.1 2023-12-04 10:01:56 -03:00
Federico Maccaroni
0a0cb7093b Merge branch 'master' into feature/maui-migration
# Conflicts:
#	.github/workflows/build.yml
2023-12-04 09:59:52 -03:00
Dinis Vieira
465e5eff76 PM-3349 PM-3350 Fix for text getting cut/truncated in both account switcher and ciphers/search lists
Issue is due to MAUI but can be avoided by using slightly different layout
2023-12-03 22:17:16 +00:00
Dinis Vieira
5b756aaf7a PM-3349 PM-3350 Added the ability for users to press "Continue" button as a fallback when using the Yubikey if the "SubmitCommand" doesn't trigger automatically. 2023-12-01 16:49:14 +00:00
Dinis Vieira
d168a7b750 PM-3349 PM-3350 Added workaround for More Options to work on Search and Groupings Page
Updated some code to MAUI Style also
2023-12-01 15:02:12 +00:00
Federico Maccaroni
7f4bbafe3c PM-3350 iOS applied workaround on the iOS Autofill and Share extension to maui embed the navigation page with its content page in the Window 2023-11-30 18:23:54 -03:00
Federico Maccaroni
a5804df6a3 PM-3350 iOS extensions TapGestureRecognizer try Window workaround 2023-11-29 18:42:39 -03:00
Federico Maccaroni
bfa2a51608 PM-3349 build.yml configured FDROID job for MAUI 2023-11-27 17:53:01 -03:00
Federico Maccaroni
32be08daae PM-3349 build.yml enabled android build workflow 2023-11-27 09:24:36 -03:00
Dinis Vieira
0a628cc8a8 PM-3349 PM-3350 Workaround to fix issues with text getting cropped/truncated when a Label has both Multiline and LinebreakMode set 2023-11-26 14:46:30 +00:00
Federico Maccaroni
80c424ed03 Update build.yml disabling iOS job to avoid long running process of publish until we can fix that 2023-11-25 10:17:48 -03:00
Federico Maccaroni
99fb5463cf PM-3350 iOS projs disable linking and set Newstandkit as weak framework 2023-11-24 17:47:12 -03:00
Federico Maccaroni
c5d941e1df PM-3350 added linkskip for iOS csprojs 2023-11-24 15:13:45 -03:00
Federico Maccaroni
3edfef6169 PM-3350 build.yml disable trimming on publish so it's faster 2023-11-24 13:59:09 -03:00
Federico Maccaroni
1c8742511a PM-3350 Added Document.Build.props to disable trimming on publish 2023-11-24 12:50:17 -03:00
Federico Maccaroni
8e424d6c05 PM-3350 build.yml changed image back to be macos-13 to see if the build is faster 2023-11-24 11:17:45 -03:00
Federico Maccaroni
390c303b90 PM-3350 build.yml try to fix ILLINK warnings and changed image to be macos-13-arm64 to see if the build is faster 2023-11-24 10:50:36 -03:00
Federico Maccaroni
443f7282b8 PM-3350 build.yml Upgraded iOS build to run on macos-13 image which has XCode 15, and set the XCode 15 version as currently the default one is 14.x 2023-11-23 18:05:58 -03:00
Federico Maccaroni
50109ee70b PM-3350 build.yml changed nuget restore for dotnet restore on iOS build to fix issue on restoring due to msbuild 2023-11-23 17:38:23 -03:00
Federico Maccaroni
9ffdfd51cc PM-3350 build.yml updated iOS build and ignore Android build to try the CI faster 2023-11-23 17:20:17 -03:00
Federico Maccaroni
04e409f3c6 PM-3349 build.yml add Android "prod" variant 2023-11-23 16:32:10 -03:00
Federico Maccaroni
6c143bad57 PM-3349 build.yml updated env helpers variables and set specific csproj to build on Android so not to build iOS extensions 2023-11-23 15:39:16 -03:00
Federico Maccaroni
286e18059a PM-3349 PM-3350 build.cake updated paths 2023-11-23 14:08:14 -03:00
Federico Maccaroni
553bf9ed0a PM-3349 build.yml commented verify format and just set qa as variant on MAUI Android for faster checks on CI 2023-11-23 13:08:18 -03:00
Federico Maccaroni
ddb27b52d3 PM-3349 build.yml update paths for MAUI Android 2023-11-23 12:49:29 -03:00
Federico Maccaroni
6c504aa710 PM-3349 Started to configure build.yml for MAUI Android 2023-11-23 12:18:31 -03:00
Dinis Vieira
62254aef8d PM-3349 PM-3350 Fixed issue where creating an account with weak/exposed password would get stuck after the Captcha (if a captcha is shown)
Changed App.xaml.cs NavigateImpl to be private
2023-11-21 22:39:44 +00:00
Dinis Vieira
06a0195a6d PM-3349 Added workaround for Android to avoid issues with setting MainPage when app is in background. They are now kept on a Queue to be executed after the app has resumed.
Updated some things on App.xaml.cs to the new MAUI style
2023-11-21 21:26:14 +00:00
Dinis Vieira
df2b0b21d5 PM-3350 Added workaround for iOS Avatar icon again. 2023-11-20 22:07:40 +00:00
Federico Maccaroni
e6b1bab860 PM-3350 Updated AppCenter package to latest version 5.0.3 and updated some things into MAUI style 2023-11-20 17:29:16 -03:00
Federico Maccaroni
ce41eb0578 PM-3350 Fixed account toolbar item and TitleView on SendAddOnlyPage, also removed comments on AvatarImageSource given the workaround is not needed anymore to draw the image successfully. 2023-11-20 16:09:23 -03:00
Federico Maccaroni
1a0b52d644 PM-3350 Fixed/Updated all MAUI-Migration TODOs 2023-11-20 13:10:03 -03:00
Federico Maccaroni
16ada4993c Merge branch 'feature/maui-migration' of https://github.com/bitwarden/mobile into feature/maui-migration
# Conflicts:
#	src/Core/Pages/Settings/OtherSettingsPageViewModel.cs
2023-11-20 10:49:42 -03:00
Federico Maccaroni
3795f3aa17 PM-3350 Added watchOS app to main project and fixed some csproj conditions for runtime identifiers on iOS. 2023-11-20 10:44:56 -03:00
Dinis Vieira
eceb506c77 Merge branch 'master' into feature/maui-migration
Fixed conflicts

# Conflicts:
#	src/App/Resources/AppResources.cs.Designer.cs
#	src/App/Resources/AppResources.da.Designer.cs
#	src/App/Resources/AppResources.de.Designer.cs
#	src/App/Resources/AppResources.es.Designer.cs
#	src/App/Resources/AppResources.fi.Designer.cs
#	src/App/Resources/AppResources.fr.Designer.cs
#	src/App/Resources/AppResources.hi.Designer.cs
#	src/App/Resources/AppResources.hr.Designer.cs
#	src/App/Resources/AppResources.hu.Designer.cs
#	src/App/Resources/AppResources.id.Designer.cs
#	src/App/Resources/AppResources.it.Designer.cs
#	src/App/Resources/AppResources.ja.Designer.cs
#	src/App/Resources/AppResources.nl.Designer.cs
#	src/App/Resources/AppResources.pl.Designer.cs
#	src/App/Resources/AppResources.pt-BR.Designer.cs
#	src/App/Resources/AppResources.pt-PT.Designer.cs
#	src/App/Resources/AppResources.ro.Designer.cs
#	src/App/Resources/AppResources.ru.Designer.cs
#	src/App/Resources/AppResources.sk.Designer.cs
#	src/App/Resources/AppResources.sv.Designer.cs
#	src/App/Resources/AppResources.th.Designer.cs
#	src/App/Resources/AppResources.tr.Designer.cs
#	src/App/Resources/AppResources.uk.Designer.cs
#	src/App/Resources/AppResources.vi.Designer.cs
#	src/App/Resources/AppResources.zh-Hans.Designer.cs
#	src/App/Resources/AppResources.zh-Hant.Designer.cs
#	src/Core/Controls/Settings/BaseSettingControlView.cs
#	src/Core/Pages/Accounts/EnvironmentPageViewModel.cs
#	src/Core/Pages/Accounts/HomePage.xaml.cs
#	src/Core/Pages/Accounts/HomePageViewModel.cs
#	src/Core/Pages/Accounts/SetPasswordPageViewModel.cs
#	src/Core/Pages/Settings/SecuritySettingsPageViewModel.cs
#	src/Core/Pages/TabsPage.cs
#	src/Core/Services/StateMigrationService.cs
#	src/Core/Utilities/BoolToColorConverter.cs
2023-11-19 15:06:02 +00:00
Dinis Vieira
2c7870d660 PM-3349 PM-3350 Removed AsyncCommand "wrapper" and added AsyncRelayCommand directly in all ViewModels that were using the other one. 2023-11-16 22:31:01 +00:00
Dinis Vieira
f02b3415a3 PM-3350 Removed workaround for iOS issue with Avatar icon as it's now fixed in latest .Net8 release. 2023-11-14 23:24:10 +00:00
Federico Maccaroni
beda4e9ff8 PM-3350 Updated PCL Crypto to latest alpha version to fix "Dll not found NCrypt" issue 2023-11-14 18:43:23 -03:00
Federico Maccaroni
df4d89cd52 PM-3350 Fixed iOS Extensions navigation to several pages and improved avoiding duplicate calls to OnNavigatedTo 2023-11-14 13:43:59 -03:00
Federico Maccaroni
5f12bb9747 PM-3350 Fixed iOS Share extension lazy views loading and an issue with the avatar loading. Also discovered issue with TapGestureRecognizer not working on MAUI Embedding 2023-11-13 21:30:56 -03:00
Federico Maccaroni
5712639492 Merge branch 'feature/maui-migration' of https://github.com/bitwarden/mobile into feature/maui-migration 2023-11-13 14:44:24 -03:00
Federico Maccaroni
e0a3c301fb PM-3349 PM-3350 Changed SendViewCell and its binding to be directly against the ViewModel 2023-11-13 14:44:19 -03:00
Dinis Vieira
27306fe353 PM-3349 PM-3350 Added the partial MAUI Community Toolkit implementation for TouchEffect. This is a temporary solution until they finalize this and add it to their nuget package.
This allows implementing the LongPressCommand in AccountSwitchingOverlay and also have the "Ripple effect" animation when touching an item in Android
2023-11-13 09:59:54 +00:00
Dinis Vieira
a31f15559f PM-3350 Fix for colored html text on iOS 2023-11-11 15:05:51 +00:00
Dinis Vieira
0e75f3f5c8 PM-3349 Fix for HTML Label on Android. Color hex doesn't need to be cropped anymore. 2023-11-10 23:30:54 +00:00
Federico Maccaroni
363da063fa PM-3349 PM-3350 Changed AccountViewCell and its binding to be directly against the ViewModel 2023-11-10 17:30:08 -03:00
Federico Maccaroni
974a571455 PM-3349 PM-3350 Changed binding set for CipherViewCell so it updates accordingly 2023-11-10 14:41:41 -03:00
Dinis Vieira
e0c721098c PM-3349 PM-3350 Migrated remaining AutomationProperties to SemanticProperties.
All 'IsInAccessibleTree="True"' were deleted.
'IsInAccessibleTree="False"' were kept and stayed in code.
2023-11-09 23:01:04 +00:00
Federico Maccaroni
a86f6e3034 PM-3350 Added configurations for Release mode (no FDroid yet) 2023-11-09 17:15:02 -03:00
Federico Maccaroni
fe17288b99 Merge branch 'feature/maui-migration' of https://github.com/bitwarden/mobile into feature/maui-migration 2023-11-09 16:16:26 -03:00
Federico Maccaroni
7324da9d47 PM-3350 Fixed iOS extensions (iOS.Extension and iOS.ShareExtension) to load and commented argon2id from debug configuration until we have the .a compiled again with the new platform/arch 2023-11-09 16:16:17 -03:00
Dinis Vieira
69aa6fc044 PM-3349 PM-3350 Started using OnNavigatedTo/From instead of On(Dis)Appearing for LoginPage and LoginSSOPage to avoid the "Modal loading" issues in iOS
Also had to add IsInitialized logic to these pages because OnNavigatedTo can be called twice in some scenario.
Some minor migrations of Device to DeviceInfo was also done
2023-11-09 15:09:37 +00:00
Dinis Vieira
e840dc2e30 Merge branch 'master' into feature/maui-migration
Fixed conflicts and added null check on ForwardEmailDomainName

# Conflicts:
#	src/Core/Pages/Vault/CipherAddEditPage.xaml
#	src/Core/Pages/Vault/CipherDetailsPage.xaml
#	src/iOS.Core/Renderers/CollectionView/ExtendedGroupableItemsViewController.cs
2023-11-05 23:59:30 +00:00
Dinis Vieira
eb25ee5d1b PM-3349 Fix for Android buttons having all letters in Caps 2023-11-04 18:39:08 +00:00
Dinis Vieira
840f24dbe5 PM-3349 Fix for TabGestureRecognizer not working inside the StackLayout area of IconLabelButton 2023-11-04 18:08:44 +00:00
Dinis Vieira
c6309173ba PM-3349 PM-3350 Migrated IconLabelButton Frames to Borders to fix issue with TapGestureRecognizer in Android
Also fixed some minor "styles" for normal Button and IconLabelButton (both Android and iOS)
2023-11-04 17:48:59 +00:00
Dinis Vieira
946c465f0c PM-3349 Added Handler that enables the ExtendedDatePicker to get IsFocused events in Android. This is a workaround for fixing the current bug where it's not possible to select the "current day" in the expiration date of a Send.
Fix for TimePicker not displaying default Time Value
Updated some "Device" code to the new MAUI "DeviceInfo"
2023-11-02 02:32:12 +00:00
Dinis Vieira
e90409d842 PM-3350 Added Scrollview on HomePage so that the "Create account" button can be accessed in smaller devices like iPhone SE. 2023-10-29 21:34:33 +00:00
Dinis Vieira
484b5a5160 PM-3349 PM-3350 Minor change to HomePage to set fixed Image height otherwise it takes more space than it did in the old Xamarin Forms app.
Added HIdeSoftInputOnTapped on several pages (the ones with Entry controls) to allow hiding the keyboard when tapping "outside" of it. (just like we did in Xamarin Forms app)
2023-10-28 19:02:54 +01:00
Dinis Vieira
2688209752 PM-3350 Removed duplicate reference to LaunchScreen.storyboard 2023-10-27 17:19:20 +01:00
Dinis Vieira
53e0e55915 PM-3350 Quick workaround to allow 2nd factor auth to not get stuck in iOS in modals.
Updated some older "Device" code to the newer MAUI code.
2023-10-27 15:36:47 +01:00
Dinis Vieira
ca57948d9f PM-3350 Removed MAUI Splash Screen. Fixed iOS Privacy Screen logo (hardcoded image to avoid it getting cropped) 2023-10-27 11:41:21 +01:00
Dinis Vieira
aaf082faba PM-3349 Fixed white tint color not appearing on images added as MAU IImage SVG
PM-3349 PM-3350 Fix for Avatar text not adjusting to white/black color correctly
2023-10-25 22:20:01 +01:00
Dinis Vieira
e7aeb08cae PM-3350 Checked some [MAUI-Migration] and changed as needed.
TimePickerHandlerMappings: Remove old code for forcing the Wheel. After testing without it wheel picker is still used so this code shouldn't be needed anymore.
AppDelegate.ContinueUserActivity: Uncommented and changed the iOS ContinueUserActivity. It needs to call Platform.ContinueUserActivity according with Xamarin Essentials migration docs.
2023-10-24 23:37:18 +01:00
Dinis Vieira
f177968958 Checked some [MAUI-Migration] and deleted when it's working as intended.
SearchBarHandlerMapping: IME options working as intended
SliderHandlerMappings: The MAUI "replacement" for Color.Default seems to be White so the old use case doesn't seem to be needed anymore.
2023-10-24 22:25:05 +01:00
Dinis Vieira
f1d59210f9 PM-3349 PRM-3350 Replaced XZing with Camera.MAUI for QRCodes 2023-10-23 15:17:14 +01:00
Dinis Vieira
62213c0aaf PM-3350 Added some missing images in iOS 2023-10-20 13:33:39 +01:00
Dinis Vieira
8be8abb8fe PM-3350 Migrated some Device to DeviceInfo and added temporary workaround with some comments to be able to see the Generated Password on iOS 2023-10-20 00:04:19 +01:00
Dinis Vieira
174acbc558 PM-3349
Added Argon libraries for Android
minor change to gitignore so that the Argon x86 lib is not ignored on the Android platform
2023-10-18 15:41:20 +01:00
Dinis Vieira
4bcc7c0d71 Enabled argon2Id for iOS 2023-10-18 12:20:41 +01:00
Dinis Vieira
14b2960f30 PM-3350 Removed ButtonHandlerMappings and some other code related with fonts as MAUI is taking care of Accessibility and no custom code should be needed
Migrated SelectableLabelRenderer to Handler
Cleaned LabelHandlerMappings and added logic to migrate the CustomLabelRenderer
2023-10-18 01:21:19 +01:00
Dinis Vieira
455c3a257c PM-3350 Migrated the CustomViewCellRenderer for iOS 2023-10-17 22:23:04 +01:00
Dinis Vieira
8c623a2067 PM-3349 ToolbarHandler created for setting text on Android go back buttons. 2023-10-16 20:45:58 +01:00
Dinis Vieira
3cdf1c2f0e PM-3349 Replaced the FabShadowEffect with the new MAUI Shadow to fix the buggy shadows on the Android Fab Button. 2023-10-16 00:47:52 +01:00
Dinis Vieira
ce9503fa0c PM-3349 PM-3350
Added (migrated) CustomNavigationHandler (which should partially fix the AvatarIcon in the NavBar in iOS)
Added (migrated) CustomContentPageHandler (which should mostly place the AvatarIcon in the navBar in the correct place for iOS)
Added Task.Delay (workaround) to allow the Avatar to load in iOS on the LoginPage
Added workaround for iOS bug with the toolbar size (more info in comment in AvatarImageSource.cs)
Went through the AccountViewCell MAUI-Migration comments. (and deleted/added more comments as needed)
Migrated some Device calls to DeviceInfo and MainThread
Added (migrated) CustomTabbedHandler (for managing the iOS TabBar)
2023-10-15 22:06:26 +01:00
Dinis Vieira
2e4da1b87d hardcoded AccountViewCell Avatar image to 40x40 to avoid current iOS/Android bugs where they fill much larger space. 2023-10-13 18:49:30 +01:00
Dinis Vieira
d63a219272 PM-3349 Implemented HybridWebViewHandler for iOS 2023-10-07 23:44:24 +01:00
Dinis Vieira
c92cd90a97 PM-3349 Implemented HybridWebViewHandler for Android which enables 2nd factor auth flows
Ensured CustomTabbedPageHandler had it's DisconnectHandler called
Some minor code upgrades of older obsolete Xamarin Forms code.
2023-10-07 17:25:29 +01:00
Dinis Vieira
1dcd3a3daa PM-3349 Changed UseMauiApp init so that Android Handlers still get added 2023-10-07 16:56:08 +01:00
Federico Maccaroni
efb8763d3c Merge branch 'feature/maui-migration' of https://github.com/bitwarden/mobile into feature/maui-migration 2023-10-04 12:45:08 -03:00
Federico Maccaroni
90649d1c8b PM-3350 MAUI Migration Fix iOS Autofill extension 2023-10-04 12:35:43 -03:00
Dinis Vieira
828055791f PM-3349 Removed Deploy from iOS.Autofill to allow running Android
Changed MainApplication SpecialFolder.Personal to SpecialFolder.LocalApplicationData
2023-10-02 21:15:33 +01:00
Dinis Vieira
87eebda55f Changes to solution to hopefully fix Config Mappings 2023-10-02 16:40:31 +01:00
Dinis Vieira
7542d1ae1c fix conflicts 2023-10-02 16:08:09 +01:00
Federico Maccaroni
990de4ea4e PM-3349 PM-3350 MAUI Migration Start iOS extensions 2023-10-02 12:05:57 -03:00
Dinis Vieira
0dbc23f734 PM-3349 PM-3350 Add null checks on CipherDetailsPageVM to avoid crash opening Secure Notes. 2023-10-01 23:15:55 +01:00
Dinis Vieira
9f6c8601d3 TabBarEffect removed and it's behavior is now taken care of by CustomTabbedPageHandler 2023-10-01 22:32:37 +01:00
Dinis Vieira
8b7f9b9fb3 PM-3349: Android
Added CustomTabbedPageHandler for Android to handle the tab "reselection" for PopToRoot.
Commented support for Windows in App.csproj
Disabled Interpreter on Android to avoid very slow app in Debug (during Login for example)
Added some null checks that were causing crashes (on GeneratorPageVM and PickerVM)
Minor TabsPage cleanup
2023-10-01 15:35:04 +01:00
Federico Maccaroni
d17789d5ee PM-3349 PM-3350 MAUI Migration fix nullable bindings and fallbacks 2023-09-29 12:27:12 -03:00
Federico Maccaroni
b8f0747dd4 PM-3349 PM-3350 MAUI Migration fix nullable exception bindings and AsyncCommand canExecute null exception 2023-09-29 12:12:01 -03:00
Federico Maccaroni
8ef9443b1e PM-3349 PM-3350 MAUI Migration Initial 2023-09-29 11:02:19 -03:00
162 changed files with 7963 additions and 508 deletions

1
.gitignore vendored
View File

@@ -148,6 +148,7 @@ publish/
# NuGet Packages # NuGet Packages
*.nupkg *.nupkg
!**/Xamarin.AndroidX.Credentials.1.0.0.nupkg
# The packages folder can be ignored because of Package Restore # The packages folder can be ignored because of Package Restore
**/packages/* **/packages/*
# except build/, which is used as an MSBuild target. # except build/, which is used as an MSBuild target.

View File

@@ -0,0 +1,8 @@
<?xml version="1.0"?>
<doc>
<assembly>
<name>Xamarin.AndroidX.Credentials</name>
</assembly>
<members>
</members>
</doc>

View File

@@ -2,5 +2,6 @@
<configuration> <configuration>
<packageSources> <packageSources>
<add key="MAUI Nightly builds" value="https://pkgs.dev.azure.com/xamarin/public/_packaging/maui-nightly/nuget/v3/index.json" /> <add key="MAUI Nightly builds" value="https://pkgs.dev.azure.com/xamarin/public/_packaging/maui-nightly/nuget/v3/index.json" />
<add key="Local AndroidX Credentials" value="lib/android/Xamarin.AndroidX.Credentials" />
</packageSources> </packageSources>
</configuration> </configuration>

View File

@@ -121,6 +121,7 @@
<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" />
<PackageReference Include="Xamarin.AndroidX.Activity.Ktx" Version="1.7.2.1" /> <PackageReference Include="Xamarin.AndroidX.Activity.Ktx" Version="1.7.2.1" />
<PackageReference Include="Xamarin.AndroidX.Credentials" Version="1.0.0" />
</ItemGroup> </ItemGroup>
<ItemGroup Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'android' AND !$(DefineConstants.Contains(FDROID))"> <ItemGroup Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'android' AND !$(DefineConstants.Contains(FDROID))">
<PackageReference Include="Xamarin.GooglePlayServices.SafetyNet" Version="118.0.1.5" /> <PackageReference Include="Xamarin.GooglePlayServices.SafetyNet" Version="118.0.1.5" />

View File

@@ -0,0 +1,9 @@
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

@@ -0,0 +1,65 @@
using System.Threading.Tasks;
using Android.App;
using Android.Content.PM;
using Android.OS;
using AndroidX.Credentials.Provider;
using AndroidX.Credentials.WebAuthn;
using Bit.Core.Abstractions;
using Bit.Core.Utilities;
using Bit.App.Droid.Utilities;
namespace Bit.Droid.Autofill
{
[Activity(
NoHistory = true,
LaunchMode = LaunchMode.SingleTop)]
public class CredentialProviderSelectionActivity : MauiAppCompatActivity
{
protected override void OnCreate(Bundle bundle)
{
Intent?.Validate();
base.OnCreate(bundle);
var cipherId = Intent?.GetStringExtra(CredentialProviderConstants.CredentialProviderCipherId);
if (string.IsNullOrEmpty(cipherId))
{
SetResult(Result.Canceled);
Finish();
return;
}
GetCipherAndPerformPasskeyAuthAsync(cipherId).FireAndForget();
}
private async Task GetCipherAndPerformPasskeyAuthAsync(string cipherId)
{
// TODO this is a work in progress
// https://developer.android.com/training/sign-in/credential-provider#passkeys-implement
var getRequest = PendingIntentHandler.RetrieveProviderGetCredentialRequest(Intent);
// var publicKeyRequest = getRequest?.CredentialOptions as PublicKeyCredentialRequestOptions;
var requestInfo = Intent.GetBundleExtra(CredentialProviderConstants.CredentialDataIntentExtra);
var credIdEnc = requestInfo?.GetString(CredentialProviderConstants.CredentialIdIntentExtra);
var cipherService = ServiceContainer.Resolve<ICipherService>();
var cipher = await cipherService.GetAsync(cipherId);
var decCipher = await cipher.DecryptAsync();
var passkey = decCipher.Login.Fido2Credentials.Find(f => f.CredentialId == credIdEnc);
var credId = Convert.FromBase64String(credIdEnc);
// var privateKey = Convert.FromBase64String(passkey.PrivateKey);
// var uid = Convert.FromBase64String(passkey.uid);
var origin = getRequest?.CallingAppInfo.Origin;
var packageName = getRequest?.CallingAppInfo.PackageName;
// --- continue WIP here (save TOTP copy as last step) ---
// Copy TOTP if needed
var autofillHandler = ServiceContainer.Resolve<IAutofillHandler>();
autofillHandler.Autofill(decCipher);
}
}
}

View File

@@ -0,0 +1,147 @@
using Android;
using Android.App;
using Android.Content;
using Android.Graphics.Drawables;
using Android.OS;
using Android.Runtime;
using AndroidX.Credentials.Provider;
using Bit.Core.Abstractions;
using Bit.Core.Utilities;
using AndroidX.Credentials.Exceptions;
using AndroidX.Credentials.WebAuthn;
using Bit.Core.Models.View;
using Resource = Microsoft.Maui.Resource;
namespace Bit.Droid.Autofill
{
[Service(Permission = Manifest.Permission.BindCredentialProviderService, Label = "Bitwarden", Exported = true)]
[IntentFilter(new string[] { "android.service.credentials.CredentialProviderService" })]
[MetaData("android.credentials.provider", Resource = "@xml/provider")]
[Register("com.x8bit.bitwarden.Autofill.CredentialProviderService")]
public class CredentialProviderService : AndroidX.Credentials.Provider.CredentialProviderService
{
private const string GetPasskeyIntentAction = "PACKAGE_NAME.GET_PASSKEY";
private const int UniqueRequestCode = 94556023;
private ICipherService _cipherService;
private IUserVerificationService _userVerificationService;
private IVaultTimeoutService _vaultTimeoutService;
private LazyResolve<ILogger> _logger = new LazyResolve<ILogger>("logger");
public override async void OnBeginCreateCredentialRequest(BeginCreateCredentialRequest request,
CancellationSignal cancellationSignal, IOutcomeReceiver callback) => throw new NotImplementedException();
public override async void OnBeginGetCredentialRequest(BeginGetCredentialRequest request,
CancellationSignal cancellationSignal, IOutcomeReceiver callback)
{
try
{
_vaultTimeoutService ??= ServiceContainer.Resolve<IVaultTimeoutService>();
await _vaultTimeoutService.CheckVaultTimeoutAsync();
var locked = await _vaultTimeoutService.IsLockedAsync();
if (!locked)
{
var response = await ProcessGetCredentialsRequestAsync(request);
callback.OnResult(response);
}
// TODO handle auth/unlock account flow
}
catch (GetCredentialException e)
{
_logger.Value.Exception(e);
callback.OnError(e.ErrorMessage ?? "Error getting credentials");
}
catch (Exception e)
{
_logger.Value.Exception(e);
throw;
}
}
private async Task<BeginGetCredentialResponse> ProcessGetCredentialsRequestAsync(
BeginGetCredentialRequest request)
{
IList<CredentialEntry> credentialEntries = null;
foreach (var option in request.BeginGetCredentialOptions)
{
var credentialOption = option as BeginGetPublicKeyCredentialOption;
if (credentialOption != null)
{
credentialEntries ??= new List<CredentialEntry>();
((List<CredentialEntry>)credentialEntries).AddRange(
await PopulatePasskeyDataAsync(request.CallingAppInfo, credentialOption));
}
}
if (credentialEntries == null)
{
return new BeginGetCredentialResponse();
}
return new BeginGetCredentialResponse.Builder()
.SetCredentialEntries(credentialEntries)
.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,
CancellationSignal cancellationSignal, IOutcomeReceiver callback) => throw new NotImplementedException();
}
}

View File

@@ -85,6 +85,12 @@ 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);
} }
#if !FDROID #if !FDROID
if (Build.VERSION.SdkInt <= BuildVersionCodes.Kitkat) if (Build.VERSION.SdkInt <= BuildVersionCodes.Kitkat)
@@ -160,7 +166,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 +189,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

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<credential-provider xmlns:android="http://schemas.android.com/apk/res/android">
<capabilities>
<capability name="androidx.credentials.TYPE_PUBLIC_KEY_CREDENTIAL" />
</capabilities>
</credential-provider>

View File

@@ -37,6 +37,23 @@ namespace Bit.Droid.Services
_eventService = eventService; _eventService = eventService;
} }
public bool CredentialProviderServiceEnabled()
{
if (Build.VERSION.SdkInt < BuildVersionCodes.UpsideDownCake)
{
return false;
}
try
{
// TODO - find a way to programmatically check if the credential provider service is enabled
return false;
}
catch
{
return false;
}
}
public bool AutofillServiceEnabled() public bool AutofillServiceEnabled()
{ {
if (Build.VERSION.SdkInt < BuildVersionCodes.O) if (Build.VERSION.SdkInt < BuildVersionCodes.O)
@@ -163,7 +180,14 @@ namespace Bit.Droid.Services
return Accessibility.AccessibilityHelpers.OverlayPermitted(); return Accessibility.AccessibilityHelpers.OverlayPermitted();
} }
public void DisableCredentialProviderService()
{
try
{
// TODO - find a way to programmatically disable the provider service, or take the user to the settings page where they can do it
}
catch { }
}
public void DisableAutofillService() public void DisableAutofillService()
{ {

View File

@@ -11,6 +11,7 @@ using Android.Text.Method;
using Android.Views; using Android.Views;
using Android.Views.InputMethods; using Android.Views.InputMethods;
using Android.Widget; using Android.Widget;
using AndroidX.Credentials;
using Bit.App.Abstractions; using Bit.App.Abstractions;
using Bit.Core.Resources.Localization; using Bit.Core.Resources.Localization;
using Bit.App.Utilities; using Bit.App.Utilities;
@@ -501,6 +502,27 @@ namespace Bit.Droid.Services
} }
} }
public void OpenCredentialProviderSettings()
{
var activity = (MainActivity)Microsoft.Maui.ApplicationModel.Platform.CurrentActivity;
try
{
var pendingIntent = CredentialManager.Create(activity).CreateSettingsPendingIntent();
pendingIntent.Send();
}
catch (ActivityNotFoundException)
{
var alertBuilder = new AlertDialog.Builder(activity);
alertBuilder.SetMessage(AppResources.BitwardenCredentialProviderGoToSettings);
alertBuilder.SetCancelable(true);
alertBuilder.SetPositiveButton(AppResources.Ok, (sender, args) =>
{
(sender as AlertDialog)?.Cancel();
});
alertBuilder.Create().Show();
}
}
public void OpenAccessibilitySettings() public void OpenAccessibilitySettings()
{ {
try try
@@ -559,6 +581,8 @@ namespace Bit.Droid.Services
return true; return true;
} }
public bool SupportsCredentialProviderService() => Build.VERSION.SdkInt >= BuildVersionCodes.UpsideDownCake;
public bool SupportsAutofillServices() => Build.VERSION.SdkInt >= BuildVersionCodes.O; public bool SupportsAutofillServices() => Build.VERSION.SdkInt >= BuildVersionCodes.O;
public bool SupportsInlineAutofill() => Build.VERSION.SdkInt >= BuildVersionCodes.R; public bool SupportsInlineAutofill() => Build.VERSION.SdkInt >= BuildVersionCodes.R;

View File

@@ -88,7 +88,7 @@ namespace Bit.iOS
Core.Constants.AutofillNeedsIdentityReplacementKey); Core.Constants.AutofillNeedsIdentityReplacementKey);
if (needsAutofillReplacement.GetValueOrDefault()) if (needsAutofillReplacement.GetValueOrDefault())
{ {
await ASHelpers.ReplaceAllIdentities(); await ASHelpers.ReplaceAllIdentitiesAsync();
} }
} }
else if (message.Command == "showAppExtension") else if (message.Command == "showAppExtension")
@@ -102,7 +102,7 @@ namespace Bit.iOS
var success = value as bool?; var success = value as bool?;
if (success.GetValueOrDefault() && _deviceActionService.SystemMajorVersion() >= 12) if (success.GetValueOrDefault() && _deviceActionService.SystemMajorVersion() >= 12)
{ {
await ASHelpers.ReplaceAllIdentities(); await ASHelpers.ReplaceAllIdentitiesAsync();
} }
} }
} }
@@ -114,22 +114,21 @@ namespace Bit.iOS
return; return;
} }
if (await ASHelpers.IdentitiesCanIncremental()) if (await ASHelpers.IdentitiesSupportIncrementalAsync())
{ {
var cipherId = message.Data as string; var cipherId = message.Data as string;
if (message.Command == "addedCipher" && !string.IsNullOrWhiteSpace(cipherId)) if (message.Command == "addedCipher" && !string.IsNullOrWhiteSpace(cipherId))
{ {
var identity = await ASHelpers.GetCipherIdentityAsync(cipherId); var identity = await ASHelpers.GetCipherPasswordIdentityAsync(cipherId);
if (identity == null) if (identity == null)
{ {
return; return;
} }
await ASCredentialIdentityStore.SharedStore?.SaveCredentialIdentitiesAsync( await ASCredentialIdentityStoreExtensions.SaveCredentialIdentitiesAsync(identity);
new ASPasswordCredentialIdentity[] { identity });
return; return;
} }
} }
await ASHelpers.ReplaceAllIdentities(); await ASHelpers.ReplaceAllIdentitiesAsync();
} }
else if (message.Command == "deletedCipher" || message.Command == "softDeletedCipher") else if (message.Command == "deletedCipher" || message.Command == "softDeletedCipher")
{ {
@@ -138,28 +137,27 @@ namespace Bit.iOS
return; return;
} }
if (await ASHelpers.IdentitiesCanIncremental()) if (await ASHelpers.IdentitiesSupportIncrementalAsync())
{ {
var identity = ASHelpers.ToCredentialIdentity( var identity = ASHelpers.ToPasswordCredentialIdentity(
message.Data as Bit.Core.Models.View.CipherView); message.Data as Bit.Core.Models.View.CipherView);
if (identity == null) if (identity == null)
{ {
return; return;
} }
await ASCredentialIdentityStore.SharedStore?.RemoveCredentialIdentitiesAsync( await ASCredentialIdentityStoreExtensions.RemoveCredentialIdentitiesAsync(identity);
new ASPasswordCredentialIdentity[] { identity });
return; return;
} }
await ASHelpers.ReplaceAllIdentities(); await ASHelpers.ReplaceAllIdentitiesAsync();
} }
else if (message.Command == "logout" && UIDevice.CurrentDevice.CheckSystemVersion(12, 0)) else if (message.Command == "logout" && UIDevice.CurrentDevice.CheckSystemVersion(12, 0))
{ {
await ASCredentialIdentityStore.SharedStore?.RemoveAllCredentialIdentitiesAsync(); await ASCredentialIdentityStore.SharedStore.RemoveAllCredentialIdentitiesAsync();
} }
else if ((message.Command == "softDeletedCipher" || message.Command == "restoredCipher") else if ((message.Command == "softDeletedCipher" || message.Command == "restoredCipher")
&& UIDevice.CurrentDevice.CheckSystemVersion(12, 0)) && UIDevice.CurrentDevice.CheckSystemVersion(12, 0))
{ {
await ASHelpers.ReplaceAllIdentities(); await ASHelpers.ReplaceAllIdentitiesAsync();
} }
else if (message.Command == AppHelpers.VAULT_TIMEOUT_ACTION_CHANGED_MESSAGE_COMMAND) else if (message.Command == AppHelpers.VAULT_TIMEOUT_ACTION_CHANGED_MESSAGE_COMMAND)
{ {
@@ -168,12 +166,12 @@ namespace Bit.iOS
{ {
if (UIDevice.CurrentDevice.CheckSystemVersion(12, 0)) if (UIDevice.CurrentDevice.CheckSystemVersion(12, 0))
{ {
await ASCredentialIdentityStore.SharedStore?.RemoveAllCredentialIdentitiesAsync(); await ASCredentialIdentityStore.SharedStore.RemoveAllCredentialIdentitiesAsync();
} }
} }
else else
{ {
await ASHelpers.ReplaceAllIdentities(); await ASHelpers.ReplaceAllIdentitiesAsync();
} }
} }
} }

View File

@@ -4,6 +4,7 @@ namespace Bit.Core.Abstractions
{ {
public interface IAutofillHandler public interface IAutofillHandler
{ {
bool CredentialProviderServiceEnabled();
bool AutofillServicesEnabled(); bool AutofillServicesEnabled();
bool SupportsAutofillService(); bool SupportsAutofillService();
void Autofill(CipherView cipher); void Autofill(CipherView cipher);
@@ -11,6 +12,7 @@ namespace Bit.Core.Abstractions
bool AutofillAccessibilityServiceRunning(); bool AutofillAccessibilityServiceRunning();
bool AutofillAccessibilityOverlayPermitted(); bool AutofillAccessibilityOverlayPermitted();
bool AutofillServiceEnabled(); bool AutofillServiceEnabled();
void DisableCredentialProviderService();
void DisableAutofillService(); void DisableAutofillService();
} }
} }

View File

@@ -1,7 +1,4 @@
using System; using Bit.Core.Enums;
using System.Collections.Generic;
using System.Threading.Tasks;
using Bit.Core.Enums;
using Bit.Core.Models.Data; using Bit.Core.Models.Data;
using Bit.Core.Models.Domain; using Bit.Core.Models.Domain;
using Bit.Core.Models.View; using Bit.Core.Models.View;
@@ -37,5 +34,7 @@ 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(Fido2ConfirmNewCredentialParams newPasskeyParams);
Task CopyTotpCodeIfNeededAsync(CipherView cipher);
} }
} }

View File

@@ -1,12 +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 AndroidWindowCreated,
AutofillIOSExtensionViewDidAppear
} }
public interface IConditionedAwaiterManager public interface IConditionedAwaiterManager
@@ -14,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,6 +1,7 @@
using System; using System;
using System.Threading.Tasks; using System.Threading.Tasks;
using Bit.Core.Enums; using Bit.Core.Enums;
using Bit.Core.Models.Domain;
namespace Bit.Core.Abstractions namespace Bit.Core.Abstractions
{ {

View File

@@ -28,6 +28,7 @@ namespace Bit.App.Abstractions
bool SupportsNfc(); bool SupportsNfc();
bool SupportsCamera(); bool SupportsCamera();
bool SupportsFido2(); bool SupportsFido2();
bool SupportsCredentialProviderService();
bool SupportsAutofillServices(); bool SupportsAutofillServices();
bool SupportsInlineAutofill(); bool SupportsInlineAutofill();
bool SupportsDrawOver(); bool SupportsDrawOver();
@@ -36,6 +37,7 @@ namespace Bit.App.Abstractions
void RateApp(); void RateApp();
void OpenAccessibilitySettings(); void OpenAccessibilitySettings();
void OpenAccessibilityOverlayPermissionSettings(); void OpenAccessibilityOverlayPermissionSettings();
void OpenCredentialProviderSettings();
void OpenAutofillSettings(); void OpenAutofillSettings();
long GetActiveTime(); long GetActiveTime();
void CloseMainApp(); void CloseMainApp();

View File

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

View File

@@ -0,0 +1,35 @@
using Bit.Core.Utilities.Fido2;
namespace Bit.Core.Abstractions
{
/// <summary>
/// This class represents an abstraction of the WebAuthn Client as described by W3C:
/// https://www.w3.org/TR/webauthn-3/#webauthn-client
///
/// The WebAuthn Client is an intermediary entity typically implemented in the user agent
/// (in whole, or in part). Conceptually, it underlies the Web Authentication API and embodies
/// the implementation of the Web Authentication API's operations.
///
/// It is responsible for both marshalling the inputs for the underlying authenticator operations,
/// and for returning the results of the latter operations to the Web Authentication API's callers.
/// </summary>
public interface IFido2ClientService
{
/// <summary>
/// Allows WebAuthn Relying Party scripts to request the creation of a new public key credential source.
/// For more information please see: https://www.w3.org/TR/webauthn-3/#sctn-createCredential
/// </summary>
/// <param name="createCredentialParams">The parameters for the credential creation operation</param>
/// <returns>The new credential</returns>
Task<Fido2ClientCreateCredentialResult> CreateCredentialAsync(Fido2ClientCreateCredentialParams createCredentialParams);
/// <summary>
/// Allows WebAuthn Relying Party scripts to discover and use an existing public key credential, with the users consent.
/// Relying Party script can optionally specify some criteria to indicate what credential sources are acceptable to it.
/// For more information please see: https://www.w3.org/TR/webauthn-3/#sctn-getAssertion
/// </summary>
/// <param name="assertCredentialParams">The parameters for the credential assertion operation</param>
/// <returns>The asserted credential</returns>
Task<Fido2ClientAssertCredentialResult> AssertCredentialAsync(Fido2ClientAssertCredentialParams assertCredentialParams);
}
}

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,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);
Task<Fido2ClientAssertCredentialResult> AssertCredentialAsync(Fido2ClientAssertCredentialParams assertCredentialParams);
Task<Fido2AuthenticatorMakeCredentialResult> MakeCredentialAsync(Fido2AuthenticatorMakeCredentialParams makeCredentialParams, IFido2MakeCredentialUserInterface userInterface);
Task<Fido2AuthenticatorGetAssertionResult> GetAssertionAsync(Fido2AuthenticatorGetAssertionParams assertionParams, IFido2GetAssertionUserInterface userInterface);
Task<Fido2AuthenticatorDiscoverableCredentialMetadata[]> SilentCredentialDiscoveryAsync(string rpId);
}
}

View File

@@ -0,0 +1,17 @@
namespace Bit.Core.Abstractions
{
public interface IFido2UserInterface
{
/// <summary>
/// Whether the vault has been unlocked during this transaction
/// </summary>
bool HasVaultBeenUnlockedInThisTransaction { get; }
/// <summary>
/// Make sure that the vault is unlocked.
/// This should open a window and ask the user to login or unlock the vault if necessary.
/// </summary>
/// <returns>When vault has been unlocked.</returns>
Task EnsureUnlockedVaultAsync();
}
}

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,7 @@ 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();
[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

@@ -34,6 +34,7 @@
<PackageReference Include="CsvHelper" Version="30.0.1" /> <PackageReference Include="CsvHelper" Version="30.0.1" />
<PackageReference Include="LiteDB" Version="5.0.17" /> <PackageReference Include="LiteDB" Version="5.0.17" />
<PackageReference Include="PCLCrypto" Version="2.1.40-alpha" /> <PackageReference Include="PCLCrypto" Version="2.1.40-alpha" />
<PackageReference Include="System.Formats.Cbor" Version="8.0.0" />
<PackageReference Include="zxcvbn-core" Version="7.0.92" /> <PackageReference Include="zxcvbn-core" Version="7.0.92" />
<PackageReference Include="MessagePack.MSBuild.Tasks" Version="2.5.124"> <PackageReference Include="MessagePack.MSBuild.Tasks" Version="2.5.124">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
@@ -52,6 +53,7 @@
<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" />
<PackageReference Include="Xamarin.AndroidX.Activity.Ktx" Version="1.7.2.1" /> <PackageReference Include="Xamarin.AndroidX.Activity.Ktx" Version="1.7.2.1" />
<PackageReference Include="Xamarin.AndroidX.Credentials" Version="1.0.0" />
</ItemGroup> </ItemGroup>
<ItemGroup Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'android' AND !$(DefineConstants.Contains(FDROID))"> <ItemGroup Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'android' AND !$(DefineConstants.Contains(FDROID))">
<PackageReference Include="Xamarin.GooglePlayServices.SafetyNet" Version="118.0.1.5" /> <PackageReference Include="Xamarin.GooglePlayServices.SafetyNet" Version="118.0.1.5" />
@@ -75,8 +77,10 @@
<Folder Include="Utilities\Automation\" /> <Folder Include="Utilities\Automation\" />
<Folder Include="Utilities\Prompts\" /> <Folder Include="Utilities\Prompts\" />
<Folder Include="Resources\Localization\" /> <Folder Include="Resources\Localization\" />
<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="Utilities\WebAuthenticatorMAUI\" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
@@ -106,8 +110,10 @@
</MauiXaml> </MauiXaml>
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<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="Utilities\WebAuthenticatorMAUI\" />
</ItemGroup> </ItemGroup>
</Project> </Project>

View File

@@ -1,5 +1,4 @@
using System; using Bit.Core.Models.Domain;
using Bit.Core.Models.Domain;
namespace Bit.Core.Models.Api namespace Bit.Core.Models.Api
{ {
@@ -21,6 +20,7 @@ namespace Bit.Core.Models.Api
RpName = fido2Key.RpName?.EncryptedString; RpName = fido2Key.RpName?.EncryptedString;
UserHandle = fido2Key.UserHandle?.EncryptedString; UserHandle = fido2Key.UserHandle?.EncryptedString;
UserName = fido2Key.UserName?.EncryptedString; UserName = fido2Key.UserName?.EncryptedString;
UserDisplayName = fido2Key.UserDisplayName?.EncryptedString;
Counter = fido2Key.Counter?.EncryptedString; Counter = fido2Key.Counter?.EncryptedString;
CreationDate = fido2Key.CreationDate; CreationDate = fido2Key.CreationDate;
} }
@@ -35,6 +35,7 @@ namespace Bit.Core.Models.Api
public string RpName { get; set; } public string RpName { get; set; }
public string UserHandle { get; set; } public string UserHandle { get; set; }
public string UserName { get; set; } public string UserName { get; set; }
public string UserDisplayName { get; set; }
public string Counter { get; set; } public string Counter { get; set; }
public DateTime CreationDate { get; set; } public DateTime CreationDate { get; set; }
} }

View File

@@ -19,6 +19,7 @@ namespace Bit.Core.Models.Data
RpName = apiData.RpName; RpName = apiData.RpName;
UserHandle = apiData.UserHandle; UserHandle = apiData.UserHandle;
UserName = apiData.UserName; UserName = apiData.UserName;
UserDisplayName = apiData.UserDisplayName;
Counter = apiData.Counter; Counter = apiData.Counter;
CreationDate = apiData.CreationDate; CreationDate = apiData.CreationDate;
} }
@@ -33,6 +34,7 @@ namespace Bit.Core.Models.Data
public string RpName { get; set; } public string RpName { get; set; }
public string UserHandle { get; set; } public string UserHandle { get; set; }
public string UserName { get; set; } public string UserName { get; set; }
public string UserDisplayName { get; set; }
public string Counter { get; set; } public string Counter { get; set; }
public DateTime CreationDate { get; set; } public DateTime CreationDate { get; set; }
} }

View File

@@ -1,8 +1,4 @@
using System; using Bit.Core.Models.Data;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Bit.Core.Models.Data;
using Bit.Core.Models.View; using Bit.Core.Models.View;
namespace Bit.Core.Models.Domain namespace Bit.Core.Models.Domain
@@ -21,6 +17,7 @@ namespace Bit.Core.Models.Domain
nameof(RpName), nameof(RpName),
nameof(UserHandle), nameof(UserHandle),
nameof(UserName), nameof(UserName),
nameof(UserDisplayName),
nameof(Counter) nameof(Counter)
}; };
@@ -48,6 +45,7 @@ namespace Bit.Core.Models.Domain
public EncString RpName { get; set; } public EncString RpName { get; set; }
public EncString UserHandle { get; set; } public EncString UserHandle { get; set; }
public EncString UserName { get; set; } public EncString UserName { get; set; }
public EncString UserDisplayName { get; set; }
public EncString Counter { get; set; } public EncString Counter { get; set; }
public DateTime CreationDate { get; set; } public DateTime CreationDate { get; set; }

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

@@ -1,7 +1,7 @@
using System; using System.Text.Json.Serialization;
using System.Collections.Generic;
using Bit.Core.Enums; using Bit.Core.Enums;
using Bit.Core.Models.Domain; using Bit.Core.Models.Domain;
using Bit.Core.Utilities;
namespace Bit.Core.Models.View namespace Bit.Core.Models.View
{ {
@@ -26,13 +26,42 @@ namespace Bit.Core.Models.View
public string RpName { get; set; } public string RpName { get; set; }
public string UserHandle { get; set; } public string UserHandle { get; set; }
public string UserName { get; set; } public string UserName { get; set; }
public string UserDisplayName { get; set; }
public string Counter { get; set; } public string Counter { get; set; }
public DateTime CreationDate { get; set; } public DateTime CreationDate { get; set; }
[JsonIgnore]
public int CounterValue {
get => int.TryParse(Counter, out var counter) ? counter : 0;
set => Counter = value.ToString();
}
[JsonIgnore]
public byte[] UserHandleValue {
get => UserHandle == null ? null : CoreHelpers.Base64UrlDecode(UserHandle);
set => UserHandle = value == null ? null : CoreHelpers.Base64UrlEncode(value);
}
[JsonIgnore]
public byte[] KeyBytes {
get => KeyValue == null ? null : CoreHelpers.Base64UrlDecode(KeyValue);
set => KeyValue = value == null ? null : CoreHelpers.Base64UrlEncode(value);
}
[JsonIgnore]
public bool DiscoverableValue {
get => bool.TryParse(Discoverable, out var discoverable) && discoverable;
set => Discoverable = value.ToString().ToLower();
}
[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>>();
public bool IsDiscoverable => !string.IsNullOrWhiteSpace(Discoverable);
[JsonIgnore]
public bool CanLaunch => !string.IsNullOrEmpty(RpId); public bool CanLaunch => !string.IsNullOrEmpty(RpId);
[JsonIgnore]
public string LaunchUri => $"https://{RpId}"; public string LaunchUri => $"https://{RpId}";
public bool IsUniqueAgainst(Fido2CredentialView fido2View) => fido2View?.RpId != RpId || fido2View?.UserName != UserName; public bool IsUniqueAgainst(Fido2CredentialView fido2View) => fido2View?.RpId != RpId || fido2View?.UserName != UserName;

View File

@@ -1,8 +1,7 @@
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;
using Bit.Core.Resources.Localization;
using Bit.Core.Utilities;
namespace Bit.Core.Models.View namespace Bit.Core.Models.View
{ {
@@ -40,4 +39,15 @@ namespace Bit.Core.Models.View
}; };
} }
} }
public static class LoginViewExtensions
{
public static string GetMainFido2CredentialUsername(this LoginView loginView)
{
return loginView.MainFido2Credential.UserName
.FallbackOnNullOrWhiteSpace(loginView.MainFido2Credential.UserDisplayName)
.FallbackOnNullOrWhiteSpace(loginView.Username)
.FallbackOnNullOrWhiteSpace(AppResources.UnknownAccount);
}
}
} }

View File

@@ -21,6 +21,10 @@
<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

@@ -515,7 +515,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

@@ -68,7 +68,8 @@ namespace Bit.App.Pages
{ {
get get
{ {
var appInfo = string.Format("{0}: {1} ({2})", // TODO: REMOVE WHEN MERGED INTO MAIN BRANCH
var appInfo = string.Format("MAUI {0}: {1} ({2})",
AppResources.Version, AppResources.Version,
_platformUtilsService.GetApplicationVersion(), _platformUtilsService.GetApplicationVersion(),
_deviceActionService.GetBuildNumber()); _deviceActionService.GetBuildNumber());

View File

@@ -5,7 +5,7 @@
x:Class="Bit.App.Pages.AutofillPage" x:Class="Bit.App.Pages.AutofillPage"
xmlns:pages="clr-namespace:Bit.App.Pages" xmlns:pages="clr-namespace:Bit.App.Pages"
xmlns:u="clr-namespace:Bit.App.Utilities" xmlns:u="clr-namespace:Bit.App.Utilities"
Title="{u:I18n PasswordAutofill}"> Title="{u:I18n SetUpAutofill}">
<ContentPage.ToolbarItems> <ContentPage.ToolbarItems>
<ToolbarItem Text="{u:I18n Close}" Clicked="Close_Clicked" Order="Primary" Priority="-1" /> <ToolbarItem Text="{u:I18n Close}" Clicked="Close_Clicked" Order="Primary" Priority="-1" />
@@ -15,26 +15,22 @@
<StackLayout Spacing="5" <StackLayout Spacing="5"
Padding="20, 20, 20, 30" Padding="20, 20, 20, 30"
VerticalOptions="FillAndExpand"> VerticalOptions="FillAndExpand">
<Label Text="{u:I18n ExtensionInstantAccess}" <Label Text="{u:I18n GetInstantAccessToYourPasswordsAndPasskeys}"
HorizontalOptions="Center" HorizontalOptions="Center"
HorizontalTextAlignment="Center" HorizontalTextAlignment="Center"
LineBreakMode="WordWrap" LineBreakMode="WordWrap"
StyleClass="text-lg" StyleClass="text-lg"
Margin="0, 0, 0, 15" /> Margin="0, 0, 0, 15" />
<Label Text="{u:I18n AutofillTurnOn}" <Label Text="{u:I18n SetUpAutoFillDescriptionLong}"
HorizontalOptions="Center" HorizontalOptions="Center"
HorizontalTextAlignment="Center" HorizontalTextAlignment="Center"
LineBreakMode="WordWrap" LineBreakMode="WordWrap"
Margin="0, 0, 0, 15" /> Margin="0, 0, 0, 15" />
<Label Text="{u:I18n AutofillTurnOn1}" <Label Text="{u:I18n FirstDotGoToYourDeviceSettingsPasswordsPasswordOptions}"
LineBreakMode="WordWrap" /> LineBreakMode="WordWrap" />
<Label Text="{u:I18n AutofillTurnOn2}" <Label Text="{u:I18n SecondDotTurnOnAutoFill}"
LineBreakMode="WordWrap" /> LineBreakMode="WordWrap" />
<Label Text="{u:I18n AutofillTurnOn3}" <Label Text="{u:I18n ThirdDotSelectBitwardenToUseForPasswordsAndPasskeys}"
LineBreakMode="WordWrap" />
<Label Text="{u:I18n AutofillTurnOn4}"
LineBreakMode="WordWrap" />
<Label Text="{u:I18n AutofillTurnOn5}"
LineBreakMode="WordWrap" /> LineBreakMode="WordWrap" />
<Image Source="autofill-kb.png" <Image Source="autofill-kb.png"
VerticalOptions="CenterAndExpand" VerticalOptions="CenterAndExpand"

View File

@@ -19,6 +19,15 @@
Text="{u:I18n Autofill}" Text="{u:I18n Autofill}"
StyleClass="settings-header" /> StyleClass="settings-header" />
<controls:SwitchItemView
Title="{u:I18n CredentialProviderService}"
Subtitle="{u:I18n CredentialProviderServiceExplanationLong}"
IsVisible="{Binding SupportsCredentialProviderService}"
IsToggled="{Binding UseCredentialProviderService}"
AutomationId="CredentialProviderServiceSwitch"
StyleClass="settings-item-view"
HorizontalOptions="FillAndExpand" />
<controls:SwitchItemView <controls:SwitchItemView
Title="{u:I18n AutofillServices}" Title="{u:I18n AutofillServices}"
Subtitle="{u:I18n AutofillServicesExplanationLong}" Subtitle="{u:I18n AutofillServicesExplanationLong}"

View File

@@ -6,12 +6,27 @@ namespace Bit.App.Pages
{ {
public partial class AutofillSettingsPageViewModel public partial class AutofillSettingsPageViewModel
{ {
private bool _useCredentialProviderService;
private bool _useAutofillServices; private bool _useAutofillServices;
private bool _useInlineAutofill; private bool _useInlineAutofill;
private bool _useAccessibility; private bool _useAccessibility;
private bool _useDrawOver; private bool _useDrawOver;
private bool _askToAddLogin; private bool _askToAddLogin;
public bool SupportsCredentialProviderService => DeviceInfo.Platform == DevicePlatform.Android && _deviceActionService.SupportsCredentialProviderService();
public bool UseCredentialProviderService
{
get => _useCredentialProviderService;
set
{
if (SetProperty(ref _useCredentialProviderService, value))
{
((ICommand)ToggleUseCredentialProviderServiceCommand).Execute(null);
}
}
}
public bool SupportsAndroidAutofillServices => DeviceInfo.Platform == DevicePlatform.Android && _deviceActionService.SupportsAutofillServices(); public bool SupportsAndroidAutofillServices => DeviceInfo.Platform == DevicePlatform.Android && _deviceActionService.SupportsAutofillServices();
public bool UseAutofillServices public bool UseAutofillServices
@@ -84,6 +99,7 @@ namespace Bit.App.Pages
} }
} }
public AsyncRelayCommand ToggleUseCredentialProviderServiceCommand { get; private set; }
public AsyncRelayCommand ToggleUseAutofillServicesCommand { get; private set; } public AsyncRelayCommand ToggleUseAutofillServicesCommand { get; private set; }
public AsyncRelayCommand ToggleUseInlineAutofillCommand { get; private set; } public AsyncRelayCommand ToggleUseInlineAutofillCommand { get; private set; }
public AsyncRelayCommand ToggleUseAccessibilityCommand { get; private set; } public AsyncRelayCommand ToggleUseAccessibilityCommand { get; private set; }
@@ -93,6 +109,7 @@ namespace Bit.App.Pages
private void InitAndroidCommands() private void InitAndroidCommands()
{ {
ToggleUseCredentialProviderServiceCommand = CreateDefaultAsyncRelayCommand(() => MainThread.InvokeOnMainThreadAsync(() => ToggleUseCredentialProviderService()), () => _inited, allowsMultipleExecutions: false);
ToggleUseAutofillServicesCommand = CreateDefaultAsyncRelayCommand(() => MainThread.InvokeOnMainThreadAsync(() => ToggleUseAutofillServices()), () => _inited, allowsMultipleExecutions: false); ToggleUseAutofillServicesCommand = CreateDefaultAsyncRelayCommand(() => MainThread.InvokeOnMainThreadAsync(() => ToggleUseAutofillServices()), () => _inited, allowsMultipleExecutions: false);
ToggleUseInlineAutofillCommand = CreateDefaultAsyncRelayCommand(() => MainThread.InvokeOnMainThreadAsync(() => ToggleUseInlineAutofillEnabledAsync()), () => _inited, allowsMultipleExecutions: false); ToggleUseInlineAutofillCommand = CreateDefaultAsyncRelayCommand(() => MainThread.InvokeOnMainThreadAsync(() => ToggleUseInlineAutofillEnabledAsync()), () => _inited, allowsMultipleExecutions: false);
ToggleUseAccessibilityCommand = CreateDefaultAsyncRelayCommand(ToggleUseAccessibilityAsync, () => _inited, allowsMultipleExecutions: false); ToggleUseAccessibilityCommand = CreateDefaultAsyncRelayCommand(ToggleUseAccessibilityAsync, () => _inited, allowsMultipleExecutions: false);
@@ -115,6 +132,9 @@ namespace Bit.App.Pages
private async Task UpdateAndroidAutofillSettingsAsync() private async Task UpdateAndroidAutofillSettingsAsync()
{ {
// TODO - uncomment once _autofillHandler.CredentialProviderServiceEnabled() returns a real value
// _useCredentialProviderService =
// SupportsCredentialProviderService && _autofillHandler.CredentialProviderServiceEnabled();
_useAutofillServices = _useAutofillServices =
_autofillHandler.SupportsAutofillService() && _autofillHandler.AutofillServiceEnabled(); _autofillHandler.SupportsAutofillService() && _autofillHandler.AutofillServiceEnabled();
_useAccessibility = _autofillHandler.AutofillAccessibilityServiceRunning(); _useAccessibility = _autofillHandler.AutofillAccessibilityServiceRunning();
@@ -123,6 +143,7 @@ namespace Bit.App.Pages
await MainThread.InvokeOnMainThreadAsync(() => await MainThread.InvokeOnMainThreadAsync(() =>
{ {
TriggerPropertyChanged(nameof(UseCredentialProviderService));
TriggerPropertyChanged(nameof(UseAutofillServices)); TriggerPropertyChanged(nameof(UseAutofillServices));
TriggerPropertyChanged(nameof(UseAccessibility)); TriggerPropertyChanged(nameof(UseAccessibility));
TriggerPropertyChanged(nameof(UseDrawOver)); TriggerPropertyChanged(nameof(UseDrawOver));
@@ -130,6 +151,18 @@ namespace Bit.App.Pages
}); });
} }
private void ToggleUseCredentialProviderService()
{
if (UseCredentialProviderService)
{
_deviceActionService.OpenCredentialProviderSettings();
}
else
{
_autofillHandler.DisableCredentialProviderService();
}
}
private void ToggleUseAutofillServices() private void ToggleUseAutofillServices()
{ {
if (UseAutofillServices) if (UseAutofillServices)

View File

@@ -370,7 +370,7 @@ namespace Bit.App.Pages
if (!_supportsBiometric if (!_supportsBiometric
|| ||
!await _platformUtilsService.AuthenticateBiometricAsync(null, DeviceInfo.Platform == DevicePlatform.Android ? "." : null)) await _platformUtilsService.AuthenticateBiometricAsync(null, DeviceInfo.Platform == DevicePlatform.Android ? "." : null) != true)
{ {
_canUnlockWithBiometrics = false; _canUnlockWithBiometrics = false;
MainThread.BeginInvokeOnMainThread(() => TriggerPropertyChanged(nameof(CanUnlockWithBiometrics))); MainThread.BeginInvokeOnMainThread(() => TriggerPropertyChanged(nameof(CanUnlockWithBiometrics)));

View File

@@ -1318,6 +1318,15 @@ namespace Bit.Core.Resources.Localization {
} }
} }
/// <summary>
/// Looks up a localized string similar to We were unable to automatically open the Android credential provider settings menu for you. You can navigate to the credential provider settings menu manually from Android Settings &gt; System &gt; Passwords &amp; accounts &gt; Passwords, passkeys and data services..
/// </summary>
public static string BitwardenCredentialProviderGoToSettings {
get {
return ResourceManager.GetString("BitwardenCredentialProviderGoToSettings", resourceCulture);
}
}
/// <summary> /// <summary>
/// Looks up a localized string similar to Bitwarden Help Center. /// Looks up a localized string similar to Bitwarden Help Center.
/// </summary> /// </summary>
@@ -1534,6 +1543,15 @@ namespace Bit.Core.Resources.Localization {
} }
} }
/// <summary>
/// Looks up a localized string similar to Choose a login to save this passkey to.
/// </summary>
public static string ChooseALoginToSaveThisPasskeyTo {
get {
return ResourceManager.GetString("ChooseALoginToSaveThisPasskeyTo", resourceCulture);
}
}
/// <summary> /// <summary>
/// Looks up a localized string similar to Choose file. /// Looks up a localized string similar to Choose file.
/// </summary> /// </summary>
@@ -1894,6 +1912,24 @@ namespace Bit.Core.Resources.Localization {
} }
} }
/// <summary>
/// Looks up a localized string similar to Credential Provider service.
/// </summary>
public static string CredentialProviderService {
get {
return ResourceManager.GetString("CredentialProviderService", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to The Android Credential Provider is used for managing passkeys for use with websites and other apps on your device..
/// </summary>
public static string CredentialProviderServiceExplanationLong {
get {
return ResourceManager.GetString("CredentialProviderServiceExplanationLong", resourceCulture);
}
}
/// <summary> /// <summary>
/// Looks up a localized string similar to Credits. /// Looks up a localized string similar to Credits.
/// </summary> /// </summary>
@@ -2596,6 +2632,24 @@ namespace Bit.Core.Resources.Localization {
} }
} }
/// <summary>
/// Looks up a localized string similar to Error creating passkey.
/// </summary>
public static string ErrorCreatingPasskey {
get {
return ResourceManager.GetString("ErrorCreatingPasskey", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Error reading passkey.
/// </summary>
public static string ErrorReadingPasskey {
get {
return ResourceManager.GetString("ErrorReadingPasskey", resourceCulture);
}
}
/// <summary> /// <summary>
/// Looks up a localized string similar to EU. /// Looks up a localized string similar to EU.
/// </summary> /// </summary>
@@ -3136,6 +3190,15 @@ namespace Bit.Core.Resources.Localization {
} }
} }
/// <summary>
/// Looks up a localized string similar to 1. Go to your device&apos;s Settings &gt; Passwords &gt; Password Options.
/// </summary>
public static string FirstDotGoToYourDeviceSettingsPasswordsPasswordOptions {
get {
return ResourceManager.GetString("FirstDotGoToYourDeviceSettingsPasswordsPasswordOptions", resourceCulture);
}
}
/// <summary> /// <summary>
/// Looks up a localized string similar to First name. /// Looks up a localized string similar to First name.
/// </summary> /// </summary>
@@ -3325,6 +3388,15 @@ namespace Bit.Core.Resources.Localization {
} }
} }
/// <summary>
/// Looks up a localized string similar to Get instant access to your passwords and passkeys!.
/// </summary>
public static string GetInstantAccessToYourPasswordsAndPasskeys {
get {
return ResourceManager.GetString("GetInstantAccessToYourPasswordsAndPasskeys", resourceCulture);
}
}
/// <summary> /// <summary>
/// Looks up a localized string similar to Get master password hint. /// Looks up a localized string similar to Get master password hint.
/// </summary> /// </summary>
@@ -5128,6 +5200,15 @@ namespace Bit.Core.Resources.Localization {
} }
} }
/// <summary>
/// Looks up a localized string similar to Overwrite passkey?.
/// </summary>
public static string OverwritePasskey {
get {
return ResourceManager.GetString("OverwritePasskey", resourceCulture);
}
}
/// <summary> /// <summary>
/// Looks up a localized string similar to Ownership. /// Looks up a localized string similar to Ownership.
/// </summary> /// </summary>
@@ -5155,6 +5236,15 @@ namespace Bit.Core.Resources.Localization {
} }
} }
/// <summary>
/// Looks up a localized string similar to Passkeys for {0}.
/// </summary>
public static string PasskeysForX {
get {
return ResourceManager.GetString("PasskeysForX", resourceCulture);
}
}
/// <summary> /// <summary>
/// Looks up a localized string similar to Passkey will not be copied. /// Looks up a localized string similar to Passkey will not be copied.
/// </summary> /// </summary>
@@ -5335,6 +5425,15 @@ namespace Bit.Core.Resources.Localization {
} }
} }
/// <summary>
/// Looks up a localized string similar to Passwords.
/// </summary>
public static string Passwords {
get {
return ResourceManager.GetString("Passwords", resourceCulture);
}
}
/// <summary> /// <summary>
/// Looks up a localized string similar to This password was not found in any known data breaches. It should be safe to use.. /// Looks up a localized string similar to This password was not found in any known data breaches. It should be safe to use..
/// </summary> /// </summary>
@@ -5344,6 +5443,15 @@ namespace Bit.Core.Resources.Localization {
} }
} }
/// <summary>
/// Looks up a localized string similar to Passwords for {0}.
/// </summary>
public static string PasswordsForX {
get {
return ResourceManager.GetString("PasswordsForX", resourceCulture);
}
}
/// <summary> /// <summary>
/// Looks up a localized string similar to Password type. /// Looks up a localized string similar to Password type.
/// </summary> /// </summary>
@@ -5849,6 +5957,24 @@ namespace Bit.Core.Resources.Localization {
} }
} }
/// <summary>
/// Looks up a localized string similar to Save passkey.
/// </summary>
public static string SavePasskey {
get {
return ResourceManager.GetString("SavePasskey", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Save passkey as new login.
/// </summary>
public static string SavePasskeyAsNewLogin {
get {
return ResourceManager.GetString("SavePasskeyAsNewLogin", resourceCulture);
}
}
/// <summary> /// <summary>
/// Looks up a localized string similar to Saving.... /// Looks up a localized string similar to Saving....
/// </summary> /// </summary>
@@ -5957,6 +6083,15 @@ namespace Bit.Core.Resources.Localization {
} }
} }
/// <summary>
/// Looks up a localized string similar to 2. Turn on AutoFill.
/// </summary>
public static string SecondDotTurnOnAutoFill {
get {
return ResourceManager.GetString("SecondDotTurnOnAutoFill", resourceCulture);
}
}
/// <summary> /// <summary>
/// Looks up a localized string similar to Secure notes. /// Looks up a localized string similar to Secure notes.
/// </summary> /// </summary>
@@ -6281,6 +6416,24 @@ namespace Bit.Core.Resources.Localization {
} }
} }
/// <summary>
/// Looks up a localized string similar to Set up auto-fill.
/// </summary>
public static string SetUpAutofill {
get {
return ResourceManager.GetString("SetUpAutofill", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to To set up password auto-fill and passkey management, set Bitwarden as your preferred provider in the iOS Settings..
/// </summary>
public static string SetUpAutoFillDescriptionLong {
get {
return ResourceManager.GetString("SetUpAutoFillDescriptionLong", resourceCulture);
}
}
/// <summary> /// <summary>
/// Looks up a localized string similar to Set up TOTP. /// Looks up a localized string similar to Set up TOTP.
/// </summary> /// </summary>
@@ -6686,6 +6839,24 @@ namespace Bit.Core.Resources.Localization {
} }
} }
/// <summary>
/// Looks up a localized string similar to There was a problem creating a passkey for {0}. Try again later..
/// </summary>
public static string ThereWasAProblemCreatingAPasskeyForXTryAgainLater {
get {
return ResourceManager.GetString("ThereWasAProblemCreatingAPasskeyForXTryAgainLater", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to There was a problem reading your passkey for {0}. Try again later..
/// </summary>
public static string ThereWasAProblemReadingAPasskeyForXTryAgainLater {
get {
return ResourceManager.GetString("ThereWasAProblemReadingAPasskeyForXTryAgainLater", resourceCulture);
}
}
/// <summary> /// <summary>
/// Looks up a localized string similar to The URI {0} is already blocked. /// Looks up a localized string similar to The URI {0} is already blocked.
/// </summary> /// </summary>
@@ -6695,6 +6866,15 @@ namespace Bit.Core.Resources.Localization {
} }
} }
/// <summary>
/// Looks up a localized string similar to 3. Select &quot;Bitwarden&quot; to use for passwords and passkeys.
/// </summary>
public static string ThirdDotSelectBitwardenToUseForPasswordsAndPasskeys {
get {
return ResourceManager.GetString("ThirdDotSelectBitwardenToUseForPasswordsAndPasskeys", resourceCulture);
}
}
/// <summary> /// <summary>
/// Looks up a localized string similar to 30 days. /// Looks up a localized string similar to 30 days.
/// </summary> /// </summary>
@@ -6722,6 +6902,15 @@ namespace Bit.Core.Resources.Localization {
} }
} }
/// <summary>
/// Looks up a localized string similar to This item already contains a passkey. Are you sure you want to overwrite the current passkey?.
/// </summary>
public static string ThisItemAlreadyContainsAPasskeyAreYouSureYouWantToOverwriteTheCurrentPasskey {
get {
return ResourceManager.GetString("ThisItemAlreadyContainsAPasskeyAreYouSureYouWantToOverwriteTheCurrentPasskey", resourceCulture);
}
}
/// <summary> /// <summary>
/// Looks up a localized string similar to This request is no longer valid. /// Looks up a localized string similar to This request is no longer valid.
/// </summary> /// </summary>
@@ -7019,6 +7208,15 @@ namespace Bit.Core.Resources.Localization {
} }
} }
/// <summary>
/// Looks up a localized string similar to Unknown account.
/// </summary>
public static string UnknownAccount {
get {
return ResourceManager.GetString("UnknownAccount", resourceCulture);
}
}
/// <summary> /// <summary>
/// Looks up a localized string similar to Unknown {0} error occurred.. /// Looks up a localized string similar to Unknown {0} error occurred..
/// </summary> /// </summary>
@@ -7505,6 +7703,24 @@ namespace Bit.Core.Resources.Localization {
} }
} }
/// <summary>
/// Looks up a localized string similar to Verification required by {0}.
/// </summary>
public static string VerificationRequiredByX {
get {
return ResourceManager.GetString("VerificationRequiredByX", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Verification required for this action. Set up an unlock method in Bitwarden to continue..
/// </summary>
public static string VerificationRequiredForThisActionSetUpAnUnlockMethodInBitwardenToContinue {
get {
return ResourceManager.GetString("VerificationRequiredForThisActionSetUpAnUnlockMethodInBitwardenToContinue", resourceCulture);
}
}
/// <summary> /// <summary>
/// Looks up a localized string similar to Verify Face ID. /// Looks up a localized string similar to Verify Face ID.
/// </summary> /// </summary>
@@ -7532,6 +7748,15 @@ namespace Bit.Core.Resources.Localization {
} }
} }
/// <summary>
/// Looks up a localized string similar to Verifying identity....
/// </summary>
public static string VerifyingIdentityEllipsis {
get {
return ResourceManager.GetString("VerifyingIdentityEllipsis", resourceCulture);
}
}
/// <summary> /// <summary>
/// Looks up a localized string similar to Verify master password. /// Looks up a localized string similar to Verify master password.
/// </summary> /// </summary>

View File

@@ -1191,6 +1191,9 @@ Scanning will happen automatically.</value>
<data name="WindowsHello" xml:space="preserve"> <data name="WindowsHello" xml:space="preserve">
<value>Windows Hello</value> <value>Windows Hello</value>
</data> </data>
<data name="BitwardenCredentialProviderGoToSettings" xml:space="preserve">
<value>We were unable to automatically open the Android credential provider settings menu for you. You can navigate to the credential provider settings menu manually from Android Settings &gt; System &gt; Passwords &amp; accounts &gt; Passwords, passkeys and data services.</value>
</data>
<data name="BitwardenAutofillGoToSettings" xml:space="preserve"> <data name="BitwardenAutofillGoToSettings" xml:space="preserve">
<value>We were unable to automatically open the Android autofill settings menu for you. You can navigate to the autofill settings menu manually from Android Settings &gt; System &gt; Languages and input &gt; Advanced &gt; Autofill service.</value> <value>We were unable to automatically open the Android autofill settings menu for you. You can navigate to the autofill settings menu manually from Android Settings &gt; System &gt; Languages and input &gt; Advanced &gt; Autofill service.</value>
</data> </data>
@@ -1816,6 +1819,9 @@ Scanning will happen automatically.</value>
<data name="AccessibilityDrawOverPermissionAlert" xml:space="preserve"> <data name="AccessibilityDrawOverPermissionAlert" xml:space="preserve">
<value>Bitwarden needs attention - Turn on "Draw-Over" in "Auto-fill Services" from Bitwarden Settings</value> <value>Bitwarden needs attention - Turn on "Draw-Over" in "Auto-fill Services" from Bitwarden Settings</value>
</data> </data>
<data name="CredentialProviderService" xml:space="preserve">
<value>Credential Provider service</value>
</data>
<data name="AutofillServices" xml:space="preserve"> <data name="AutofillServices" xml:space="preserve">
<value>Auto-fill services</value> <value>Auto-fill services</value>
</data> </data>
@@ -2799,6 +2805,9 @@ Do you want to switch to this account?</value>
<data name="XHours" xml:space="preserve"> <data name="XHours" xml:space="preserve">
<value>{0} hours</value> <value>{0} hours</value>
</data> </data>
<data name="CredentialProviderServiceExplanationLong" xml:space="preserve">
<value>The Android Credential Provider is used for managing passkeys for use with websites and other apps on your device.</value>
</data>
<data name="AutofillServicesExplanationLong" xml:space="preserve"> <data name="AutofillServicesExplanationLong" xml:space="preserve">
<value>The Android Autofill Framework is used to assist in filling login information into other apps on your device.</value> <value>The Android Autofill Framework is used to assist in filling login information into other apps on your device.</value>
</data> </data>
@@ -2877,6 +2886,27 @@ Do you want to switch to this account?</value>
<data name="SetUpAnUnlockOptionToChangeYourVaultTimeoutAction" xml:space="preserve"> <data name="SetUpAnUnlockOptionToChangeYourVaultTimeoutAction" xml:space="preserve">
<value>Set up an unlock option to change your vault timeout action.</value> <value>Set up an unlock option to change your vault timeout action.</value>
</data> </data>
<data name="ChooseALoginToSaveThisPasskeyTo" xml:space="preserve">
<value>Choose a login to save this passkey to</value>
</data>
<data name="SavePasskeyAsNewLogin" xml:space="preserve">
<value>Save passkey as new login</value>
</data>
<data name="SavePasskey" xml:space="preserve">
<value>Save passkey</value>
</data>
<data name="PasskeysForX" xml:space="preserve">
<value>Passkeys for {0}</value>
</data>
<data name="PasswordsForX" xml:space="preserve">
<value>Passwords for {0}</value>
</data>
<data name="OverwritePasskey" xml:space="preserve">
<value>Overwrite passkey?</value>
</data>
<data name="ThisItemAlreadyContainsAPasskeyAreYouSureYouWantToOverwriteTheCurrentPasskey" xml:space="preserve">
<value>This item already contains a passkey. Are you sure you want to overwrite the current passkey?</value>
</data>
<data name="DuoTwoStepLoginIsRequiredForYourAccount" xml:space="preserve"> <data name="DuoTwoStepLoginIsRequiredForYourAccount" xml:space="preserve">
<value>Duo two-step login is required for your account. </value> <value>Duo two-step login is required for your account. </value>
</data> </data>
@@ -2886,4 +2916,51 @@ Do you want to switch to this account?</value>
<data name="LaunchDuo" xml:space="preserve"> <data name="LaunchDuo" xml:space="preserve">
<value>Launch Duo</value> <value>Launch Duo</value>
</data> </data>
<data name="VerificationRequiredByX" xml:space="preserve">
<value>Verification required by {0}</value>
</data>
<data name="VerificationRequiredForThisActionSetUpAnUnlockMethodInBitwardenToContinue" xml:space="preserve">
<value>Verification required for this action. Set up an unlock method in Bitwarden to continue.</value>
</data>
<data name="ErrorCreatingPasskey" xml:space="preserve">
<value>Error creating passkey</value>
</data>
<data name="ErrorReadingPasskey" xml:space="preserve">
<value>Error reading passkey</value>
</data>
<data name="ThereWasAProblemCreatingAPasskeyForXTryAgainLater" xml:space="preserve">
<value>There was a problem creating a passkey for {0}. Try again later.</value>
<comment>The parameter is the RpId</comment>
</data>
<data name="ThereWasAProblemReadingAPasskeyForXTryAgainLater" xml:space="preserve">
<value>There was a problem reading your passkey for {0}. Try again later.</value>
<comment>The parameter is the RpId</comment>
</data>
<data name="VerifyingIdentityEllipsis" xml:space="preserve">
<value>Verifying identity...</value>
</data>
<data name="Passwords" xml:space="preserve">
<value>Passwords</value>
</data>
<data name="UnknownAccount" xml:space="preserve">
<value>Unknown account</value>
</data>
<data name="SetUpAutofill" xml:space="preserve">
<value>Set up auto-fill</value>
</data>
<data name="GetInstantAccessToYourPasswordsAndPasskeys" xml:space="preserve">
<value>Get instant access to your passwords and passkeys!</value>
</data>
<data name="SetUpAutoFillDescriptionLong" xml:space="preserve">
<value>To set up password auto-fill and passkey management, set Bitwarden as your preferred provider in the iOS Settings.</value>
</data>
<data name="FirstDotGoToYourDeviceSettingsPasswordsPasswordOptions" xml:space="preserve">
<value>1. Go to your device's Settings &gt; Passwords &gt; Password Options</value>
</data>
<data name="SecondDotTurnOnAutoFill" xml:space="preserve">
<value>2. Turn on AutoFill</value>
</data>
<data name="ThirdDotSelectBitwardenToUseForPasswordsAndPasskeys" xml:space="preserve">
<value>3. Select "Bitwarden" to use for passwords and passkeys</value>
</data>
</root> </root>

View File

@@ -34,6 +34,8 @@ namespace Bit.Core.Services
private readonly II18nService _i18nService; private readonly II18nService _i18nService;
private readonly Func<ISearchService> _searchService; private readonly Func<ISearchService> _searchService;
private readonly IConfigService _configService; private readonly IConfigService _configService;
private readonly ITotpService _totpService;
private readonly IClipboardService _clipboardService;
private readonly string _clearCipherCacheKey; private readonly string _clearCipherCacheKey;
private readonly string[] _allClearCipherCacheKeys; private readonly string[] _allClearCipherCacheKeys;
private Dictionary<string, HashSet<string>> _domainMatchBlacklist = new Dictionary<string, HashSet<string>> private Dictionary<string, HashSet<string>> _domainMatchBlacklist = new Dictionary<string, HashSet<string>>
@@ -53,6 +55,8 @@ namespace Bit.Core.Services
II18nService i18nService, II18nService i18nService,
Func<ISearchService> searchService, Func<ISearchService> searchService,
IConfigService configService, IConfigService configService,
ITotpService totpService,
IClipboardService clipboardService,
string clearCipherCacheKey, string clearCipherCacheKey,
string[] allClearCipherCacheKeys) string[] allClearCipherCacheKeys)
{ {
@@ -65,6 +69,8 @@ namespace Bit.Core.Services
_i18nService = i18nService; _i18nService = i18nService;
_searchService = searchService; _searchService = searchService;
_configService = configService; _configService = configService;
_totpService = totpService;
_clipboardService = clipboardService;
_clearCipherCacheKey = clearCipherCacheKey; _clearCipherCacheKey = clearCipherCacheKey;
_allClearCipherCacheKeys = allClearCipherCacheKeys; _allClearCipherCacheKeys = allClearCipherCacheKeys;
} }
@@ -1286,6 +1292,51 @@ namespace Bit.Core.Services
cipher.PasswordHistory = encPhs; cipher.PasswordHistory = encPhs;
} }
public async Task<string> CreateNewLoginForPasskeyAsync(Fido2ConfirmNewCredentialParams newPasskeyParams)
{
var newCipher = new CipherView
{
Name = newPasskeyParams.CredentialName,
Type = CipherType.Login,
Login = new LoginView
{
Username = newPasskeyParams.UserName,
Uris = new List<LoginUriView>
{
new LoginUriView { Uri = newPasskeyParams.RpId }
}
},
Card = new CardView(),
Identity = new IdentityView(),
SecureNote = new SecureNoteView
{
Type = SecureNoteType.Generic
},
Reprompt = CipherRepromptType.None
};
var encryptedCipher = await EncryptAsync(newCipher);
await SaveWithServerAsync(encryptedCipher);
return encryptedCipher.Id;
}
public async Task CopyTotpCodeIfNeededAsync(CipherView cipher)
{
if (string.IsNullOrWhiteSpace(cipher?.Login?.Totp)
||
await _stateService.GetDisableAutoTotpCopyAsync() == true)
{
return;
}
if (cipher.OrganizationUseTotp || await _stateService.CanAccessPremiumAsync())
{
var totpCode = await _totpService.GetCodeAsync(cipher.Login.Totp);
await _clipboardService.CopyTextAsync(totpCode);
}
}
private class CipherLocaleComparer : IComparer<CipherView> private class CipherLocaleComparer : IComparer<CipherView>
{ {
private readonly II18nService _i18nService; private readonly II18nService _i18nService;

View File

@@ -1,7 +1,4 @@
using System; using System.Collections.Concurrent;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Threading.Tasks;
using Bit.Core.Abstractions; using Bit.Core.Abstractions;
namespace Bit.Core.Services namespace Bit.Core.Services
@@ -11,7 +8,8 @@ namespace Bit.Core.Services
private readonly ConcurrentDictionary<AwaiterPrecondition, TaskCompletionSource<bool>> _preconditionsTasks = new ConcurrentDictionary<AwaiterPrecondition, TaskCompletionSource<bool>> private readonly ConcurrentDictionary<AwaiterPrecondition, TaskCompletionSource<bool>> _preconditionsTasks = new ConcurrentDictionary<AwaiterPrecondition, TaskCompletionSource<bool>>
{ {
[AwaiterPrecondition.EnvironmentUrlsInited] = new TaskCompletionSource<bool>(), [AwaiterPrecondition.EnvironmentUrlsInited] = new TaskCompletionSource<bool>(),
[AwaiterPrecondition.AndroidWindowCreated] = new TaskCompletionSource<bool>() [AwaiterPrecondition.AndroidWindowCreated] = new TaskCompletionSource<bool>(),
[AwaiterPrecondition.AutofillIOSExtensionViewDidAppear] = new TaskCompletionSource<bool>()
}; };
public Task GetAwaiterForPrecondition(AwaiterPrecondition awaiterPrecondition) public Task GetAwaiterForPrecondition(AwaiterPrecondition awaiterPrecondition)
@@ -39,5 +37,15 @@ namespace Bit.Core.Services
tcs.TrySetException(ex); tcs.TrySetException(ex);
} }
} }
public void Recreate(AwaiterPrecondition awaiterPrecondition)
{
if (_preconditionsTasks.TryRemove(awaiterPrecondition, out var oldTcs))
{
oldTcs.TrySetCanceled();
_preconditionsTasks.TryAdd(awaiterPrecondition, new TaskCompletionSource<bool>());
}
}
} }
} }

View File

@@ -0,0 +1,509 @@
using Bit.Core.Abstractions;
using Bit.Core.Models.View;
using Bit.Core.Enums;
using Bit.Core.Utilities.Fido2;
using Bit.Core.Utilities;
using System.Formats.Cbor;
using System.Security.Cryptography;
namespace Bit.Core.Services
{
public class Fido2AuthenticatorService : IFido2AuthenticatorService
{
// AAGUID: d548826e-79b4-db40-a3d8-11116f7e8349
public static readonly byte[] AAGUID = new byte[] { 0xd5, 0x48, 0x82, 0x6e, 0x79, 0xb4, 0xdb, 0x40, 0xa3, 0xd8, 0x11, 0x11, 0x6f, 0x7e, 0x83, 0x49 };
private readonly ICipherService _cipherService;
private readonly ISyncService _syncService;
private readonly ICryptoFunctionService _cryptoFunctionService;
private readonly IUserVerificationMediatorService _userVerificationMediatorService;
public Fido2AuthenticatorService(ICipherService cipherService,
ISyncService syncService,
ICryptoFunctionService cryptoFunctionService,
IUserVerificationMediatorService userVerificationMediatorService)
{
_cipherService = cipherService;
_syncService = syncService;
_cryptoFunctionService = cryptoFunctionService;
_userVerificationMediatorService = userVerificationMediatorService;
}
public async Task<Fido2AuthenticatorMakeCredentialResult> MakeCredentialAsync(Fido2AuthenticatorMakeCredentialParams makeCredentialParams, IFido2MakeCredentialUserInterface userInterface)
{
if (makeCredentialParams.CredTypesAndPubKeyAlgs.All((p) => p.Alg != (int)Fido2AlgorithmIdentifier.ES256))
{
throw new NotSupportedError();
}
await userInterface.EnsureUnlockedVaultAsync();
await _syncService.FullSyncAsync(false);
var existingCipherIds = await FindExcludedCredentialsAsync(
makeCredentialParams.ExcludeCredentialDescriptorList
);
if (existingCipherIds.Length > 0)
{
await userInterface.InformExcludedCredentialAsync(existingCipherIds);
throw new NotAllowedError();
}
var response = await userInterface.ConfirmNewCredentialAsync(new Fido2ConfirmNewCredentialParams
{
CredentialName = makeCredentialParams.RpEntity.Name,
UserName = makeCredentialParams.UserEntity.Name,
UserVerificationPreference = makeCredentialParams.UserVerificationPreference,
RpId = makeCredentialParams.RpEntity.Id
});
var cipherId = response.CipherId;
var userVerified = response.UserVerified;
string credentialId;
if (cipherId == null)
{
throw new NotAllowedError();
}
try
{
var keyPair = GenerateKeyPair();
var fido2Credential = CreateCredentialView(makeCredentialParams, keyPair.privateKey);
var encrypted = await _cipherService.GetAsync(cipherId);
var cipher = await encrypted.DecryptAsync();
if (!userVerified
&&
await _userVerificationMediatorService.ShouldEnforceFido2RequiredUserVerificationAsync(new Fido2UserVerificationOptions(
cipher.Reprompt != CipherRepromptType.None,
makeCredentialParams.UserVerificationPreference,
userInterface.HasVaultBeenUnlockedInThisTransaction)))
{
throw new NotAllowedError();
}
cipher.Login.Fido2Credentials = new List<Fido2CredentialView> { fido2Credential };
var reencrypted = await _cipherService.EncryptAsync(cipher);
await _cipherService.SaveWithServerAsync(reencrypted);
credentialId = fido2Credential.CredentialId;
var authData = await GenerateAuthDataAsync(
rpId: makeCredentialParams.RpEntity.Id,
counter: fido2Credential.CounterValue,
userPresence: true,
userVerification: userVerified,
credentialId: credentialId.GuidToRawFormat(),
publicKey: keyPair.publicKey
);
return new Fido2AuthenticatorMakeCredentialResult
{
CredentialId = credentialId.GuidToRawFormat(),
AttestationObject = EncodeAttestationObject(authData),
AuthData = authData,
PublicKey = keyPair.publicKey.ExportDer(),
PublicKeyAlgorithm = (int)Fido2AlgorithmIdentifier.ES256,
};
}
catch (NotAllowedError)
{
throw;
}
catch (Exception ex)
{
LoggerHelper.LogEvenIfCantBeResolved(ex);
throw new UnknownError();
}
}
public async Task<Fido2AuthenticatorGetAssertionResult> GetAssertionAsync(Fido2AuthenticatorGetAssertionParams assertionParams, IFido2GetAssertionUserInterface userInterface)
{
List<CipherView> cipherOptions;
await userInterface.EnsureUnlockedVaultAsync();
await _syncService.FullSyncAsync(false);
if (assertionParams.AllowCredentialDescriptorList?.Length > 0)
{
cipherOptions = await FindCredentialsByIdAsync(
assertionParams.AllowCredentialDescriptorList,
assertionParams.RpId
);
}
else
{
cipherOptions = await FindCredentialsByRpAsync(assertionParams.RpId);
}
if (cipherOptions.Count == 0)
{
throw new NotAllowedError();
}
var response = await userInterface.PickCredentialAsync(
cipherOptions.Select((cipher) => new Fido2GetAssertionUserInterfaceCredential
{
CipherId = cipher.Id,
UserVerificationPreference = Fido2UserVerificationPreferenceExtensions.GetUserVerificationPreferenceFrom(assertionParams.UserVerificationPreference, cipher.Reprompt)
}).ToArray()
);
var selectedCipherId = response.CipherId;
var userVerified = response.UserVerified;
var selectedCipher = cipherOptions.FirstOrDefault((c) => c.Id == selectedCipherId);
if (selectedCipher == null)
{
throw new NotAllowedError();
}
if (!userVerified
&&
await _userVerificationMediatorService.ShouldEnforceFido2RequiredUserVerificationAsync(new Fido2UserVerificationOptions(
selectedCipher.Reprompt != CipherRepromptType.None,
assertionParams.UserVerificationPreference,
userInterface.HasVaultBeenUnlockedInThisTransaction)))
{
throw new NotAllowedError();
}
try
{
var selectedFido2Credential = selectedCipher.Login.MainFido2Credential;
var selectedCredentialId = selectedFido2Credential.CredentialId;
await _cipherService.UpdateLastUsedDateAsync(selectedCipher.Id);
if (selectedFido2Credential.CounterValue != 0)
{
++selectedFido2Credential.CounterValue;
var encrypted = await _cipherService.EncryptAsync(selectedCipher);
await _cipherService.SaveWithServerAsync(encrypted);
}
var authenticatorData = await GenerateAuthDataAsync(
rpId: selectedFido2Credential.RpId,
userPresence: true,
userVerification: userVerified,
counter: selectedFido2Credential.CounterValue
);
var signature = GenerateSignature(
authData: authenticatorData,
clientDataHash: assertionParams.Hash,
privateKey: selectedFido2Credential.KeyBytes
);
return new Fido2AuthenticatorGetAssertionResult
{
SelectedCredential = new Fido2AuthenticatorGetAssertionSelectedCredential
{
Id = selectedCredentialId.GuidToRawFormat(),
UserHandle = selectedFido2Credential.UserHandleValue,
Cipher = selectedCipher
},
AuthenticatorData = authenticatorData,
Signature = signature
};
}
catch (Exception ex)
{
LoggerHelper.LogEvenIfCantBeResolved(ex);
throw new UnknownError();
}
}
public async Task<Fido2AuthenticatorDiscoverableCredentialMetadata[]> SilentCredentialDiscoveryAsync(string rpId)
{
var credentials = (await FindCredentialsByRpAsync(rpId)).Select(cipher => new Fido2AuthenticatorDiscoverableCredentialMetadata
{
Type = Constants.DefaultFido2CredentialType,
Id = cipher.Login.MainFido2Credential.CredentialId.GuidToRawFormat(),
RpId = cipher.Login.MainFido2Credential.RpId,
UserHandle = cipher.Login.MainFido2Credential.UserHandleValue,
UserName = cipher.Login.MainFido2Credential.UserName
}).ToArray();
return credentials;
}
/// <summary>
/// Finds existing crendetials and returns the `CipherId` for each one
/// </summary>
private async Task<string[]> FindExcludedCredentialsAsync(
PublicKeyCredentialDescriptor[] credentials
)
{
if (credentials == null || credentials.Length == 0)
{
return Array.Empty<string>();
}
var ids = new List<string>();
foreach (var credential in credentials)
{
try
{
ids.Add(credential.Id.GuidToStandardFormat());
}
catch { }
}
if (ids.Count == 0)
{
return Array.Empty<string>();
}
var ciphers = await _cipherService.GetAllDecryptedAsync();
return ciphers
.FindAll(
(cipher) =>
!cipher.IsDeleted &&
cipher.OrganizationId == null &&
cipher.Type == CipherType.Login &&
cipher.Login.HasFido2Credentials &&
ids.Contains(cipher.Login.MainFido2Credential.CredentialId)
)
.Select((cipher) => cipher.Id)
.ToArray();
}
private async Task<List<CipherView>> FindCredentialsByIdAsync(PublicKeyCredentialDescriptor[] credentials, string rpId)
{
var ids = new List<string>();
foreach (var credential in credentials)
{
try
{
ids.Add(credential.Id.GuidToStandardFormat());
}
catch { }
}
if (ids.Count == 0)
{
return new List<CipherView>();
}
var ciphers = await _cipherService.GetAllDecryptedAsync();
return ciphers.FindAll((cipher) =>
!cipher.IsDeleted &&
cipher.Type == CipherType.Login &&
cipher.Login.HasFido2Credentials &&
cipher.Login.MainFido2Credential.RpId == rpId &&
ids.Contains(cipher.Login.MainFido2Credential.CredentialId)
);
}
private async Task<List<CipherView>> FindCredentialsByRpAsync(string rpId)
{
var ciphers = await _cipherService.GetAllDecryptedAsync();
return ciphers.FindAll((cipher) =>
!cipher.IsDeleted &&
cipher.Type == CipherType.Login &&
cipher.Login.HasFido2Credentials &&
cipher.Login.MainFido2Credential.RpId == rpId &&
cipher.Login.MainFido2Credential.DiscoverableValue
);
}
// TODO: Move this to a separate service
private (PublicKey publicKey, byte[] privateKey) GenerateKeyPair()
{
var dsa = ECDsa.Create();
dsa.GenerateKey(ECCurve.NamedCurves.nistP256);
var privateKey = dsa.ExportPkcs8PrivateKey();
return (new PublicKey(dsa), privateKey);
}
private Fido2CredentialView CreateCredentialView(Fido2AuthenticatorMakeCredentialParams makeCredentialsParams, byte[] privateKey)
{
return new Fido2CredentialView
{
CredentialId = Guid.NewGuid().ToString(),
KeyType = Constants.DefaultFido2CredentialType,
KeyAlgorithm = Constants.DefaultFido2CredentialAlgorithm,
KeyCurve = Constants.DefaultFido2CredentialCurve,
KeyValue = CoreHelpers.Base64UrlEncode(privateKey),
RpId = makeCredentialsParams.RpEntity.Id,
UserHandle = CoreHelpers.Base64UrlEncode(makeCredentialsParams.UserEntity.Id),
UserName = makeCredentialsParams.UserEntity.Name,
CounterValue = 0,
RpName = makeCredentialsParams.RpEntity.Name,
UserDisplayName = makeCredentialsParams.UserEntity.DisplayName,
DiscoverableValue = makeCredentialsParams.RequireResidentKey,
CreationDate = DateTime.UtcNow
};
}
private async Task<byte[]> GenerateAuthDataAsync(
string rpId,
bool userVerification,
bool userPresence,
int counter,
byte[] credentialId = null,
PublicKey publicKey = null
)
{
var isAttestation = credentialId != null && publicKey != null;
List<byte> authData = new List<byte>();
var rpIdHash = await _cryptoFunctionService.HashAsync(rpId, CryptoHashAlgorithm.Sha256);
authData.AddRange(rpIdHash);
var flags = AuthDataFlags(
extensionData: false,
attestationData: isAttestation,
userVerification: userVerification,
userPresence: userPresence
);
authData.Add(flags);
authData.AddRange(new List<byte> {
(byte)(counter >> 24),
(byte)(counter >> 16),
(byte)(counter >> 8),
(byte)counter
});
if (isAttestation)
{
var attestedCredentialData = new List<byte>();
attestedCredentialData.AddRange(AAGUID);
// credentialIdLength (2 bytes) and credential Id
var credentialIdLength = new byte[] {
(byte)((credentialId.Length - (credentialId.Length & 0xff)) / 256),
(byte)(credentialId.Length & 0xff)
};
attestedCredentialData.AddRange(credentialIdLength);
attestedCredentialData.AddRange(credentialId);
attestedCredentialData.AddRange(publicKey.ExportCose());
authData.AddRange(attestedCredentialData);
}
return authData.ToArray();
}
private byte AuthDataFlags(bool extensionData, bool attestationData, bool userVerification, bool userPresence, bool backupEligibility = true, bool backupState = true)
{
byte flags = 0;
if (extensionData)
{
flags |= 0b1000000;
}
if (attestationData)
{
flags |= 0b01000000;
}
if (backupState)
{
flags |= 0b00010000;
}
if (backupEligibility)
{
flags |= 0b00001000;
}
if (userVerification)
{
flags |= 0b00000100;
}
if (userPresence)
{
flags |= 0b00000001;
}
return flags;
}
private byte[] EncodeAttestationObject(byte[] authData)
{
var attestationObject = new CborWriter(CborConformanceMode.Ctap2Canonical);
attestationObject.WriteStartMap(3);
attestationObject.WriteTextString("fmt");
attestationObject.WriteTextString("none");
attestationObject.WriteTextString("attStmt");
attestationObject.WriteStartMap(0);
attestationObject.WriteEndMap();
attestationObject.WriteTextString("authData");
attestationObject.WriteByteString(authData);
attestationObject.WriteEndMap();
return attestationObject.Encode();
}
// TODO: Move this to a separate service
private byte[] GenerateSignature(byte[] authData, byte[] clientDataHash, byte[] privateKey)
{
var sigBase = authData.Concat(clientDataHash).ToArray();
var dsa = ECDsa.Create();
dsa.ImportPkcs8PrivateKey(privateKey, out var bytesRead);
if (bytesRead == 0)
{
throw new Exception("Failed to import private key");
}
return dsa.SignData(sigBase, HashAlgorithmName.SHA256, DSASignatureFormat.Rfc3279DerSequence);
}
private class PublicKey
{
private readonly ECDsa _dsa;
public PublicKey(ECDsa dsa)
{
_dsa = dsa;
}
public byte[] X => _dsa.ExportParameters(false).Q.X;
public byte[] Y => _dsa.ExportParameters(false).Q.Y;
public byte[] ExportDer()
{
return _dsa.ExportSubjectPublicKeyInfo();
}
public byte[] ExportCose()
{
var result = new CborWriter(CborConformanceMode.Ctap2Canonical);
result.WriteStartMap(5);
// kty = EC2
result.WriteInt32(1);
result.WriteInt32(2);
// alg = ES256
result.WriteInt32(3);
result.WriteInt32((int)Fido2AlgorithmIdentifier.ES256);
// crv = P-256
result.WriteInt32(-1);
result.WriteInt32(1);
// x
result.WriteInt32(-2);
result.WriteByteString(X);
// y
result.WriteInt32(-3);
result.WriteByteString(Y);
result.WriteEndMap();
return result.Encode();
}
}
}
}

View File

@@ -0,0 +1,261 @@
using System.Text;
using System.Text.Json;
using Bit.Core.Abstractions;
using Bit.Core.Enums;
using Bit.Core.Utilities;
using Bit.Core.Utilities.Fido2;
namespace Bit.Core.Services
{
public class Fido2ClientService : IFido2ClientService
{
private readonly IStateService _stateService;
private readonly IEnvironmentService _environmentService;
private readonly ICryptoFunctionService _cryptoFunctionService;
private readonly IFido2AuthenticatorService _fido2AuthenticatorService;
private readonly IFido2GetAssertionUserInterface _getAssertionUserInterface;
private readonly IFido2MakeCredentialUserInterface _makeCredentialUserInterface;
public Fido2ClientService(
IStateService stateService,
IEnvironmentService environmentService,
ICryptoFunctionService cryptoFunctionService,
IFido2AuthenticatorService fido2AuthenticatorService,
IFido2GetAssertionUserInterface getAssertionUserInterface,
IFido2MakeCredentialUserInterface makeCredentialUserInterface)
{
_stateService = stateService;
_environmentService = environmentService;
_cryptoFunctionService = cryptoFunctionService;
_fido2AuthenticatorService = fido2AuthenticatorService;
_getAssertionUserInterface = getAssertionUserInterface;
_makeCredentialUserInterface = makeCredentialUserInterface;
}
public async Task<Fido2ClientCreateCredentialResult> CreateCredentialAsync(Fido2ClientCreateCredentialParams createCredentialParams)
{
var blockedUris = await _stateService.GetAutofillBlacklistedUrisAsync();
var domain = CoreHelpers.GetHostname(createCredentialParams.Origin);
if (blockedUris != null && blockedUris.Contains(domain))
{
throw new Fido2ClientException(
Fido2ClientException.ErrorCode.UriBlockedError,
"Origin is blocked by the user");
}
if (!await _stateService.IsAuthenticatedAsync())
{
throw new Fido2ClientException(
Fido2ClientException.ErrorCode.InvalidStateError,
"No user is logged in");
}
if (createCredentialParams.Origin == _environmentService.GetWebVaultUrl())
{
throw new Fido2ClientException(
Fido2ClientException.ErrorCode.NotAllowedError,
"Saving Bitwarden credentials in a Bitwarden vault is not allowed");
}
if (!createCredentialParams.SameOriginWithAncestors)
{
throw new Fido2ClientException(
Fido2ClientException.ErrorCode.NotAllowedError,
"Credential creation is now allowed from embedded contexts with different origins");
}
if (createCredentialParams.User.Id.Length < 1 || createCredentialParams.User.Id.Length > 64)
{
throw new Fido2ClientException(
Fido2ClientException.ErrorCode.TypeError,
"The length of user.id is not between 1 and 64 bytes (inclusive)");
}
if (!createCredentialParams.Origin.StartsWith("https://"))
{
throw new Fido2ClientException(
Fido2ClientException.ErrorCode.SecurityError,
"Origin is not a valid https origin");
}
if (!Fido2DomainUtils.IsValidRpId(createCredentialParams.Rp.Id, createCredentialParams.Origin))
{
throw new Fido2ClientException(
Fido2ClientException.ErrorCode.SecurityError,
"RP ID cannot be used with this origin");
}
PublicKeyCredentialParameters[] credTypesAndPubKeyAlgs;
if (createCredentialParams.PubKeyCredParams?.Length > 0)
{
// Filter out all unsupported algorithms
credTypesAndPubKeyAlgs = createCredentialParams.PubKeyCredParams
.Where(kp => kp.Alg == (int)Fido2AlgorithmIdentifier.ES256 && kp.Type == Constants.DefaultFido2CredentialType)
.ToArray();
}
else
{
// Assign default algorithms
credTypesAndPubKeyAlgs = new PublicKeyCredentialParameters[]
{
new PublicKeyCredentialParameters { Alg = (int) Fido2AlgorithmIdentifier.ES256, Type = Constants.DefaultFido2CredentialType },
new PublicKeyCredentialParameters { Alg = (int) Fido2AlgorithmIdentifier.RS256, Type = Constants.DefaultFido2CredentialType }
};
}
if (credTypesAndPubKeyAlgs.Length == 0)
{
throw new Fido2ClientException(Fido2ClientException.ErrorCode.NotSupportedError, "No supported algorithms found");
}
var clientDataJSON = JsonSerializer.Serialize(new
{
type = "webauthn.create",
challenge = CoreHelpers.Base64UrlEncode(createCredentialParams.Challenge),
origin = createCredentialParams.Origin,
crossOrigin = !createCredentialParams.SameOriginWithAncestors,
// tokenBinding: {} // Not supported
});
var clientDataJSONBytes = Encoding.UTF8.GetBytes(clientDataJSON);
var clientDataHash = await _cryptoFunctionService.HashAsync(clientDataJSONBytes, CryptoHashAlgorithm.Sha256);
var makeCredentialParams = MapToMakeCredentialParams(createCredentialParams, credTypesAndPubKeyAlgs, clientDataHash);
try
{
var makeCredentialResult = await _fido2AuthenticatorService.MakeCredentialAsync(makeCredentialParams, _makeCredentialUserInterface);
return new Fido2ClientCreateCredentialResult
{
CredentialId = makeCredentialResult.CredentialId,
AttestationObject = makeCredentialResult.AttestationObject,
AuthData = makeCredentialResult.AuthData,
ClientDataJSON = clientDataJSONBytes,
PublicKey = makeCredentialResult.PublicKey,
PublicKeyAlgorithm = makeCredentialResult.PublicKeyAlgorithm,
Transports = createCredentialParams.Rp.Id == "google.com" ? new string[] { "internal", "usb" } : new string[] { "internal" } // workaround for a bug on Google's side
};
}
catch (InvalidStateError)
{
throw new Fido2ClientException(Fido2ClientException.ErrorCode.InvalidStateError, "Unknown invalid state encountered");
}
catch (Exception)
{
throw new Fido2ClientException(Fido2ClientException.ErrorCode.UnknownError, $"An unknown error occurred");
}
}
public async Task<Fido2ClientAssertCredentialResult> AssertCredentialAsync(Fido2ClientAssertCredentialParams assertCredentialParams)
{
var blockedUris = await _stateService.GetAutofillBlacklistedUrisAsync();
var domain = CoreHelpers.GetHostname(assertCredentialParams.Origin);
if (blockedUris != null && blockedUris.Contains(domain))
{
throw new Fido2ClientException(
Fido2ClientException.ErrorCode.UriBlockedError,
"Origin is blocked by the user");
}
if (!await _stateService.IsAuthenticatedAsync())
{
throw new Fido2ClientException(
Fido2ClientException.ErrorCode.InvalidStateError,
"No user is logged in");
}
if (assertCredentialParams.Origin == _environmentService.GetWebVaultUrl())
{
throw new Fido2ClientException(
Fido2ClientException.ErrorCode.NotAllowedError,
"Saving Bitwarden credentials in a Bitwarden vault is not allowed");
}
if (!assertCredentialParams.Origin.StartsWith("https://"))
{
throw new Fido2ClientException(
Fido2ClientException.ErrorCode.SecurityError,
"Origin is not a valid https origin");
}
if (!Fido2DomainUtils.IsValidRpId(assertCredentialParams.RpId, assertCredentialParams.Origin))
{
throw new Fido2ClientException(
Fido2ClientException.ErrorCode.SecurityError,
"RP ID cannot be used with this origin");
}
var clientDataJSON = JsonSerializer.Serialize(new
{
type = "webauthn.get",
challenge = CoreHelpers.Base64UrlEncode(assertCredentialParams.Challenge),
origin = assertCredentialParams.Origin,
crossOrigin = !assertCredentialParams.SameOriginWithAncestors,
});
var clientDataJSONBytes = Encoding.UTF8.GetBytes(clientDataJSON);
var clientDataHash = await _cryptoFunctionService.HashAsync(clientDataJSONBytes, CryptoHashAlgorithm.Sha256);
var getAssertionParams = MapToGetAssertionParams(assertCredentialParams, clientDataHash);
try
{
var getAssertionResult = await _fido2AuthenticatorService.GetAssertionAsync(getAssertionParams, _getAssertionUserInterface);
return new Fido2ClientAssertCredentialResult
{
AuthenticatorData = getAssertionResult.AuthenticatorData,
ClientDataJSON = clientDataJSONBytes,
Id = CoreHelpers.Base64UrlEncode(getAssertionResult.SelectedCredential.Id),
RawId = getAssertionResult.SelectedCredential.Id,
Signature = getAssertionResult.Signature,
UserHandle = getAssertionResult.SelectedCredential.UserHandle,
Cipher = getAssertionResult.SelectedCredential.Cipher
};
}
catch (InvalidStateError)
{
throw new Fido2ClientException(Fido2ClientException.ErrorCode.InvalidStateError, "Unknown invalid state encountered");
}
catch (Exception)
{
throw new Fido2ClientException(Fido2ClientException.ErrorCode.UnknownError, $"An unknown error occurred");
}
throw new NotImplementedException();
}
private Fido2AuthenticatorMakeCredentialParams MapToMakeCredentialParams(
Fido2ClientCreateCredentialParams createCredentialParams,
PublicKeyCredentialParameters[] credTypesAndPubKeyAlgs,
byte[] clientDataHash)
{
var requireResidentKey = createCredentialParams.AuthenticatorSelection?.ResidentKey == "required" ||
createCredentialParams.AuthenticatorSelection?.ResidentKey == "preferred" ||
(createCredentialParams.AuthenticatorSelection?.ResidentKey == null &&
createCredentialParams.AuthenticatorSelection?.RequireResidentKey == true);
return new Fido2AuthenticatorMakeCredentialParams
{
RequireResidentKey = requireResidentKey,
UserVerificationPreference = Fido2UserVerificationPreferenceExtensions.ToFido2UserVerificationPreference(createCredentialParams.AuthenticatorSelection?.UserVerification),
ExcludeCredentialDescriptorList = createCredentialParams.ExcludeCredentials,
CredTypesAndPubKeyAlgs = credTypesAndPubKeyAlgs,
Hash = clientDataHash,
RpEntity = createCredentialParams.Rp,
UserEntity = createCredentialParams.User,
Extensions = createCredentialParams.Extensions
};
}
private Fido2AuthenticatorGetAssertionParams MapToGetAssertionParams(
Fido2ClientAssertCredentialParams assertCredentialParams,
byte[] cliendDataHash)
{
return new Fido2AuthenticatorGetAssertionParams {
RpId = assertCredentialParams.RpId,
Challenge = assertCredentialParams.Challenge,
AllowCredentialDescriptorList = assertCredentialParams.AllowCredentials,
UserVerificationPreference = Fido2UserVerificationPreferenceExtensions.ToFido2UserVerificationPreference(assertCredentialParams?.UserVerification),
Hash = cliendDataHash
};
}
}
}

View File

@@ -0,0 +1,60 @@
using Bit.Core.Abstractions;
using Bit.Core.Utilities.Fido2;
namespace Bit.Core.Services
{
public class Fido2MediatorService : IFido2MediatorService
{
private readonly IFido2AuthenticatorService _fido2AuthenticatorService;
private readonly IFido2ClientService _fido2ClientService;
private readonly ICipherService _cipherService;
public Fido2MediatorService(IFido2AuthenticatorService fido2AuthenticatorService,
IFido2ClientService fido2ClientService,
ICipherService cipherService)
{
_fido2AuthenticatorService = fido2AuthenticatorService;
_fido2ClientService = fido2ClientService;
_cipherService = cipherService;
}
public async Task<Fido2ClientAssertCredentialResult> AssertCredentialAsync(Fido2ClientAssertCredentialParams assertCredentialParams)
{
var result = await _fido2ClientService.AssertCredentialAsync(assertCredentialParams);
if (result?.Cipher != null)
{
await _cipherService.CopyTotpCodeIfNeededAsync(result.Cipher);
}
return result;
}
public Task<Fido2ClientCreateCredentialResult> CreateCredentialAsync(Fido2ClientCreateCredentialParams createCredentialParams)
{
return _fido2ClientService.CreateCredentialAsync(createCredentialParams);
}
public async Task<Fido2AuthenticatorGetAssertionResult> GetAssertionAsync(Fido2AuthenticatorGetAssertionParams assertionParams, IFido2GetAssertionUserInterface userInterface)
{
var result = await _fido2AuthenticatorService.GetAssertionAsync(assertionParams, userInterface);
if (result?.SelectedCredential?.Cipher != null)
{
await _cipherService.CopyTotpCodeIfNeededAsync(result.SelectedCredential.Cipher);
}
return result;
}
public Task<Fido2AuthenticatorMakeCredentialResult> MakeCredentialAsync(Fido2AuthenticatorMakeCredentialParams makeCredentialParams, IFido2MakeCredentialUserInterface userInterface)
{
return _fido2AuthenticatorService.MakeCredentialAsync(makeCredentialParams, userInterface);
}
public Task<Fido2AuthenticatorDiscoverableCredentialMetadata[]> SilentCredentialDiscoveryAsync(string rpId)
{
return _fido2AuthenticatorService.SilentCredentialDiscoveryAsync(rpId);
}
}
}

View File

@@ -0,0 +1,66 @@
using System.Runtime.CompilerServices;
using System.Text;
using Bit.Core.Abstractions;
#if IOS
using UIKit;
#endif
namespace Bit.Core.Services
{
/// <summary>
/// This logger can be used to help debug iOS extensions where we cannot use the .NET debugger yet
/// so we can use this that copies the logs to the clipboard so one
/// can paste them and analyze its output.
/// </summary>
public class ClipLogger : ILogger
{
private static readonly StringBuilder _currentBreadcrumbs = new StringBuilder();
static ILogger _instance;
public static ILogger Instance
{
get
{
if (_instance is null)
{
_instance = new ClipLogger();
}
return _instance;
}
}
protected ClipLogger()
{
}
public static void Log(string breadcrumb)
{
_currentBreadcrumbs.AppendLine($"{DateTime.Now.ToShortTimeString()}: {breadcrumb}");
#if IOS
MainThread.BeginInvokeOnMainThread(() => UIPasteboard.General.String = _currentBreadcrumbs.ToString());
#endif
}
public void Error(string message, IDictionary<string, string> extraData = null, [CallerMemberName] string memberName = "", [CallerFilePath] string sourceFilePath = "", [CallerLineNumber] int sourceLineNumber = 0)
{
var classAndMethod = $"{Path.GetFileNameWithoutExtension(sourceFilePath)}.{memberName}";
var filePathAndLineNumber = $"{Path.GetFileName(sourceFilePath)}:{sourceLineNumber}";
var properties = new Dictionary<string, string>
{
["File"] = filePathAndLineNumber,
["Method"] = memberName
};
Log(message ?? $"Error found in: {classAndMethod}, {filePathAndLineNumber}");
}
public void Exception(Exception ex) => Log(ex?.ToString());
public Task InitAsync() => Task.CompletedTask;
public Task<bool> IsEnabled() => Task.FromResult(true);
public Task SetEnabled(bool value) => Task.CompletedTask;
}
}

View File

@@ -1,5 +1,4 @@
using System; using Bit.Core.Abstractions;
using Bit.Core.Abstractions;
using Bit.Core.Utilities; using Bit.Core.Utilities;
namespace Bit.Core.Services namespace Bit.Core.Services
@@ -22,9 +21,9 @@ namespace Bit.Core.Services
#if !FDROID #if !FDROID
// just in case the caller throws the exception in a moment where the logger can't be resolved // just in case the caller throws the exception in a moment where the logger can't be resolved
// we need to track the error as well // we need to track the error as well
Microsoft.AppCenter.Crashes.Crashes.TrackError(ex); //Microsoft.AppCenter.Crashes.Crashes.TrackError(ex);
ClipLogger.Log(ex?.ToString());
#endif #endif
} }
} }
} }

View File

@@ -57,7 +57,7 @@ namespace Bit.App.Services
return passwordValid; return passwordValid;
} }
private async Task<bool> ShouldByPassMasterPasswordRepromptAsync() public async Task<bool> ShouldByPassMasterPasswordRepromptAsync()
{ {
return await _cryptoService.GetMasterKeyHashAsync() is null; return await _cryptoService.GetMasterKeyHashAsync() is null;
} }

View File

@@ -1,19 +1,12 @@
using System; using Bit.App.Abstractions;
using System.Collections.Generic;
using System.Threading.Tasks;
using Bit.App.Abstractions;
using Bit.App.Models; using Bit.App.Models;
using Bit.Core.Resources.Localization;
using Bit.Core.Abstractions; using Bit.Core.Abstractions;
using Bit.Core.Enums; using Bit.Core.Enums;
using Bit.Core.Resources.Localization;
using Bit.Core.Services;
using Bit.Core.Utilities; using Bit.Core.Utilities;
using Plugin.Fingerprint; using Plugin.Fingerprint;
using Plugin.Fingerprint.Abstractions; using Plugin.Fingerprint.Abstractions;
using Microsoft.Maui.ApplicationModel.DataTransfer;
using Microsoft.Maui.ApplicationModel;
using Microsoft.Maui.Devices;
using Microsoft.Maui.Controls;
using Microsoft.Maui;
namespace Bit.App.Services namespace Bit.App.Services
{ {
@@ -245,31 +238,34 @@ namespace Bit.App.Services
return await stateService.IsAccountBiometricIntegrityValidAsync(bioIntegritySrcKey); return await stateService.IsAccountBiometricIntegrityValidAsync(bioIntegritySrcKey);
} }
public async Task<bool> AuthenticateBiometricAsync(string text = null, string fallbackText = null, public async Task<bool?> AuthenticateBiometricAsync(string text = null, string fallbackText = null,
Action fallback = null, bool logOutOnTooManyAttempts = false) Action fallback = null, bool logOutOnTooManyAttempts = false, bool allowAlternativeAuthentication = false)
{ {
try try
{ {
if (text == null) if (text == null)
{ {
text = AppResources.BiometricsDirection; text = AppResources.BiometricsDirection;
// 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 IOS
if (Device.RuntimePlatform == Device.iOS) var supportsFace = await _deviceActionService.SupportsFaceBiometricAsync();
{ text = supportsFace ? AppResources.FaceIDDirection : AppResources.FingerprintDirection;
var supportsFace = await _deviceActionService.SupportsFaceBiometricAsync(); #endif
text = supportsFace ? AppResources.FaceIDDirection : AppResources.FingerprintDirection;
}
} }
var biometricRequest = new AuthenticationRequestConfiguration(AppResources.Bitwarden, text) var biometricRequest = new AuthenticationRequestConfiguration(AppResources.Bitwarden, text)
{ {
CancelTitle = AppResources.Cancel, CancelTitle = AppResources.Cancel,
FallbackTitle = fallbackText FallbackTitle = fallbackText,
AllowAlternativeAuthentication = allowAlternativeAuthentication
}; };
var result = await CrossFingerprint.Current.AuthenticateAsync(biometricRequest); var result = await CrossFingerprint.Current.AuthenticateAsync(biometricRequest);
if (result.Authenticated) if (result.Authenticated)
{ {
return true; return true;
} }
if (result.Status == FingerprintAuthenticationResultStatus.Canceled)
{
return null;
}
if (result.Status == FingerprintAuthenticationResultStatus.FallbackRequested) if (result.Status == FingerprintAuthenticationResultStatus.FallbackRequested)
{ {
fallback?.Invoke(); fallback?.Invoke();

View File

@@ -4,6 +4,7 @@ using System.Text;
using System.Threading.Tasks; using System.Threading.Tasks;
using Bit.Core.Abstractions; using Bit.Core.Abstractions;
using Bit.Core.Enums; using Bit.Core.Enums;
using Bit.Core.Models.Domain;
using PCLCrypto; using PCLCrypto;
using static PCLCrypto.WinRTCrypto; using static PCLCrypto.WinRTCrypto;

View File

@@ -1688,6 +1688,11 @@ namespace Bit.Core.Services
await _storageService.SaveAsync(Constants.iOSExtensionActiveUserIdKey, userId); await _storageService.SaveAsync(Constants.iOSExtensionActiveUserIdKey, userId);
} }
public async Task ReloadStateAsync()
{
_state = await GetStateFromStorageAsync() ?? new State();
}
private async Task CheckStateAsync() private async Task CheckStateAsync()
{ {
if (!_migrationChecked) if (!_migrationChecked)
@@ -1699,7 +1704,7 @@ namespace Bit.Core.Services
if (_state == null) if (_state == null)
{ {
_state = await GetStateFromStorageAsync() ?? new State(); await ReloadStateAsync();
} }
} }

View File

@@ -1,5 +1,6 @@
using System.Threading.Tasks; using Bit.Core.Abstractions;
using Bit.Core.Abstractions; using Bit.Core.Models.Domain;
using Bit.Core.Services;
namespace Bit.App.Services namespace Bit.App.Services
{ {
@@ -7,11 +8,25 @@ namespace Bit.App.Services
{ {
private readonly IStateService _stateService; private readonly IStateService _stateService;
private readonly ICryptoService _cryptoService; private readonly ICryptoService _cryptoService;
private readonly IVaultTimeoutService _vaultTimeoutService;
public UserPinService(IStateService stateService, ICryptoService cryptoService) public UserPinService(IStateService stateService, ICryptoService cryptoService, IVaultTimeoutService vaultTimeoutService)
{ {
_stateService = stateService; _stateService = stateService;
_cryptoService = cryptoService; _cryptoService = cryptoService;
_vaultTimeoutService = vaultTimeoutService;
}
public async Task<bool> IsPinLockEnabledAsync()
{
var pinLockType = await _vaultTimeoutService.GetPinLockTypeAsync();
var ephemeralPinSet = await _stateService.GetPinKeyEncryptedUserKeyEphemeralAsync()
?? await _stateService.GetPinProtectedKeyAsync();
return (pinLockType == PinLockType.Transient && ephemeralPinSet != null)
||
pinLockType == PinLockType.Persistent;
} }
public async Task SetupPinAsync(string pin, bool requireMasterPasswordOnRestart) public async Task SetupPinAsync(string pin, bool requireMasterPasswordOnRestart)
@@ -34,5 +49,59 @@ namespace Bit.App.Services
await _stateService.SetPinKeyEncryptedUserKeyAsync(protectedPinKey); await _stateService.SetPinKeyEncryptedUserKeyAsync(protectedPinKey);
} }
} }
public async Task<bool> VerifyPinAsync(string inputPin)
{
var (email, kdfConfig) = await _stateService.GetActiveUserCustomDataAsync(a => a?.Profile is null ? (null, default) : (a.Profile.Email, new KdfConfig(a.Profile)));
if (kdfConfig.Type is null)
{
return false;
}
return await VerifyPinAsync(inputPin, email, kdfConfig, await _vaultTimeoutService.GetPinLockTypeAsync());
}
public async Task<bool> VerifyPinAsync(string inputPin, string email, KdfConfig kdfConfig, PinLockType pinLockType)
{
EncString userKeyPin = null;
EncString oldPinProtected = null;
if (pinLockType == PinLockType.Persistent)
{
userKeyPin = await _stateService.GetPinKeyEncryptedUserKeyAsync();
var oldEncryptedKey = await _stateService.GetPinProtectedAsync();
oldPinProtected = oldEncryptedKey != null ? new EncString(oldEncryptedKey) : null;
}
else if (pinLockType == PinLockType.Transient)
{
userKeyPin = await _stateService.GetPinKeyEncryptedUserKeyEphemeralAsync();
oldPinProtected = await _stateService.GetPinProtectedKeyAsync();
}
UserKey userKey;
if (oldPinProtected != null)
{
userKey = await _cryptoService.DecryptAndMigrateOldPinKeyAsync(
pinLockType == PinLockType.Transient,
inputPin,
email,
kdfConfig,
oldPinProtected
);
}
else
{
userKey = await _cryptoService.DecryptUserKeyWithPinAsync(
inputPin,
email,
kdfConfig,
userKeyPin
);
}
var protectedPin = await _stateService.GetProtectedPinAsync();
var decryptedPin = await _cryptoService.DecryptToUtf8Async(new EncString(protectedPin), userKey);
return decryptedPin == inputPin;
}
} }
} }

View File

@@ -0,0 +1,41 @@
using Bit.Core.Abstractions;
using Bit.Core.Utilities;
using Bit.Core.Utilities.Fido2;
namespace Bit.Core.Services.UserVerification
{
public class Fido2UserVerificationPreferredServiceStrategy : IUserVerificationServiceStrategy
{
private readonly IUserVerificationMediatorService _userVerificationMediatorService;
public Fido2UserVerificationPreferredServiceStrategy(IUserVerificationMediatorService userVerificationMediatorService)
{
_userVerificationMediatorService = userVerificationMediatorService;
}
public async Task<CancellableResult<bool>> VerifyUserForFido2Async(Fido2UserVerificationOptions options)
{
if (options.HasVaultBeenUnlockedInTransaction)
{
return new CancellableResult<bool>(true);
}
if (options.OnNeedUITask != null)
{
await options.OnNeedUITask();
}
var osUnlockVerification = await _userVerificationMediatorService.PerformOSUnlockAsync();
if (osUnlockVerification.IsCancelled)
{
return new CancellableResult<bool>(false, true);
}
if (osUnlockVerification.Result.CanPerform)
{
return new CancellableResult<bool>(osUnlockVerification.Result.IsVerified);
}
return new CancellableResult<bool>(false);
}
}
}

View File

@@ -0,0 +1,71 @@
using Bit.Core.Abstractions;
using Bit.Core.Resources.Localization;
using Bit.Core.Utilities;
using Bit.Core.Utilities.Fido2;
namespace Bit.Core.Services.UserVerification
{
public class Fido2UserVerificationRequiredServiceStrategy : IUserVerificationServiceStrategy
{
private readonly IUserVerificationMediatorService _userVerificationMediatorService;
private readonly IPlatformUtilsService _platformUtilsService;
public Fido2UserVerificationRequiredServiceStrategy(IUserVerificationMediatorService userVerificationMediatorService,
IPlatformUtilsService platformUtilsService)
{
_userVerificationMediatorService = userVerificationMediatorService;
_platformUtilsService = platformUtilsService;
}
public async Task<CancellableResult<bool>> VerifyUserForFido2Async(Fido2UserVerificationOptions options)
{
if (options.HasVaultBeenUnlockedInTransaction)
{
return new CancellableResult<bool>(true);
}
if (options.OnNeedUITask != null)
{
await options.OnNeedUITask();
}
var osUnlockVerification = await _userVerificationMediatorService.PerformOSUnlockAsync();
if (osUnlockVerification.IsCancelled)
{
return new CancellableResult<bool>(false, true);
}
if (osUnlockVerification.Result.CanPerform)
{
return new CancellableResult<bool>(osUnlockVerification.Result.IsVerified);
}
var pinVerification = await _userVerificationMediatorService.VerifyPinCodeAsync();
if (pinVerification.IsCancelled)
{
return new CancellableResult<bool>(false, true);
}
if (pinVerification.Result.CanPerform)
{
return new CancellableResult<bool>(pinVerification.Result.IsVerified);
}
var mpVerification = await _userVerificationMediatorService.VerifyMasterPasswordAsync(false);
if (mpVerification.IsCancelled)
{
return new CancellableResult<bool>(false, true);
}
if (mpVerification.Result.CanPerform)
{
return new CancellableResult<bool>(mpVerification.Result.IsVerified);
}
// TODO: Setup PIN code. For the sake of simplicity, we're not implementing this step now and just telling the user to do it in the main app.
await _platformUtilsService.ShowDialogAsync(AppResources.VerificationRequiredForThisActionSetUpAnUnlockMethodInBitwardenToContinue,
string.Format(AppResources.VerificationRequiredByX, options.RpId),
AppResources.Ok);
return new CancellableResult<bool>(false);
}
}
}

View File

@@ -0,0 +1,10 @@
using Bit.Core.Utilities;
using Bit.Core.Utilities.Fido2;
namespace Bit.Core.Services.UserVerification
{
public interface IUserVerificationServiceStrategy
{
Task<CancellableResult<bool>> VerifyUserForFido2Async(Fido2UserVerificationOptions options);
}
}

View File

@@ -0,0 +1,211 @@
using Bit.App.Abstractions;
using Bit.Core.Abstractions;
using Bit.Core.Models.Domain;
using Bit.Core.Resources.Localization;
using Bit.Core.Utilities;
using Bit.Core.Utilities.Fido2;
using Plugin.Fingerprint;
using static Bit.Core.Abstractions.IUserVerificationMediatorService;
using FingerprintAvailability = Plugin.Fingerprint.Abstractions.FingerprintAvailability;
namespace Bit.Core.Services.UserVerification
{
public class UserVerificationMediatorService : IUserVerificationMediatorService
{
private const byte MAX_ATTEMPTS = 5;
private readonly IPlatformUtilsService _platformUtilsService;
private readonly IPasswordRepromptService _passwordRepromptService;
private readonly IUserPinService _userPinService;
private readonly IDeviceActionService _deviceActionService;
private readonly IUserVerificationService _userVerificationService;
private readonly Dictionary<Fido2UserVerificationPreference, IUserVerificationServiceStrategy> _fido2UserVerificationStrategies = new Dictionary<Fido2UserVerificationPreference, IUserVerificationServiceStrategy>();
public UserVerificationMediatorService(
IPlatformUtilsService platformUtilsService,
IPasswordRepromptService passwordRepromptService,
IUserPinService userPinService,
IDeviceActionService deviceActionService,
IUserVerificationService userVerificationService)
{
_platformUtilsService = platformUtilsService;
_passwordRepromptService = passwordRepromptService;
_userPinService = userPinService;
_deviceActionService = deviceActionService;
_userVerificationService = userVerificationService;
_fido2UserVerificationStrategies.Add(Fido2UserVerificationPreference.Required, new Fido2UserVerificationRequiredServiceStrategy(this, _platformUtilsService));
_fido2UserVerificationStrategies.Add(Fido2UserVerificationPreference.Preferred, new Fido2UserVerificationPreferredServiceStrategy(this));
}
public async Task<CancellableResult<bool>> VerifyUserForFido2Async(Fido2UserVerificationOptions options)
{
if (await ShouldPerformMasterPasswordRepromptAsync(options))
{
if (options.OnNeedUITask != null)
{
await options.OnNeedUITask();
}
var mpVerification = await VerifyMasterPasswordAsync(true);
return new CancellableResult<bool>(
!mpVerification.IsCancelled && mpVerification.Result.CanPerform && mpVerification.Result.IsVerified,
mpVerification.IsCancelled
);
}
if (!_fido2UserVerificationStrategies.TryGetValue(options.UserVerificationPreference, out var userVerificationServiceStrategy))
{
return new CancellableResult<bool>(false, false);
}
return await userVerificationServiceStrategy.VerifyUserForFido2Async(options);
}
public async Task<bool> CanPerformUserVerificationPreferredAsync(Fido2UserVerificationOptions options)
{
if (await ShouldPerformMasterPasswordRepromptAsync(options))
{
return true;
}
return options.HasVaultBeenUnlockedInTransaction
||
await CrossFingerprint.Current.GetAvailabilityAsync() == FingerprintAvailability.Available
||
await CrossFingerprint.Current.GetAvailabilityAsync(true) == FingerprintAvailability.Available;
}
public async Task<bool> ShouldPerformMasterPasswordRepromptAsync(Fido2UserVerificationOptions options)
{
return options.ShouldCheckMasterPasswordReprompt && !await _passwordRepromptService.ShouldByPassMasterPasswordRepromptAsync();
}
public async Task<bool> ShouldEnforceFido2RequiredUserVerificationAsync(Fido2UserVerificationOptions options)
{
switch (options.UserVerificationPreference)
{
case Fido2UserVerificationPreference.Required:
return true;
case Fido2UserVerificationPreference.Discouraged:
return await ShouldPerformMasterPasswordRepromptAsync(options);
default:
return await CanPerformUserVerificationPreferredAsync(options);
}
}
public async Task<CancellableResult<UVResult>> PerformOSUnlockAsync()
{
var availability = await CrossFingerprint.Current.GetAvailabilityAsync();
if (availability == FingerprintAvailability.Available)
{
var isValid = await _platformUtilsService.AuthenticateBiometricAsync(null, DeviceInfo.Platform == DevicePlatform.Android ? "." : null);
if (!isValid.HasValue)
{
return new UVResult(false, false).AsCancellable(true);
}
return new UVResult(true, isValid.Value).AsCancellable();
}
var alternativeAuthAvailability = await CrossFingerprint.Current.GetAvailabilityAsync(true);
if (alternativeAuthAvailability == FingerprintAvailability.Available)
{
var isNonBioValid = await _platformUtilsService.AuthenticateBiometricAsync(null, DeviceInfo.Platform == DevicePlatform.Android ? "." : null, allowAlternativeAuthentication: true);
if (!isNonBioValid.HasValue)
{
return new UVResult(false, false).AsCancellable(true);
}
return new UVResult(true, isNonBioValid.Value).AsCancellable();
}
return new UVResult(false, false).AsCancellable();
}
public async Task<CancellableResult<UVResult>> VerifyPinCodeAsync()
{
return await VerifyWithAttemptsAsync(async () =>
{
if (!await _userPinService.IsPinLockEnabledAsync())
{
return new UVResult(false, false).AsCancellable();
}
var pin = await _deviceActionService.DisplayPromptAync(AppResources.EnterPIN,
AppResources.VerifyPIN, null, AppResources.Ok, AppResources.Cancel, password: true);
if (pin is null)
{
// cancelled by the user
return new UVResult(true, false).AsCancellable(true);
}
try
{
var isVerified = await _userPinService.VerifyPinAsync(pin);
return new UVResult(true, isVerified).AsCancellable();
}
catch (SymmetricCryptoKey.ArgumentKeyNullException)
{
return new UVResult(true, false).AsCancellable();
}
catch (SymmetricCryptoKey.InvalidKeyOperationException)
{
return new UVResult(true, false).AsCancellable();
}
});
}
public async Task<CancellableResult<UVResult>> VerifyMasterPasswordAsync(bool isMasterPasswordReprompt)
{
return await VerifyWithAttemptsAsync(async () =>
{
if (!await _userVerificationService.HasMasterPasswordAsync(true))
{
return new UVResult(false, false).AsCancellable();
}
var title = isMasterPasswordReprompt ? AppResources.PasswordConfirmation : AppResources.MasterPassword;
var body = isMasterPasswordReprompt ? AppResources.PasswordConfirmationDesc : string.Empty;
var (password, isValid) = await _platformUtilsService.ShowPasswordDialogAndGetItAsync(title, body, _userVerificationService.VerifyMasterPasswordAsync);
if (password is null)
{
return new UVResult(true, false).AsCancellable(true);
}
return new UVResult(true, isValid).AsCancellable();
});
}
private async Task<CancellableResult<UVResult>> VerifyWithAttemptsAsync(Func<Task<CancellableResult<UVResult>>> verifyAsync)
{
byte attempts = 0;
do
{
var verification = await verifyAsync();
if (verification.IsCancelled)
{
return new UVResult(false, false).AsCancellable(true);
}
if (!verification.Result.CanPerform)
{
return new UVResult(false, false).AsCancellable();
}
if (verification.Result.IsVerified)
{
return new UVResult(true, true).AsCancellable();
}
} while (++attempts < MAX_ATTEMPTS);
return new UVResult(true, false).AsCancellable();
}
}
public static class UVResultExtensions
{
public static CancellableResult<UVResult> AsCancellable(this UVResult result, bool isCancelled = false)
{
return new CancellableResult<UVResult>(result, isCancelled);
}
}
}

View File

@@ -25,7 +25,7 @@ namespace Bit.Core.Services
_keyConnectorService = keyConnectorService; _keyConnectorService = keyConnectorService;
} }
async public Task<bool> VerifyUser(string secret, VerificationType verificationType) public async Task<bool> VerifyUser(string secret, VerificationType verificationType)
{ {
if (string.IsNullOrEmpty(secret)) if (string.IsNullOrEmpty(secret))
{ {
@@ -61,6 +61,12 @@ namespace Bit.Core.Services
return true; return true;
} }
public async Task<bool> VerifyMasterPasswordAsync(string masterPassword)
{
var masterKey = await _cryptoService.GetOrDeriveMasterKeyAsync(masterPassword);
return await _cryptoService.CompareAndUpdateKeyHashAsync(masterPassword, masterKey);
}
async private Task InvalidSecretErrorAsync(VerificationType verificationType) async private Task InvalidSecretErrorAsync(VerificationType verificationType)
{ {
var errorMessage = verificationType == VerificationType.OTP var errorMessage = verificationType == VerificationType.OTP

View File

@@ -0,0 +1,15 @@
namespace Bit.Core.Utilities
{
public readonly struct CancellableResult<T>
{
public CancellableResult(T result, bool isCancelled = false)
{
Result = result;
IsCancelled = isCancelled;
}
public T Result { get; }
public bool IsCancelled { get; }
}
}

View File

@@ -38,12 +38,38 @@ namespace Bit.Core.Utilities
#endif #endif
} }
/// <summary>
/// Returns the host (and not port) of the given uri.
/// Does not support plain hostnames without a protocol.
///
/// Input => Output examples:
/// <para>https://bitwarden.com => bitwarden.com</para>
/// <para>https://login.bitwarden.com:1337 => login.bitwarden.com</para>
/// <para>https://sub.login.bitwarden.com:1337 => sub.login.bitwarden.com</para>
/// <para>https://localhost:8080 => localhost</para>
/// <para>localhost => null</para>
/// <para>bitwarden => null</para>
/// <para>127.0.0.1 => 127.0.0.1</para>
/// </summary>
public static string GetHostname(string uriString) public static string GetHostname(string uriString)
{ {
var uri = GetUri(uriString); var uri = GetUri(uriString);
return string.IsNullOrEmpty(uri?.Host) ? null : uri.Host; return string.IsNullOrEmpty(uri?.Host) ? null : uri.Host;
} }
/// <summary>
/// Returns the host and port of the given uri.
/// Does not support plain hostnames without
///
/// Input => Output examples:
/// <para>https://bitwarden.com => bitwarden.com</para>
/// <para>https://login.bitwarden.com:1337 => login.bitwarden.com:1337</para>
/// <para>https://sub.login.bitwarden.com:1337 => sub.login.bitwarden.com:1337</para>
/// <para>https://localhost:8080 => localhost:8080</para>
/// <para>localhost => null</para>
/// <para>bitwarden => null</para>
/// <para>127.0.0.1 => 127.0.0.1</para>
/// </summary>
public static string GetHost(string uriString) public static string GetHost(string uriString)
{ {
var uri = GetUri(uriString); var uri = GetUri(uriString);
@@ -61,6 +87,19 @@ namespace Bit.Core.Utilities
return null; return null;
} }
/// <summary>
/// Returns the second and top level domain of the given uri.
/// Does not support plain hostnames without
///
/// Input => Output examples:
/// <para>https://bitwarden.com => bitwarden.com</para>
/// <para>https://login.bitwarden.com:1337 => bitwarden.com</para>
/// <para>https://sub.login.bitwarden.com:1337 => bitwarden.com</para>
/// <para>https://localhost:8080 => localhost</para>
/// <para>localhost => null</para>
/// <para>bitwarden => null</para>
/// <para>127.0.0.1 => 127.0.0.1</para>
/// </summary>
public static string GetDomain(string uriString) public static string GetDomain(string uriString)
{ {
var uri = GetUri(uriString); var uri = GetUri(uriString);

View File

@@ -0,0 +1,18 @@
namespace Bit.Core.Utilities.Fido2
{
#nullable enable
/// <summary>
/// The Relying Party's requirements of the authenticator used in the creation of the credential.
/// </summary>
public class AuthenticatorSelectionCriteria
{
public bool? RequireResidentKey { get; set; }
public string? ResidentKey { get; set; }
public string UserVerification { get; set; } = "preferred";
/// <summary>
/// This member is intended for use by Relying Parties that wish to select the appropriate authenticators to participate in the create() operation.
/// </summary>
// public AuthenticatorAttachment? AuthenticatorAttachment { get; set; } // not used
}
}

View File

@@ -0,0 +1,8 @@
namespace Bit.Core.Utilities.Fido2
{
public enum Fido2AlgorithmIdentifier : int
{
ES256 = -7,
RS256 = -257,
}
}

View File

@@ -0,0 +1,16 @@
/// <summary>
/// Represents the metadata of a discoverable credential for a FIDO2 authenticator.
/// See: https://www.w3.org/TR/webauthn-3/#sctn-op-silent-discovery
/// </summary>
public class Fido2AuthenticatorDiscoverableCredentialMetadata
{
public string Type { get; set; }
public byte[] Id { get; set; }
public string RpId { get; set; }
public byte[] UserHandle { get; set; }
public string UserName { get; set; }
}

View File

@@ -0,0 +1,37 @@
namespace Bit.Core.Utilities.Fido2
{
public class Fido2AuthenticatorException : Exception
{
public Fido2AuthenticatorException(string message) : base(message)
{
}
}
public class NotAllowedError : Fido2AuthenticatorException
{
public NotAllowedError() : base("NotAllowedError")
{
}
}
public class NotSupportedError : Fido2AuthenticatorException
{
public NotSupportedError() : base("NotSupportedError")
{
}
}
public class InvalidStateError : Fido2AuthenticatorException
{
public InvalidStateError() : base("InvalidStateError")
{
}
}
public class UnknownError : Fido2AuthenticatorException
{
public UnknownError() : base("UnknownError")
{
}
}
}

View File

@@ -0,0 +1,26 @@
namespace Bit.Core.Utilities.Fido2
{
public class Fido2AuthenticatorGetAssertionParams
{
/** The callers RP ID, as determined by the user agent and the client. */
public string RpId { get; set; }
/** The hash of the serialized client data, provided by the client. */
public byte[] Hash { get; set; }
public PublicKeyCredentialDescriptor[] AllowCredentialDescriptorList { get; set; }
/// <summary>
/// Instructs the authenticator the user verification preference in order to complete the request. Examples of UV gestures are fingerprint scan or a PIN.
/// </summary>
public Fido2UserVerificationPreference UserVerificationPreference { get; set; }
/// <summary>
/// The challenge to be signed by the authenticator.
/// </summary>
public byte[] Challenge { get; set; }
public object Extensions { get; set; }
}
}

View File

@@ -0,0 +1,23 @@
using Bit.Core.Models.View;
namespace Bit.Core.Utilities.Fido2
{
public class Fido2AuthenticatorGetAssertionResult
{
public byte[] AuthenticatorData { get; set; }
public byte[] Signature { get; set; }
public Fido2AuthenticatorGetAssertionSelectedCredential SelectedCredential { get; set; }
}
public class Fido2AuthenticatorGetAssertionSelectedCredential {
public byte[] Id { get; set; }
#nullable enable
public byte[]? UserHandle { get; set; }
public CipherView? Cipher { get; set; }
}
}

View File

@@ -0,0 +1,53 @@
namespace Bit.Core.Utilities.Fido2
{
public class Fido2AuthenticatorMakeCredentialParams
{
/// <summary>
/// The Relying Party's PublicKeyCredentialRpEntity.
/// </summary>
public PublicKeyCredentialRpEntity RpEntity { get; set; }
/// <summary>
/// The Relying Party's PublicKeyCredentialRpEntity.
/// </summary>
public PublicKeyCredentialUserEntity UserEntity { get; set; }
/// <summary>
/// The hash of the serialized client data, provided by the client.
/// </summary>
public byte[] Hash { get; set; }
/// <summary>
/// A sequence of pairs of PublicKeyCredentialType and public key algorithms (COSEAlgorithmIdentifier) requested by the Relying Party. This sequence is ordered from most preferred to least preferred. The authenticator makes a best-effort to create the most preferred credential that it can.
/// </summary>
public PublicKeyCredentialParameters[] CredTypesAndPubKeyAlgs { get; set; }
/// <summary>
///An OPTIONAL list of PublicKeyCredentialDescriptor objects provided by the Relying Party with the intention that, if any of these are known to the authenticator, it SHOULD NOT create a new credential. excludeCredentialDescriptorList contains a list of known credentials.
/// </summary>
public PublicKeyCredentialDescriptor[] ExcludeCredentialDescriptorList { get; set; }
/// <summary>
/// The effective resident key requirement for credential creation, a Boolean value determined by the client. Resident is synonymous with discoverable. */
/// </summary>
public bool RequireResidentKey { get; set; }
/// <summary>
/// The effective user verification preference for assertion, provided by the client.
/// </summary>
public Fido2UserVerificationPreference UserVerificationPreference { get; set; }
/// <summary>
/// CTAP2 authenticators support setting this to false, but we only support the WebAuthn authenticator model which does not have that option.
/// </summary>
// public bool RequireUserPresence { get; set; } // Always required
/// <summary>
/// The authenticator's attestation preference, a string provided by the client. This is a hint that the client gives to the authenticator about what kind of attestation statement it would like. The authenticator makes a best-effort to satisfy the preference.
/// Note: Attestation statements are not supported at this time.
/// </summary>
// public string AttestationPreference { get; set; }
public object Extensions { get; set; }
}
}

View File

@@ -0,0 +1,16 @@
namespace Bit.Core.Utilities.Fido2
{
public class Fido2AuthenticatorMakeCredentialResult
{
public byte[] CredentialId { get; set; }
public byte[] AttestationObject { get; set; }
public byte[] AuthData { get; set; }
public byte[] PublicKey { get; set; }
public int PublicKeyAlgorithm { get; set; }
}
}

View File

@@ -0,0 +1,57 @@
namespace Bit.Core.Utilities.Fido2
{
#nullable enable
/// <summary>
/// Parameters for asserting a credential.
///
/// This class is an extended version of the WebAuthn struct:
/// https://www.w3.org/TR/webauthn-2/#dictdef-publickeycredentialrequestoptions
/// </summary>
public class Fido2ClientAssertCredentialParams
{
/// <summary>
/// A value which is true if and only if the callers environment settings object is same-origin with its ancestors.
/// It is false if caller is cross-origin.
/// </summary>
public bool SameOriginWithAncestors { get; set; }
/// <summary>
/// The challenge that the selected authenticator signs, along with other data, when producing an authentication
/// assertion.
/// </summary>
public required byte[] Challenge { get; set; }
/// <summary>
/// The relying party identifier claimed by the caller. If omitted, its value will be the CredentialsContainer
/// object's relevant settings object's origin's effective domain.
/// </summary>
public string RpId { get; set; }
/// <summary>
/// The Relying Party's origin (e.g., "https://example.com").
/// </summary>
public string Origin { get; set; }
/// <summary>
/// A list of PublicKeyCredentialDescriptor objects representing public key credentials acceptable to the caller,
/// in descending order of the callers preference (the first item in the list is the most preferred credential,
/// and so on down the list).
/// </summary>
public PublicKeyCredentialDescriptor[] AllowCredentials { get; set; } = [];
/// <summary>
/// The Relying Party's requirements regarding user verification for the get() operation.
/// </summary>
public string UserVerification { get; set; } = "preferred";
/// <summary>
/// This time, in milliseconds, that the caller is willing to wait for the call to complete.
/// This is treated as a hint, and MAY be overridden by the client.
/// </summary>
/// <remarks>
/// This is not currently supported.
/// </remarks>
public int? Timeout { get; set; }
}
}

View File

@@ -0,0 +1,49 @@
using Bit.Core.Models.View;
namespace Bit.Core.Utilities.Fido2
{
/// <summary>
/// The result of asserting a credential.
///
/// See: https://www.w3.org/TR/webauthn-2/#publickeycredential
/// </summary>
public class Fido2ClientAssertCredentialResult
{
/// <summary>
/// Base64url encoding of the credential identifer.
/// </summary>
public required string Id { get; set; }
/// <summary>
/// The credential identifier.
/// </summary>
public required byte[] RawId { get; set; }
/// <summary>
/// The JSON-compatible serialization of client datapassed to the authenticator by the client in
/// order to generate this assertion.
/// </summary>
public required byte[] ClientDataJSON { get; set; }
/// <summary>
/// The authenticator data returned by the authenticator.
/// </summary>
public required byte[] AuthenticatorData { get; set; }
/// <summary>
/// The raw signature returned from the authenticator.
/// </summary>
public required byte[] Signature { get; set; }
/// <summary>
/// The user handle returned from the authenticator, or null if the authenticator did not
/// return a user handle.
/// </summary>
public byte[]? UserHandle { get; set; }
/// <summary>
/// The selected cipher login item that has the credential
/// </summary>
public CipherView? Cipher { get; set; }
}
}

View File

@@ -0,0 +1,35 @@
namespace Bit.Core.Utilities.Fido2
{
/// <summary>
/// This class represents an authenticator's response to a client's request for generation of a
/// new authentication assertion given the WebAuthn Relying Party's challenge.
/// This response contains a cryptographic signature proving possession of the credential private key,
/// and optionally evidence of user consent to a specific transaction.
///
/// See: https://www.w3.org/TR/webauthn-2/#iface-authenticatorassertionresponse
/// </summary>
public class Fido2ClientAuthenticatorAssertionResponse
{
/// <summary>
/// The JSON-compatible serialization of client data passed to the authenticator by the client
/// in order to generate this assertion. The exact JSON serialization MUST be preserved, as the
/// hash of the serialized client data has been computed over it.
/// </summary>
public required byte[] ClientDataJSON { get; set; }
/// <summary>
/// The authenticator data returned by the authenticator.
/// </summary>
public required byte[] AuthenticatorData { get; set; }
/// <summary>
/// Raw signature returned from the authenticator.
/// </summary>
public required byte[] Signature { get; set; }
/// <summary>
/// The user handle returned from the authenticator, or null if the authenticator did not return a user handle.
/// </summary>
public byte[] UserHandle { get; set; } = null;
}
}

View File

@@ -0,0 +1,75 @@
namespace Bit.Core.Utilities.Fido2
{
#nullable enable
/// <summary>
/// Parameters for creating a new credential.
/// </summary>
public class Fido2ClientCreateCredentialParams
{
/// <summary>
/// The Relaying Parties origin, see: https://html.spec.whatwg.org/multipage/browsers.html#concept-origin
/// </summary>
public required string Origin { get; set; }
/// <summary>
/// A value which is true if and only if the callers environment settings object is same-origin with its ancestors.
/// It is false if caller is cross-origin.
/// </summary>
public bool SameOriginWithAncestors { get; set; }
/// <summary>
/// The Relying Party's preference for attestation conveyance
/// </summary>
public string? Attestation { get; set; } = "none";
/// <summary>
/// The Relying Party's requirements of the authenticator used in the creation of the credential.
/// </summary>
public AuthenticatorSelectionCriteria? AuthenticatorSelection { get; set; }
/// <summary>
/// Challenge intended to be used for generating the newly created credential's attestation object.
/// </summary>
public required byte[] Challenge { get; set; } // base64url encoded
/// <summary>
/// This member is intended for use by Relying Parties that wish to limit the creation of multiple credentials for
/// the same account on a single authenticator. The client is requested to return an error if the new credential would
/// be created on an authenticator that also contains one of the credentials enumerated in this parameter.
/// </summary>
public PublicKeyCredentialDescriptor[]? ExcludeCredentials { get; set; }
/// <summary>
/// This member contains additional parameters requesting additional processing by the client and authenticator.
/// Not currently supported.
/// </summary>
public object? Extensions { get; set; }
/// <summary>
/// This member contains information about the desired properties of the credential to be created.
/// The sequence is ordered from most preferred to least preferred.
/// The client makes a best-effort to create the most preferred credential that it can.
/// </summary>
public required PublicKeyCredentialParameters[] PubKeyCredParams { get; set; }
/// <summary>
/// Data about the Relying Party responsible for the request.
/// </summary>
public required PublicKeyCredentialRpEntity Rp { get; set; }
/// <summary>
/// Data about the user account for which the Relying Party is requesting attestation.
/// </summary>
public required PublicKeyCredentialUserEntity User { get; set; }
/// <summary>
/// This member specifies a time, in milliseconds, that the caller is willing to wait for the call to complete.
/// This is treated as a hint, and MAY be overridden by the client.
/// </summary>
/// <remarks>
/// This is not currently supported.
/// </remarks>
public int? Timeout { get; set; }
}
}

View File

@@ -0,0 +1,19 @@
namespace Bit.Core.Utilities.Fido2
{
/// <summary>
/// The result of creating a new credential.
///
/// This class is an extended version of the WebAuthn struct:
/// https://www.w3.org/TR/webauthn-3/#credentialcreationdata-attestationobjectresult
/// </summary>
public class Fido2ClientCreateCredentialResult
{
public byte[] CredentialId { get; set; }
public byte[] ClientDataJSON { get; set; }
public byte[] AttestationObject { get; set; }
public byte[] AuthData { get; set; }
public byte[] PublicKey { get; set; }
public int PublicKeyAlgorithm { get; set; }
public string[] Transports { get; set; }
}
}

View File

@@ -0,0 +1,25 @@
namespace Bit.Core.Utilities.Fido2
{
public class Fido2ClientException : Exception
{
public enum ErrorCode
{
NotAllowedError,
TypeError,
SecurityError,
UriBlockedError,
NotSupportedError,
InvalidStateError,
UnknownError
}
public ErrorCode Code { get; }
public string Reason { get; }
public Fido2ClientException(ErrorCode code, string reason) : base($"{code} ({reason})")
{
Code = code;
Reason = reason;
}
}
}

View File

@@ -0,0 +1,40 @@
using System.Text.RegularExpressions;
namespace Bit.Core.Utilities.Fido2
{
public class Fido2DomainUtils
{
// Loosely based on:
// https://html.spec.whatwg.org/multipage/browsers.html#is-a-registrable-domain-suffix-of-or-is-equal-to
public static bool IsValidRpId(string rpId, string origin)
{
if (rpId == null || rpId == "" || origin == null)
{
return false;
}
// We only care about the domain part of the origin, not the protocol or port so we remove them here,
// while still keeping ipv6 intact.
// https is enforced in the client, so we don't need to worry about that here
var originWithoutProtocolPortOrPath = Regex.Replace(origin, @"(https?://)?([^:/]+)(:\d+)?(/.*)?$", "$2");
if (Uri.CheckHostName(rpId) != UriHostNameType.Dns || Uri.CheckHostName(originWithoutProtocolPortOrPath) != UriHostNameType.Dns)
{
return false;
}
if (rpId == originWithoutProtocolPortOrPath)
{
return true;
}
if (!DomainName.TryParse(rpId, out var parsedRpId) || !DomainName.TryParse(originWithoutProtocolPortOrPath, out var parsedOrgin))
{
return false;
}
return parsedOrgin.Tld == parsedRpId.Tld &&
parsedOrgin.Domain == parsedRpId.Domain &&
(parsedOrgin.SubDomain == parsedRpId.SubDomain || parsedOrgin.SubDomain.EndsWith(parsedRpId.SubDomain));
}
}
}

View File

@@ -0,0 +1,58 @@
using Bit.Core.Abstractions;
namespace Bit.Core.Utilities.Fido2
{
/// <summary>
/// This implementation is used when all interactions are handled by the operating system.
/// Most often the user has already picked a credential by the time the Authenticator is called,
/// so this class just returns those values.
///
/// This class has no corresponding attestation variant, because that operation requires that the
/// user interacts with the app directly.
/// </summary>
public class Fido2GetAssertionUserInterface : IFido2GetAssertionUserInterface
{
private readonly string _cipherId;
private readonly bool _userVerified = false;
private readonly Func<Task> _ensureUnlockedVaultCallback;
private readonly Func<bool> _hasVaultBeenUnlockedInThisTransaction;
private readonly Func<string, Fido2UserVerificationPreference, Task<bool>> _verifyUserCallback;
/// <param name="cipherId">The cipherId for the credential that the user has already picker</param>
/// <param name="userVerified">True if the user has already been verified by the operating system</param>
public Fido2GetAssertionUserInterface(string cipherId,
bool userVerified,
Func<Task> ensureUnlockedVaultCallback,
Func<bool> hasVaultBeenUnlockedInThisTransaction,
Func<string, Fido2UserVerificationPreference, Task<bool>> verifyUserCallback)
{
_cipherId = cipherId;
_userVerified = userVerified;
_ensureUnlockedVaultCallback = ensureUnlockedVaultCallback;
_hasVaultBeenUnlockedInThisTransaction = hasVaultBeenUnlockedInThisTransaction;
_verifyUserCallback = verifyUserCallback;
}
public bool HasVaultBeenUnlockedInThisTransaction { get; private set; }
public async Task<(string CipherId, bool UserVerified)> PickCredentialAsync(Fido2GetAssertionUserInterfaceCredential[] credentials)
{
if (credentials.Length == 0 || !credentials.Any(c => c.CipherId == _cipherId))
{
throw new NotAllowedError();
}
var credential = credentials.First(c => c.CipherId == _cipherId);
var verified = _userVerified || await _verifyUserCallback(_cipherId, credential.UserVerificationPreference);
return (CipherId: _cipherId, UserVerified: verified);
}
public async Task EnsureUnlockedVaultAsync()
{
await _ensureUnlockedVaultCallback();
HasVaultBeenUnlockedInThisTransaction = _hasVaultBeenUnlockedInThisTransaction();
}
}
}

View File

@@ -0,0 +1,24 @@
namespace Bit.Core.Utilities.Fido2
{
public readonly struct Fido2UserVerificationOptions
{
public Fido2UserVerificationOptions(bool shouldCheckMasterPasswordReprompt,
Fido2UserVerificationPreference userVerificationPreference,
bool hasVaultBeenUnlockedInTransaction,
string rpId = null,
Func<Task> onNeedUITask = null)
{
ShouldCheckMasterPasswordReprompt = shouldCheckMasterPasswordReprompt;
UserVerificationPreference = userVerificationPreference;
HasVaultBeenUnlockedInTransaction = hasVaultBeenUnlockedInTransaction;
RpId = rpId;
OnNeedUITask = onNeedUITask;
}
public bool ShouldCheckMasterPasswordReprompt { get; }
public Fido2UserVerificationPreference UserVerificationPreference { get; }
public bool HasVaultBeenUnlockedInTransaction { get; }
public string RpId { get; }
public Func<Task> OnNeedUITask { get; }
}
}

View File

@@ -0,0 +1,39 @@
#nullable enable
using Bit.Core.Enums;
namespace Bit.Core.Utilities.Fido2
{
public enum Fido2UserVerificationPreference
{
Discouraged,
Preferred,
Required
}
public static class Fido2UserVerificationPreferenceExtensions
{
public static Fido2UserVerificationPreference ToFido2UserVerificationPreference(string? preference)
{
switch (preference)
{
case "required":
return Fido2UserVerificationPreference.Required;
case "discouraged":
return Fido2UserVerificationPreference.Discouraged;
default:
return Fido2UserVerificationPreference.Preferred;
}
}
public static Fido2UserVerificationPreference GetUserVerificationPreferenceFrom(Fido2UserVerificationPreference preference, CipherRepromptType repromptType)
{
if (repromptType != CipherRepromptType.None)
{
return Fido2UserVerificationPreference.Required;
}
return preference;
}
}
}

View File

@@ -0,0 +1,9 @@
namespace Bit.Core.Utilities.Fido2
{
public class PublicKeyCredentialAlgorithmDescriptor {
public byte[] Id {get; set;}
public string[] Transports;
public string Type;
public int Algorithm;
}
}

View File

@@ -0,0 +1,9 @@
namespace Bit.Core.Utilities.Fido2
{
public class PublicKeyCredentialDescriptor {
public byte[] Id { get; set; }
public string[] Transports { get; set; }
public string Type { get; set; }
}
}

View File

@@ -0,0 +1,15 @@
namespace Bit.Core.Utilities.Fido2
{
/// <summary>
/// A description of a key type and algorithm.
///</example>
public class PublicKeyCredentialParameters
{
public string Type { get; set; }
/// <summary>
/// Cose algorithm identifier, e.g. -7 for ES256.
/// </summary>
public int Alg { get; set; }
}
}

View File

@@ -0,0 +1,9 @@
namespace Bit.Core.Utilities.Fido2
{
public class PublicKeyCredentialRpEntity
{
public string Id { get; set; }
public string Name { get; set; }
}
}

View File

@@ -0,0 +1,9 @@
namespace Bit.Core.Utilities.Fido2
{
public class PublicKeyCredentialUserEntity {
public byte[] Id { get; set; }
public string Name { get; set; }
public string DisplayName { get; set; }
public string Icon { get; set; }
}
}

View File

@@ -0,0 +1,70 @@
using System.Globalization;
using System.Text.RegularExpressions;
namespace Bit.Core.Utilities
{
/// <summary>
/// Extension methods for converting between standard and raw GUID formats.
///
/// Note: Not optimized for performance. Don't use in performance-critical code.
/// </summary>
public static class GuidExtensions
{
public static byte[] GuidToRawFormat(this string guidString)
{
if (guidString == null)
{
throw new ArgumentException("GUID parameter is null", nameof(guidString));
}
if (!IsValidGuid(guidString)) {
throw new FormatException("GUID parameter is invalid");
}
var arr = new byte[16];
arr[0] = byte.Parse(guidString.Substring(0, 2), NumberStyles.HexNumber); // Parse ##......-....-....-....-............
arr[1] = byte.Parse(guidString.Substring(2, 2), NumberStyles.HexNumber); // Parse ..##....-....-....-....-............
arr[2] = byte.Parse(guidString.Substring(4, 2), NumberStyles.HexNumber); // Parse ....##..-....-....-....-............
arr[3] = byte.Parse(guidString.Substring(6, 2), NumberStyles.HexNumber); // Parse ......##-....-....-....-............
arr[4] = byte.Parse(guidString.Substring(9, 2), NumberStyles.HexNumber); // Parse ........-##..-....-....-............
arr[5] = byte.Parse(guidString.Substring(11, 2), NumberStyles.HexNumber); // Parse ........-..##-....-....-............
arr[6] = byte.Parse(guidString.Substring(14, 2), NumberStyles.HexNumber); // Parse ........-....-##..-....-............
arr[7] = byte.Parse(guidString.Substring(16, 2), NumberStyles.HexNumber); // Parse ........-....-..##-....-............
arr[8] = byte.Parse(guidString.Substring(19, 2), NumberStyles.HexNumber); // Parse ........-....-....-##..-............
arr[9] = byte.Parse(guidString.Substring(21, 2), NumberStyles.HexNumber); // Parse ........-....-....-..##-............
arr[10] = byte.Parse(guidString.Substring(24, 2), NumberStyles.HexNumber); // Parse ........-....-....-....-##..........
arr[11] = byte.Parse(guidString.Substring(26, 2), NumberStyles.HexNumber); // Parse ........-....-....-....-..##........
arr[12] = byte.Parse(guidString.Substring(28, 2), NumberStyles.HexNumber); // Parse ........-....-....-....-....##......
arr[13] = byte.Parse(guidString.Substring(30, 2), NumberStyles.HexNumber); // Parse ........-....-....-....-......##....
arr[14] = byte.Parse(guidString.Substring(32, 2), NumberStyles.HexNumber); // Parse ........-....-....-....-........##..
arr[15] = byte.Parse(guidString.Substring(34, 2), NumberStyles.HexNumber); // Parse ........-....-....-....-..........##
return arr;
}
public static string GuidToStandardFormat(this byte[] guidBytes)
{
if (guidBytes == null)
{
throw new ArgumentException("GUID parameter is null", nameof(guidBytes));
}
if (guidBytes.Length != 16)
{
throw new ArgumentException("Invalid raw GUID format", nameof(guidBytes));
}
return Convert.ToHexString(guidBytes).ToLower().Insert(8, "-").Insert(13, "-").Insert(18, "-").Insert(23, "-" );
}
public static bool IsValidGuid(string guid)
{
return Regex.IsMatch(guid, @"^[0-9a-f]{8}-(?:[0-9a-f]{4}-){3}[0-9a-f]{12}$", RegexOptions.ECMAScript);
}
}
}

View File

@@ -1,9 +1,6 @@
using System; using System.Collections.Concurrent;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Globalization; using System.Globalization;
using System.Text; using System.Text;
using System.Threading.Tasks;
using Bit.Core.Abstractions; using Bit.Core.Abstractions;
using Bit.Core.Services; using Bit.Core.Services;
@@ -30,6 +27,7 @@ namespace Bit.Core.Utilities
var messagingService = Resolve<IMessagingService>("messagingService"); var messagingService = Resolve<IMessagingService>("messagingService");
var cryptoFunctionService = Resolve<ICryptoFunctionService>("cryptoFunctionService"); var cryptoFunctionService = Resolve<ICryptoFunctionService>("cryptoFunctionService");
var cryptoService = Resolve<ICryptoService>("cryptoService"); var cryptoService = Resolve<ICryptoService>("cryptoService");
var clipboardService = Resolve<IClipboardService>();
var logger = Resolve<ILogger>(); var logger = Resolve<ILogger>();
SearchService searchService = null; SearchService searchService = null;
@@ -46,8 +44,9 @@ namespace Bit.Core.Utilities
var settingsService = new SettingsService(stateService); var settingsService = new SettingsService(stateService);
var fileUploadService = new FileUploadService(apiService); var fileUploadService = new FileUploadService(apiService);
var configService = new ConfigService(apiService, stateService, logger); var configService = new ConfigService(apiService, stateService, logger);
var totpService = new TotpService(cryptoFunctionService);
var cipherService = new CipherService(cryptoService, stateService, settingsService, apiService, var cipherService = new CipherService(cryptoService, stateService, settingsService, apiService,
fileUploadService, storageService, i18nService, () => searchService, configService, clearCipherCacheKey, fileUploadService, storageService, i18nService, () => searchService, configService, totpService, clipboardService, clearCipherCacheKey,
allClearCipherCacheKeys); allClearCipherCacheKeys);
var folderService = new FolderService(cryptoService, stateService, apiService, i18nService, cipherService); var folderService = new FolderService(cryptoService, stateService, apiService, i18nService, cipherService);
var collectionService = new CollectionService(cryptoService, stateService, i18nService); var collectionService = new CollectionService(cryptoService, stateService, i18nService);
@@ -79,7 +78,6 @@ namespace Bit.Core.Utilities
return Task.CompletedTask; return Task.CompletedTask;
}); });
var passwordGenerationService = new PasswordGenerationService(cryptoService, stateService, cryptoFunctionService, policyService); var passwordGenerationService = new PasswordGenerationService(cryptoService, stateService, cryptoFunctionService, policyService);
var totpService = new TotpService(cryptoFunctionService);
var deviceTrustCryptoService = new DeviceTrustCryptoService(apiService, appIdService, cryptoFunctionService, cryptoService, stateService); var deviceTrustCryptoService = new DeviceTrustCryptoService(apiService, appIdService, cryptoFunctionService, cryptoService, stateService);
var passwordResetEnrollmentService = new PasswordResetEnrollmentService(apiService, cryptoService, organizationService, stateService); var passwordResetEnrollmentService = new PasswordResetEnrollmentService(apiService, cryptoService, organizationService, stateService);
var authService = new AuthService(cryptoService, cryptoFunctionService, apiService, stateService, var authService = new AuthService(cryptoService, cryptoFunctionService, apiService, stateService,

View File

@@ -1,5 +1,4 @@
using System; using System.Globalization;
using System.Globalization;
using System.Text; using System.Text;
namespace Bit.Core.Utilities namespace Bit.Core.Utilities
@@ -30,5 +29,7 @@ namespace Bit.Core.Utilities
.ToString() .ToString()
.Normalize(NormalizationForm.FormC); .Normalize(NormalizationForm.FormC);
} }
public static string FallbackOnNullOrWhiteSpace(this string s, string fallback) => string.IsNullOrWhiteSpace(s) ? fallback : s;
} }
} }

View File

@@ -154,7 +154,11 @@ namespace Bit.App.Utilities
// Currently on iOS when resuming the app after showing a System "Share/Sheet" (or other similar UI) // Currently on iOS when resuming the app after showing a System "Share/Sheet" (or other similar UI)
// MAUI reports the incorrect Theme. To avoid this we are fetching the current OS Theme directly on iOS from the iOS API. // MAUI reports the incorrect Theme. To avoid this we are fetching the current OS Theme directly on iOS from the iOS API.
// MAUI Issue: https://github.com/dotnet/maui/issues/19614 // MAUI Issue: https://github.com/dotnet/maui/issues/19614
#if IOS
public static bool OsDarkModeEnabled(UITraitCollection? traitCollection = null)
#else
public static bool OsDarkModeEnabled() public static bool OsDarkModeEnabled()
#endif
{ {
#if UT #if UT
return false; return false;
@@ -167,8 +171,19 @@ namespace Bit.App.Utilities
if (!OperatingSystem.IsIOSVersionAtLeast(13, 0)) if (!OperatingSystem.IsIOSVersionAtLeast(13, 0))
return false; return false;
var traits = InvokeOnMainThread(() => WindowStateManager.Default.GetCurrentUIViewController()?.TraitCollection) ?? UITraitCollection.CurrentTraitCollection; ClipLogger.Log($"TC, UIStyle: {traitCollection?.UserInterfaceStyle}");
var uiStyle = traits.UserInterfaceStyle; var uiStyle = traitCollection?.UserInterfaceStyle;
if (traitCollection is null)
{
ClipLogger.Log($"TC null getting trait collection from wsm");
var traits = InvokeOnMainThread(() =>
{
return WindowStateManager.Default.GetCurrentUIViewController()?.TraitCollection;
}) ?? UITraitCollection.CurrentTraitCollection;
uiStyle = traits.UserInterfaceStyle;
}
ClipLogger.Log($"UIStyle: {uiStyle}");
requestedTheme = uiStyle switch requestedTheme = uiStyle switch
{ {

View File

@@ -0,0 +1,48 @@
Additions allow you to add arbitrary C# to the generated classes
before they are compiled. This can be helpful for providing convenience
methods or adding pure C# classes.
== Adding Methods to Generated Classes ==
Let's say the library being bound has a Rectangle class with a constructor
that takes an x and y position, and a width and length size. It will look like
this:
public partial class Rectangle
{
public Rectangle (int x, int y, int width, int height)
{
// JNI bindings
}
}
Imagine we want to add a constructor to this class that takes a Point and
Size structure instead of 4 ints. We can add a new file called Rectangle.cs
with a partial class containing our new method:
public partial class Rectangle
{
public Rectangle (Point location, Size size) :
this (location.X, location.Y, size.Width, size.Height)
{
}
}
At compile time, the additions class will be added to the generated class
and the final assembly will a Rectangle class with both constructors.
== Adding C# Classes ==
Another thing that can be done is adding fully C# managed classes to the
generated library. In the above example, let's assume that there isn't a
Point class available in Java or our library. The one we create doesn't need
to interact with Java, so we'll create it like a normal class in C#.
By adding a Point.cs file with this class, it will end up in the binding library:
public class Point
{
public int X { get; set; }
public int Y { get; set; }
}

View File

@@ -0,0 +1,15 @@
<enum-field-mappings>
<!--
This example converts the constants Fragment_id, Fragment_name,
and Fragment_tag from android.support.v4.app.FragmentActivity.FragmentTag
to an enum called Android.Support.V4.App.FragmentTagType with values
Id, Name, and Tag.
<mapping jni-class="android/support/v4/app/FragmentActivity$FragmentTag" clr-enum-type="Android.Support.V4.App.FragmentTagType">
<field jni-name="Fragment_name" clr-name="Name" value="0" />
<field jni-name="Fragment_id" clr-name="Id" value="1" />
<field jni-name="Fragment_tag" clr-name="Tag" value="2" />
</mapping>
-->
</enum-field-mappings>

View File

@@ -0,0 +1,14 @@
<enum-method-mappings>
<!--
This example changes the Java method:
android.support.v4.app.Fragment.SavedState.writeToParcel (int flags)
to be:
android.support.v4.app.Fragment.SavedState.writeToParcel (Android.OS.ParcelableWriteFlags flags)
when bound in C#.
<mapping jni-class="android/support/v4/app/Fragment.SavedState">
<method jni-name="writeToParcel" parameter="flags" clr-enum-type="Android.OS.ParcelableWriteFlags" />
</mapping>
-->
</enum-method-mappings>

View File

@@ -0,0 +1,12 @@
<metadata>
<attr path="/api/package[@name='androidx.credentials']" name="managedName">AndroidX.Credentials</attr>
<attr path="/api/package[@name='androidx.credentials.provider']" name="managedName">AndroidX.Credentials.Provider</attr>
<attr path="/api/package[@name='androidx.credentials.exceptions']" name="managedName">AndroidX.Credentials.Exceptions</attr>
<attr path="/api/package[@name='androidx.credentials.webauthn']" name="managedName">AndroidX.Credentials.WebAuthn</attr>
<!-- fix companions -->
<attr path="/api/package/class[substring(@name,string-length(@name)-9)='.Companion']" name="managedName">CompanionStatic</attr>
<remove-node path="/api/package/class[substring(@name,string-length(@name)-9)='.Companion' and count(method)=0 and count(field)=0]" />
<attr path="/api/package/class[substring(@name,string-length(@name)-7)='.Default']" name="managedName">DefaultStatic</attr>
<remove-node path="/api/package/class[substring(@name,string-length(@name)-7)='.Default' and count(method)=0 and count(field)=0]" />
</metadata>

View File

@@ -0,0 +1,18 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0-android</TargetFramework>
<SupportedOSPlatformVersion>21</SupportedOSPlatformVersion>
<!--<SupportedOSPlatformVersion Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'ios'">11.0</SupportedOSPlatformVersion>
<SupportedOSPlatformVersion Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'android'">21.0</SupportedOSPlatformVersion>-->
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<RootNamespace>XamarinBinding.AndroidX.Credentials</RootNamespace>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Xamarin.Kotlin.StdLib" Version="1.9.10.1" />
</ItemGroup>
</Project>

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