mirror of
https://github.com/bitwarden/mobile.git
synced 2024-09-27 03:52:57 +02:00
[PM-5153] Android Passkey Implementation (#3020)
* Initial WIP implementation for the app unlock flow when called from Passkey. Still needs code organization and to be finished. Also added a new Window workaround in App.xaml.cs to allow CredentialProviderSelectionActivity to launch separately. * Added missing IDeviceActionService.cs implementation for iOS to build. * Added Async to ReturnToPasskeyAfterUnlockMethod Changed i18n to AppResource.Unlock Removed unecessary cast * minor code change (added comment) * Added back the case for loading a specific Window for CredentialProviverSelectionActivity * Added fix for Intent not passing properly to CredentialProviderSelectionActivity Added Activity cancellation on error during execution of ReturnToPasskeyAfterUnlockAsync() * Added WIP code for Android passkey implementation. Currently returns a mostly complete response that is missing the ClientDataJson * Added WIP code for creating passkeys on Android. Still missing unlock flow and response of passkey creation is still not correct. Removed unused throw NotImplementedException from Fido2ClientService Added CredentialCreationActivity for passkey creation Added alternative code on CredentialProviderSelectionActivity to try to debug issue with response not being valid * Started working on logic to adding unlock flow. It's already handling the unlock but not passing the PendingIntentHandler info for CredentialCreation to CredentialCreationActivity * Changed "cross-platform" to "platform" * Created CredentialHelpers.cs class to share code used for Populating Passkeys in Android. * Added Passkey Credential Creation shared code to CredentialHelpers. Unlock flow for Passkey creation should now be working also. * Updated code for checking if the CredentialProviderService has been enabled by the user or not. Still WIP, somes notes in code due to Credential API not being complete. Also changed the disable code to open the Credential Settings. * Replaced the AndroidX.Credential helpers with custom JSON creation to fix the response for Credential Creation * minor code cleanup on CredentialProviderSelectionActivity * added todo comment * Feature/maui migraton passkeys android unlock fix andreas (#3077) * fix: bitwarden providing too many/wrong credentials * feat: use authenticator instead of client --------- Co-authored-by: Dinis Vieira <dinisvieira@outlook.com> * Removed / commented some older Passkey Proof of concept code. Auth and creation of passkey should still work both when device is unlocked (and not) Added some initial code in AutofillCiphersPageViewModel and CipherAddEditPageViewModel for handling Passkey creation * PM-6829 Implemented Fido2...UserInterfaces on Android and necessary logic to get/make a credential with those * Added IFido2MediatorService registrations Inverted two IsLockedAsync checks * Added navigation to autofillCipher when creating passkey * Updated LockPage to avoid multiple executions of SubmitAsync * Added new flow for creating new passkey on Android with the Cipher page for editing details * Changed the Credential Provider Switch to an external link control * Added i18n for Passkey Settings * Cleanup of older Credentials code used for Android Fido2 POC. Removed CredentialCreationActivity as it's no longer needed * fixed merge conflict/error and added error check to Fido2 navigation in App.xaml.cs * Removed from MainActivity casts from DeviceActionService Changed CredentialProviderServiceActivity to handle Fido errors and exceptions gracefully and show the user an error. Still not with the correct messages. * Added some error messages. Still need to confirm the Text Resource to use and change. * Changed some messages to use AppResources * Cleanup of Credential Android code and added exception result if the clientCreateCredentialResult is null * Updated Add new item button text when creating a new passkey * Added AccountSwitchedException for the Fido Mediator Service * Removed TODO that is no longer needed * Updated some todo messages in Android AutofillHandler * When authenticating a passkey on Android the "showDialog" callback can be called and there's no MainPage available so it was changed for that specific scenario to use _deviceActionService instead of MainPage. --------- Co-authored-by: Andreas Coroiu <andreas.coroiu@gmail.com> Co-authored-by: Federico Maccaroni <fedemkr@gmail.com>
This commit is contained in:
parent
8644fe598e
commit
ca944025d7
159
src/App/Platforms/Android/Autofill/CredentialHelpers.cs
Normal file
159
src/App/Platforms/Android/Autofill/CredentialHelpers.cs
Normal file
@ -0,0 +1,159 @@
|
||||
using Android.App;
|
||||
using Android.Content;
|
||||
using Android.OS;
|
||||
using AndroidX.Credentials;
|
||||
using AndroidX.Credentials.Exceptions;
|
||||
using AndroidX.Credentials.Provider;
|
||||
using AndroidX.Credentials.WebAuthn;
|
||||
using Bit.Core.Abstractions;
|
||||
using Bit.Core.Utilities;
|
||||
using Bit.Droid;
|
||||
using Org.Json;
|
||||
using Activity = Android.App.Activity;
|
||||
using Drawables = Android.Graphics.Drawables;
|
||||
|
||||
namespace Bit.App.Platforms.Android.Autofill
|
||||
{
|
||||
public static class CredentialHelpers
|
||||
{
|
||||
public static async Task<List<CredentialEntry>> PopulatePasskeyDataAsync(CallingAppInfo callingAppInfo,
|
||||
BeginGetPublicKeyCredentialOption option, Context context, bool hasVaultBeenUnlockedInThisTransaction)
|
||||
{
|
||||
var passkeyEntries = new List<CredentialEntry>();
|
||||
var requestOptions = new PublicKeyCredentialRequestOptions(option.RequestJson);
|
||||
|
||||
var authenticator = Bit.Core.Utilities.ServiceContainer.Resolve<IFido2AuthenticatorService>();
|
||||
var credentials = await authenticator.SilentCredentialDiscoveryAsync(requestOptions.RpId);
|
||||
|
||||
passkeyEntries = credentials.Select(credential => MapCredential(credential, option, context, hasVaultBeenUnlockedInThisTransaction) as CredentialEntry).ToList();
|
||||
|
||||
return passkeyEntries;
|
||||
}
|
||||
|
||||
private static PublicKeyCredentialEntry MapCredential(Fido2AuthenticatorDiscoverableCredentialMetadata credential, BeginGetPublicKeyCredentialOption option, Context context, bool hasVaultBeenUnlockedInThisTransaction)
|
||||
{
|
||||
var credDataBundle = new Bundle();
|
||||
credDataBundle.PutByteArray(Bit.Core.Utilities.Fido2.CredentialProviderConstants.CredentialIdIntentExtra, credential.Id);
|
||||
|
||||
var intent = new Intent(context, typeof(Bit.Droid.Autofill.CredentialProviderSelectionActivity))
|
||||
.SetAction(Bit.Droid.Autofill.CredentialProviderService.GetFido2IntentAction).SetPackage(Constants.PACKAGE_NAME);
|
||||
intent.PutExtra(Bit.Core.Utilities.Fido2.CredentialProviderConstants.CredentialDataIntentExtra, credDataBundle);
|
||||
intent.PutExtra(Bit.Core.Utilities.Fido2.CredentialProviderConstants.CredentialProviderCipherId, credential.CipherId);
|
||||
intent.PutExtra(Bit.Core.Utilities.Fido2.CredentialProviderConstants.CredentialHasVaultBeenUnlockedInThisTransactionExtra, hasVaultBeenUnlockedInThisTransaction);
|
||||
var pendingIntent = PendingIntent.GetActivity(context, Bit.Droid.Autofill.CredentialProviderService.UniqueGetRequestCode, intent,
|
||||
PendingIntentFlags.Mutable | PendingIntentFlags.UpdateCurrent);
|
||||
|
||||
return new PublicKeyCredentialEntry.Builder(
|
||||
context,
|
||||
credential.UserName ?? "No username",
|
||||
pendingIntent,
|
||||
option)
|
||||
.SetDisplayName(credential.UserName ?? "No username")
|
||||
.SetIcon(Drawables.Icon.CreateWithResource(context, Microsoft.Maui.Resource.Drawable.icon))
|
||||
.Build();
|
||||
}
|
||||
|
||||
public static async Task CreateCipherPasskeyAsync(ProviderCreateCredentialRequest getRequest, Activity activity)
|
||||
{
|
||||
var callingRequest = getRequest?.CallingRequest as CreatePublicKeyCredentialRequest;
|
||||
var origin = callingRequest.Origin;
|
||||
var credentialCreationOptions = new PublicKeyCredentialCreationOptions(callingRequest.RequestJson);
|
||||
|
||||
var rp = new Core.Utilities.Fido2.PublicKeyCredentialRpEntity()
|
||||
{
|
||||
Id = credentialCreationOptions.Rp.Id,
|
||||
Name = credentialCreationOptions.Rp.Name
|
||||
};
|
||||
|
||||
var user = new Core.Utilities.Fido2.PublicKeyCredentialUserEntity()
|
||||
{
|
||||
Id = credentialCreationOptions.User.GetId(),
|
||||
Name = credentialCreationOptions.User.Name,
|
||||
DisplayName = credentialCreationOptions.User.DisplayName
|
||||
};
|
||||
|
||||
var pubKeyCredParams = new List<Core.Utilities.Fido2.PublicKeyCredentialParameters>();
|
||||
foreach (var pubKeyCredParam in credentialCreationOptions.PubKeyCredParams)
|
||||
{
|
||||
pubKeyCredParams.Add(new Core.Utilities.Fido2.PublicKeyCredentialParameters() { Alg = Convert.ToInt32(pubKeyCredParam.Alg), Type = pubKeyCredParam.Type });
|
||||
}
|
||||
|
||||
var excludeCredentials = new List<Core.Utilities.Fido2.PublicKeyCredentialDescriptor>();
|
||||
foreach (var excludeCred in credentialCreationOptions.ExcludeCredentials)
|
||||
{
|
||||
excludeCredentials.Add(new Core.Utilities.Fido2.PublicKeyCredentialDescriptor(){ Id = excludeCred.GetId(), Type = excludeCred.Type, Transports = excludeCred.Transports.ToArray() });
|
||||
}
|
||||
|
||||
var authenticatorSelection = new Core.Utilities.Fido2.AuthenticatorSelectionCriteria()
|
||||
{
|
||||
UserVerification = credentialCreationOptions.AuthenticatorSelection.UserVerification,
|
||||
ResidentKey = credentialCreationOptions.AuthenticatorSelection.ResidentKey,
|
||||
RequireResidentKey = credentialCreationOptions.AuthenticatorSelection.RequireResidentKey
|
||||
};
|
||||
|
||||
var timeout = Convert.ToInt32(credentialCreationOptions.Timeout);
|
||||
|
||||
var credentialCreateParams = new Bit.Core.Utilities.Fido2.Fido2ClientCreateCredentialParams()
|
||||
{
|
||||
Challenge = credentialCreationOptions.GetChallenge(),
|
||||
Origin = origin,
|
||||
PubKeyCredParams = pubKeyCredParams.ToArray(),
|
||||
Rp = rp,
|
||||
User = user,
|
||||
Timeout = timeout,
|
||||
Attestation = credentialCreationOptions.Attestation,
|
||||
AuthenticatorSelection = authenticatorSelection,
|
||||
ExcludeCredentials = excludeCredentials.ToArray(),
|
||||
//Extensions = // Can be improved later to add support for 'credProps'
|
||||
SameOriginWithAncestors = true
|
||||
};
|
||||
|
||||
var fido2MediatorService = ServiceContainer.Resolve<IFido2MediatorService>();
|
||||
var clientCreateCredentialResult = await fido2MediatorService.CreateCredentialAsync(credentialCreateParams);
|
||||
if (clientCreateCredentialResult == null)
|
||||
{
|
||||
var resultErrorIntent = new Intent();
|
||||
PendingIntentHandler.SetCreateCredentialException(resultErrorIntent, new CreateCredentialUnknownException());
|
||||
activity.SetResult(Result.Ok, resultErrorIntent);
|
||||
activity.Finish();
|
||||
return;
|
||||
}
|
||||
|
||||
var transportsArray = new JSONArray();
|
||||
if (clientCreateCredentialResult.Transports != null)
|
||||
{
|
||||
foreach (var transport in clientCreateCredentialResult.Transports)
|
||||
{
|
||||
transportsArray.Put(transport);
|
||||
}
|
||||
}
|
||||
|
||||
var responseInnerAndroidJson = new JSONObject();
|
||||
responseInnerAndroidJson.Put("clientDataJSON", CoreHelpers.Base64UrlEncode(clientCreateCredentialResult.ClientDataJSON));
|
||||
responseInnerAndroidJson.Put("authenticatorData", CoreHelpers.Base64UrlEncode(clientCreateCredentialResult.AuthData));
|
||||
responseInnerAndroidJson.Put("attestationObject", CoreHelpers.Base64UrlEncode(clientCreateCredentialResult.AttestationObject));
|
||||
responseInnerAndroidJson.Put("transports", transportsArray);
|
||||
responseInnerAndroidJson.Put("publicKeyAlgorithm", clientCreateCredentialResult.PublicKeyAlgorithm);
|
||||
responseInnerAndroidJson.Put("publicKey", CoreHelpers.Base64UrlEncode(clientCreateCredentialResult.PublicKey));
|
||||
|
||||
var rootAndroidJson = new JSONObject();
|
||||
rootAndroidJson.Put("id", CoreHelpers.Base64UrlEncode(clientCreateCredentialResult.CredentialId));
|
||||
rootAndroidJson.Put("rawId", CoreHelpers.Base64UrlEncode(clientCreateCredentialResult.CredentialId));
|
||||
rootAndroidJson.Put("authenticatorAttachment", "platform");
|
||||
rootAndroidJson.Put("type", "public-key");
|
||||
rootAndroidJson.Put("clientExtensionResults", new JSONObject());
|
||||
rootAndroidJson.Put("response", responseInnerAndroidJson);
|
||||
|
||||
var responseAndroidJson = rootAndroidJson.ToString();
|
||||
|
||||
System.Diagnostics.Debug.WriteLine(responseAndroidJson);
|
||||
|
||||
var result = new Intent();
|
||||
var publicKeyResponse = new CreatePublicKeyCredentialResponse(responseAndroidJson);
|
||||
PendingIntentHandler.SetCreateCredentialResponse(result, publicKeyResponse);
|
||||
|
||||
activity.SetResult(Result.Ok, result);
|
||||
activity.Finish();
|
||||
}
|
||||
}
|
||||
}
|
@ -1,9 +0,0 @@
|
||||
namespace Bit.Droid.Autofill
|
||||
{
|
||||
public class CredentialProviderConstants
|
||||
{
|
||||
public const string CredentialProviderCipherId = "credentialProviderCipherId";
|
||||
public const string CredentialDataIntentExtra = "CREDENTIAL_DATA";
|
||||
public const string CredentialIdIntentExtra = "credId";
|
||||
}
|
||||
}
|
@ -1,12 +1,18 @@
|
||||
using System.Threading.Tasks;
|
||||
using Android.App;
|
||||
using Android.App;
|
||||
using Android.Content;
|
||||
using Android.Content.PM;
|
||||
using Android.OS;
|
||||
using AndroidX.Credentials;
|
||||
using AndroidX.Credentials.Provider;
|
||||
using AndroidX.Credentials.WebAuthn;
|
||||
using Bit.App.Abstractions;
|
||||
using Bit.Core.Abstractions;
|
||||
using Bit.Core.Utilities;
|
||||
using Bit.App.Droid.Utilities;
|
||||
using Bit.Core.Resources.Localization;
|
||||
using Bit.Core.Utilities.Fido2;
|
||||
using Java.Security;
|
||||
using Bit.Core.Services;
|
||||
|
||||
namespace Bit.Droid.Autofill
|
||||
{
|
||||
@ -15,6 +21,13 @@ namespace Bit.Droid.Autofill
|
||||
LaunchMode = LaunchMode.SingleTop)]
|
||||
public class CredentialProviderSelectionActivity : MauiAppCompatActivity
|
||||
{
|
||||
private LazyResolve<IFido2MediatorService> _fido2MediatorService = new LazyResolve<IFido2MediatorService>();
|
||||
private LazyResolve<IVaultTimeoutService> _vaultTimeoutService = new LazyResolve<IVaultTimeoutService>();
|
||||
private LazyResolve<IStateService> _stateService = new LazyResolve<IStateService>();
|
||||
private LazyResolve<ICipherService> _cipherService = new LazyResolve<ICipherService>();
|
||||
private LazyResolve<IUserVerificationMediatorService> _userVerificationMediatorService = new LazyResolve<IUserVerificationMediatorService>();
|
||||
private LazyResolve<IDeviceActionService> _deviceActionService = new LazyResolve<IDeviceActionService>();
|
||||
|
||||
protected override void OnCreate(Bundle bundle)
|
||||
{
|
||||
Intent?.Validate();
|
||||
@ -23,43 +36,142 @@ namespace Bit.Droid.Autofill
|
||||
var cipherId = Intent?.GetStringExtra(CredentialProviderConstants.CredentialProviderCipherId);
|
||||
if (string.IsNullOrEmpty(cipherId))
|
||||
{
|
||||
SetResult(Result.Canceled);
|
||||
Finish();
|
||||
return;
|
||||
}
|
||||
|
||||
GetCipherAndPerformPasskeyAuthAsync(cipherId).FireAndForget();
|
||||
GetCipherAndPerformFido2AuthAsync(cipherId).FireAndForget();
|
||||
}
|
||||
|
||||
private async Task GetCipherAndPerformPasskeyAuthAsync(string cipherId)
|
||||
//Used to avoid crash on MAUI when doing back
|
||||
public override void OnBackPressed()
|
||||
{
|
||||
// TODO this is a work in progress
|
||||
// https://developer.android.com/training/sign-in/credential-provider#passkeys-implement
|
||||
Finish();
|
||||
}
|
||||
|
||||
var getRequest = PendingIntentHandler.RetrieveProviderGetCredentialRequest(Intent);
|
||||
// var publicKeyRequest = getRequest?.CredentialOptions as PublicKeyCredentialRequestOptions;
|
||||
private async Task GetCipherAndPerformFido2AuthAsync(string cipherId)
|
||||
{
|
||||
string RpId = string.Empty;
|
||||
try
|
||||
{
|
||||
var getRequest = PendingIntentHandler.RetrieveProviderGetCredentialRequest(Intent);
|
||||
|
||||
var requestInfo = Intent.GetBundleExtra(CredentialProviderConstants.CredentialDataIntentExtra);
|
||||
var credIdEnc = requestInfo?.GetString(CredentialProviderConstants.CredentialIdIntentExtra);
|
||||
var credentialOption = getRequest?.CredentialOptions.FirstOrDefault();
|
||||
var credentialPublic = credentialOption as GetPublicKeyCredentialOption;
|
||||
|
||||
var cipherService = ServiceContainer.Resolve<ICipherService>();
|
||||
var cipher = await cipherService.GetAsync(cipherId);
|
||||
var decCipher = await cipher.DecryptAsync();
|
||||
var requestOptions = new PublicKeyCredentialRequestOptions(credentialPublic.RequestJson);
|
||||
RpId = requestOptions.RpId;
|
||||
|
||||
var passkey = decCipher.Login.Fido2Credentials.Find(f => f.CredentialId == credIdEnc);
|
||||
var requestInfo = Intent.GetBundleExtra(CredentialProviderConstants.CredentialDataIntentExtra);
|
||||
var credentialId = requestInfo?.GetByteArray(CredentialProviderConstants.CredentialIdIntentExtra);
|
||||
var hasVaultBeenUnlockedInThisTransaction = Intent.GetBooleanExtra(CredentialProviderConstants.CredentialHasVaultBeenUnlockedInThisTransactionExtra, false);
|
||||
|
||||
var credId = Convert.FromBase64String(credIdEnc);
|
||||
// var privateKey = Convert.FromBase64String(passkey.PrivateKey);
|
||||
// var uid = Convert.FromBase64String(passkey.uid);
|
||||
var androidOrigin = AppInfoToOrigin(getRequest?.CallingAppInfo);
|
||||
var packageName = getRequest?.CallingAppInfo.PackageName;
|
||||
|
||||
var origin = getRequest?.CallingAppInfo.Origin;
|
||||
var packageName = getRequest?.CallingAppInfo.PackageName;
|
||||
var userInterface = new Fido2GetAssertionUserInterface(
|
||||
cipherId: cipherId,
|
||||
userVerified: false,
|
||||
ensureUnlockedVaultCallback: EnsureUnlockedVaultAsync,
|
||||
hasVaultBeenUnlockedInThisTransaction: () => hasVaultBeenUnlockedInThisTransaction,
|
||||
verifyUserCallback: (cipherId, uvPreference) => VerifyUserAsync(cipherId, uvPreference, RpId, hasVaultBeenUnlockedInThisTransaction));
|
||||
|
||||
// --- continue WIP here (save TOTP copy as last step) ---
|
||||
var assertParams = new Fido2AuthenticatorGetAssertionParams
|
||||
{
|
||||
Challenge = requestOptions.GetChallenge(),
|
||||
RpId = RpId,
|
||||
UserVerificationPreference = Fido2UserVerificationPreferenceExtensions.ToFido2UserVerificationPreference(requestOptions.UserVerification),
|
||||
Hash = credentialPublic.GetClientDataHash(),
|
||||
AllowCredentialDescriptorList = new Core.Utilities.Fido2.PublicKeyCredentialDescriptor[] { new Core.Utilities.Fido2.PublicKeyCredentialDescriptor { Id = credentialId } },
|
||||
Extensions = new object()
|
||||
};
|
||||
|
||||
// Copy TOTP if needed
|
||||
var autofillHandler = ServiceContainer.Resolve<IAutofillHandler>();
|
||||
autofillHandler.Autofill(decCipher);
|
||||
var assertResult = await _fido2MediatorService.Value.GetAssertionAsync(assertParams, userInterface);
|
||||
|
||||
var response = new AuthenticatorAssertionResponse(
|
||||
requestOptions,
|
||||
assertResult.SelectedCredential.Id,
|
||||
androidOrigin,
|
||||
false, // These flags have no effect, we set our own within `SetAuthenticatorData`
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
assertResult.SelectedCredential.UserHandle,
|
||||
packageName,
|
||||
credentialPublic.GetClientDataHash() //clientDataHash
|
||||
);
|
||||
response.SetAuthenticatorData(assertResult.AuthenticatorData);
|
||||
response.SetSignature(assertResult.Signature);
|
||||
|
||||
var result = new Intent();
|
||||
var fidoCredential = new FidoPublicKeyCredential(assertResult.SelectedCredential.Id, response, "platform");
|
||||
var cred = new PublicKeyCredential(fidoCredential.Json());
|
||||
var credResponse = new GetCredentialResponse(cred);
|
||||
PendingIntentHandler.SetGetCredentialResponse(result, credResponse);
|
||||
|
||||
await MainThread.InvokeOnMainThreadAsync(() =>
|
||||
{
|
||||
SetResult(Result.Ok, result);
|
||||
Finish();
|
||||
});
|
||||
}
|
||||
catch (NotAllowedError)
|
||||
{
|
||||
await MainThread.InvokeOnMainThreadAsync(async() =>
|
||||
{
|
||||
await _deviceActionService.Value.DisplayAlertAsync(AppResources.ErrorReadingPasskey, string.Format(AppResources.ThereWasAProblemReadingAPasskeyForXTryAgainLater, RpId), AppResources.Ok);
|
||||
Finish();
|
||||
});
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LoggerHelper.LogEvenIfCantBeResolved(ex);
|
||||
await MainThread.InvokeOnMainThreadAsync(async() =>
|
||||
{
|
||||
await _deviceActionService.Value.DisplayAlertAsync(AppResources.ErrorReadingPasskey, string.Format(AppResources.ThereWasAProblemReadingAPasskeyForXTryAgainLater, RpId), AppResources.Ok);
|
||||
Finish();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private async Task EnsureUnlockedVaultAsync()
|
||||
{
|
||||
if (!await _stateService.Value.IsAuthenticatedAsync() || await _vaultTimeoutService.Value.IsLockedAsync())
|
||||
{
|
||||
// this should never happen but just in case.
|
||||
throw new InvalidOperationException("Not authed or vault locked");
|
||||
}
|
||||
}
|
||||
|
||||
internal async Task<bool> VerifyUserAsync(string selectedCipherId, Fido2UserVerificationPreference userVerificationPreference, string rpId, bool vaultUnlockedDuringThisTransaction)
|
||||
{
|
||||
try
|
||||
{
|
||||
var encrypted = await _cipherService.Value.GetAsync(selectedCipherId);
|
||||
var cipher = await encrypted.DecryptAsync();
|
||||
|
||||
var userVerification = await _userVerificationMediatorService.Value.VerifyUserForFido2Async(
|
||||
new Fido2UserVerificationOptions(
|
||||
cipher?.Reprompt == Bit.Core.Enums.CipherRepromptType.Password,
|
||||
userVerificationPreference,
|
||||
vaultUnlockedDuringThisTransaction,
|
||||
rpId)
|
||||
);
|
||||
return !userVerification.IsCancelled && userVerification.Result;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LoggerHelper.LogEvenIfCantBeResolved(ex);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private string AppInfoToOrigin(CallingAppInfo info)
|
||||
{
|
||||
var cert = info.SigningInfo.GetApkContentsSigners()[0].ToByteArray();
|
||||
var md = MessageDigest.GetInstance("SHA-256");
|
||||
var certHash = md.Digest(cert);
|
||||
return $"android:apk-key-hash:${CoreHelpers.Base64UrlEncode(certHash)}";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,16 +1,15 @@
|
||||
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;
|
||||
using Bit.App.Droid.Utilities;
|
||||
using Bit.Core.Resources.Localization;
|
||||
using Bit.Core.Utilities.Fido2;
|
||||
|
||||
namespace Bit.Droid.Autofill
|
||||
{
|
||||
@ -20,32 +19,53 @@ namespace Bit.Droid.Autofill
|
||||
[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;
|
||||
public const string GetFido2IntentAction = "PACKAGE_NAME.GET_PASSKEY";
|
||||
public const string CreateFido2IntentAction = "PACKAGE_NAME.CREATE_PASSKEY";
|
||||
public const int UniqueGetRequestCode = 94556023;
|
||||
public const int UniqueCreateRequestCode = 94556024;
|
||||
|
||||
private ICipherService _cipherService;
|
||||
private IUserVerificationService _userVerificationService;
|
||||
private IVaultTimeoutService _vaultTimeoutService;
|
||||
private LazyResolve<ILogger> _logger = new LazyResolve<ILogger>("logger");
|
||||
private readonly LazyResolve<IVaultTimeoutService> _vaultTimeoutService = new LazyResolve<IVaultTimeoutService>();
|
||||
private readonly LazyResolve<ILogger> _logger = new LazyResolve<ILogger>();
|
||||
|
||||
public override async void OnBeginCreateCredentialRequest(BeginCreateCredentialRequest request,
|
||||
CancellationSignal cancellationSignal, IOutcomeReceiver callback) => throw new NotImplementedException();
|
||||
public override void OnBeginCreateCredentialRequest(BeginCreateCredentialRequest request,
|
||||
CancellationSignal cancellationSignal, IOutcomeReceiver callback)
|
||||
{
|
||||
var response = ProcessCreateCredentialsRequestAsync(request);
|
||||
if (response != null)
|
||||
{
|
||||
callback.OnResult(response);
|
||||
}
|
||||
else
|
||||
{
|
||||
callback.OnError("Error creating credential");
|
||||
}
|
||||
}
|
||||
|
||||
public override async void OnBeginGetCredentialRequest(BeginGetCredentialRequest request,
|
||||
CancellationSignal cancellationSignal, IOutcomeReceiver callback)
|
||||
{
|
||||
try
|
||||
{
|
||||
_vaultTimeoutService ??= ServiceContainer.Resolve<IVaultTimeoutService>();
|
||||
|
||||
await _vaultTimeoutService.CheckVaultTimeoutAsync();
|
||||
var locked = await _vaultTimeoutService.IsLockedAsync();
|
||||
await _vaultTimeoutService.Value.CheckVaultTimeoutAsync();
|
||||
var locked = await _vaultTimeoutService.Value.IsLockedAsync();
|
||||
if (!locked)
|
||||
{
|
||||
var response = await ProcessGetCredentialsRequestAsync(request);
|
||||
callback.OnResult(response);
|
||||
return;
|
||||
}
|
||||
// TODO handle auth/unlock account flow
|
||||
|
||||
var intent = new Intent(ApplicationContext, typeof(MainActivity));
|
||||
intent.PutExtra(CredentialProviderConstants.Fido2CredentialAction, CredentialProviderConstants.Fido2CredentialGet);
|
||||
var pendingIntent = PendingIntent.GetActivity(ApplicationContext, UniqueGetRequestCode, intent,
|
||||
AndroidHelpers.AddPendingIntentMutabilityFlag(PendingIntentFlags.UpdateCurrent, true));
|
||||
|
||||
var unlockAction = new AuthenticationAction(AppResources.Unlock, pendingIntent);
|
||||
|
||||
var unlockResponse = new BeginGetCredentialResponse.Builder()
|
||||
.SetAuthenticationActions(new List<AuthenticationAction>() { unlockAction } )
|
||||
.Build();
|
||||
callback.OnResult(unlockResponse);
|
||||
}
|
||||
catch (GetCredentialException e)
|
||||
{
|
||||
@ -54,28 +74,59 @@ namespace Bit.Droid.Autofill
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_logger.Value.Exception(e);
|
||||
_logger.Value.Exception(e);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
private BeginCreateCredentialResponse ProcessCreateCredentialsRequestAsync(
|
||||
BeginCreateCredentialRequest request)
|
||||
{
|
||||
if (request == null) { return null; }
|
||||
|
||||
if (request is BeginCreatePasswordCredentialRequest beginCreatePasswordCredentialRequest)
|
||||
{
|
||||
//This flow can be used if Password flow needs to be implemented
|
||||
throw new NotImplementedException();
|
||||
//return HandleCreatePasswordQuery(beginCreatePasswordCredentialRequest);
|
||||
}
|
||||
else if (request is BeginCreatePublicKeyCredentialRequest beginCreatePublicKeyCredentialRequest)
|
||||
{
|
||||
return HandleCreatePasskeyQuery(beginCreatePublicKeyCredentialRequest);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private BeginCreateCredentialResponse HandleCreatePasskeyQuery(BeginCreatePublicKeyCredentialRequest optionRequest)
|
||||
{
|
||||
var intent = new Intent(ApplicationContext, typeof(MainActivity));
|
||||
intent.PutExtra(CredentialProviderConstants.Fido2CredentialAction, CredentialProviderConstants.Fido2CredentialCreate);
|
||||
var pendingIntent = PendingIntent.GetActivity(ApplicationContext, UniqueCreateRequestCode, intent,
|
||||
AndroidHelpers.AddPendingIntentMutabilityFlag(PendingIntentFlags.UpdateCurrent, true));
|
||||
|
||||
//TODO: i81n needs to be done
|
||||
var createEntryBuilder = new CreateEntry.Builder("Bitwarden Vault", pendingIntent)
|
||||
.SetDescription("Your passkey will be saved securely to the Bitwarden Vault. You can use it from any other device for sign-in in the future.")
|
||||
.Build();
|
||||
|
||||
var createCredentialResponse = new BeginCreateCredentialResponse.Builder()
|
||||
.AddCreateEntry(createEntryBuilder);
|
||||
|
||||
return createCredentialResponse.Build();
|
||||
}
|
||||
|
||||
private async Task<BeginGetCredentialResponse> ProcessGetCredentialsRequestAsync(
|
||||
BeginGetCredentialRequest request)
|
||||
{
|
||||
IList<CredentialEntry> credentialEntries = null;
|
||||
var credentialEntries = new List<CredentialEntry>();
|
||||
|
||||
foreach (var option in request.BeginGetCredentialOptions)
|
||||
foreach (var option in request.BeginGetCredentialOptions.OfType<BeginGetPublicKeyCredentialOption>())
|
||||
{
|
||||
var credentialOption = option as BeginGetPublicKeyCredentialOption;
|
||||
if (credentialOption != null)
|
||||
{
|
||||
credentialEntries ??= new List<CredentialEntry>();
|
||||
((List<CredentialEntry>)credentialEntries).AddRange(
|
||||
await PopulatePasskeyDataAsync(request.CallingAppInfo, credentialOption));
|
||||
}
|
||||
credentialEntries.AddRange(await Bit.App.Platforms.Android.Autofill.CredentialHelpers.PopulatePasskeyDataAsync(request.CallingAppInfo, option, ApplicationContext, false));
|
||||
}
|
||||
|
||||
if (credentialEntries == null)
|
||||
if (!credentialEntries.Any())
|
||||
{
|
||||
return new BeginGetCredentialResponse();
|
||||
}
|
||||
@ -85,63 +136,10 @@ namespace Bit.Droid.Autofill
|
||||
.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();
|
||||
CancellationSignal cancellationSignal, IOutcomeReceiver callback)
|
||||
{
|
||||
callback.OnResult(null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,21 @@
|
||||
using Bit.Core.Abstractions;
|
||||
|
||||
namespace Bit.App.Platforms.Android.Autofill
|
||||
{
|
||||
//TODO: WIP: Temporary Dummy implementation
|
||||
public class Fido2GetAssertionUserInterface : IFido2GetAssertionUserInterface
|
||||
{
|
||||
public bool HasVaultBeenUnlockedInThisTransaction => true;
|
||||
|
||||
public Task EnsureUnlockedVaultAsync()
|
||||
{
|
||||
return Task.FromResult(true);
|
||||
}
|
||||
|
||||
public Task<(string CipherId, bool UserVerified)> PickCredentialAsync(Fido2GetAssertionUserInterfaceCredential[] credentials)
|
||||
{
|
||||
var credential = credentials[0];
|
||||
return Task.FromResult<(string CipherId, bool UserVerified)>((credential.CipherId, true));
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,151 @@
|
||||
using Bit.App.Abstractions;
|
||||
using Bit.Core.Abstractions;
|
||||
using Bit.Core.Resources.Localization;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Utilities;
|
||||
using Bit.Core.Utilities.Fido2;
|
||||
|
||||
namespace Bit.App.Platforms.Android.Autofill
|
||||
{
|
||||
public class Fido2MakeCredentialUserInterface : IFido2MakeCredentialConfirmationUserInterface
|
||||
{
|
||||
private readonly IStateService _stateService;
|
||||
private readonly IVaultTimeoutService _vaultTimeoutService;
|
||||
private readonly ICipherService _cipherService;
|
||||
private readonly IUserVerificationMediatorService _userVerificationMediatorService;
|
||||
private readonly IDeviceActionService _deviceActionService;
|
||||
|
||||
private TaskCompletionSource<(string cipherId, bool? userVerified)> _confirmCredentialTcs;
|
||||
Fido2UserVerificationOptions? _currentDefaultUserVerificationOptions;
|
||||
|
||||
public Fido2MakeCredentialUserInterface(IStateService stateService,
|
||||
IVaultTimeoutService vaultTimeoutService,
|
||||
ICipherService cipherService,
|
||||
IUserVerificationMediatorService userVerificationMediatorService,
|
||||
IDeviceActionService deviceActionService)
|
||||
{
|
||||
_stateService = stateService;
|
||||
_vaultTimeoutService = vaultTimeoutService;
|
||||
_cipherService = cipherService;
|
||||
_userVerificationMediatorService = userVerificationMediatorService;
|
||||
_deviceActionService = deviceActionService;
|
||||
}
|
||||
|
||||
public bool HasVaultBeenUnlockedInThisTransaction => true;
|
||||
|
||||
public async Task<(string CipherId, bool UserVerified)> ConfirmNewCredentialAsync(Fido2ConfirmNewCredentialParams confirmNewCredentialParams)
|
||||
{
|
||||
_confirmCredentialTcs?.TrySetCanceled();
|
||||
_confirmCredentialTcs = null;
|
||||
_confirmCredentialTcs = new TaskCompletionSource<(string cipherId, bool? userVerified)>();
|
||||
|
||||
_currentDefaultUserVerificationOptions = new Fido2UserVerificationOptions(false, confirmNewCredentialParams.UserVerificationPreference, true, confirmNewCredentialParams.RpId);
|
||||
|
||||
var messagingService = ServiceContainer.Resolve<IMessagingService>("messagingService");
|
||||
messagingService?.Send("fidoNavigateToAutofillCipher", confirmNewCredentialParams);
|
||||
var (cipherId, isUserVerified) = await _confirmCredentialTcs.Task;
|
||||
|
||||
|
||||
var verified = isUserVerified;
|
||||
if (verified is null)
|
||||
{
|
||||
var userVerification = await VerifyUserAsync(cipherId, confirmNewCredentialParams.UserVerificationPreference, confirmNewCredentialParams.RpId);
|
||||
// TODO: If cancelled then let the user choose another cipher.
|
||||
// I think this can be done by showing a message to the uesr and recursive calling of this method ConfirmNewCredentialAsync
|
||||
verified = !userVerification.IsCancelled && userVerification.Result;
|
||||
}
|
||||
|
||||
if (cipherId is null)
|
||||
{
|
||||
return await CreateNewLoginForFido2CredentialAsync(confirmNewCredentialParams, verified.Value);
|
||||
}
|
||||
|
||||
return (cipherId, verified.Value);
|
||||
}
|
||||
|
||||
private async Task<(string CipherId, bool UserVerified)> CreateNewLoginForFido2CredentialAsync(Fido2ConfirmNewCredentialParams confirmNewCredentialParams, bool userVerified)
|
||||
{
|
||||
if (!userVerified && await _userVerificationMediatorService.ShouldEnforceFido2RequiredUserVerificationAsync(new Fido2UserVerificationOptions
|
||||
(
|
||||
false,
|
||||
confirmNewCredentialParams.UserVerificationPreference,
|
||||
true,
|
||||
confirmNewCredentialParams.RpId
|
||||
)))
|
||||
{
|
||||
return (null, false);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
await _deviceActionService.ShowLoadingAsync(AppResources.Loading);
|
||||
|
||||
var cipherId = await _cipherService.CreateNewLoginForPasskeyAsync(confirmNewCredentialParams);
|
||||
|
||||
await _deviceActionService.HideLoadingAsync();
|
||||
|
||||
return (cipherId, userVerified);
|
||||
}
|
||||
catch
|
||||
{
|
||||
await _deviceActionService.HideLoadingAsync();
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task EnsureUnlockedVaultAsync()
|
||||
{
|
||||
if (!await _stateService.IsAuthenticatedAsync() || await _vaultTimeoutService.IsLockedAsync())
|
||||
{
|
||||
// this should never happen but just in case.
|
||||
throw new InvalidOperationException("Not authed or vault locked");
|
||||
}
|
||||
}
|
||||
|
||||
public Task InformExcludedCredentialAsync(string[] existingCipherIds)
|
||||
{
|
||||
// TODO: Show excluded credential to the user in some screen.
|
||||
return Task.FromResult(true);
|
||||
}
|
||||
|
||||
public void Confirm(string cipherId, bool? userVerified) => _confirmCredentialTcs?.TrySetResult((cipherId, userVerified));
|
||||
|
||||
public void Cancel() => _confirmCredentialTcs?.TrySetCanceled();
|
||||
|
||||
public void OnConfirmationException(Exception ex) => _confirmCredentialTcs?.TrySetException(ex);
|
||||
|
||||
private async Task<CancellableResult<bool>> VerifyUserAsync(string selectedCipherId, Fido2UserVerificationPreference userVerificationPreference, string rpId)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (selectedCipherId is null && userVerificationPreference == Fido2UserVerificationPreference.Discouraged)
|
||||
{
|
||||
return new CancellableResult<bool>(false);
|
||||
}
|
||||
|
||||
var shouldCheckMasterPasswordReprompt = false;
|
||||
if (selectedCipherId != null)
|
||||
{
|
||||
var encrypted = await _cipherService.GetAsync(selectedCipherId);
|
||||
var cipher = await encrypted.DecryptAsync();
|
||||
shouldCheckMasterPasswordReprompt = cipher?.Reprompt == Core.Enums.CipherRepromptType.Password;
|
||||
}
|
||||
|
||||
return await _userVerificationMediatorService.VerifyUserForFido2Async(
|
||||
new Fido2UserVerificationOptions(
|
||||
shouldCheckMasterPasswordReprompt,
|
||||
userVerificationPreference,
|
||||
true,
|
||||
rpId)
|
||||
);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LoggerHelper.LogEvenIfCantBeResolved(ex);
|
||||
return new CancellableResult<bool>(false);
|
||||
}
|
||||
}
|
||||
|
||||
public Fido2UserVerificationOptions? GetCurrentUserVerificationOptions() => _currentDefaultUserVerificationOptions;
|
||||
}
|
||||
}
|
@ -24,6 +24,7 @@ using Bit.App.Droid.Utilities;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using FileProvider = AndroidX.Core.Content.FileProvider;
|
||||
using Bit.Core.Utilities.Fido2;
|
||||
|
||||
namespace Bit.Droid
|
||||
{
|
||||
@ -325,12 +326,15 @@ namespace Bit.Droid
|
||||
|
||||
private AppOptions GetOptions()
|
||||
{
|
||||
var fido2CredentialAction = Intent.GetStringExtra(CredentialProviderConstants.Fido2CredentialAction);
|
||||
var options = new AppOptions
|
||||
{
|
||||
Uri = Intent.GetStringExtra("uri") ?? Intent.GetStringExtra(AutofillConstants.AutofillFrameworkUri),
|
||||
MyVaultTile = Intent.GetBooleanExtra("myVaultTile", false),
|
||||
GeneratorTile = Intent.GetBooleanExtra("generatorTile", false),
|
||||
FromAutofillFramework = Intent.GetBooleanExtra(AutofillConstants.AutofillFramework, false),
|
||||
Fido2CredentialAction = fido2CredentialAction,
|
||||
FromFido2Framework = !string.IsNullOrWhiteSpace(fido2CredentialAction),
|
||||
CreateSend = GetCreateSendRequest(Intent)
|
||||
};
|
||||
var fillType = Intent.GetIntExtra(AutofillConstants.AutofillFrameworkFillType, 0);
|
||||
|
@ -20,7 +20,10 @@ using Bit.App.Utilities;
|
||||
using Bit.App.Pages;
|
||||
using Bit.App.Utilities.AccountManagement;
|
||||
using Bit.App.Controls;
|
||||
using Bit.App.Platforms.Android.Autofill;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Services.UserVerification;
|
||||
|
||||
#if !FDROID
|
||||
using Android.Gms.Security;
|
||||
#endif
|
||||
@ -91,6 +94,43 @@ namespace Bit.Droid
|
||||
ServiceContainer.Resolve<ICryptoService>(),
|
||||
ServiceContainer.Resolve<IVaultTimeoutService>());
|
||||
ServiceContainer.Register<IUserPinService>(userPinService);
|
||||
|
||||
var userVerificationMediatorService = new UserVerificationMediatorService(
|
||||
ServiceContainer.Resolve<IPlatformUtilsService>("platformUtilsService"),
|
||||
ServiceContainer.Resolve<IPasswordRepromptService>("passwordRepromptService"),
|
||||
userPinService,
|
||||
deviceActionService,
|
||||
ServiceContainer.Resolve<IUserVerificationService>());
|
||||
ServiceContainer.Register<IUserVerificationMediatorService>(userVerificationMediatorService);
|
||||
|
||||
var fido2AuthenticatorService = new Fido2AuthenticatorService(
|
||||
ServiceContainer.Resolve<ICipherService>(),
|
||||
ServiceContainer.Resolve<ISyncService>(),
|
||||
ServiceContainer.Resolve<ICryptoFunctionService>(),
|
||||
userVerificationMediatorService);
|
||||
ServiceContainer.Register<IFido2AuthenticatorService>(fido2AuthenticatorService);
|
||||
|
||||
var fido2MakeCredentialUserInterface = new Fido2MakeCredentialUserInterface(
|
||||
ServiceContainer.Resolve<IStateService>(),
|
||||
ServiceContainer.Resolve<IVaultTimeoutService>(),
|
||||
ServiceContainer.Resolve<ICipherService>(),
|
||||
ServiceContainer.Resolve<IUserVerificationMediatorService>(),
|
||||
ServiceContainer.Resolve<IDeviceActionService>());
|
||||
ServiceContainer.Register<IFido2MakeCredentialConfirmationUserInterface>(fido2MakeCredentialUserInterface);
|
||||
|
||||
var fido2ClientService = new Fido2ClientService(
|
||||
ServiceContainer.Resolve<IStateService>(),
|
||||
ServiceContainer.Resolve<IEnvironmentService>(),
|
||||
ServiceContainer.Resolve<ICryptoFunctionService>(),
|
||||
ServiceContainer.Resolve<IFido2AuthenticatorService>(),
|
||||
new Fido2GetAssertionUserInterface(),
|
||||
fido2MakeCredentialUserInterface);
|
||||
ServiceContainer.Register<IFido2ClientService>(fido2ClientService);
|
||||
|
||||
ServiceContainer.Register<IFido2MediatorService>(new Fido2MediatorService(
|
||||
fido2AuthenticatorService,
|
||||
fido2ClientService,
|
||||
ServiceContainer.Resolve<ICipherService>()));
|
||||
}
|
||||
#if !FDROID
|
||||
if (Build.VERSION.SdkInt <= BuildVersionCodes.Kitkat)
|
||||
|
@ -1,11 +1,11 @@
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Android.App;
|
||||
using Android.App;
|
||||
using Android.App.Assist;
|
||||
using Android.Content;
|
||||
using Android.Credentials;
|
||||
using Android.OS;
|
||||
using Android.Provider;
|
||||
using Android.Views.Autofill;
|
||||
using Bit.App.Abstractions;
|
||||
using Bit.Core.Resources.Localization;
|
||||
using Bit.Core.Abstractions;
|
||||
using Bit.Core.Enums;
|
||||
@ -43,9 +43,28 @@ namespace Bit.Droid.Services
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
// TODO - find a way to programmatically check if the credential provider service is enabled
|
||||
var activity = (MainActivity)Platform.CurrentActivity;
|
||||
if (activity == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var credManager = activity.GetSystemService(Java.Lang.Class.FromType(typeof(CredentialManager))) as CredentialManager;
|
||||
if (credManager == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var credentialProviderServiceComponentName = new ComponentName(activity, Java.Lang.Class.FromType(typeof(CredentialProviderService)));
|
||||
return credManager.IsEnabledCredentialProviderService(credentialProviderServiceComponentName);
|
||||
}
|
||||
catch (Java.Lang.NullPointerException)
|
||||
{
|
||||
// CredentialManager API is not working fully and may return a NullPointerException even if the CredentialProviderService is working and enabled
|
||||
// Info Here: https://developer.android.com/reference/android/credentials/CredentialManager#isEnabledCredentialProviderService(android.content.ComponentName)
|
||||
return false;
|
||||
}
|
||||
catch
|
||||
@ -184,7 +203,10 @@ namespace Bit.Droid.Services
|
||||
{
|
||||
try
|
||||
{
|
||||
// TODO - find a way to programmatically disable the provider service, or take the user to the settings page where they can do it
|
||||
// We should try to find a way to programmatically disable the provider service when the API allows for it.
|
||||
// For now we'll take the user to Credential Settings so they can manually disable it
|
||||
var deviceActionService = ServiceContainer.Resolve<IDeviceActionService>();
|
||||
deviceActionService.OpenCredentialProviderSettings();
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
|
@ -1,6 +1,4 @@
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using Android.App;
|
||||
using Android.App;
|
||||
using Android.Content;
|
||||
using Android.Content.PM;
|
||||
using Android.Nfc;
|
||||
@ -17,11 +15,14 @@ using Bit.Core.Resources.Localization;
|
||||
using Bit.App.Utilities;
|
||||
using Bit.App.Utilities.Prompts;
|
||||
using Bit.Core.Abstractions;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.App.Droid.Utilities;
|
||||
using Bit.App.Models;
|
||||
using Bit.Droid.Autofill;
|
||||
using Microsoft.Maui.Controls.Compatibility.Platform.Android;
|
||||
using Resource = Bit.Core.Resource;
|
||||
using Application = Android.App.Application;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Utilities.Fido2;
|
||||
|
||||
namespace Bit.Droid.Services
|
||||
{
|
||||
@ -204,7 +205,7 @@ namespace Bit.Droid.Services
|
||||
string text = null, string okButtonText = null, string cancelButtonText = null,
|
||||
bool numericKeyboard = false, bool autofocus = true, bool password = false)
|
||||
{
|
||||
var activity = (MainActivity)Microsoft.Maui.ApplicationModel.Platform.CurrentActivity;
|
||||
var activity = Microsoft.Maui.ApplicationModel.Platform.CurrentActivity;
|
||||
if (activity == null)
|
||||
{
|
||||
return Task.FromResult<string>(null);
|
||||
@ -261,7 +262,7 @@ namespace Bit.Droid.Services
|
||||
|
||||
public Task<ValidatablePromptResponse?> DisplayValidatablePromptAsync(ValidatablePromptConfig config)
|
||||
{
|
||||
var activity = (MainActivity)Microsoft.Maui.ApplicationModel.Platform.CurrentActivity;
|
||||
var activity = Microsoft.Maui.ApplicationModel.Platform.CurrentActivity;
|
||||
if (activity == null)
|
||||
{
|
||||
return Task.FromResult<ValidatablePromptResponse?>(null);
|
||||
@ -338,7 +339,7 @@ namespace Bit.Droid.Services
|
||||
|
||||
public void RateApp()
|
||||
{
|
||||
var activity = (MainActivity)Microsoft.Maui.ApplicationModel.Platform.CurrentActivity;
|
||||
var activity = Microsoft.Maui.ApplicationModel.Platform.CurrentActivity;
|
||||
try
|
||||
{
|
||||
var rateIntent = RateIntentForUrl("market://details", activity);
|
||||
@ -371,14 +372,14 @@ namespace Bit.Droid.Services
|
||||
|
||||
public bool SupportsNfc()
|
||||
{
|
||||
var activity = (MainActivity)Microsoft.Maui.ApplicationModel.Platform.CurrentActivity;
|
||||
var activity = Microsoft.Maui.ApplicationModel.Platform.CurrentActivity;
|
||||
var manager = activity.GetSystemService(Context.NfcService) as NfcManager;
|
||||
return manager.DefaultAdapter?.IsEnabled ?? false;
|
||||
}
|
||||
|
||||
public bool SupportsCamera()
|
||||
{
|
||||
var activity = (MainActivity)Microsoft.Maui.ApplicationModel.Platform.CurrentActivity;
|
||||
var activity = Microsoft.Maui.ApplicationModel.Platform.CurrentActivity;
|
||||
return activity.PackageManager.HasSystemFeature(PackageManager.FeatureCamera);
|
||||
}
|
||||
|
||||
@ -394,7 +395,7 @@ namespace Bit.Droid.Services
|
||||
|
||||
public Task<string> DisplayAlertAsync(string title, string message, string cancel, params string[] buttons)
|
||||
{
|
||||
var activity = (MainActivity)Microsoft.Maui.ApplicationModel.Platform.CurrentActivity;
|
||||
var activity = Microsoft.Maui.ApplicationModel.Platform.CurrentActivity;
|
||||
if (activity == null)
|
||||
{
|
||||
return Task.FromResult<string>(null);
|
||||
@ -475,7 +476,7 @@ namespace Bit.Droid.Services
|
||||
|
||||
public void OpenAccessibilityOverlayPermissionSettings()
|
||||
{
|
||||
var activity = (MainActivity)Microsoft.Maui.ApplicationModel.Platform.CurrentActivity;
|
||||
var activity = Microsoft.Maui.ApplicationModel.Platform.CurrentActivity;
|
||||
try
|
||||
{
|
||||
var intent = new Intent(Settings.ActionManageOverlayPermission);
|
||||
@ -504,10 +505,10 @@ namespace Bit.Droid.Services
|
||||
|
||||
public void OpenCredentialProviderSettings()
|
||||
{
|
||||
var activity = (MainActivity)Microsoft.Maui.ApplicationModel.Platform.CurrentActivity;
|
||||
var activity = Microsoft.Maui.ApplicationModel.Platform.CurrentActivity;
|
||||
try
|
||||
{
|
||||
var pendingIntent = CredentialManager.Create(activity).CreateSettingsPendingIntent();
|
||||
var pendingIntent = ICredentialManager.Create(activity).CreateSettingsPendingIntent();
|
||||
pendingIntent.Send();
|
||||
}
|
||||
catch (ActivityNotFoundException)
|
||||
@ -527,7 +528,7 @@ namespace Bit.Droid.Services
|
||||
{
|
||||
try
|
||||
{
|
||||
var activity = (MainActivity)Microsoft.Maui.ApplicationModel.Platform.CurrentActivity;
|
||||
var activity = Microsoft.Maui.ApplicationModel.Platform.CurrentActivity;
|
||||
var intent = new Intent(Settings.ActionAccessibilitySettings);
|
||||
activity.StartActivity(intent);
|
||||
}
|
||||
@ -536,7 +537,7 @@ namespace Bit.Droid.Services
|
||||
|
||||
public void OpenAutofillSettings()
|
||||
{
|
||||
var activity = (MainActivity)Microsoft.Maui.ApplicationModel.Platform.CurrentActivity;
|
||||
var activity = Microsoft.Maui.ApplicationModel.Platform.CurrentActivity;
|
||||
try
|
||||
{
|
||||
var intent = new Intent(Settings.ActionRequestSetAutofillService);
|
||||
@ -564,10 +565,88 @@ namespace Bit.Droid.Services
|
||||
// ref: https://developer.android.com/reference/android/os/SystemClock#elapsedRealtime()
|
||||
return SystemClock.ElapsedRealtime();
|
||||
}
|
||||
|
||||
public async Task ExecuteFido2CredentialActionAsync(AppOptions appOptions)
|
||||
{
|
||||
var activity = Microsoft.Maui.ApplicationModel.Platform.CurrentActivity;
|
||||
if (activity == null || string.IsNullOrWhiteSpace(appOptions.Fido2CredentialAction))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (appOptions.Fido2CredentialAction == CredentialProviderConstants.Fido2CredentialGet)
|
||||
{
|
||||
await ExecuteFido2GetCredentialAsync(appOptions);
|
||||
}
|
||||
else if (appOptions.Fido2CredentialAction == CredentialProviderConstants.Fido2CredentialCreate)
|
||||
{
|
||||
await ExecuteFido2CreateCredentialAsync();
|
||||
}
|
||||
|
||||
appOptions.Fido2CredentialAction = null; //Clear CredentialAction Value
|
||||
}
|
||||
|
||||
private async Task ExecuteFido2GetCredentialAsync(AppOptions appOptions)
|
||||
{
|
||||
var activity = Microsoft.Maui.ApplicationModel.Platform.CurrentActivity;
|
||||
if (activity == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var request = AndroidX.Credentials.Provider.PendingIntentHandler.RetrieveBeginGetCredentialRequest(activity.Intent);
|
||||
var response = new AndroidX.Credentials.Provider.BeginGetCredentialResponse();;
|
||||
var credentialEntries = new List<AndroidX.Credentials.Provider.CredentialEntry>();
|
||||
foreach (var option in request.BeginGetCredentialOptions.OfType<AndroidX.Credentials.Provider.BeginGetPublicKeyCredentialOption>())
|
||||
{
|
||||
credentialEntries.AddRange(await Bit.App.Platforms.Android.Autofill.CredentialHelpers.PopulatePasskeyDataAsync(request.CallingAppInfo, option, activity, appOptions.HasUnlockedInThisTransaction));
|
||||
}
|
||||
|
||||
if (credentialEntries.Any())
|
||||
{
|
||||
response = new AndroidX.Credentials.Provider.BeginGetCredentialResponse.Builder()
|
||||
.SetCredentialEntries(credentialEntries)
|
||||
.Build();
|
||||
}
|
||||
|
||||
var result = new Android.Content.Intent();
|
||||
AndroidX.Credentials.Provider.PendingIntentHandler.SetBeginGetCredentialResponse(result, response);
|
||||
activity.SetResult(Result.Ok, result);
|
||||
activity.Finish();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Bit.Core.Services.LoggerHelper.LogEvenIfCantBeResolved(ex);
|
||||
|
||||
activity.SetResult(Result.Canceled);
|
||||
activity.Finish();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task ExecuteFido2CreateCredentialAsync()
|
||||
{
|
||||
var activity = Microsoft.Maui.ApplicationModel.Platform.CurrentActivity;
|
||||
if (activity == null) { return; }
|
||||
|
||||
try
|
||||
{
|
||||
var getRequest = AndroidX.Credentials.Provider.PendingIntentHandler.RetrieveProviderCreateCredentialRequest(activity.Intent);
|
||||
await Bit.App.Platforms.Android.Autofill.CredentialHelpers.CreateCipherPasskeyAsync(getRequest, activity);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Bit.Core.Services.LoggerHelper.LogEvenIfCantBeResolved(ex);
|
||||
|
||||
activity.SetResult(Result.Canceled);
|
||||
activity.Finish();
|
||||
}
|
||||
}
|
||||
|
||||
public void CloseMainApp()
|
||||
{
|
||||
var activity = (MainActivity)Microsoft.Maui.ApplicationModel.Platform.CurrentActivity;
|
||||
var activity = Microsoft.Maui.ApplicationModel.Platform.CurrentActivity;
|
||||
if (activity == null)
|
||||
{
|
||||
return;
|
||||
@ -608,7 +687,7 @@ namespace Bit.Droid.Services
|
||||
|
||||
public float GetSystemFontSizeScale()
|
||||
{
|
||||
var activity = Microsoft.Maui.ApplicationModel.Platform.CurrentActivity as MainActivity;
|
||||
var activity = Microsoft.Maui.ApplicationModel.Platform.CurrentActivity;
|
||||
return activity?.Resources?.Configuration?.FontScale ?? 1;
|
||||
}
|
||||
|
||||
|
@ -1,4 +1,5 @@
|
||||
using System.Threading.Tasks;
|
||||
using Bit.App.Models;
|
||||
using Bit.App.Utilities.Prompts;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Models;
|
||||
@ -40,6 +41,7 @@ namespace Bit.App.Abstractions
|
||||
void OpenCredentialProviderSettings();
|
||||
void OpenAutofillSettings();
|
||||
long GetActiveTime();
|
||||
Task ExecuteFido2CredentialActionAsync(AppOptions appOptions);
|
||||
void CloseMainApp();
|
||||
float GetSystemFontSizeScale();
|
||||
Task OnAccountSwitchCompleteAsync();
|
||||
|
@ -0,0 +1,32 @@
|
||||
using Bit.Core.Utilities.Fido2;
|
||||
|
||||
namespace Bit.Core.Abstractions
|
||||
{
|
||||
public interface IFido2MakeCredentialConfirmationUserInterface : IFido2MakeCredentialUserInterface
|
||||
{
|
||||
/// <summary>
|
||||
/// Call this method after the use chose where to save the new Fido2 credential.
|
||||
/// </summary>
|
||||
/// <param name="cipherId">
|
||||
/// Cipher ID where to save the new credential.
|
||||
/// If <c>null</c> a new default passkey cipher item will be created
|
||||
/// </param>
|
||||
/// <param name="userVerified">
|
||||
/// Whether the user has been verified or not.
|
||||
/// If <c>null</c> verification has not taken place yet.
|
||||
/// </param>
|
||||
void Confirm(string cipherId, bool? userVerified);
|
||||
|
||||
/// <summary>
|
||||
/// Cancels the current flow to make a credential
|
||||
/// </summary>
|
||||
void Cancel();
|
||||
|
||||
/// <summary>
|
||||
/// Call this if an exception needs to happen on the credential making process
|
||||
/// </summary>
|
||||
void OnConfirmationException(Exception ex);
|
||||
|
||||
Fido2UserVerificationOptions? GetCurrentUserVerificationOptions();
|
||||
}
|
||||
}
|
@ -14,6 +14,7 @@ using Bit.Core.Models.Response;
|
||||
using Bit.Core.Pages;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Utilities;
|
||||
using Bit.Core.Utilities.Fido2;
|
||||
|
||||
[assembly: XamlCompilation(XamlCompilationOptions.Compile)]
|
||||
namespace Bit.App
|
||||
@ -104,6 +105,8 @@ namespace Bit.App
|
||||
Options.MyVaultTile = appOptions.MyVaultTile;
|
||||
Options.GeneratorTile = appOptions.GeneratorTile;
|
||||
Options.FromAutofillFramework = appOptions.FromAutofillFramework;
|
||||
Options.FromFido2Framework = appOptions.FromFido2Framework;
|
||||
Options.Fido2CredentialAction = appOptions.Fido2CredentialAction;
|
||||
Options.CreateSend = appOptions.CreateSend;
|
||||
}
|
||||
}
|
||||
@ -120,6 +123,15 @@ namespace Bit.App
|
||||
return new Window(new NavigationPage()); //No actual page needed. Only used for auto-filling the fields directly (externally)
|
||||
}
|
||||
|
||||
//When executing from CredentialProviderSelectionActivity we don't have "Options" so we need to filter "manually"
|
||||
//In the CredentialProviderSelectionActivity we don't need to show any Page, so we just create a "dummy" Window with a NavigationPage to avoid crashing.
|
||||
if (activationState != null
|
||||
&& activationState.State.ContainsKey("CREDENTIAL_DATA")
|
||||
&& activationState.State.ContainsKey("credentialProviderCipherId"))
|
||||
{
|
||||
return new Window(new NavigationPage()); //No actual page needed. Only used for auto-filling the fields directly (externally)
|
||||
}
|
||||
|
||||
_isResumed = true;
|
||||
return new ResumeWindow(new NavigationPage(new AndroidNavigationRedirectPage(Options)));
|
||||
}
|
||||
@ -182,7 +194,6 @@ namespace Bit.App
|
||||
{
|
||||
var details = message.Data as DialogDetails;
|
||||
ArgumentNullException.ThrowIfNull(details);
|
||||
ArgumentNullException.ThrowIfNull(MainPage);
|
||||
|
||||
var confirmed = true;
|
||||
var confirmText = string.IsNullOrWhiteSpace(details.ConfirmText) ?
|
||||
@ -192,12 +203,14 @@ namespace Bit.App
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(details.CancelText))
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(MainPage);
|
||||
|
||||
confirmed = await MainPage.DisplayAlert(details.Title, details.Text, confirmText,
|
||||
details.CancelText);
|
||||
}
|
||||
else
|
||||
{
|
||||
await MainPage.DisplayAlert(details.Title, details.Text, confirmText);
|
||||
await _deviceActionService.DisplayAlertAsync(details.Title, details.Text, confirmText);
|
||||
}
|
||||
_messagingService.Send("showDialogResolve", new Tuple<int, bool>(details.DialogId, confirmed));
|
||||
}
|
||||
@ -218,17 +231,17 @@ namespace Bit.App
|
||||
await _accountsManager.NavigateOnAccountChangeAsync();
|
||||
}
|
||||
else if (message.Command == POP_ALL_AND_GO_TO_TAB_GENERATOR_MESSAGE ||
|
||||
message.Command == POP_ALL_AND_GO_TO_TAB_MYVAULT_MESSAGE ||
|
||||
message.Command == POP_ALL_AND_GO_TO_TAB_SEND_MESSAGE ||
|
||||
message.Command == POP_ALL_AND_GO_TO_AUTOFILL_CIPHERS_MESSAGE ||
|
||||
message.Command == DeepLinkContext.NEW_OTP_MESSAGE)
|
||||
message.Command == POP_ALL_AND_GO_TO_TAB_MYVAULT_MESSAGE ||
|
||||
message.Command == POP_ALL_AND_GO_TO_TAB_SEND_MESSAGE ||
|
||||
message.Command == POP_ALL_AND_GO_TO_AUTOFILL_CIPHERS_MESSAGE ||
|
||||
message.Command == DeepLinkContext.NEW_OTP_MESSAGE)
|
||||
{
|
||||
if (message.Command == DeepLinkContext.NEW_OTP_MESSAGE)
|
||||
{
|
||||
Options.OtpData = new OtpData((string)message.Data);
|
||||
}
|
||||
|
||||
await MainThread.InvokeOnMainThreadAsync(ExecuteNavigationAction);
|
||||
await MainThread.InvokeOnMainThreadAsync(ExecuteNavigationAction);
|
||||
async Task ExecuteNavigationAction()
|
||||
{
|
||||
if (MainPage is TabsPage tabsPage)
|
||||
@ -239,6 +252,7 @@ namespace Bit.App
|
||||
{
|
||||
await tabsPage.Navigation.PopModalAsync(false);
|
||||
}
|
||||
|
||||
if (message.Command == POP_ALL_AND_GO_TO_AUTOFILL_CIPHERS_MESSAGE)
|
||||
{
|
||||
MainPage = new NavigationPage(new CipherSelectionPage(Options));
|
||||
@ -266,6 +280,19 @@ namespace Bit.App
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (message.Command == "fidoNavigateToAutofillCipher" && message.Data is Fido2ConfirmNewCredentialParams createParams)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(MainPage);
|
||||
ArgumentNullException.ThrowIfNull(Options);
|
||||
await MainThread.InvokeOnMainThreadAsync(NavigateToCipherSelectionPageAction);
|
||||
void NavigateToCipherSelectionPageAction()
|
||||
{
|
||||
Options.Uri = createParams.RpId;
|
||||
Options.SaveUsername = createParams.UserName;
|
||||
Options.SaveName = createParams.CredentialName;
|
||||
MainPage = new NavigationPage(new CipherSelectionPage(Options));
|
||||
}
|
||||
}
|
||||
else if (message.Command == "convertAccountToKeyConnector")
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(MainPage);
|
||||
@ -304,6 +331,12 @@ namespace Bit.App
|
||||
|| message.Command == "unlocked"
|
||||
|| message.Command == AccountsManagerMessageCommands.ACCOUNT_SWITCH_COMPLETED)
|
||||
{
|
||||
if (message.Command == AccountsManagerMessageCommands.ACCOUNT_SWITCH_COMPLETED)
|
||||
{
|
||||
var userVerificationMediatorService = ServiceContainer.Resolve<IFido2MakeCredentialConfirmationUserInterface>();
|
||||
userVerificationMediatorService?.OnConfirmationException(new AccountSwitchedException());
|
||||
}
|
||||
|
||||
lock (_processingLoginRequestLock)
|
||||
{
|
||||
// lock doesn't allow for async execution
|
||||
|
22
src/Core/Controls/Settings/ExternalLinkSubtitleItemView.xaml
Normal file
22
src/Core/Controls/Settings/ExternalLinkSubtitleItemView.xaml
Normal file
@ -0,0 +1,22 @@
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<controls:BaseSettingItemView
|
||||
xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
|
||||
xmlns:controls="clr-namespace:Bit.App.Controls"
|
||||
xmlns:core="clr-namespace:Bit.Core"
|
||||
x:Class="Bit.App.Controls.ExternalLinkSubtitleItemView"
|
||||
x:Name="_contentView"
|
||||
ControlTemplate="{StaticResource SettingControlTemplate}">
|
||||
<controls:BaseSettingItemView.GestureRecognizers>
|
||||
<TapGestureRecognizer Tapped="ContentView_Tapped" />
|
||||
</controls:BaseSettingItemView.GestureRecognizers>
|
||||
|
||||
<controls:IconLabel
|
||||
Text="{Binding Source={x:Static core:BitwardenIcons.ExternalLink}}"
|
||||
TextColor="{DynamicResource TextColor}"
|
||||
FontSize="25"
|
||||
Margin="6,0,7,0"
|
||||
HorizontalOptions="End"
|
||||
VerticalOptions="Center"
|
||||
SemanticProperties.Description="{Binding Title, Mode=OneWay, Source={x:Reference _contentView}}" />
|
||||
</controls:BaseSettingItemView>
|
@ -0,0 +1,26 @@
|
||||
using System.Windows.Input;
|
||||
|
||||
namespace Bit.App.Controls
|
||||
{
|
||||
public partial class ExternalLinkSubtitleItemView : BaseSettingItemView
|
||||
{
|
||||
public static readonly BindableProperty GoToLinkCommandProperty = BindableProperty.Create(
|
||||
nameof(GoToLinkCommand), typeof(ICommand), typeof(ExternalLinkSubtitleItemView));
|
||||
|
||||
public ExternalLinkSubtitleItemView()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
|
||||
public ICommand GoToLinkCommand
|
||||
{
|
||||
get => GetValue(GoToLinkCommandProperty) as ICommand;
|
||||
set => SetValue(GoToLinkCommandProperty, value);
|
||||
}
|
||||
|
||||
void ContentView_Tapped(System.Object sender, System.EventArgs e)
|
||||
{
|
||||
GoToLinkCommand?.Execute(null);
|
||||
}
|
||||
}
|
||||
}
|
@ -95,6 +95,9 @@
|
||||
<LastGenOutput>AppResources.Designer.cs</LastGenOutput>
|
||||
<Generator>PublicResXFileCodeGenerator</Generator>
|
||||
</EmbeddedResource>
|
||||
<Compile Update="Controls\Settings\ExternalLinkSubtitleItemView.xaml.cs">
|
||||
<DependentUpon>ExternalLinkSubtitleItemView.xaml</DependentUpon>
|
||||
</Compile>
|
||||
<Compile Update="Pages\AndroidNavigationRedirectPage.xaml.cs">
|
||||
<DependentUpon>AndroidNavigationRedirectPage.xaml</DependentUpon>
|
||||
</Compile>
|
||||
@ -105,6 +108,9 @@
|
||||
</Compile>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<MauiXaml Update="Controls\Settings\ExternalLinkSubtitleItemView.xaml">
|
||||
<Generator>MSBuild:Compile</Generator>
|
||||
</MauiXaml>
|
||||
<MauiXaml Update="Pages\AndroidNavigationRedirectPage.xaml">
|
||||
<Generator>MSBuild:Compile</Generator>
|
||||
</MauiXaml>
|
||||
|
@ -1,5 +1,4 @@
|
||||
using System;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Utilities;
|
||||
|
||||
namespace Bit.App.Models
|
||||
@ -9,6 +8,8 @@ namespace Bit.App.Models
|
||||
public bool MyVaultTile { get; set; }
|
||||
public bool GeneratorTile { get; set; }
|
||||
public bool FromAutofillFramework { get; set; }
|
||||
public bool FromFido2Framework { get; set; }
|
||||
public string Fido2CredentialAction { get; set; }
|
||||
public CipherType? FillType { get; set; }
|
||||
public string Uri { get; set; }
|
||||
public CipherType? SaveType { get; set; }
|
||||
@ -25,6 +26,7 @@ namespace Bit.App.Models
|
||||
public bool CopyInsteadOfShareAfterSaving { get; set; }
|
||||
public bool HideAccountSwitcher { get; set; }
|
||||
public OtpData? OtpData { get; set; }
|
||||
public bool HasUnlockedInThisTransaction { get; set; }
|
||||
|
||||
public void SetAllFrom(AppOptions o)
|
||||
{
|
||||
@ -35,6 +37,7 @@ namespace Bit.App.Models
|
||||
MyVaultTile = o.MyVaultTile;
|
||||
GeneratorTile = o.GeneratorTile;
|
||||
FromAutofillFramework = o.FromAutofillFramework;
|
||||
Fido2CredentialAction = o.Fido2CredentialAction;
|
||||
FillType = o.FillType;
|
||||
Uri = o.Uri;
|
||||
SaveType = o.SaveType;
|
||||
@ -51,6 +54,7 @@ namespace Bit.App.Models
|
||||
CopyInsteadOfShareAfterSaving = o.CopyInsteadOfShareAfterSaving;
|
||||
HideAccountSwitcher = o.HideAccountSwitcher;
|
||||
OtpData = o.OtpData;
|
||||
HasUnlockedInThisTransaction = o.HasUnlockedInThisTransaction;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -168,7 +168,7 @@ namespace Bit.App.Pages
|
||||
var tasks = Task.Run(async () =>
|
||||
{
|
||||
await Task.Delay(50);
|
||||
MainThread.BeginInvokeOnMainThread(async () => await _vm.SubmitAsync());
|
||||
_vm.SubmitCommand.Execute(null);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -1,5 +1,6 @@
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows.Input;
|
||||
using Bit.App.Abstractions;
|
||||
using Bit.App.Controls;
|
||||
using Bit.Core.Resources.Localization;
|
||||
@ -73,7 +74,7 @@ namespace Bit.App.Pages
|
||||
|
||||
PageTitle = AppResources.VerifyMasterPassword;
|
||||
TogglePasswordCommand = new Command(TogglePassword);
|
||||
SubmitCommand = new Command(async () => await SubmitAsync());
|
||||
SubmitCommand = CreateDefaultAsyncRelayCommand(SubmitAsync, onException: _logger.Exception, allowsMultipleExecutions: false);
|
||||
|
||||
AccountSwitchingOverlayViewModel =
|
||||
new AccountSwitchingOverlayViewModel(_stateService, _messagingService, _logger)
|
||||
@ -157,7 +158,7 @@ namespace Bit.App.Pages
|
||||
|
||||
public AccountSwitchingOverlayViewModel AccountSwitchingOverlayViewModel { get; }
|
||||
|
||||
public Command SubmitCommand { get; }
|
||||
public ICommand SubmitCommand { get; }
|
||||
public Command TogglePasswordCommand { get; }
|
||||
|
||||
public string ShowPasswordIcon => ShowPassword ? BitwardenIcons.EyeSlash : BitwardenIcons.Eye;
|
||||
@ -233,8 +234,8 @@ namespace Bit.App.Pages
|
||||
}
|
||||
BiometricButtonVisible = true;
|
||||
BiometricButtonText = AppResources.UseBiometricsToUnlock;
|
||||
// TODO Xamarin.Forms.Device.RuntimePlatform is no longer supported. Use Microsoft.Maui.Devices.DeviceInfo.Platform instead. For more details see https://learn.microsoft.com/en-us/dotnet/maui/migration/forms-projects#device-changes
|
||||
if (Device.RuntimePlatform == Device.iOS)
|
||||
|
||||
if (DeviceInfo.Platform == DevicePlatform.iOS)
|
||||
{
|
||||
var supportsFace = await _deviceActionService.SupportsFaceBiometricAsync();
|
||||
BiometricButtonText = supportsFace ? AppResources.UseFaceIDToUnlock :
|
||||
@ -330,6 +331,7 @@ namespace Bit.App.Pages
|
||||
Pin = string.Empty;
|
||||
await AppHelpers.ResetInvalidUnlockAttemptsAsync();
|
||||
await SetUserKeyAndContinueAsync(userKey);
|
||||
await Task.Delay(150); //Workaround Delay to avoid "duplicate" execution of SubmitAsync on Android when invoked from the ReturnCommand
|
||||
}
|
||||
}
|
||||
catch (LegacyUserException)
|
||||
@ -418,6 +420,7 @@ namespace Bit.App.Pages
|
||||
var userKey = await _cryptoService.DecryptUserKeyWithMasterKeyAsync(masterKey);
|
||||
await _cryptoService.SetMasterKeyAsync(masterKey);
|
||||
await SetUserKeyAndContinueAsync(userKey);
|
||||
await Task.Delay(150); //Workaround Delay to avoid "duplicate" execution of SubmitAsync on Android when invoked from the ReturnCommand
|
||||
|
||||
// Re-enable biometrics
|
||||
if (BiometricEnabled & !BiometricIntegrityValid)
|
||||
|
@ -19,15 +19,6 @@
|
||||
Text="{u:I18n Autofill}"
|
||||
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
|
||||
Title="{u:I18n AutofillServices}"
|
||||
Subtitle="{u:I18n AutofillServicesExplanationLong}"
|
||||
@ -47,6 +38,15 @@
|
||||
StyleClass="settings-item-view"
|
||||
HorizontalOptions="FillAndExpand" />
|
||||
|
||||
<controls:ExternalLinkSubtitleItemView
|
||||
Title="{u:I18n PasskeyManagement}"
|
||||
Subtitle="{u:I18n PasskeyManagementExplanationLong}"
|
||||
IsVisible="{Binding SupportsCredentialProviderService}"
|
||||
GoToLinkCommand="{Binding GoToCredentialProviderSettingsCommand}"
|
||||
AutomationId="CredentialProviderServiceSwitch"
|
||||
StyleClass="settings-item-view"
|
||||
HorizontalOptions="FillAndExpand" />
|
||||
|
||||
<controls:SwitchItemView
|
||||
Title="{u:I18n Accessibility}"
|
||||
Subtitle="{Binding UseAccessibilityDescription}"
|
||||
|
@ -6,7 +6,6 @@ namespace Bit.App.Pages
|
||||
{
|
||||
public partial class AutofillSettingsPageViewModel
|
||||
{
|
||||
private bool _useCredentialProviderService;
|
||||
private bool _useAutofillServices;
|
||||
private bool _useInlineAutofill;
|
||||
private bool _useAccessibility;
|
||||
@ -15,18 +14,6 @@ namespace Bit.App.Pages
|
||||
|
||||
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 UseAutofillServices
|
||||
@ -99,23 +86,23 @@ namespace Bit.App.Pages
|
||||
}
|
||||
}
|
||||
|
||||
public AsyncRelayCommand ToggleUseCredentialProviderServiceCommand { get; private set; }
|
||||
public AsyncRelayCommand ToggleUseAutofillServicesCommand { get; private set; }
|
||||
public AsyncRelayCommand ToggleUseInlineAutofillCommand { get; private set; }
|
||||
public AsyncRelayCommand ToggleUseAccessibilityCommand { get; private set; }
|
||||
public AsyncRelayCommand ToggleUseDrawOverCommand { get; private set; }
|
||||
public AsyncRelayCommand ToggleAskToAddLoginCommand { get; private set; }
|
||||
public ICommand GoToBlockAutofillUrisCommand { get; private set; }
|
||||
public ICommand GoToCredentialProviderSettingsCommand { get; private set; }
|
||||
|
||||
private void InitAndroidCommands()
|
||||
{
|
||||
ToggleUseCredentialProviderServiceCommand = CreateDefaultAsyncRelayCommand(() => MainThread.InvokeOnMainThreadAsync(() => ToggleUseCredentialProviderService()), () => _inited, allowsMultipleExecutions: false);
|
||||
ToggleUseAutofillServicesCommand = CreateDefaultAsyncRelayCommand(() => MainThread.InvokeOnMainThreadAsync(() => ToggleUseAutofillServices()), () => _inited, allowsMultipleExecutions: false);
|
||||
ToggleUseInlineAutofillCommand = CreateDefaultAsyncRelayCommand(() => MainThread.InvokeOnMainThreadAsync(() => ToggleUseInlineAutofillEnabledAsync()), () => _inited, allowsMultipleExecutions: false);
|
||||
ToggleUseAccessibilityCommand = CreateDefaultAsyncRelayCommand(ToggleUseAccessibilityAsync, () => _inited, allowsMultipleExecutions: false);
|
||||
ToggleUseDrawOverCommand = CreateDefaultAsyncRelayCommand(() => MainThread.InvokeOnMainThreadAsync(() => ToggleDrawOver()), () => _inited, allowsMultipleExecutions: false);
|
||||
ToggleAskToAddLoginCommand = CreateDefaultAsyncRelayCommand(ToggleAskToAddLoginAsync, () => _inited, allowsMultipleExecutions: false);
|
||||
GoToBlockAutofillUrisCommand = CreateDefaultAsyncRelayCommand(() => Page.Navigation.PushAsync(new BlockAutofillUrisPage()), allowsMultipleExecutions: false);
|
||||
GoToCredentialProviderSettingsCommand = CreateDefaultAsyncRelayCommand(() => MainThread.InvokeOnMainThreadAsync(() => GoToCredentialProviderSettings()), () => _inited, allowsMultipleExecutions: false);
|
||||
}
|
||||
|
||||
private async Task InitAndroidAutofillSettingsAsync()
|
||||
@ -132,9 +119,6 @@ namespace Bit.App.Pages
|
||||
|
||||
private async Task UpdateAndroidAutofillSettingsAsync()
|
||||
{
|
||||
// TODO - uncomment once _autofillHandler.CredentialProviderServiceEnabled() returns a real value
|
||||
// _useCredentialProviderService =
|
||||
// SupportsCredentialProviderService && _autofillHandler.CredentialProviderServiceEnabled();
|
||||
_useAutofillServices =
|
||||
_autofillHandler.SupportsAutofillService() && _autofillHandler.AutofillServiceEnabled();
|
||||
_useAccessibility = _autofillHandler.AutofillAccessibilityServiceRunning();
|
||||
@ -143,7 +127,6 @@ namespace Bit.App.Pages
|
||||
|
||||
await MainThread.InvokeOnMainThreadAsync(() =>
|
||||
{
|
||||
TriggerPropertyChanged(nameof(UseCredentialProviderService));
|
||||
TriggerPropertyChanged(nameof(UseAutofillServices));
|
||||
TriggerPropertyChanged(nameof(UseAccessibility));
|
||||
TriggerPropertyChanged(nameof(UseDrawOver));
|
||||
@ -151,16 +134,15 @@ namespace Bit.App.Pages
|
||||
});
|
||||
}
|
||||
|
||||
private void ToggleUseCredentialProviderService()
|
||||
private async Task GoToCredentialProviderSettings()
|
||||
{
|
||||
if (UseCredentialProviderService)
|
||||
var confirmed = await _platformUtilsService.ShowDialogAsync(AppResources.SetBitwardenAsPasskeyManagerDescription, AppResources.ContinueToDeviceSettings,
|
||||
AppResources.Continue,
|
||||
AppResources.Cancel);
|
||||
if (confirmed)
|
||||
{
|
||||
_deviceActionService.OpenCredentialProviderSettings();
|
||||
}
|
||||
else
|
||||
{
|
||||
_autofillHandler.DisableCredentialProviderService();
|
||||
}
|
||||
}
|
||||
|
||||
private void ToggleUseAutofillServices()
|
||||
|
@ -1,17 +1,23 @@
|
||||
using Bit.App.Models;
|
||||
using Bit.App.Utilities;
|
||||
using Bit.Core;
|
||||
using Bit.Core.Abstractions;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.Models.View;
|
||||
using Bit.Core.Resources.Localization;
|
||||
using Bit.Core.Utilities;
|
||||
using Bit.Core.Utilities.Fido2;
|
||||
|
||||
namespace Bit.App.Pages
|
||||
{
|
||||
public class AutofillCiphersPageViewModel : CipherSelectionPageViewModel
|
||||
{
|
||||
private CipherType? _fillType;
|
||||
private bool _isAndroidFido2CredentialCreation;
|
||||
private AppOptions _appOptions;
|
||||
|
||||
private readonly LazyResolve<IFido2MakeCredentialConfirmationUserInterface> _fido2MakeCredentialConfirmationUserInterface = new LazyResolve<IFido2MakeCredentialConfirmationUserInterface>();
|
||||
|
||||
public string Uri { get; set; }
|
||||
|
||||
@ -19,6 +25,8 @@ namespace Bit.App.Pages
|
||||
{
|
||||
Uri = appOptions?.Uri;
|
||||
_fillType = appOptions.FillType;
|
||||
_isAndroidFido2CredentialCreation = appOptions.FromFido2Framework;
|
||||
_appOptions = appOptions;
|
||||
|
||||
string name = null;
|
||||
if (Uri?.StartsWith(Constants.AndroidAppProtocol) ?? false)
|
||||
@ -36,6 +44,7 @@ namespace Bit.App.Pages
|
||||
Name = name;
|
||||
PageTitle = string.Format(AppResources.ItemsForUri, Name ?? "--");
|
||||
NoDataText = string.Format(AppResources.NoItemsForUri, Name ?? "--");
|
||||
AddNewItemText = appOptions.FromFido2Framework ? AppResources.SavePasskeyAsNewLogin : AppResources.AddAnItem;
|
||||
}
|
||||
|
||||
protected override async Task<List<GroupingsPageListGroup>> LoadGroupedItemsAsync()
|
||||
@ -78,6 +87,15 @@ namespace Bit.App.Pages
|
||||
return;
|
||||
}
|
||||
|
||||
if (_appOptions.FromFido2Framework)
|
||||
{
|
||||
if (_appOptions.Fido2CredentialAction == CredentialProviderConstants.Fido2CredentialCreate)
|
||||
{
|
||||
await CreateFido2CredentialIntoAsync(cipher);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (!await _passwordRepromptService.PromptAndCheckPasswordIfNeededAsync(cipher.Reprompt))
|
||||
{
|
||||
return;
|
||||
@ -130,8 +148,46 @@ namespace Bit.App.Pages
|
||||
}
|
||||
}
|
||||
|
||||
private async Task CreateFido2CredentialIntoAsync(CipherView cipher)
|
||||
{
|
||||
if (cipher.Login.HasFido2Credentials
|
||||
&&
|
||||
!await _platformUtilsService.ShowDialogAsync(
|
||||
AppResources.ThisItemAlreadyContainsAPasskeyAreYouSureYouWantToOverwriteTheCurrentPasskey,
|
||||
AppResources.OverwritePasskey,
|
||||
AppResources.Yes,
|
||||
AppResources.No))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_fido2MakeCredentialConfirmationUserInterface.Value.Confirm(cipher.Id, null);
|
||||
}
|
||||
|
||||
protected override async Task AddFabCipherAsync()
|
||||
{
|
||||
//Scenario for creating a new Fido2 credential on Android but showing the Cipher Page
|
||||
if (_isAndroidFido2CredentialCreation)
|
||||
{
|
||||
var pageForOther = new CipherAddEditPage(null, CipherType.Login, appOptions: _appOptions);
|
||||
await Page.Navigation.PushModalAsync(new NavigationPage(pageForOther));
|
||||
return;
|
||||
}
|
||||
else
|
||||
{
|
||||
await AddCipherAsync();
|
||||
}
|
||||
}
|
||||
|
||||
protected override async Task AddCipherAsync()
|
||||
{
|
||||
//Scenario for creating a new Fido2 credential on Android
|
||||
if (_isAndroidFido2CredentialCreation)
|
||||
{
|
||||
_fido2MakeCredentialConfirmationUserInterface.Value.Confirm(null, null);
|
||||
return;
|
||||
}
|
||||
|
||||
if (_fillType.HasValue && _fillType != CipherType.Login)
|
||||
{
|
||||
var pageForOther = new CipherAddEditPage(type: _fillType, fromAutofill: true);
|
||||
@ -143,5 +199,15 @@ namespace Bit.App.Pages
|
||||
fromAutofill: true);
|
||||
await Page.Navigation.PushModalAsync(new NavigationPage(pageForLogin));
|
||||
}
|
||||
|
||||
public void Cancel()
|
||||
{
|
||||
if (_appOptions?.FromFido2Framework == true
|
||||
&&
|
||||
_appOptions?.Fido2CredentialAction == CredentialProviderConstants.Fido2CredentialCreate)
|
||||
{
|
||||
_fido2MakeCredentialConfirmationUserInterface.Value.Cancel();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -112,7 +112,7 @@
|
||||
StyleClass="box-header, box-header-platform" />
|
||||
</StackLayout>
|
||||
<StackLayout StyleClass="box-row, box-row-input"
|
||||
IsVisible="{Binding EditMode, Converter={StaticResource inverseBool}}">
|
||||
IsVisible="{Binding TypeEditMode, Converter={StaticResource inverseBool}}">
|
||||
<Label
|
||||
Text="{u:I18n Type}"
|
||||
StyleClass="box-label" />
|
||||
@ -649,9 +649,11 @@
|
||||
Grid.Row="1"
|
||||
Grid.Column="0"
|
||||
SemanticProperties.Description="{u:I18n URI}"
|
||||
IsEnabled="{Binding BindingContext.IsFromFido2Framework, Source={x:Reference _page}, Converter={StaticResource inverseBool}}"
|
||||
AutomationId="LoginUriEntry" />
|
||||
<controls:IconButton
|
||||
StyleClass="box-row-button, box-row-button-platform"
|
||||
IsVisible="{Binding BindingContext.IsFromFido2Framework, Source={x:Reference _page}, Converter={StaticResource inverseBool}}"
|
||||
Text="{Binding Source={x:Static core:BitwardenIcons.Cog}}"
|
||||
Command="{Binding BindingContext.UriOptionsCommand, Source={x:Reference _page}}"
|
||||
CommandParameter="{Binding .}"
|
||||
@ -665,6 +667,7 @@
|
||||
</BindableLayout.ItemTemplate>
|
||||
</StackLayout>
|
||||
<Button Text="{u:I18n NewUri}" StyleClass="box-button-row"
|
||||
IsVisible="{Binding IsFromFido2Framework, Converter={StaticResource inverseBool}}"
|
||||
Clicked="NewUri_Clicked"
|
||||
AutomationId="LoginAddNewUriButton"></Button>
|
||||
</StackLayout>
|
||||
|
@ -45,6 +45,7 @@ namespace Bit.App.Pages
|
||||
_appOptions = appOptions;
|
||||
_fromAutofill = fromAutofill;
|
||||
FromAutofillFramework = _appOptions?.FromAutofillFramework ?? false;
|
||||
FromAndroidFido2Framework = _appOptions?.FromFido2Framework ?? false;
|
||||
InitializeComponent();
|
||||
_vm = BindingContext as CipherAddEditPageViewModel;
|
||||
_vm.Page = this;
|
||||
@ -144,6 +145,7 @@ namespace Bit.App.Pages
|
||||
}
|
||||
|
||||
public bool FromAutofillFramework { get; set; }
|
||||
public bool FromAndroidFido2Framework { get; set; }
|
||||
public CipherAddEditPageViewModel ViewModel => _vm;
|
||||
|
||||
protected override async void OnAppearing()
|
||||
|
@ -17,6 +17,8 @@ using Microsoft.Maui.Controls;
|
||||
using Microsoft.Maui;
|
||||
using Bit.App.Utilities;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
using Bit.Core.Utilities.Fido2;
|
||||
using Bit.Core.Services;
|
||||
|
||||
#nullable enable
|
||||
|
||||
@ -37,7 +39,9 @@ namespace Bit.App.Pages
|
||||
private readonly IAutofillHandler _autofillHandler;
|
||||
private readonly IWatchDeviceService _watchDeviceService;
|
||||
private readonly IAccountsManager _accountsManager;
|
||||
|
||||
private readonly IFido2MakeCredentialConfirmationUserInterface _fido2MakeCredentialConfirmationUserInterface;
|
||||
private readonly IUserVerificationMediatorService _userVerificationMediatorService;
|
||||
|
||||
private bool _showNotesSeparator;
|
||||
private bool _showPassword;
|
||||
private bool _showCardNumber;
|
||||
@ -92,6 +96,8 @@ namespace Bit.App.Pages
|
||||
_autofillHandler = ServiceContainer.Resolve<IAutofillHandler>();
|
||||
_watchDeviceService = ServiceContainer.Resolve<IWatchDeviceService>();
|
||||
_accountsManager = ServiceContainer.Resolve<IAccountsManager>();
|
||||
_fido2MakeCredentialConfirmationUserInterface = ServiceContainer.Resolve<IFido2MakeCredentialConfirmationUserInterface>();
|
||||
_userVerificationMediatorService = ServiceContainer.Resolve<IUserVerificationMediatorService>();
|
||||
|
||||
GeneratePasswordCommand = new Command(GeneratePassword);
|
||||
TogglePasswordCommand = new Command(TogglePassword);
|
||||
@ -292,7 +298,9 @@ namespace Bit.App.Pages
|
||||
});
|
||||
}
|
||||
public bool ShowCollections => (!EditMode || CloneMode) && Cipher?.OrganizationId != null;
|
||||
public bool IsFromFido2Framework { get; set; }
|
||||
public bool EditMode => !string.IsNullOrWhiteSpace(CipherId);
|
||||
public bool TypeEditMode => !string.IsNullOrWhiteSpace(CipherId) || IsFromFido2Framework;
|
||||
public bool ShowOwnershipOptions => !EditMode || CloneMode;
|
||||
public bool OwnershipPolicyInEffect => ShowOwnershipOptions && !AllowPersonal;
|
||||
public bool CloneMode { get; set; }
|
||||
@ -324,6 +332,7 @@ namespace Bit.App.Pages
|
||||
public async Task<bool> LoadAsync(AppOptions appOptions = null)
|
||||
{
|
||||
_fromOtp = appOptions?.OtpData != null;
|
||||
IsFromFido2Framework = appOptions?.FromFido2Framework ?? false;
|
||||
|
||||
var myEmail = await _stateService.GetEmailAsync();
|
||||
OwnershipOptions.Add(new KeyValuePair<string, string>(myEmail, null));
|
||||
@ -536,6 +545,26 @@ namespace Bit.App.Pages
|
||||
}
|
||||
try
|
||||
{
|
||||
bool isFido2UserVerified = false;
|
||||
if (IsFromFido2Framework)
|
||||
{
|
||||
// Verify the user and prevent saving cipher if enforcing is needed and it's not verified.
|
||||
var userVerification = await VerifyUserAsync();
|
||||
if (userVerification.IsCancelled)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
isFido2UserVerified = userVerification.Result;
|
||||
|
||||
var options = _fido2MakeCredentialConfirmationUserInterface.GetCurrentUserVerificationOptions();
|
||||
|
||||
if (!isFido2UserVerified && await _userVerificationMediatorService.ShouldEnforceFido2RequiredUserVerificationAsync(options.Value))
|
||||
{
|
||||
await _platformUtilsService.ShowDialogAsync(AppResources.ErrorCreatingPasskey, AppResources.SavePasskey);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
await _deviceActionService.ShowLoadingAsync(AppResources.Saving);
|
||||
|
||||
await _cipherService.SaveWithServerAsync(cipher);
|
||||
@ -554,6 +583,11 @@ namespace Bit.App.Pages
|
||||
// Close and go back to app
|
||||
_autofillHandler.CloseAutofill();
|
||||
}
|
||||
else if (IsFromFido2Framework)
|
||||
{
|
||||
_fido2MakeCredentialConfirmationUserInterface.Confirm(cipher.Id, isFido2UserVerified);
|
||||
return true;
|
||||
}
|
||||
else if (_fromOtp)
|
||||
{
|
||||
await _accountsManager.StartDefaultNavigationFlowAsync(op => op.OtpData = null);
|
||||
@ -589,6 +623,27 @@ namespace Bit.App.Pages
|
||||
return false;
|
||||
}
|
||||
|
||||
private async Task<CancellableResult<bool>> VerifyUserAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
var options = _fido2MakeCredentialConfirmationUserInterface.GetCurrentUserVerificationOptions();
|
||||
ArgumentNullException.ThrowIfNull(options);
|
||||
|
||||
if (options.Value.UserVerificationPreference == Fido2UserVerificationPreference.Discouraged)
|
||||
{
|
||||
return new CancellableResult<bool>(false);
|
||||
}
|
||||
|
||||
return await _userVerificationMediatorService.VerifyUserForFido2Async(options.Value);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LoggerHelper.LogEvenIfCantBeResolved(ex);
|
||||
return new CancellableResult<bool>(false);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<bool> DeleteAsync()
|
||||
{
|
||||
if (Microsoft.Maui.Networking.Connectivity.NetworkAccess == Microsoft.Maui.Networking.NetworkAccess.None)
|
||||
|
@ -83,7 +83,7 @@
|
||||
Text="{Binding NoDataText}"
|
||||
HorizontalTextAlignment="Center"></Label>
|
||||
<Button
|
||||
Text="{u:I18n AddAnItem}"
|
||||
Text="{Binding AddNewItemText}"
|
||||
Command="{Binding AddCipherCommand}" />
|
||||
</StackLayout>
|
||||
|
||||
@ -133,7 +133,7 @@
|
||||
<Button
|
||||
x:Name="_fab"
|
||||
ImageSource="plus.png"
|
||||
Command="{Binding AddCipherCommand}"
|
||||
Command="{Binding AddFabCipherCommand}"
|
||||
Style="{StaticResource btn-fab}"
|
||||
IsVisible="{OnPlatform iOS=false, Android=true}"
|
||||
AbsoluteLayout.LayoutFlags="PositionProportional"
|
||||
|
@ -127,6 +127,11 @@ namespace Bit.App.Pages
|
||||
|
||||
#if ANDROID
|
||||
_appOptions.Uri = null;
|
||||
|
||||
if (BindingContext is AutofillCiphersPageViewModel autofillVM)
|
||||
{
|
||||
autofillVM.Cancel();
|
||||
}
|
||||
#endif
|
||||
return base.OnBackButtonPressed();
|
||||
}
|
||||
@ -175,6 +180,11 @@ namespace Bit.App.Pages
|
||||
if (DoOnce())
|
||||
{
|
||||
_accountsManager.StartDefaultNavigationFlowAsync(op => op.OtpData = null).FireAndForget();
|
||||
|
||||
if (BindingContext is AutofillCiphersPageViewModel autofillVM)
|
||||
{
|
||||
autofillVM.Cancel();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -4,6 +4,7 @@ using Bit.App.Controls;
|
||||
using Bit.App.Utilities;
|
||||
using Bit.Core.Abstractions;
|
||||
using Bit.Core.Models.View;
|
||||
using Bit.Core.Resources.Localization;
|
||||
using Bit.Core.Utilities;
|
||||
|
||||
namespace Bit.App.Pages
|
||||
@ -22,6 +23,7 @@ namespace Bit.App.Pages
|
||||
protected bool _showNoData;
|
||||
protected bool _showList;
|
||||
protected string _noDataText;
|
||||
protected string _addNewItemText;
|
||||
protected bool _websiteIconsEnabled;
|
||||
|
||||
public CipherSelectionPageViewModel()
|
||||
@ -42,6 +44,9 @@ namespace Bit.App.Pages
|
||||
SelectCipherCommand = CreateDefaultAsyncRelayCommand<IGroupingsPageListItem>(SelectCipherAsync,
|
||||
onException: ex => HandleException(ex),
|
||||
allowsMultipleExecutions: false);
|
||||
AddFabCipherCommand = CreateDefaultAsyncRelayCommand(AddFabCipherAsync,
|
||||
onException: ex => HandleException(ex),
|
||||
allowsMultipleExecutions: false);
|
||||
AddCipherCommand = CreateDefaultAsyncRelayCommand(AddCipherAsync,
|
||||
onException: ex => HandleException(ex),
|
||||
allowsMultipleExecutions: false);
|
||||
@ -50,6 +55,8 @@ namespace Bit.App.Pages
|
||||
{
|
||||
AllowAddAccountRow = false
|
||||
};
|
||||
|
||||
AddNewItemText = AppResources.AddAnItem;
|
||||
}
|
||||
|
||||
public string Name { get; set; }
|
||||
@ -60,6 +67,7 @@ namespace Bit.App.Pages
|
||||
public ICommand CipherOptionsCommand { get; set; }
|
||||
public ICommand SelectCipherCommand { get; set; }
|
||||
public ICommand AddCipherCommand { get; set; }
|
||||
public ICommand AddFabCipherCommand { get; set; }
|
||||
|
||||
public bool ShowNoData
|
||||
{
|
||||
@ -79,6 +87,12 @@ namespace Bit.App.Pages
|
||||
set => SetProperty(ref _noDataText, value);
|
||||
}
|
||||
|
||||
public string AddNewItemText
|
||||
{
|
||||
get => _addNewItemText;
|
||||
set => SetProperty(ref _addNewItemText, value);
|
||||
}
|
||||
|
||||
public bool WebsiteIconsEnabled
|
||||
{
|
||||
get => _websiteIconsEnabled;
|
||||
@ -153,5 +167,6 @@ namespace Bit.App.Pages
|
||||
protected abstract Task SelectCipherAsync(IGroupingsPageListItem item);
|
||||
|
||||
protected abstract Task AddCipherAsync();
|
||||
protected abstract Task AddFabCipherAsync();
|
||||
}
|
||||
}
|
||||
|
@ -46,6 +46,9 @@ namespace Bit.App.Pages
|
||||
CipherOptionsCommand = CreateDefaultAsyncRelayCommand<CipherView>(cipher => Utilities.AppHelpers.CipherListOptions(Page, cipher, _passwordRepromptService),
|
||||
onException: ex => HandleException(ex),
|
||||
allowsMultipleExecutions: false);
|
||||
AddFabCipherCommand = CreateDefaultAsyncRelayCommand(AddCipherAsync,
|
||||
onException: ex => HandleException(ex),
|
||||
allowsMultipleExecutions: false);
|
||||
AddCipherCommand = CreateDefaultAsyncRelayCommand(AddCipherAsync,
|
||||
onException: ex => HandleException(ex),
|
||||
allowsMultipleExecutions: false);
|
||||
@ -53,6 +56,7 @@ namespace Bit.App.Pages
|
||||
|
||||
public ICommand CipherOptionsCommand { get; }
|
||||
public ICommand AddCipherCommand { get; }
|
||||
public ICommand AddFabCipherCommand { get; }
|
||||
public ExtendedObservableCollection<CipherItemViewModel> Ciphers { get; set; }
|
||||
public Func<CipherView, bool> Filter { get; set; }
|
||||
public string AutofillUrl { get; set; }
|
||||
|
@ -70,5 +70,10 @@ namespace Bit.App.Pages
|
||||
var pageForLogin = new CipherAddEditPage(null, CipherType.Login, name: Name, appOptions: _appOptions);
|
||||
await Page.Navigation.PushModalAsync(new NavigationPage(pageForLogin));
|
||||
}
|
||||
|
||||
protected override async Task AddFabCipherAsync()
|
||||
{
|
||||
await AddCipherAsync();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,7 @@
|
||||
//------------------------------------------------------------------------------
|
||||
//------------------------------------------------------------------------------
|
||||
// <auto-generated>
|
||||
// This code was generated by a tool.
|
||||
// Runtime Version:4.0.30319.42000
|
||||
//
|
||||
// Changes to this file may cause incorrect behavior and will be lost if
|
||||
// the code is regenerated.
|
||||
@ -13,10 +14,12 @@ namespace Bit.Core.Resources.Localization {
|
||||
|
||||
/// <summary>
|
||||
/// A strongly-typed resource class, for looking up localized strings, etc.
|
||||
/// This class was generated by MSBuild using the GenerateResource task.
|
||||
/// To add or remove a member, edit your .resx file then rerun MSBuild.
|
||||
/// </summary>
|
||||
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Build.Tasks.StronglyTypedResourceBuilder", "15.1.0.0")]
|
||||
// This class was auto-generated by the StronglyTypedResourceBuilder
|
||||
// class via a tool like ResGen or Visual Studio.
|
||||
// To add or remove a member, edit your .ResX file then rerun ResGen
|
||||
// with the /str option, or rebuild your VS project.
|
||||
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")]
|
||||
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
|
||||
[global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
|
||||
public class AppResources {
|
||||
@ -1723,6 +1726,15 @@ namespace Bit.Core.Resources.Localization {
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Continue to device Settings?.
|
||||
/// </summary>
|
||||
public static string ContinueToDeviceSettings {
|
||||
get {
|
||||
return ResourceManager.GetString("ContinueToDeviceSettings", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Continue to Help center?.
|
||||
/// </summary>
|
||||
@ -1912,24 +1924,6 @@ 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>
|
||||
/// Looks up a localized string similar to Credits.
|
||||
/// </summary>
|
||||
@ -5227,6 +5221,24 @@ namespace Bit.Core.Resources.Localization {
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Passkey management.
|
||||
/// </summary>
|
||||
public static string PasskeyManagement {
|
||||
get {
|
||||
return ResourceManager.GetString("PasskeyManagement", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Use Bitwarden to save new passkeys and log in with passkeys stored in your vault..
|
||||
/// </summary>
|
||||
public static string PasskeyManagementExplanationLong {
|
||||
get {
|
||||
return ResourceManager.GetString("PasskeyManagementExplanationLong", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Passkeys.
|
||||
/// </summary>
|
||||
@ -6353,6 +6365,15 @@ namespace Bit.Core.Resources.Localization {
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Set Bitwarden as your passkey provider in device settings..
|
||||
/// </summary>
|
||||
public static string SetBitwardenAsPasskeyManagerDescription {
|
||||
get {
|
||||
return ResourceManager.GetString("SetBitwardenAsPasskeyManagerDescription", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Set master password.
|
||||
/// </summary>
|
||||
|
@ -421,6 +421,9 @@
|
||||
<data name="AutofillService" xml:space="preserve">
|
||||
<value>Auto-fill service</value>
|
||||
</data>
|
||||
<data name="SetBitwardenAsPasskeyManagerDescription" xml:space="preserve">
|
||||
<value>Set Bitwarden as your passkey provider in device settings.</value>
|
||||
</data>
|
||||
<data name="AvoidAmbiguousCharacters" xml:space="preserve">
|
||||
<value>Avoid ambiguous characters</value>
|
||||
</data>
|
||||
@ -1819,8 +1822,8 @@ Scanning will happen automatically.</value>
|
||||
<data name="AccessibilityDrawOverPermissionAlert" xml:space="preserve">
|
||||
<value>Bitwarden needs attention - Turn on "Draw-Over" in "Auto-fill Services" from Bitwarden Settings</value>
|
||||
</data>
|
||||
<data name="CredentialProviderService" xml:space="preserve">
|
||||
<value>Credential Provider service</value>
|
||||
<data name="PasskeyManagement" xml:space="preserve">
|
||||
<value>Passkey management</value>
|
||||
</data>
|
||||
<data name="AutofillServices" xml:space="preserve">
|
||||
<value>Auto-fill services</value>
|
||||
@ -2805,8 +2808,8 @@ Do you want to switch to this account?</value>
|
||||
<data name="XHours" xml:space="preserve">
|
||||
<value>{0} hours</value>
|
||||
</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 name="PasskeyManagementExplanationLong" xml:space="preserve">
|
||||
<value>Use Bitwarden to save new passkeys and log in with passkeys stored in your vault.</value>
|
||||
</data>
|
||||
<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>
|
||||
@ -2836,6 +2839,9 @@ Do you want to switch to this account?</value>
|
||||
<data name="ContinueToAppStore" xml:space="preserve">
|
||||
<value>Continue to app store?</value>
|
||||
</data>
|
||||
<data name="ContinueToDeviceSettings" xml:space="preserve">
|
||||
<value>Continue to device Settings?</value>
|
||||
</data>
|
||||
<data name="TwoStepLoginDescriptionLong" xml:space="preserve">
|
||||
<value>Make your account more secure by setting up two-step login in the Bitwarden web app.</value>
|
||||
</data>
|
||||
|
@ -87,11 +87,11 @@ namespace Bit.Core.Services
|
||||
var cipher = await encrypted.DecryptAsync();
|
||||
|
||||
if (!userVerified
|
||||
&&
|
||||
await _userVerificationMediatorService.ShouldEnforceFido2RequiredUserVerificationAsync(new Fido2UserVerificationOptions(
|
||||
cipher.Reprompt != CipherRepromptType.None,
|
||||
makeCredentialParams.UserVerificationPreference,
|
||||
userInterface.HasVaultBeenUnlockedInThisTransaction)))
|
||||
&&
|
||||
await _userVerificationMediatorService.ShouldEnforceFido2RequiredUserVerificationAsync(new Fido2UserVerificationOptions(
|
||||
cipher.Reprompt != CipherRepromptType.None,
|
||||
makeCredentialParams.UserVerificationPreference,
|
||||
userInterface.HasVaultBeenUnlockedInThisTransaction)))
|
||||
{
|
||||
throw new NotAllowedError();
|
||||
}
|
||||
@ -184,7 +184,7 @@ namespace Bit.Core.Services
|
||||
{
|
||||
throw new NotAllowedError();
|
||||
}
|
||||
|
||||
|
||||
if (!userVerified
|
||||
&&
|
||||
await _userVerificationMediatorService.ShouldEnforceFido2RequiredUserVerificationAsync(new Fido2UserVerificationOptions(
|
||||
@ -249,7 +249,8 @@ namespace Bit.Core.Services
|
||||
Id = cipher.Login.MainFido2Credential.CredentialId.GuidToRawFormat(),
|
||||
RpId = cipher.Login.MainFido2Credential.RpId,
|
||||
UserHandle = cipher.Login.MainFido2Credential.UserHandleValue,
|
||||
UserName = cipher.Login.MainFido2Credential.UserName
|
||||
UserName = cipher.Login.MainFido2Credential.UserName,
|
||||
CipherId = cipher.Id,
|
||||
}).ToArray();
|
||||
|
||||
return credentials;
|
||||
|
@ -232,8 +232,6 @@ namespace Bit.Core.Services
|
||||
{
|
||||
throw new Fido2ClientException(Fido2ClientException.ErrorCode.UnknownError, $"An unknown error occurred");
|
||||
}
|
||||
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
private Fido2AuthenticatorMakeCredentialParams MapToMakeCredentialParams(
|
||||
|
@ -4,63 +4,72 @@
|
||||
|
||||
//#if IOS
|
||||
//using UIKit;
|
||||
//#elif ANDROID
|
||||
//using Android.Content;
|
||||
//#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();
|
||||
// /// <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;
|
||||
// }
|
||||
// }
|
||||
// static ILogger _instance;
|
||||
// public static ILogger Instance
|
||||
// {
|
||||
// get
|
||||
// {
|
||||
// if (_instance is null)
|
||||
// {
|
||||
// _instance = new ClipLogger();
|
||||
// }
|
||||
// return _instance;
|
||||
// }
|
||||
// }
|
||||
|
||||
// protected ClipLogger()
|
||||
// {
|
||||
// }
|
||||
// protected ClipLogger()
|
||||
// {
|
||||
// }
|
||||
|
||||
// public static void Log(string breadcrumb)
|
||||
// {
|
||||
// var formattedText = $"{DateTime.Now.ToShortTimeString()}: {breadcrumb}";
|
||||
// _currentBreadcrumbs.AppendLine(formattedText);
|
||||
|
||||
// public static void Log(string breadcrumb)
|
||||
// {
|
||||
// _currentBreadcrumbs.AppendLine($"{DateTime.Now.ToShortTimeString()}: {breadcrumb}");
|
||||
//#if IOS
|
||||
// MainThread.BeginInvokeOnMainThread(() => UIPasteboard.General.String = _currentBreadcrumbs.ToString());
|
||||
//#elif ANDROID
|
||||
// var clipboardManager = Android.App.Application.Context.GetSystemService(Context.ClipboardService) as ClipboardManager;
|
||||
// var clipData = ClipData.NewPlainText("bitwarden", _currentBreadcrumbs.ToString());
|
||||
// clipboardManager.PrimaryClip = clipData;
|
||||
// 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
|
||||
// };
|
||||
// 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}");
|
||||
// }
|
||||
// Log(message ?? $"Error found in: {classAndMethod}, {filePathAndLineNumber}");
|
||||
// }
|
||||
|
||||
// public void Exception(Exception ex) => Log(ex?.ToString());
|
||||
// public void Exception(Exception ex) => Log(ex?.ToString());
|
||||
|
||||
// public Task InitAsync() => Task.CompletedTask;
|
||||
// public Task InitAsync() => Task.CompletedTask;
|
||||
|
||||
// public Task<bool> IsEnabled() => Task.FromResult(true);
|
||||
// public Task<bool> IsEnabled() => Task.FromResult(true);
|
||||
|
||||
// public Task SetEnabled(bool value) => Task.CompletedTask;
|
||||
// }
|
||||
// public Task SetEnabled(bool value) => Task.CompletedTask;
|
||||
// }
|
||||
//}
|
||||
|
@ -100,6 +100,11 @@ namespace Bit.App.Utilities.AccountManagement
|
||||
{
|
||||
_accountsManagerHost.Navigate(NavigationTarget.AddEditCipher);
|
||||
}
|
||||
else if (Options.FromFido2Framework)
|
||||
{
|
||||
var deviceActionService = Bit.Core.Utilities.ServiceContainer.Resolve<IDeviceActionService>();
|
||||
deviceActionService.ExecuteFido2CredentialActionAsync(Options).FireAndForget();
|
||||
}
|
||||
else if (Options.Uri != null)
|
||||
{
|
||||
_accountsManagerHost.Navigate(NavigationTarget.AutofillCiphers);
|
||||
|
@ -429,11 +429,20 @@ namespace Bit.App.Utilities
|
||||
{
|
||||
if (appOptions != null)
|
||||
{
|
||||
// this is called after login in or unlocking so we can assume the vault has been unlocked in this transaction here.
|
||||
appOptions.HasUnlockedInThisTransaction = true;
|
||||
|
||||
if (appOptions.FromAutofillFramework && appOptions.SaveType.HasValue)
|
||||
{
|
||||
App.MainPage = new NavigationPage(new CipherAddEditPage(appOptions: appOptions));
|
||||
return true;
|
||||
}
|
||||
if (appOptions.FromFido2Framework && !string.IsNullOrWhiteSpace(appOptions.Fido2CredentialAction))
|
||||
{
|
||||
var deviceActionService = Bit.Core.Utilities.ServiceContainer.Resolve<IDeviceActionService>();
|
||||
deviceActionService.ExecuteFido2CredentialActionAsync(appOptions).FireAndForget();
|
||||
return true;
|
||||
}
|
||||
if (appOptions.Uri != null
|
||||
||
|
||||
appOptions.OtpData != null)
|
||||
|
13
src/Core/Utilities/Fido2/CredentialProviderConstants.cs
Normal file
13
src/Core/Utilities/Fido2/CredentialProviderConstants.cs
Normal file
@ -0,0 +1,13 @@
|
||||
namespace Bit.Core.Utilities.Fido2
|
||||
{
|
||||
public class CredentialProviderConstants
|
||||
{
|
||||
public const string Fido2CredentialCreate = "fido2CredentialCreate";
|
||||
public const string Fido2CredentialGet = "fido2CredentialGet";
|
||||
public const string Fido2CredentialAction = "fido2CredentialAction";
|
||||
public const string CredentialProviderCipherId = "credentialProviderCipherId";
|
||||
public const string CredentialDataIntentExtra = "CREDENTIAL_DATA";
|
||||
public const string CredentialIdIntentExtra = "credId";
|
||||
public const string CredentialHasVaultBeenUnlockedInThisTransactionExtra = "hasVaultBeenUnlockedInThisTransaction";
|
||||
}
|
||||
}
|
@ -13,4 +13,6 @@ public class Fido2AuthenticatorDiscoverableCredentialMetadata
|
||||
public byte[] UserHandle { get; set; }
|
||||
|
||||
public string UserName { get; set; }
|
||||
|
||||
public string CipherId { get; set; }
|
||||
}
|
||||
|
@ -2,6 +2,7 @@
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Bit.App.Abstractions;
|
||||
using Bit.App.Models;
|
||||
using Bit.Core.Resources.Localization;
|
||||
using Bit.App.Utilities.Prompts;
|
||||
using Bit.Core.Enums;
|
||||
@ -315,6 +316,12 @@ namespace Bit.iOS.Core.Services
|
||||
return iOSHelpers.GetSystemUpTimeMilliseconds() ?? DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
|
||||
}
|
||||
|
||||
public Task ExecuteFido2CredentialActionAsync(AppOptions appOptions)
|
||||
{
|
||||
// only used by Android
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public void CloseMainApp()
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
|
@ -82,7 +82,8 @@ namespace Bit.Core.Test.Services
|
||||
Id = c.Login.MainFido2Credential.CredentialId.GuidToRawFormat(),
|
||||
RpId = "bitwarden.com",
|
||||
UserHandle = c.Login.MainFido2Credential.UserHandleValue,
|
||||
UserName = c.Login.MainFido2Credential.UserName
|
||||
UserName = c.Login.MainFido2Credential.UserName,
|
||||
CipherId = c.Id,
|
||||
}), new MetadataComparer())
|
||||
);
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user