mirror of
https://github.com/bitwarden/mobile
synced 2025-12-22 19:23:58 +00:00
ios autofill extension implemented
This commit is contained in:
@@ -5,23 +5,38 @@ using Bit.iOS.Core.Views;
|
||||
using Bit.App.Resources;
|
||||
using Bit.iOS.Core.Utilities;
|
||||
using Bit.App.Abstractions;
|
||||
using System.Linq;
|
||||
using Bit.iOS.Core.Controllers;
|
||||
using Bit.Core.Abstractions;
|
||||
using Bit.Core.Utilities;
|
||||
using System.Threading.Tasks;
|
||||
using Bit.Core.Models.Domain;
|
||||
using Bit.Core.Enums;
|
||||
|
||||
namespace Bit.iOS.Core.Controllers
|
||||
{
|
||||
public abstract class LockPasswordViewController : ExtendedUITableViewController
|
||||
{
|
||||
//private IAuthService _authService;
|
||||
//private ICryptoService _cryptoService;
|
||||
private ILockService _lockService;
|
||||
private ICryptoService _cryptoService;
|
||||
private IDeviceActionService _deviceActionService;
|
||||
private IUserService _userService;
|
||||
private IStorageService _storageService;
|
||||
private IStorageService _secureStorageService;
|
||||
private IPlatformUtilsService _platformUtilsService;
|
||||
private Tuple<bool, bool> _pinSet;
|
||||
private bool _hasKey;
|
||||
private bool _pinLock;
|
||||
private bool _fingerprintLock;
|
||||
private int _invalidPinAttempts;
|
||||
|
||||
public LockPasswordViewController(IntPtr handle) : base(handle)
|
||||
public LockPasswordViewController(IntPtr handle)
|
||||
: base(handle)
|
||||
{ }
|
||||
|
||||
public abstract UINavigationItem BaseNavItem { get; }
|
||||
public abstract UIBarButtonItem BaseCancelButton { get; }
|
||||
public abstract UIBarButtonItem BaseSubmitButton { get; }
|
||||
public abstract Action Success { get; }
|
||||
public abstract Action Cancel { get; }
|
||||
|
||||
public FormEntryTableViewCell MasterPasswordCell { get; set; } = new FormEntryTableViewCell(
|
||||
AppResources.MasterPassword, useLabelAsPlaceholder: true);
|
||||
@@ -35,21 +50,32 @@ namespace Bit.iOS.Core.Controllers
|
||||
|
||||
public override void ViewDidLoad()
|
||||
{
|
||||
// _authService = Resolver.Resolve<IAuthService>();
|
||||
// _cryptoService = Resolver.Resolve<ICryptoService>();
|
||||
_lockService = ServiceContainer.Resolve<ILockService>("lockService");
|
||||
_cryptoService = ServiceContainer.Resolve<ICryptoService>("cryptoService");
|
||||
_deviceActionService = ServiceContainer.Resolve<IDeviceActionService>("deviceActionService");
|
||||
_userService = ServiceContainer.Resolve<IUserService>("userService");
|
||||
_storageService = ServiceContainer.Resolve<IStorageService>("storageService");
|
||||
_secureStorageService = ServiceContainer.Resolve<IStorageService>("secureStorageService");
|
||||
_platformUtilsService = ServiceContainer.Resolve<IPlatformUtilsService>("platformUtilsService");
|
||||
|
||||
BaseNavItem.Title = AppResources.VerifyMasterPassword;
|
||||
_pinSet = _lockService.IsPinLockSetAsync().GetAwaiter().GetResult();
|
||||
_hasKey = _cryptoService.HasKeyAsync().GetAwaiter().GetResult();
|
||||
_pinLock = (_pinSet.Item1 && _hasKey) || _pinSet.Item2;
|
||||
_fingerprintLock = _lockService.IsFingerprintLockSetAsync().GetAwaiter().GetResult();
|
||||
|
||||
BaseNavItem.Title = _pinLock ? AppResources.VerifyPIN : AppResources.VerifyMasterPassword;
|
||||
BaseCancelButton.Title = AppResources.Cancel;
|
||||
BaseSubmitButton.Title = AppResources.Submit;
|
||||
View.BackgroundColor = new UIColor(red: 0.94f, green: 0.94f, blue: 0.96f, alpha: 1.0f);
|
||||
|
||||
var descriptor = UIFontDescriptor.PreferredBody;
|
||||
|
||||
MasterPasswordCell.TextField.Placeholder = _pinLock ? AppResources.PIN : AppResources.MasterPassword;
|
||||
MasterPasswordCell.TextField.SecureTextEntry = true;
|
||||
MasterPasswordCell.TextField.ReturnKeyType = UIReturnKeyType.Go;
|
||||
MasterPasswordCell.TextField.ShouldReturn += (UITextField tf) =>
|
||||
{
|
||||
// CheckPassword();
|
||||
CheckPasswordAsync().GetAwaiter().GetResult();
|
||||
return true;
|
||||
};
|
||||
|
||||
@@ -59,49 +85,156 @@ namespace Bit.iOS.Core.Controllers
|
||||
TableView.AllowsSelection = true;
|
||||
|
||||
base.ViewDidLoad();
|
||||
|
||||
if(_fingerprintLock)
|
||||
{
|
||||
var fingerprintButtonText = _deviceActionService.SupportsFaceId() ? AppResources.UseFaceIDToUnlock :
|
||||
AppResources.UseFingerprintToUnlock;
|
||||
// TODO: set button text
|
||||
var tasks = Task.Run(async () =>
|
||||
{
|
||||
await Task.Delay(500);
|
||||
PromptFingerprintAsync().GetAwaiter().GetResult();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public override void ViewDidAppear(bool animated)
|
||||
{
|
||||
base.ViewDidAppear(animated);
|
||||
MasterPasswordCell.TextField.BecomeFirstResponder();
|
||||
if(!_fingerprintLock)
|
||||
{
|
||||
MasterPasswordCell.TextField.BecomeFirstResponder();
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
protected void CheckPassword()
|
||||
// TODO: Try fingerprint again button action
|
||||
|
||||
protected async Task CheckPasswordAsync()
|
||||
{
|
||||
if(string.IsNullOrWhiteSpace(MasterPasswordCell.TextField.Text))
|
||||
{
|
||||
var alert = Dialogs.CreateAlert(AppResources.AnErrorHasOccurred,
|
||||
string.Format(AppResources.ValidationFieldRequired, AppResources.MasterPassword), AppResources.Ok);
|
||||
string.Format(AppResources.ValidationFieldRequired,
|
||||
_pinLock ? AppResources.PIN : AppResources.MasterPassword),
|
||||
AppResources.Ok);
|
||||
PresentViewController(alert, true, null);
|
||||
return;
|
||||
}
|
||||
|
||||
var key = _cryptoService.MakeKeyFromPassword(MasterPasswordCell.TextField.Text, _authService.Email,
|
||||
_authService.Kdf, _authService.KdfIterations);
|
||||
if(key.Key.SequenceEqual(_cryptoService.Key.Key))
|
||||
var email = await _userService.GetEmailAsync();
|
||||
var kdf = await _userService.GetKdfAsync();
|
||||
var kdfIterations = await _userService.GetKdfIterationsAsync();
|
||||
var inputtedValue = MasterPasswordCell.TextField.Text;
|
||||
|
||||
if(_pinLock)
|
||||
{
|
||||
_appSettingsService.Locked = false;
|
||||
MasterPasswordCell.TextField.ResignFirstResponder();
|
||||
Success();
|
||||
var failed = true;
|
||||
try
|
||||
{
|
||||
if(_pinSet.Item1)
|
||||
{
|
||||
var protectedPin = await _storageService.GetAsync<string>(Bit.Core.Constants.ProtectedPin);
|
||||
var decPin = await _cryptoService.DecryptToUtf8Async(new CipherString(protectedPin));
|
||||
failed = decPin != inputtedValue;
|
||||
_lockService.PinLocked = failed;
|
||||
if(!failed)
|
||||
{
|
||||
DoContinue();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
var key2 = await _cryptoService.MakeKeyFromPinAsync(inputtedValue, email,
|
||||
kdf.GetValueOrDefault(KdfType.PBKDF2_SHA256), kdfIterations.GetValueOrDefault(5000));
|
||||
failed = false;
|
||||
await SetKeyAndContinueAsync(key2);
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
failed = true;
|
||||
}
|
||||
if(failed)
|
||||
{
|
||||
_invalidPinAttempts++;
|
||||
if(_invalidPinAttempts >= 5)
|
||||
{
|
||||
Cancel?.Invoke();
|
||||
return;
|
||||
}
|
||||
InvalidValue();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// TODO: keep track of invalid attempts and logout?
|
||||
var key2 = await _cryptoService.MakeKeyAsync(inputtedValue, email, kdf, kdfIterations);
|
||||
var keyHash = await _cryptoService.HashPasswordAsync(inputtedValue, key2);
|
||||
var storedKeyHash = await _cryptoService.GetKeyHashAsync();
|
||||
if(storedKeyHash == null)
|
||||
{
|
||||
var oldKey = await _secureStorageService.GetAsync<string>("oldKey");
|
||||
if(key2.KeyB64 == oldKey)
|
||||
{
|
||||
await _secureStorageService.RemoveAsync("oldKey");
|
||||
await _cryptoService.SetKeyHashAsync(keyHash);
|
||||
storedKeyHash = keyHash;
|
||||
}
|
||||
}
|
||||
if(storedKeyHash != null && keyHash != null && storedKeyHash == keyHash)
|
||||
{
|
||||
await SetKeyAndContinueAsync(key2);
|
||||
}
|
||||
else
|
||||
{
|
||||
InvalidValue();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var alert = Dialogs.CreateAlert(AppResources.AnErrorHasOccurred,
|
||||
string.Format(null, AppResources.InvalidMasterPassword), AppResources.Ok, (a) =>
|
||||
private async Task SetKeyAndContinueAsync(SymmetricCryptoKey key)
|
||||
{
|
||||
if(!_hasKey)
|
||||
{
|
||||
await _cryptoService.SetKeyAsync(key);
|
||||
}
|
||||
DoContinue();
|
||||
}
|
||||
|
||||
private void DoContinue()
|
||||
{
|
||||
MasterPasswordCell.TextField.ResignFirstResponder();
|
||||
Success();
|
||||
}
|
||||
|
||||
public async Task PromptFingerprintAsync()
|
||||
{
|
||||
if(!_fingerprintLock)
|
||||
{
|
||||
return;
|
||||
}
|
||||
var success = await _platformUtilsService.AuthenticateFingerprintAsync(null,
|
||||
_pinLock ? AppResources.PIN : AppResources.MasterPassword,
|
||||
() => MasterPasswordCell.TextField.BecomeFirstResponder());
|
||||
_lockService.FingerprintLocked = !success;
|
||||
if(success)
|
||||
{
|
||||
DoContinue();
|
||||
}
|
||||
}
|
||||
|
||||
private void InvalidValue()
|
||||
{
|
||||
var alert = Dialogs.CreateAlert(AppResources.AnErrorHasOccurred,
|
||||
string.Format(null, _pinLock ? AppResources.PIN : AppResources.InvalidMasterPassword),
|
||||
AppResources.Ok, (a) =>
|
||||
{
|
||||
|
||||
MasterPasswordCell.TextField.Text = string.Empty;
|
||||
MasterPasswordCell.TextField.BecomeFirstResponder();
|
||||
});
|
||||
|
||||
PresentViewController(alert, true, null);
|
||||
}
|
||||
PresentViewController(alert, true, null);
|
||||
}
|
||||
*/
|
||||
|
||||
public class TableSource : UITableViewSource
|
||||
{
|
||||
@@ -121,7 +254,6 @@ namespace Bit.iOS.Core.Controllers
|
||||
return _controller.MasterPasswordCell;
|
||||
}
|
||||
}
|
||||
|
||||
return new UITableViewCell();
|
||||
}
|
||||
|
||||
@@ -141,7 +273,6 @@ namespace Bit.iOS.Core.Controllers
|
||||
{
|
||||
return 1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -159,15 +290,12 @@ namespace Bit.iOS.Core.Controllers
|
||||
{
|
||||
tableView.DeselectRow(indexPath, true);
|
||||
tableView.EndEditing(true);
|
||||
|
||||
var cell = tableView.CellAt(indexPath);
|
||||
if(cell == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var selectableCell = cell as ISelectable;
|
||||
if(selectableCell != null)
|
||||
if(cell is ISelectable selectableCell)
|
||||
{
|
||||
selectableCell.Select();
|
||||
}
|
||||
|
||||
109
src/iOS.Core/Utilities/iOSCoreHelpers.cs
Normal file
109
src/iOS.Core/Utilities/iOSCoreHelpers.cs
Normal file
@@ -0,0 +1,109 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using Bit.App.Abstractions;
|
||||
using Bit.App.Services;
|
||||
using Bit.App.Utilities;
|
||||
using Bit.Core.Abstractions;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Utilities;
|
||||
using Bit.iOS.Core.Services;
|
||||
using Foundation;
|
||||
using HockeyApp.iOS;
|
||||
using UIKit;
|
||||
|
||||
namespace Bit.iOS.Core.Utilities
|
||||
{
|
||||
public static class iOSCoreHelpers
|
||||
{
|
||||
public static string AppId = "com.8bit.bitwarden";
|
||||
public static string AppGroupId = "group.com.8bit.bitwarden";
|
||||
public static string AccessGroup = "LTZ2PFU5D6.com.8bit.bitwarden";
|
||||
|
||||
public static void RegisterHockeyApp()
|
||||
{
|
||||
var crashManagerDelegate = new HockeyAppCrashManagerDelegate(
|
||||
ServiceContainer.Resolve<IAppIdService>("appIdService"),
|
||||
ServiceContainer.Resolve<IUserService>("userService"));
|
||||
var manager = BITHockeyManager.SharedHockeyManager;
|
||||
manager.Configure("51f96ae568ba45f699a18ad9f63046c3", crashManagerDelegate);
|
||||
manager.CrashManager.CrashManagerStatus = BITCrashManagerStatus.AutoSend;
|
||||
manager.StartManager();
|
||||
manager.Authenticator.AuthenticateInstallation();
|
||||
manager.DisableMetricsManager = manager.DisableFeedbackManager = manager.DisableUpdateManager = true;
|
||||
var task = crashManagerDelegate.InitAsync(manager);
|
||||
}
|
||||
|
||||
public static void RegisterLocalServices()
|
||||
{
|
||||
if(ServiceContainer.Resolve<ILogService>("logService", true) == null)
|
||||
{
|
||||
ServiceContainer.Register<ILogService>("logService", 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
|
||||
});
|
||||
});
|
||||
|
||||
var preferencesStorage = new PreferencesStorageService(AppGroupId);
|
||||
var appGroupContainer = new NSFileManager().GetContainerUrl(AppGroupId);
|
||||
var liteDbStorage = new LiteDbStorageService(
|
||||
Path.Combine(appGroupContainer.Path, "Library", "bitwarden.db"));
|
||||
liteDbStorage.InitAsync();
|
||||
var localizeService = new LocalizeService();
|
||||
var broadcasterService = new BroadcasterService();
|
||||
var messagingService = new MobileBroadcasterMessagingService(broadcasterService);
|
||||
var i18nService = new MobileI18nService(localizeService.GetCurrentCultureInfo());
|
||||
var secureStorageService = new KeyChainStorageService(AppId, AccessGroup);
|
||||
var cryptoPrimitiveService = new CryptoPrimitiveService();
|
||||
var mobileStorageService = new MobileStorageService(preferencesStorage, liteDbStorage);
|
||||
var deviceActionService = new DeviceActionService(mobileStorageService, messagingService);
|
||||
var platformUtilsService = new MobilePlatformUtilsService(deviceActionService, messagingService,
|
||||
broadcasterService);
|
||||
|
||||
ServiceContainer.Register<IBroadcasterService>("broadcasterService", broadcasterService);
|
||||
ServiceContainer.Register<IMessagingService>("messagingService", messagingService);
|
||||
ServiceContainer.Register<ILocalizeService>("localizeService", localizeService);
|
||||
ServiceContainer.Register<II18nService>("i18nService", i18nService);
|
||||
ServiceContainer.Register<ICryptoPrimitiveService>("cryptoPrimitiveService", cryptoPrimitiveService);
|
||||
ServiceContainer.Register<IStorageService>("storageService", mobileStorageService);
|
||||
ServiceContainer.Register<IStorageService>("secureStorageService", secureStorageService);
|
||||
ServiceContainer.Register<IDeviceActionService>("deviceActionService", deviceActionService);
|
||||
ServiceContainer.Register<IPlatformUtilsService>("platformUtilsService", platformUtilsService);
|
||||
}
|
||||
|
||||
public static void Bootstrap()
|
||||
{
|
||||
(ServiceContainer.Resolve<II18nService>("i18nService") as MobileI18nService).Init();
|
||||
ServiceContainer.Resolve<IAuthService>("authService").Init();
|
||||
// Note: This is not awaited
|
||||
var bootstrapTask = BootstrapAsync();
|
||||
}
|
||||
|
||||
public static void AppearanceAdjustments()
|
||||
{
|
||||
ThemeHelpers.SetAppearance(ThemeManager.GetTheme(false));
|
||||
UIApplication.SharedApplication.StatusBarHidden = false;
|
||||
UIApplication.SharedApplication.StatusBarStyle = UIStatusBarStyle.LightContent;
|
||||
}
|
||||
|
||||
private static async Task BootstrapAsync()
|
||||
{
|
||||
var disableFavicon = await ServiceContainer.Resolve<IStorageService>("storageService").GetAsync<bool?>(
|
||||
Bit.Core.Constants.DisableFaviconKey);
|
||||
await ServiceContainer.Resolve<IStateService>("stateService").SaveAsync(
|
||||
Bit.Core.Constants.DisableFaviconKey, disableFavicon);
|
||||
await ServiceContainer.Resolve<IEnvironmentService>("environmentService").SetUrlsFromStorageAsync();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -22,6 +22,7 @@ namespace Bit.iOS.Core.Views
|
||||
protected ICipherService _cipherService;
|
||||
protected ITotpService _totpService;
|
||||
protected IUserService _userService;
|
||||
protected ISearchService _searchService;
|
||||
private AppExtensionContext _context;
|
||||
private UIViewController _controller;
|
||||
|
||||
@@ -30,6 +31,7 @@ namespace Bit.iOS.Core.Views
|
||||
_cipherService = ServiceContainer.Resolve<ICipherService>("cipherService");
|
||||
_totpService = ServiceContainer.Resolve<ITotpService>("totpService");
|
||||
_userService = ServiceContainer.Resolve<IUserService>("userService");
|
||||
_searchService = ServiceContainer.Resolve<ISearchService>("searchService");
|
||||
_context = context;
|
||||
_controller = controller;
|
||||
}
|
||||
@@ -61,8 +63,6 @@ namespace Bit.iOS.Core.Views
|
||||
_allItems = combinedLogins
|
||||
.Where(c => c.Type == Bit.Core.Enums.CipherType.Login)
|
||||
.Select(s => new CipherViewModel(s))
|
||||
.OrderBy(s => s.Name)
|
||||
.ThenBy(s => s.Username)
|
||||
.ToList() ?? new List<CipherViewModel>();
|
||||
FilterResults(searchFilter, new CancellationToken());
|
||||
}
|
||||
@@ -78,12 +78,9 @@ namespace Bit.iOS.Core.Views
|
||||
else
|
||||
{
|
||||
searchFilter = searchFilter.ToLower();
|
||||
Items = _allItems
|
||||
.Where(s => s.Name?.ToLower().Contains(searchFilter) ?? false ||
|
||||
(s.Username?.ToLower().Contains(searchFilter) ?? false) ||
|
||||
(s.Uris?.FirstOrDefault()?.Uri?.ToLower().Contains(searchFilter) ?? false))
|
||||
.TakeWhile(s => !ct.IsCancellationRequested)
|
||||
.ToArray();
|
||||
var results = _searchService.SearchCiphersAsync(searchFilter,
|
||||
c => c.Type == Bit.Core.Enums.CipherType.Login, null, ct).GetAwaiter().GetResult();
|
||||
Items = results.Select(s => new CipherViewModel(s)).ToArray();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -70,6 +70,7 @@
|
||||
<Compile Include="Services\CryptoPrimitiveService.cs" />
|
||||
<Compile Include="Services\KeyChainStorageService.cs" />
|
||||
<Compile Include="Services\LocalizeService.cs" />
|
||||
<Compile Include="Utilities\iOSCoreHelpers.cs" />
|
||||
<Compile Include="Utilities\ThemeHelpers.cs" />
|
||||
<Compile Include="Views\ExtensionSearchDelegate.cs" />
|
||||
<Compile Include="Views\ExtensionTableSource.cs" />
|
||||
|
||||
Reference in New Issue
Block a user