mirror of
https://github.com/bitwarden/mobile.git
synced 2024-11-29 12:45:20 +01:00
PM-5154 Start implementing Passkeys Autofill in iOS
This commit is contained in:
parent
a1e4f0aaa2
commit
275ae76761
@ -89,7 +89,7 @@ namespace Bit.iOS
|
||||
Core.Constants.AutofillNeedsIdentityReplacementKey);
|
||||
if (needsAutofillReplacement.GetValueOrDefault())
|
||||
{
|
||||
await ASHelpers.ReplaceAllIdentities();
|
||||
await ASHelpers.ReplaceAllIdentitiesAsync();
|
||||
}
|
||||
}
|
||||
else if (message.Command == "showAppExtension")
|
||||
@ -103,7 +103,7 @@ namespace Bit.iOS
|
||||
var success = data["successfully"] as bool?;
|
||||
if (success.GetValueOrDefault() && _deviceActionService.SystemMajorVersion() >= 12)
|
||||
{
|
||||
await ASHelpers.ReplaceAllIdentities();
|
||||
await ASHelpers.ReplaceAllIdentitiesAsync();
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -112,65 +112,63 @@ namespace Bit.iOS
|
||||
{
|
||||
if (_deviceActionService.SystemMajorVersion() >= 12)
|
||||
{
|
||||
if (await ASHelpers.IdentitiesCanIncremental())
|
||||
if (await ASHelpers.IdentitiesSupportIncrementalAsync())
|
||||
{
|
||||
var cipherId = message.Data as string;
|
||||
if (message.Command == "addedCipher" && !string.IsNullOrWhiteSpace(cipherId))
|
||||
{
|
||||
var identity = await ASHelpers.GetCipherIdentityAsync(cipherId);
|
||||
var identity = await ASHelpers.GetCipherPasswordIdentityAsync(cipherId);
|
||||
if (identity == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
await ASCredentialIdentityStore.SharedStore?.SaveCredentialIdentitiesAsync(
|
||||
new ASPasswordCredentialIdentity[] { identity });
|
||||
await ASCredentialIdentityStoreExtensions.SaveCredentialIdentitiesAsync(identity);
|
||||
return;
|
||||
}
|
||||
}
|
||||
await ASHelpers.ReplaceAllIdentities();
|
||||
await ASHelpers.ReplaceAllIdentitiesAsync();
|
||||
}
|
||||
}
|
||||
else if (message.Command == "deletedCipher" || message.Command == "softDeletedCipher")
|
||||
{
|
||||
if (_deviceActionService.SystemMajorVersion() >= 12)
|
||||
if (UIDevice.CurrentDevice.CheckSystemVersion(12, 0))
|
||||
{
|
||||
if (await ASHelpers.IdentitiesCanIncremental())
|
||||
if (await ASHelpers.IdentitiesSupportIncrementalAsync())
|
||||
{
|
||||
var identity = ASHelpers.ToCredentialIdentity(
|
||||
var identity = ASHelpers.ToPasswordCredentialIdentity(
|
||||
message.Data as Bit.Core.Models.View.CipherView);
|
||||
if (identity == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
await ASCredentialIdentityStore.SharedStore?.RemoveCredentialIdentitiesAsync(
|
||||
new ASPasswordCredentialIdentity[] { identity });
|
||||
await ASCredentialIdentityStoreExtensions.RemoveCredentialIdentitiesAsync(identity);
|
||||
return;
|
||||
}
|
||||
await ASHelpers.ReplaceAllIdentities();
|
||||
await ASHelpers.ReplaceAllIdentitiesAsync();
|
||||
}
|
||||
}
|
||||
else if (message.Command == "logout")
|
||||
{
|
||||
if (_deviceActionService.SystemMajorVersion() >= 12)
|
||||
if (UIDevice.CurrentDevice.CheckSystemVersion(12, 0))
|
||||
{
|
||||
await ASCredentialIdentityStore.SharedStore?.RemoveAllCredentialIdentitiesAsync();
|
||||
await ASCredentialIdentityStore.SharedStore.RemoveAllCredentialIdentitiesAsync();
|
||||
}
|
||||
}
|
||||
else if ((message.Command == "softDeletedCipher" || message.Command == "restoredCipher")
|
||||
&& _deviceActionService.SystemMajorVersion() >= 12)
|
||||
{
|
||||
await ASHelpers.ReplaceAllIdentities();
|
||||
&& UIDevice.CurrentDevice.CheckSystemVersion(12, 0))
|
||||
{
|
||||
await ASHelpers.ReplaceAllIdentitiesAsync();
|
||||
}
|
||||
else if (message.Command == AppHelpers.VAULT_TIMEOUT_ACTION_CHANGED_MESSAGE_COMMAND)
|
||||
{
|
||||
var timeoutAction = await _stateService.GetVaultTimeoutActionAsync();
|
||||
if (timeoutAction == VaultTimeoutAction.Logout)
|
||||
if (timeoutAction == VaultTimeoutAction.Logout && UIDevice.CurrentDevice.CheckSystemVersion(12, 0))
|
||||
{
|
||||
await ASCredentialIdentityStore.SharedStore?.RemoveAllCredentialIdentitiesAsync();
|
||||
await ASCredentialIdentityStore.SharedStore.RemoveAllCredentialIdentitiesAsync();
|
||||
}
|
||||
else
|
||||
{
|
||||
await ASHelpers.ReplaceAllIdentities();
|
||||
await ASHelpers.ReplaceAllIdentitiesAsync();
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -190,30 +188,34 @@ namespace Bit.iOS
|
||||
|
||||
public override void OnResignActivation(UIApplication uiApplication)
|
||||
{
|
||||
if (UIApplication.SharedApplication.KeyWindow != null)
|
||||
if (UIApplication.SharedApplication.KeyWindow is null)
|
||||
{
|
||||
var view = new UIView(UIApplication.SharedApplication.KeyWindow.Frame)
|
||||
{
|
||||
Tag = SPLASH_VIEW_TAG
|
||||
};
|
||||
var backgroundView = new UIView(UIApplication.SharedApplication.KeyWindow.Frame)
|
||||
{
|
||||
BackgroundColor = ThemeManager.GetResourceColor("SplashBackgroundColor").ToPlatform()
|
||||
};
|
||||
var logo = new UIImage(!ThemeManager.UsingLightTheme ? "logo_white.png" : "logo.png");
|
||||
var frame = new CGRect(0, 0, 280, 100); //Setting image width to avoid it being larger and getting cropped on smaller devices. This harcoded size should be good even for very small devices.
|
||||
var imageView = new UIImageView(frame)
|
||||
{
|
||||
Image = logo,
|
||||
Center = new CGPoint(view.Center.X, view.Center.Y - 30),
|
||||
ContentMode = UIViewContentMode.ScaleAspectFit
|
||||
};
|
||||
view.AddSubview(backgroundView);
|
||||
view.AddSubview(imageView);
|
||||
UIApplication.SharedApplication.KeyWindow.AddSubview(view);
|
||||
UIApplication.SharedApplication.KeyWindow.BringSubviewToFront(view);
|
||||
UIApplication.SharedApplication.KeyWindow.EndEditing(true);
|
||||
base.OnResignActivation(uiApplication);
|
||||
return;
|
||||
}
|
||||
|
||||
var view = new UIView(UIApplication.SharedApplication.KeyWindow.Frame)
|
||||
{
|
||||
Tag = SPLASH_VIEW_TAG
|
||||
};
|
||||
var backgroundView = new UIView(UIApplication.SharedApplication.KeyWindow.Frame)
|
||||
{
|
||||
BackgroundColor = ThemeManager.GetResourceColor("SplashBackgroundColor").ToPlatform()
|
||||
};
|
||||
var logo = new UIImage(!ThemeManager.UsingLightTheme ? "logo_white.png" : "logo.png");
|
||||
var frame = new CGRect(0, 0, 280, 100); //Setting image width to avoid it being larger and getting cropped on smaller devices. This harcoded size should be good even for very small devices.
|
||||
var imageView = new UIImageView(frame)
|
||||
{
|
||||
Image = logo,
|
||||
Center = new CGPoint(view.Center.X, view.Center.Y - 30),
|
||||
ContentMode = UIViewContentMode.ScaleAspectFit
|
||||
};
|
||||
view.AddSubview(backgroundView);
|
||||
view.AddSubview(imageView);
|
||||
UIApplication.SharedApplication.KeyWindow.AddSubview(view);
|
||||
UIApplication.SharedApplication.KeyWindow.BringSubviewToFront(view);
|
||||
UIApplication.SharedApplication.KeyWindow.EndEditing(true);
|
||||
|
||||
base.OnResignActivation(uiApplication);
|
||||
}
|
||||
|
||||
@ -304,17 +306,6 @@ namespace Bit.iOS
|
||||
// Migration services
|
||||
ServiceContainer.Register<INativeLogService>("nativeLogService", new ConsoleLogService());
|
||||
|
||||
// Note: This might cause a race condition. Investigate more.
|
||||
//Task.Run(() =>
|
||||
//{
|
||||
// FFImageLoading.Forms.Platform.CachedImageRenderer.Init();
|
||||
// FFImageLoading.ImageService.Instance.Initialize(new FFImageLoading.Config.Configuration
|
||||
// {
|
||||
// FadeAnimationEnabled = false,
|
||||
// FadeAnimationForCachedImages = false
|
||||
// });
|
||||
//});
|
||||
|
||||
iOSCoreHelpers.RegisterLocalServices();
|
||||
RegisterPush();
|
||||
var deviceActionService = ServiceContainer.Resolve<IDeviceActionService>("deviceActionService");
|
||||
|
@ -1,7 +1,4 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Models.Domain;
|
||||
|
||||
namespace Bit.Core.Models.View
|
||||
|
@ -1,6 +1,4 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Models.Domain;
|
||||
|
||||
namespace Bit.Core.Models.View
|
||||
|
@ -1,5 +1,4 @@
|
||||
using System;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using AuthenticationServices;
|
||||
using Bit.App.Abstractions;
|
||||
@ -104,27 +103,100 @@ namespace Bit.iOS.Autofill
|
||||
}
|
||||
}
|
||||
|
||||
public override async void ProvideCredentialWithoutUserInteraction(IASCredentialRequest credentialRequest)
|
||||
{
|
||||
try
|
||||
{
|
||||
switch (credentialRequest)
|
||||
{
|
||||
case ASPasswordCredentialRequest passwordRequest:
|
||||
await ProvideCredentialWithoutUserInteractionAsync(passwordRequest.CredentialIdentity as ASPasswordCredentialIdentity);
|
||||
break;
|
||||
case ASPasskeyCredentialRequest passkeyRequest:
|
||||
await ProvideCredentialWithoutUserInteractionAsync(passkeyRequest.CredentialIdentity as ASPasskeyCredentialIdentity);
|
||||
break;
|
||||
default:
|
||||
ExtensionContext?.CancelRequest(new NSError(ASExtensionErrorCodeExtensions.GetDomain(ASExtensionErrorCode.Failed), (int)ASExtensionErrorCode.Failed));
|
||||
break;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
OnProvidingCredentialException(ex);
|
||||
}
|
||||
}
|
||||
|
||||
public override async void ProvideCredentialWithoutUserInteraction(ASPasswordCredentialIdentity credentialIdentity)
|
||||
{
|
||||
try
|
||||
{
|
||||
InitAppIfNeeded();
|
||||
await _stateService.Value.SetPasswordRepromptAutofillAsync(false);
|
||||
await _stateService.Value.SetPasswordVerifiedAutofillAsync(false);
|
||||
if (!await IsAuthed() || await IsLocked())
|
||||
{
|
||||
var err = new NSError(new NSString("ASExtensionErrorDomain"),
|
||||
Convert.ToInt32(ASExtensionErrorCode.UserInteractionRequired), null);
|
||||
ExtensionContext.CancelRequest(err);
|
||||
return;
|
||||
}
|
||||
_context.CredentialIdentity = credentialIdentity;
|
||||
await ProvideCredentialAsync(false);
|
||||
await ProvideCredentialWithoutUserInteractionAsync(credentialIdentity);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LoggerHelper.LogEvenIfCantBeResolved(ex);
|
||||
throw;
|
||||
OnProvidingCredentialException(ex);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnProvidingCredentialException(Exception ex)
|
||||
{
|
||||
//LoggerHelper.LogEvenIfCantBeResolved(ex);
|
||||
UIPasteboard.General.String = ex.ToString();
|
||||
ExtensionContext?.CancelRequest(new NSError(ASExtensionErrorCodeExtensions.GetDomain(ASExtensionErrorCode.Failed), (int)ASExtensionErrorCode.Failed));
|
||||
}
|
||||
|
||||
private async Task ProvideCredentialWithoutUserInteractionAsync(ASPasswordCredentialIdentity credentialIdentity)
|
||||
{
|
||||
InitAppIfNeeded();
|
||||
await _stateService.Value.SetPasswordRepromptAutofillAsync(false);
|
||||
await _stateService.Value.SetPasswordVerifiedAutofillAsync(false);
|
||||
if (!await IsAuthed() || await IsLocked())
|
||||
{
|
||||
var err = new NSError(new NSString("ASExtensionErrorDomain"),
|
||||
Convert.ToInt32(ASExtensionErrorCode.UserInteractionRequired), null);
|
||||
ExtensionContext.CancelRequest(err);
|
||||
return;
|
||||
}
|
||||
_context.PasswordCredentialIdentity = credentialIdentity;
|
||||
await ProvideCredentialAsync(false);
|
||||
}
|
||||
|
||||
private async Task ProvideCredentialWithoutUserInteractionAsync(ASPasskeyCredentialIdentity passkeyIdentity)
|
||||
{
|
||||
InitAppIfNeeded();
|
||||
await _stateService.Value.SetPasswordRepromptAutofillAsync(false);
|
||||
await _stateService.Value.SetPasswordVerifiedAutofillAsync(false);
|
||||
if (!await IsAuthed() || await IsLocked())
|
||||
{
|
||||
var err = new NSError(new NSString("ASExtensionErrorDomain"),
|
||||
Convert.ToInt32(ASExtensionErrorCode.UserInteractionRequired), null);
|
||||
ExtensionContext.CancelRequest(err);
|
||||
return;
|
||||
}
|
||||
_context.PasskeyCredentialIdentity = passkeyIdentity;
|
||||
await ProvideCredentialAsync(false);
|
||||
}
|
||||
|
||||
public override async void PrepareInterfaceToProvideCredential(IASCredentialRequest credentialRequest)
|
||||
{
|
||||
try
|
||||
{
|
||||
switch (credentialRequest)
|
||||
{
|
||||
case ASPasswordCredentialRequest passwordRequest:
|
||||
PrepareInterfaceToProvideCredential(passwordRequest.CredentialIdentity as ASPasswordCredentialIdentity);
|
||||
break;
|
||||
case ASPasskeyCredentialRequest passkeyRequest:
|
||||
await PrepareInterfaceToProvideCredentialAsync(c => c.PasskeyCredentialIdentity = passkeyRequest.CredentialIdentity as ASPasskeyCredentialIdentity);
|
||||
break;
|
||||
default:
|
||||
ExtensionContext?.CancelRequest(new NSError(ASExtensionErrorCodeExtensions.GetDomain(ASExtensionErrorCode.Failed), (int)ASExtensionErrorCode.Failed));
|
||||
break;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
OnProvidingCredentialException(ex);
|
||||
}
|
||||
}
|
||||
|
||||
@ -132,22 +204,27 @@ namespace Bit.iOS.Autofill
|
||||
{
|
||||
try
|
||||
{
|
||||
InitAppIfNeeded();
|
||||
if (!await IsAuthed())
|
||||
{
|
||||
await _accountsManager.NavigateOnAccountChangeAsync(false);
|
||||
return;
|
||||
}
|
||||
_context.CredentialIdentity = credentialIdentity;
|
||||
await CheckLockAsync(async () => await ProvideCredentialAsync());
|
||||
await PrepareInterfaceToProvideCredentialAsync(c => c.PasswordCredentialIdentity = credentialIdentity);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LoggerHelper.LogEvenIfCantBeResolved(ex);
|
||||
throw;
|
||||
OnProvidingCredentialException(ex);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task PrepareInterfaceToProvideCredentialAsync(Action<Context> updateContext)
|
||||
{
|
||||
InitAppIfNeeded();
|
||||
if (!await IsAuthed())
|
||||
{
|
||||
await _accountsManager.NavigateOnAccountChangeAsync(false);
|
||||
return;
|
||||
}
|
||||
updateContext(_context);
|
||||
await CheckLockAsync(async () => await ProvideCredentialAsync());
|
||||
}
|
||||
|
||||
|
||||
public override async void PrepareInterfaceForExtensionConfiguration()
|
||||
{
|
||||
try
|
||||
@ -205,6 +282,23 @@ namespace Bit.iOS.Autofill
|
||||
});
|
||||
}
|
||||
|
||||
public void CompleteAssertionRequest(ASPasskeyAssertionCredential assertionCredential)
|
||||
{
|
||||
if (_context == null)
|
||||
{
|
||||
ServiceContainer.Reset();
|
||||
CancelRequest(ASExtensionErrorCode.UserCanceled);
|
||||
return;
|
||||
}
|
||||
|
||||
NSRunLoop.Main.BeginInvokeOnMainThread(() =>
|
||||
{
|
||||
ServiceContainer.Reset();
|
||||
ASExtensionContext?.CompleteAssertionRequest(assertionCredential, null);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
public override void PrepareForSegue(UIStoryboardSegue segue, NSObject sender)
|
||||
{
|
||||
try
|
||||
@ -268,7 +362,7 @@ namespace Bit.iOS.Autofill
|
||||
{
|
||||
try
|
||||
{
|
||||
if (_context.CredentialIdentity != null)
|
||||
if (_context.PasswordCredentialIdentity != null)
|
||||
{
|
||||
await MainThread.InvokeOnMainThreadAsync(() => ProvideCredentialAsync());
|
||||
return;
|
||||
@ -295,63 +389,85 @@ namespace Bit.iOS.Autofill
|
||||
}
|
||||
}
|
||||
|
||||
private void CancelRequest(ASExtensionErrorCode code)
|
||||
{
|
||||
//var err = new NSError(new NSString("ASExtensionErrorDomain"), Convert.ToInt32(code), null);
|
||||
var err = new NSError(ASExtensionErrorCodeExtensions.GetDomain(code), (int)code);
|
||||
ExtensionContext?.CancelRequest(err);
|
||||
}
|
||||
|
||||
private async Task ProvideCredentialAsync(bool userInteraction = true)
|
||||
{
|
||||
try
|
||||
{
|
||||
var cipherService = ServiceContainer.Resolve<ICipherService>("cipherService", true);
|
||||
Bit.Core.Models.Domain.Cipher cipher = null;
|
||||
var cancel = cipherService == null || _context.CredentialIdentity?.RecordIdentifier == null;
|
||||
if (!cancel)
|
||||
if (!ServiceContainer.TryResolve<ICipherService>(out var cipherService)
|
||||
||
|
||||
_context.RecordIdentifier == null)
|
||||
{
|
||||
cipher = await cipherService.GetAsync(_context.CredentialIdentity.RecordIdentifier);
|
||||
cancel = cipher == null || cipher.Type != Bit.Core.Enums.CipherType.Login || cipher.Login == null;
|
||||
CancelRequest(ASExtensionErrorCode.CredentialIdentityNotFound);
|
||||
return;
|
||||
}
|
||||
if (cancel)
|
||||
|
||||
var cipher = await cipherService.GetAsync(_context.RecordIdentifier);
|
||||
if (cipher?.Login is null || cipher.Type != CipherType.Login)
|
||||
{
|
||||
var err = new NSError(new NSString("ASExtensionErrorDomain"),
|
||||
Convert.ToInt32(ASExtensionErrorCode.CredentialIdentityNotFound), null);
|
||||
ExtensionContext?.CancelRequest(err);
|
||||
CancelRequest(ASExtensionErrorCode.CredentialIdentityNotFound);
|
||||
return;
|
||||
}
|
||||
|
||||
var decCipher = await cipher.DecryptAsync();
|
||||
if (decCipher.Reprompt != Bit.Core.Enums.CipherRepromptType.None)
|
||||
|
||||
if (_context.PasskeyCredentialIdentity != null && !decCipher.Login.HasFido2Credentials)
|
||||
{
|
||||
CancelRequest(ASExtensionErrorCode.CredentialIdentityNotFound);
|
||||
return;
|
||||
}
|
||||
|
||||
if (decCipher.Reprompt != CipherRepromptType.None)
|
||||
{
|
||||
// Prompt for password using either the lock screen or dialog unless
|
||||
// already verified the password.
|
||||
if (!userInteraction)
|
||||
{
|
||||
await _stateService.Value.SetPasswordRepromptAutofillAsync(true);
|
||||
var err = new NSError(new NSString("ASExtensionErrorDomain"),
|
||||
Convert.ToInt32(ASExtensionErrorCode.UserInteractionRequired), null);
|
||||
ExtensionContext?.CancelRequest(err);
|
||||
CancelRequest(ASExtensionErrorCode.UserInteractionRequired);
|
||||
return;
|
||||
}
|
||||
else if (!await _stateService.Value.GetPasswordVerifiedAutofillAsync())
|
||||
|
||||
if (!await _stateService.Value.GetPasswordVerifiedAutofillAsync())
|
||||
{
|
||||
// Add a timeout to resolve keyboard not always showing up.
|
||||
await Task.Delay(250);
|
||||
var passwordRepromptService = ServiceContainer.Resolve<IPasswordRepromptService>("passwordRepromptService");
|
||||
var passwordRepromptService = ServiceContainer.Resolve<IPasswordRepromptService>();
|
||||
if (!await passwordRepromptService.PromptAndCheckPasswordIfNeededAsync())
|
||||
{
|
||||
var err = new NSError(new NSString("ASExtensionErrorDomain"),
|
||||
Convert.ToInt32(ASExtensionErrorCode.UserCanceled), null);
|
||||
ExtensionContext?.CancelRequest(err);
|
||||
CancelRequest(ASExtensionErrorCode.UserCanceled);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
string totpCode = null;
|
||||
var disableTotpCopy = await _stateService.Value.GetDisableAutoTotpCopyAsync();
|
||||
if (!disableTotpCopy.GetValueOrDefault(false))
|
||||
|
||||
if (UIDevice.CurrentDevice.CheckSystemVersion(17, 0) && _context.IsPasskey)
|
||||
{
|
||||
var canAccessPremiumAsync = await _stateService.Value.CanAccessPremiumAsync();
|
||||
if (!string.IsNullOrWhiteSpace(decCipher.Login.Totp) &&
|
||||
(canAccessPremiumAsync || cipher.OrganizationUseTotp))
|
||||
CompleteAssertionRequest(new ASPasskeyAssertionCredential(
|
||||
decCipher.Login.MainFido2Credential.UserHandle,
|
||||
decCipher.Login.MainFido2Credential.RpId,
|
||||
"qweq",
|
||||
"adfas",
|
||||
"adfas",
|
||||
decCipher.Login.MainFido2Credential.CredentialId
|
||||
));
|
||||
return;
|
||||
}
|
||||
|
||||
string totpCode = null;
|
||||
if (await _stateService.Value.GetDisableAutoTotpCopyAsync() != true)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(decCipher.Login.Totp)
|
||||
&&
|
||||
(cipher.OrganizationUseTotp || await _stateService.Value.CanAccessPremiumAsync()))
|
||||
{
|
||||
var totpService = ServiceContainer.Resolve<ITotpService>("totpService");
|
||||
totpCode = await totpService.GetCodeAsync(decCipher.Login.Totp);
|
||||
totpCode = await ServiceContainer.Resolve<ITotpService>().GetCodeAsync(decCipher.Login.Totp);
|
||||
}
|
||||
}
|
||||
|
||||
@ -360,7 +476,7 @@ namespace Bit.iOS.Autofill
|
||||
catch (Exception ex)
|
||||
{
|
||||
LoggerHelper.LogEvenIfCantBeResolved(ex);
|
||||
throw;
|
||||
CancelRequest(ASExtensionErrorCode.Failed);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -93,8 +93,15 @@
|
||||
<string>com.apple.authentication-services-credential-provider-ui</string>
|
||||
<key>NSExtensionAttributes</key>
|
||||
<dict>
|
||||
<key>ASCredentialProviderExtensionShowsConfigurationUI</key>
|
||||
<true/>
|
||||
<key>ASCredentialProviderExtensionCapabilities</key>
|
||||
<dict>
|
||||
<key>ProvidesPasskeys</key>
|
||||
<true/>
|
||||
<key>ProvidesPasswords</key>
|
||||
<true/>
|
||||
<key>ShowsConfigurationUI</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</dict>
|
||||
</dict>
|
||||
</dict>
|
||||
|
@ -57,7 +57,7 @@ namespace Bit.iOS.Autofill
|
||||
Core.Constants.AutofillNeedsIdentityReplacementKey);
|
||||
if (needsAutofillReplacement.GetValueOrDefault())
|
||||
{
|
||||
await ASHelpers.ReplaceAllIdentities();
|
||||
await ASHelpers.ReplaceAllIdentitiesAsync();
|
||||
}
|
||||
|
||||
_accountSwitchingOverlayHelper = new AccountSwitchingOverlayHelper();
|
||||
|
@ -1,6 +1,7 @@
|
||||
using AuthenticationServices;
|
||||
using Bit.iOS.Core.Models;
|
||||
using Foundation;
|
||||
using UIKit;
|
||||
|
||||
namespace Bit.iOS.Autofill.Models
|
||||
{
|
||||
@ -8,7 +9,28 @@ namespace Bit.iOS.Autofill.Models
|
||||
{
|
||||
public NSExtensionContext ExtContext { get; set; }
|
||||
public ASCredentialServiceIdentifier[] ServiceIdentifiers { get; set; }
|
||||
public ASPasswordCredentialIdentity CredentialIdentity { get; set; }
|
||||
public ASPasswordCredentialIdentity PasswordCredentialIdentity { get; set; }
|
||||
public ASPasskeyCredentialIdentity PasskeyCredentialIdentity { get; set; }
|
||||
public bool Configuring { get; set; }
|
||||
|
||||
public string? RecordIdentifier
|
||||
{
|
||||
get
|
||||
{
|
||||
if (PasswordCredentialIdentity?.RecordIdentifier is string id)
|
||||
{
|
||||
return id;
|
||||
}
|
||||
|
||||
if (UIDevice.CurrentDevice.CheckSystemVersion(17, 0))
|
||||
{
|
||||
return PasskeyCredentialIdentity?.RecordIdentifier;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public bool IsPasskey => PasskeyCredentialIdentity != null;
|
||||
}
|
||||
}
|
||||
|
@ -31,7 +31,7 @@ namespace Bit.iOS.Autofill
|
||||
|
||||
BackButton.Title = AppResources.Back;
|
||||
base.ViewDidLoad();
|
||||
var task = ASHelpers.ReplaceAllIdentities();
|
||||
var task = ASHelpers.ReplaceAllIdentitiesAsync();
|
||||
}
|
||||
|
||||
partial void BackButton_Activated(UIBarButtonItem sender)
|
||||
|
@ -188,18 +188,17 @@ namespace Bit.iOS.Core.Controllers
|
||||
await _cipherService.SaveWithServerAsync(cipherDomain);
|
||||
await loadingAlert.DismissViewControllerAsync(true);
|
||||
await _storageService.SaveAsync(Bit.Core.Constants.ClearCiphersCacheKey, true);
|
||||
if (await ASHelpers.IdentitiesCanIncremental())
|
||||
if (await ASHelpers.IdentitiesSupportIncrementalAsync())
|
||||
{
|
||||
var identity = await ASHelpers.GetCipherIdentityAsync(cipherDomain.Id);
|
||||
var identity = await ASHelpers.GetCipherPasswordIdentityAsync(cipherDomain.Id);
|
||||
if (identity != null)
|
||||
{
|
||||
await ASCredentialIdentityStore.SharedStore.SaveCredentialIdentitiesAsync(
|
||||
new ASPasswordCredentialIdentity[] { identity });
|
||||
await ASCredentialIdentityStoreExtensions.SaveCredentialIdentitiesAsync(identity);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
await ASHelpers.ReplaceAllIdentities();
|
||||
await ASHelpers.ReplaceAllIdentitiesAsync();
|
||||
}
|
||||
Success(cipherDomain.Id);
|
||||
}
|
||||
@ -229,7 +228,7 @@ namespace Bit.iOS.Core.Controllers
|
||||
var appOptions = new AppOptions { IosExtension = true };
|
||||
var app = new App.App(appOptions);
|
||||
|
||||
var generatorPage = new GeneratorPage(false, selectAction: async (username) =>
|
||||
var generatorPage = new GeneratorPage(false, selectAction: (username) =>
|
||||
{
|
||||
UsernameCell.TextField.Text = username;
|
||||
DismissViewController(false, null);
|
||||
|
@ -375,7 +375,7 @@ namespace Bit.iOS.Core.Services
|
||||
|
||||
public async Task OnAccountSwitchCompleteAsync()
|
||||
{
|
||||
await ASHelpers.ReplaceAllIdentities();
|
||||
await ASHelpers.ReplaceAllIdentitiesAsync();
|
||||
}
|
||||
|
||||
public Task SetScreenCaptureAllowedAsync()
|
||||
|
@ -0,0 +1,39 @@
|
||||
using AuthenticationServices;
|
||||
using Foundation;
|
||||
using UIKit;
|
||||
|
||||
namespace Bit.iOS.Core.Utilities
|
||||
{
|
||||
public static class ASCredentialIdentityStoreExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Saves password credential identities to the shared store of <see cref="ASCredentialIdentityStore"/>
|
||||
/// Note: This is added to provide the proper method depending on the OS version.
|
||||
/// </summary>
|
||||
/// <param name="identities">Password identities to save</param>
|
||||
public static Task<Tuple<bool, NSError>> SaveCredentialIdentitiesAsync(params ASPasswordCredentialIdentity[] identities)
|
||||
{
|
||||
if (UIDevice.CurrentDevice.CheckSystemVersion(17, 0))
|
||||
{
|
||||
return ASCredentialIdentityStore.SharedStore.SaveCredentialIdentityEntriesAsync(identities);
|
||||
}
|
||||
|
||||
return ASCredentialIdentityStore.SharedStore.SaveCredentialIdentitiesAsync(identities);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Removes password credential identities of the shared store of <see cref="ASCredentialIdentityStore"/>
|
||||
/// Note: This is added to provide the proper method depending on the OS version.
|
||||
/// </summary>
|
||||
/// <param name="identities">Password identities to remove</param>
|
||||
public static Task<Tuple<bool, NSError>> RemoveCredentialIdentitiesAsync(params ASPasswordCredentialIdentity[] identities)
|
||||
{
|
||||
if (UIDevice.CurrentDevice.CheckSystemVersion(17, 0))
|
||||
{
|
||||
return ASCredentialIdentityStore.SharedStore.RemoveCredentialIdentityEntriesAsync(identities);
|
||||
}
|
||||
|
||||
return ASCredentialIdentityStore.SharedStore.RemoveCredentialIdentitiesAsync(identities);
|
||||
}
|
||||
}
|
||||
}
|
@ -1,93 +1,124 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using AuthenticationServices;
|
||||
using AuthenticationServices;
|
||||
using Bit.Core.Abstractions;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Models.View;
|
||||
using Bit.Core.Utilities;
|
||||
using UIKit;
|
||||
|
||||
namespace Bit.iOS.Core.Utilities
|
||||
{
|
||||
public static class ASHelpers
|
||||
{
|
||||
public static async Task ReplaceAllIdentities()
|
||||
public static async Task ReplaceAllIdentitiesAsync()
|
||||
{
|
||||
if (await AutofillEnabled())
|
||||
if (!await IsAutofillEnabledAsync())
|
||||
{
|
||||
var storageService = ServiceContainer.Resolve<IStorageService>("storageService");
|
||||
var stateService = ServiceContainer.Resolve<IStateService>("stateService");
|
||||
var timeoutAction = await stateService.GetVaultTimeoutActionAsync();
|
||||
if (timeoutAction == VaultTimeoutAction.Logout)
|
||||
{
|
||||
await ASCredentialIdentityStore.SharedStore?.RemoveAllCredentialIdentitiesAsync();
|
||||
return;
|
||||
}
|
||||
var vaultTimeoutService = ServiceContainer.Resolve<IVaultTimeoutService>("vaultTimeoutService");
|
||||
if (await vaultTimeoutService.IsLockedAsync())
|
||||
{
|
||||
await ASCredentialIdentityStore.SharedStore?.RemoveAllCredentialIdentitiesAsync();
|
||||
await storageService.SaveAsync(Constants.AutofillNeedsIdentityReplacementKey, true);
|
||||
return;
|
||||
}
|
||||
var cipherService = ServiceContainer.Resolve<ICipherService>("cipherService");
|
||||
var identities = new List<ASPasswordCredentialIdentity>();
|
||||
var ciphers = await cipherService.GetAllDecryptedAsync();
|
||||
foreach (var cipher in ciphers.Where(x => !x.IsDeleted))
|
||||
{
|
||||
var identity = ToCredentialIdentity(cipher);
|
||||
if (identity != null)
|
||||
{
|
||||
identities.Add(identity);
|
||||
}
|
||||
}
|
||||
if (identities.Any())
|
||||
{
|
||||
await ASCredentialIdentityStore.SharedStore?.ReplaceCredentialIdentitiesAsync(identities.ToArray());
|
||||
await storageService.SaveAsync(Constants.AutofillNeedsIdentityReplacementKey, false);
|
||||
return;
|
||||
}
|
||||
await ASCredentialIdentityStore.SharedStore?.RemoveAllCredentialIdentitiesAsync();
|
||||
return;
|
||||
}
|
||||
|
||||
var storageService = ServiceContainer.Resolve<IStorageService>("storageService");
|
||||
var stateService = ServiceContainer.Resolve<IStateService>();
|
||||
var timeoutAction = await stateService.GetVaultTimeoutActionAsync();
|
||||
if (timeoutAction == VaultTimeoutAction.Logout)
|
||||
{
|
||||
await ASCredentialIdentityStore.SharedStore.RemoveAllCredentialIdentitiesAsync();
|
||||
return;
|
||||
}
|
||||
|
||||
var vaultTimeoutService = ServiceContainer.Resolve<IVaultTimeoutService>();
|
||||
if (await vaultTimeoutService.IsLockedAsync())
|
||||
{
|
||||
await ASCredentialIdentityStore.SharedStore.RemoveAllCredentialIdentitiesAsync();
|
||||
await storageService.SaveAsync(Constants.AutofillNeedsIdentityReplacementKey, true);
|
||||
return;
|
||||
}
|
||||
|
||||
var cipherService = ServiceContainer.Resolve<ICipherService>();
|
||||
if (UIDevice.CurrentDevice.CheckSystemVersion(17, 0))
|
||||
{
|
||||
await ReplaceAllIdentitiesIOS17Async(cipherService, storageService);
|
||||
}
|
||||
else
|
||||
{
|
||||
await ReplaceAllIdentitiesIOS12Async(cipherService, storageService);
|
||||
}
|
||||
}
|
||||
|
||||
public static async Task<bool> IdentitiesCanIncremental()
|
||||
private static async Task ReplaceAllIdentitiesIOS12Async(ICipherService cipherService, IStorageService storageService)
|
||||
{
|
||||
var stateService = ServiceContainer.Resolve<IStateService>("stateService");
|
||||
var ciphers = await cipherService.GetAllDecryptedAsync();
|
||||
var identities = ciphers.Where(c => !c.IsDeleted)
|
||||
.Select(ToPasswordCredentialIdentity)
|
||||
.Where(i => i != null)
|
||||
.Cast<ASPasswordCredentialIdentity>()
|
||||
.ToList();
|
||||
if (!identities.Any())
|
||||
{
|
||||
await ASCredentialIdentityStore.SharedStore.RemoveAllCredentialIdentitiesAsync();
|
||||
return;
|
||||
}
|
||||
|
||||
#pragma warning disable CA1422 // Validate platform compatibility
|
||||
await ASCredentialIdentityStore.SharedStore.ReplaceCredentialIdentitiesAsync(identities.ToArray());
|
||||
#pragma warning restore CA1422 // Validate platform compatibility
|
||||
await storageService.SaveAsync(Constants.AutofillNeedsIdentityReplacementKey, false);
|
||||
}
|
||||
|
||||
private static async Task ReplaceAllIdentitiesIOS17Async(ICipherService cipherService, IStorageService storageService)
|
||||
{
|
||||
var ciphers = await cipherService.GetAllDecryptedAsync();
|
||||
var identities = ciphers.Where(c => !c.IsDeleted)
|
||||
.Select(ToCredentialIdentity)
|
||||
.Where(i => i != null)
|
||||
.Cast<IASCredentialIdentity>()
|
||||
.ToList();
|
||||
if (!identities.Any())
|
||||
{
|
||||
await ASCredentialIdentityStore.SharedStore.RemoveAllCredentialIdentitiesAsync();
|
||||
return;
|
||||
}
|
||||
|
||||
await ASCredentialIdentityStore.SharedStore.ReplaceCredentialIdentityEntriesAsync(identities.ToArray());
|
||||
await storageService.SaveAsync(Constants.AutofillNeedsIdentityReplacementKey, false);
|
||||
}
|
||||
|
||||
public static async Task<bool> IdentitiesSupportIncrementalAsync()
|
||||
{
|
||||
var stateService = ServiceContainer.Resolve<IStateService>();
|
||||
var timeoutAction = await stateService.GetVaultTimeoutActionAsync();
|
||||
if (timeoutAction == VaultTimeoutAction.Logout)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
var state = await ASCredentialIdentityStore.SharedStore?.GetCredentialIdentityStoreStateAsync();
|
||||
var state = await ASCredentialIdentityStore.SharedStore.GetCredentialIdentityStoreStateAsync();
|
||||
return state != null && state.Enabled && state.SupportsIncrementalUpdates;
|
||||
}
|
||||
|
||||
public static async Task<bool> AutofillEnabled()
|
||||
public static async Task<bool> IsAutofillEnabledAsync()
|
||||
{
|
||||
var state = await ASCredentialIdentityStore.SharedStore?.GetCredentialIdentityStoreStateAsync();
|
||||
var state = await ASCredentialIdentityStore.SharedStore.GetCredentialIdentityStoreStateAsync();
|
||||
return state != null && state.Enabled;
|
||||
}
|
||||
|
||||
public static async Task<ASPasswordCredentialIdentity> GetCipherIdentityAsync(string cipherId)
|
||||
public static async Task<ASPasswordCredentialIdentity?> GetCipherPasswordIdentityAsync(string cipherId)
|
||||
{
|
||||
var cipherService = ServiceContainer.Resolve<ICipherService>("cipherService");
|
||||
var cipherService = ServiceContainer.Resolve<ICipherService>();
|
||||
var cipher = await cipherService.GetAsync(cipherId);
|
||||
if (cipher == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
var cipherView = await cipher.DecryptAsync();
|
||||
return ToCredentialIdentity(cipherView);
|
||||
return ToPasswordCredentialIdentity(cipherView);
|
||||
}
|
||||
|
||||
public static ASPasswordCredentialIdentity ToCredentialIdentity(CipherView cipher)
|
||||
public static ASPasswordCredentialIdentity? ToPasswordCredentialIdentity(CipherView cipher)
|
||||
{
|
||||
if (!cipher?.Login?.Uris?.Any() ?? true)
|
||||
if (cipher?.Login?.Uris?.Any() != true)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
var uri = cipher.Login.Uris.FirstOrDefault(u => u.Match != Bit.Core.Enums.UriMatchType.Never)?.Uri;
|
||||
var uri = cipher.Login.Uris.FirstOrDefault(u => u.Match != UriMatchType.Never)?.Uri;
|
||||
if (string.IsNullOrWhiteSpace(uri))
|
||||
{
|
||||
return null;
|
||||
@ -100,5 +131,24 @@ namespace Bit.iOS.Core.Utilities
|
||||
var serviceId = new ASCredentialServiceIdentifier(uri, ASCredentialServiceIdentifierType.Url);
|
||||
return new ASPasswordCredentialIdentity(serviceId, username, cipher.Id);
|
||||
}
|
||||
|
||||
public static IASCredentialIdentity? ToCredentialIdentity(CipherView cipher)
|
||||
{
|
||||
if (!cipher.HasFido2Credential)
|
||||
{
|
||||
return ToPasswordCredentialIdentity(cipher);
|
||||
}
|
||||
|
||||
if (!cipher.Login.MainFido2Credential.IsDiscoverable)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return new ASPasskeyCredentialIdentity(cipher.Login.MainFido2Credential.RpId,
|
||||
cipher.Login.MainFido2Credential.UserName,
|
||||
cipher.Login.MainFido2Credential.CredentialId,
|
||||
cipher.Login.MainFido2Credential.UserHandle,
|
||||
cipher.Id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user