From 1124c48c8d125504d22d2414be9d677e43b2d821 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Fri, 21 Jul 2017 11:39:22 -0400 Subject: [PATCH] copy totp code on autofill --- src/Android/MainActivity.cs | 15 +++++- src/App/Constants.cs | 1 + src/App/Models/Page/VaultListPageModel.cs | 2 + .../Pages/Settings/SettingsFeaturesPage.cs | 54 +++++++++++++++---- src/App/Resources/AppResources.Designer.cs | 40 +++++++++++++- src/App/Resources/AppResources.resx | 14 ++++- src/iOS.Extension/LoadingViewController.cs | 7 ++- src/iOS.Extension/LoginAddViewController.cs | 3 +- src/iOS.Extension/LoginListViewController.cs | 54 +++++++++++++++++-- src/iOS.Extension/Models/LoginViewModel.cs | 3 ++ 10 files changed, 171 insertions(+), 22 deletions(-) diff --git a/src/Android/MainActivity.cs b/src/Android/MainActivity.cs index 91817fd5d..67b84ff55 100644 --- a/src/Android/MainActivity.cs +++ b/src/Android/MainActivity.cs @@ -29,6 +29,8 @@ namespace Bit.Android private const string HockeyAppId = "d3834185b4a643479047b86c65293d42"; private DateTime? _lastAction; private Java.Util.Regex.Pattern _otpPattern = Java.Util.Regex.Pattern.Compile("^.*?([cbdefghijklnrtuv]{32,64})$"); + private IDeviceActionService _deviceActionService; + private ISettings _settings; protected override void OnCreate(Bundle bundle) { @@ -65,6 +67,8 @@ namespace Bit.Android typeof(Color).GetProperty("Accent", BindingFlags.Public | BindingFlags.Static) .SetValue(null, Color.FromHex("d2d6de")); + _deviceActionService = Resolver.Resolve(); + _settings = Resolver.Resolve(); LoadApplication(new App.App( uri, Resolver.Resolve(), @@ -72,13 +76,13 @@ namespace Bit.Android Resolver.Resolve(), Resolver.Resolve(), Resolver.Resolve(), - Resolver.Resolve(), + _settings, Resolver.Resolve(), Resolver.Resolve(), Resolver.Resolve(), Resolver.Resolve(), Resolver.Resolve(), - Resolver.Resolve())); + _deviceActionService)); MessagingCenter.Subscribe( Xamarin.Forms.Application.Current, "DismissKeyboard", (sender) => @@ -129,6 +133,13 @@ namespace Bit.Android } else { + var isPremium = Resolver.Resolve()?.TokenPremium ?? false; + var autoCopyEnabled = !_settings.GetValueOrDefault(Constants.SettingDisableTotpCopy, false); + if(isPremium && autoCopyEnabled && _deviceActionService != null && login.Totp.Value != null) + { + _deviceActionService.CopyToClipboard(App.Utilities.Crypto.Totp(login.Totp.Value)); + } + data.PutExtra("uri", login.Uri.Value); data.PutExtra("username", login.Username); data.PutExtra("password", login.Password.Value); diff --git a/src/App/Constants.cs b/src/App/Constants.cs index 684c8c7e4..aaa1f0282 100644 --- a/src/App/Constants.cs +++ b/src/App/Constants.cs @@ -8,6 +8,7 @@ public const string SettingPinUnlockOn = "setting:pinUnlockOn"; public const string SettingLockSeconds = "setting:lockSeconds"; public const string SettingGaOptOut = "setting:googleAnalyticsOptOut"; + public const string SettingDisableTotpCopy = "setting:disableAutoCopyTotp"; public const string AutofillPersistNotification = "setting:persistNotification"; public const string AutofillPasswordField = "setting:autofillPasswordField"; diff --git a/src/App/Models/Page/VaultListPageModel.cs b/src/App/Models/Page/VaultListPageModel.cs index 16bd7c826..5ac43851c 100644 --- a/src/App/Models/Page/VaultListPageModel.cs +++ b/src/App/Models/Page/VaultListPageModel.cs @@ -17,6 +17,7 @@ namespace Bit.App.Models.Page Username = login.Username?.Decrypt(login.OrganizationId) ?? " "; Password = new Lazy(() => login.Password?.Decrypt(login.OrganizationId)); Uri = new Lazy(() => login.Uri?.Decrypt(login.OrganizationId)); + Totp = new Lazy(() => login.Totp?.Decrypt(login.OrganizationId)); } public string Id { get; set; } @@ -26,6 +27,7 @@ namespace Bit.App.Models.Page public string Username { get; set; } public Lazy Password { get; set; } public Lazy Uri { get; set; } + public Lazy Totp { get; set; } } public class AutofillLogin : Login diff --git a/src/App/Pages/Settings/SettingsFeaturesPage.cs b/src/App/Pages/Settings/SettingsFeaturesPage.cs index af67eec1f..21d3666d6 100644 --- a/src/App/Pages/Settings/SettingsFeaturesPage.cs +++ b/src/App/Pages/Settings/SettingsFeaturesPage.cs @@ -27,6 +27,8 @@ namespace Bit.App.Pages } private StackLayout StackLayout { get; set; } + private ExtendedSwitchCell CopyTotpCell { get; set; } + private Label CopyTotpLabel { get; set; } private ExtendedSwitchCell AnalyticsCell { get; set; } private Label AnalyticsLabel { get; set; } private ExtendedSwitchCell AutofillPersistNotificationCell { get; set; } @@ -38,6 +40,23 @@ namespace Bit.App.Pages private void Init() { + CopyTotpCell = new ExtendedSwitchCell + { + Text = AppResources.DisableAutoTotpCopy, + On = _settings.GetValueOrDefault(Constants.SettingDisableTotpCopy, false) + }; + + var totpTable = new FormTableView(true) + { + Root = new TableRoot + { + new TableSection(" ") + { + CopyTotpCell + } + } + }; + AnalyticsCell = new ExtendedSwitchCell { Text = AppResources.DisableGA, @@ -55,18 +74,19 @@ namespace Bit.App.Pages } }; - AnalyticsLabel = new Label + CopyTotpLabel = new FormTableLabel(this) { - Text = AppResources.DisbaleGADescription, - LineBreakMode = LineBreakMode.WordWrap, - FontSize = Device.GetNamedSize(NamedSize.Small, typeof(Label)), - Style = (Style)Application.Current.Resources["text-muted"], - Margin = new Thickness(15, (this.IsLandscape() ? 5 : 0), 15, 25) + Text = AppResources.DisableAutoTotpCopyDescription + }; + + AnalyticsLabel = new FormTableLabel(this) + { + Text = AppResources.DisableGADescription }; StackLayout = new StackLayout { - Children = { analyticsTable, AnalyticsLabel }, + Children = { totpTable, CopyTotpLabel, analyticsTable, AnalyticsLabel }, Spacing = 0 }; @@ -78,7 +98,7 @@ namespace Bit.App.Pages On = !_appSettings.AutofillPersistNotification && !_appSettings.AutofillPasswordField }; - var autofillAlwaysTable = new FormTableView + var autofillAlwaysTable = new FormTableView(true) { Root = new TableRoot { @@ -102,7 +122,6 @@ namespace Bit.App.Pages var autofillPersistNotificationTable = new FormTableView { - NoHeader = true, Root = new TableRoot { new TableSection(" ") @@ -125,7 +144,6 @@ namespace Bit.App.Pages var autofillPasswordFieldTable = new FormTableView { - NoHeader = true, Root = new TableRoot { new TableSection(" ") @@ -169,6 +187,7 @@ namespace Bit.App.Pages base.OnAppearing(); AnalyticsCell.OnChanged += AnalyticsCell_Changed; + CopyTotpCell.OnChanged += CopyTotpCell_OnChanged; StackLayout.LayoutChanged += Layout_LayoutChanged; if(Device.RuntimePlatform == Device.Android) @@ -184,6 +203,7 @@ namespace Bit.App.Pages base.OnDisappearing(); AnalyticsCell.OnChanged -= AnalyticsCell_Changed; + CopyTotpCell.OnChanged -= CopyTotpCell_OnChanged; StackLayout.LayoutChanged -= Layout_LayoutChanged; if(Device.RuntimePlatform == Device.Android) @@ -211,6 +231,17 @@ namespace Bit.App.Pages _googleAnalyticsService.SetAppOptOut(cell.On); } + private void CopyTotpCell_OnChanged(object sender, ToggledEventArgs e) + { + var cell = sender as ExtendedSwitchCell; + if(cell == null) + { + return; + } + + _settings.AddOrUpdateValue(Constants.SettingDisableTotpCopy, cell.On); + } + private void AutofillAlwaysCell_OnChanged(object sender, ToggledEventArgs e) { var cell = sender as ExtendedSwitchCell; @@ -262,7 +293,7 @@ namespace Bit.App.Pages private class FormTableView : ExtendedTableView { - public FormTableView() + public FormTableView(bool header = false) { Intent = TableIntent.Settings; EnableScrolling = false; @@ -270,6 +301,7 @@ namespace Bit.App.Pages EnableSelection = true; VerticalOptions = LayoutOptions.Start; NoFooter = true; + NoHeader = !header; } } diff --git a/src/App/Resources/AppResources.Designer.cs b/src/App/Resources/AppResources.Designer.cs index 0c98919a7..4002a697f 100644 --- a/src/App/Resources/AppResources.Designer.cs +++ b/src/App/Resources/AppResources.Designer.cs @@ -574,6 +574,15 @@ namespace Bit.App.Resources { } } + /// + /// Looks up a localized string similar to Copied TOTP!. + /// + public static string CopiedTotp { + get { + return ResourceManager.GetString("CopiedTotp", resourceCulture); + } + } + /// /// Looks up a localized string similar to Copied username!. /// @@ -601,6 +610,15 @@ namespace Bit.App.Resources { } } + /// + /// Looks up a localized string similar to Copy TOTP. + /// + public static string CopyTotp { + get { + return ResourceManager.GetString("CopyTotp", resourceCulture); + } + } + /// /// Looks up a localized string similar to Copy Username. /// @@ -664,6 +682,24 @@ namespace Bit.App.Resources { } } + /// + /// Looks up a localized string similar to Disable Automatic TOTP Copy. + /// + public static string DisableAutoTotpCopy { + get { + return ResourceManager.GetString("DisableAutoTotpCopy", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to If your login has an authenticator key attached to it, the TOTP verification code is automatically copied to your clipboard whenever you auto-fill the login.. + /// + public static string DisableAutoTotpCopyDescription { + get { + return ResourceManager.GetString("DisableAutoTotpCopyDescription", resourceCulture); + } + } + /// /// Looks up a localized string similar to Disabled. /// @@ -685,9 +721,9 @@ namespace Bit.App.Resources { /// /// Looks up a localized string similar to We use analytics to better learn how the app is being used so that we can make it better. All data collection is completely anonymous.. /// - public static string DisbaleGADescription { + public static string DisableGADescription { get { - return ResourceManager.GetString("DisbaleGADescription", resourceCulture); + return ResourceManager.GetString("DisableGADescription", resourceCulture); } } diff --git a/src/App/Resources/AppResources.resx b/src/App/Resources/AppResources.resx index cc40859a0..910bd7c2f 100644 --- a/src/App/Resources/AppResources.resx +++ b/src/App/Resources/AppResources.resx @@ -822,7 +822,7 @@ Create an organization to securely share your logins with other users. - + We use analytics to better learn how the app is being used so that we can make it better. All data collection is completely anonymous. @@ -950,4 +950,16 @@ Photos + + Copied TOTP! + + + Copy TOTP + + + If your login has an authenticator key attached to it, the TOTP verification code is automatically copied to your clipboard whenever you auto-fill the login. + + + Disable Automatic TOTP Copy + \ No newline at end of file diff --git a/src/iOS.Extension/LoadingViewController.cs b/src/iOS.Extension/LoadingViewController.cs index 333eee3e3..2d40038d8 100644 --- a/src/iOS.Extension/LoadingViewController.cs +++ b/src/iOS.Extension/LoadingViewController.cs @@ -191,7 +191,7 @@ namespace Bit.iOS.Extension } } - public void CompleteUsernamePasswordRequest(string username, string password) + public void CompleteUsernamePasswordRequest(string username, string password, string totp) { NSDictionary itemData = null; if(_context.ProviderType == UTType.PropertyList) @@ -227,6 +227,11 @@ namespace Bit.iOS.Extension Constants.AppExtensionOldPasswordKey, password); } + if(!string.IsNullOrWhiteSpace(totp)) + { + UIPasteboard.General.String = totp; + } + CompleteRequest(itemData); } diff --git a/src/iOS.Extension/LoginAddViewController.cs b/src/iOS.Extension/LoginAddViewController.cs index c459ee641..402d58eee 100644 --- a/src/iOS.Extension/LoginAddViewController.cs +++ b/src/iOS.Extension/LoginAddViewController.cs @@ -176,7 +176,8 @@ namespace Bit.iOS.Extension } else if(LoadingController != null) { - LoadingController.CompleteUsernamePasswordRequest(UsernameCell.TextField.Text, PasswordCell.TextField.Text); + LoadingController.CompleteUsernamePasswordRequest(UsernameCell.TextField.Text, PasswordCell.TextField.Text, + null); } } else if(saveTask.Result.Errors.Count() > 0) diff --git a/src/iOS.Extension/LoginListViewController.cs b/src/iOS.Extension/LoginListViewController.cs index 1993bfb60..45f6091e3 100644 --- a/src/iOS.Extension/LoginListViewController.cs +++ b/src/iOS.Extension/LoginListViewController.cs @@ -104,17 +104,22 @@ namespace Bit.iOS.Extension private IEnumerable _tableItems = new List(); private Context _context; private LoginListViewController _controller; + private ILoginService _loginService; + private ISettings _settings; + private bool _isPremium; public TableSource(LoginListViewController controller) { _context = controller.Context; _controller = controller; + _isPremium = Resolver.Resolve()?.TokenPremium ?? false; + _loginService = Resolver.Resolve(); + _settings = Resolver.Resolve(); } public async Task LoadItemsAsync() { - var loginService = Resolver.Resolve(); - var logins = await loginService.GetAllAsync(_context.UrlString); + var logins = await _loginService.GetAllAsync(_context.UrlString); _tableItems = logins?.Item1?.Select(s => new LoginViewModel(s)) .OrderBy(s => s.Name) .ThenBy(s => s.Username) @@ -184,9 +189,16 @@ namespace Bit.iOS.Extension if(_controller.CanAutoFill() && !string.IsNullOrWhiteSpace(item.Password)) { - _controller.LoadingController.CompleteUsernamePasswordRequest(item.Username, item.Password); + string totp = null; + if(!_settings.GetValueOrDefault(App.Constants.SettingDisableTotpCopy, false)) + { + totp = GetTotp(item); + } + + _controller.LoadingController.CompleteUsernamePasswordRequest(item.Username, item.Password, totp); } - else if(!string.IsNullOrWhiteSpace(item.Username) || !string.IsNullOrWhiteSpace(item.Password)) + else if(!string.IsNullOrWhiteSpace(item.Username) || !string.IsNullOrWhiteSpace(item.Password) || + !string.IsNullOrWhiteSpace(item.Totp.Value)) { var sheet = Dialogs.CreateActionSheet(item.Name, _controller); if(!string.IsNullOrWhiteSpace(item.Username)) @@ -217,6 +229,26 @@ namespace Bit.iOS.Extension })); } + if(!string.IsNullOrWhiteSpace(item.Totp.Value)) + { + sheet.AddAction(UIAlertAction.Create(AppResources.CopyTotp, UIAlertActionStyle.Default, a => + { + var totp = GetTotp(item); + if(string.IsNullOrWhiteSpace(totp)) + { + return; + } + + UIPasteboard clipboard = UIPasteboard.General; + clipboard.String = totp; + var alert = Dialogs.CreateMessageAlert(AppResources.CopiedTotp); + _controller.PresentViewController(alert, true, () => + { + _controller.DismissViewController(true, null); + }); + })); + } + sheet.AddAction(UIAlertAction.Create(AppResources.Cancel, UIAlertActionStyle.Cancel, null)); _controller.PresentViewController(sheet, true, null); } @@ -226,6 +258,20 @@ namespace Bit.iOS.Extension _controller.PresentViewController(alert, true, null); } } + + private string GetTotp(LoginViewModel item) + { + string totp = null; + if(_isPremium) + { + if(item != null && !string.IsNullOrWhiteSpace(item.Totp.Value)) + { + totp = App.Utilities.Crypto.Totp(item.Totp.Value); + } + } + + return totp; + } } } } diff --git a/src/iOS.Extension/Models/LoginViewModel.cs b/src/iOS.Extension/Models/LoginViewModel.cs index 6adca6e55..a5e4bd5b8 100644 --- a/src/iOS.Extension/Models/LoginViewModel.cs +++ b/src/iOS.Extension/Models/LoginViewModel.cs @@ -1,4 +1,5 @@ using Bit.App.Models; +using System; namespace Bit.iOS.Extension.Models { @@ -11,6 +12,7 @@ namespace Bit.iOS.Extension.Models Username = login.Username?.Decrypt(login.OrganizationId); Password = login.Password?.Decrypt(login.OrganizationId); Uri = login.Uri?.Decrypt(login.OrganizationId); + Totp = new Lazy(() => login.Totp?.Decrypt(login.OrganizationId)); } public string Id { get; set; } @@ -18,5 +20,6 @@ namespace Bit.iOS.Extension.Models public string Username { get; set; } public string Password { get; set; } public string Uri { get; set; } + public Lazy Totp { get; set; } } }