diff --git a/src/Android/Android.csproj b/src/Android/Android.csproj index 692618109..740a83ff9 100644 --- a/src/Android/Android.csproj +++ b/src/Android/Android.csproj @@ -860,6 +860,18 @@ + + + + + + + + + + + + diff --git a/src/Android/MainActivity.cs b/src/Android/MainActivity.cs index 8acccc73a..8e4d2432a 100644 --- a/src/Android/MainActivity.cs +++ b/src/Android/MainActivity.cs @@ -51,7 +51,7 @@ namespace Bit.Android Console.WriteLine("A OnCreate"); Window.SetSoftInputMode(SoftInput.StateHidden); - Window.AddFlags(WindowManagerFlags.Secure); + //Window.AddFlags(WindowManagerFlags.Secure); var appIdService = Resolver.Resolve(); var authService = Resolver.Resolve(); @@ -105,10 +105,10 @@ namespace Bit.Android LaunchApp(args); }); - MessagingCenter.Subscribe( - Xamarin.Forms.Application.Current, "ListenYubiKeyOTP", (sender) => + MessagingCenter.Subscribe( + Xamarin.Forms.Application.Current, "ListenYubiKeyOTP", (sender, listen) => { - ListenYubiKey(); + ListenYubiKey(listen); }); } @@ -142,6 +142,7 @@ namespace Bit.Android { Console.WriteLine("A OnPause"); base.OnPause(); + ListenYubiKey(false); } protected override void OnDestroy() @@ -176,6 +177,18 @@ namespace Bit.Android // workaround for app compat bug // ref https://bugzilla.xamarin.com/show_bug.cgi?id=36907 Task.Delay(10).Wait(); + + if(Utilities.NfcEnabled()) + { + MessagingCenter.Send(Xamarin.Forms.Application.Current, "ResumeYubiKey"); + } + } + + protected override void OnNewIntent(Intent intent) + { + base.OnNewIntent(intent); + Console.WriteLine("A OnNewIntent"); + ParseYubiKey(intent.DataString); } public void RateApp() @@ -237,35 +250,47 @@ namespace Bit.Android } } - private void ListenYubiKey() + private void ListenYubiKey(bool listen) { - var intent = new Intent(this, Class); - intent.AddFlags(ActivityFlags.SingleTop); - var pendingIntent = PendingIntent.GetActivity(this, 0, intent, 0); - - // register for all NDEF tags starting with http och https - var ndef = new IntentFilter(NfcAdapter.ActionNdefDiscovered); - ndef.AddDataScheme("http"); - ndef.AddDataScheme("https"); - - // register for foreground dispatch so we'll receive tags according to our intent filters - var adapter = NfcAdapter.GetDefaultAdapter(this); - adapter.EnableForegroundDispatch(this, pendingIntent, new IntentFilter[] { ndef }, null); - - var data = Intent.DataString; - if(data != null) + if(!Utilities.NfcEnabled()) { - var otpMatch = _otpPattern.Matcher(data); - if(otpMatch.Matches()) - { - var otp = otpMatch.Group(1); - Console.WriteLine("Got OTP: " + otp); - MessagingCenter.Send(Xamarin.Forms.Application.Current, "GotYubiKeyOTP", otp); - } - else - { - Console.WriteLine("Data from ndef didn't match, it was: " + data); - } + return; + } + + var adapter = NfcAdapter.GetDefaultAdapter(this); + if(listen) + { + var intent = new Intent(this, Class); + intent.AddFlags(ActivityFlags.SingleTop); + var pendingIntent = PendingIntent.GetActivity(this, 0, intent, 0); + + // register for all NDEF tags starting with http och https + var ndef = new IntentFilter(NfcAdapter.ActionNdefDiscovered); + ndef.AddDataScheme("http"); + ndef.AddDataScheme("https"); + var filters = new IntentFilter[] { ndef }; + + // register for foreground dispatch so we'll receive tags according to our intent filters + adapter.EnableForegroundDispatch(this, pendingIntent, filters, null); + } + else + { + adapter.DisableForegroundDispatch(this); + } + } + + private void ParseYubiKey(string data) + { + if(data == null) + { + return; + } + + var otpMatch = _otpPattern.Matcher(data); + if(otpMatch.Matches()) + { + var otp = otpMatch.Group(1); + MessagingCenter.Send(Xamarin.Forms.Application.Current, "GotYubiKeyOTP", otp); } } } diff --git a/src/Android/Resources/Resource.Designer.cs b/src/Android/Resources/Resource.Designer.cs index 041a15faf..519598826 100644 --- a/src/Android/Resources/Resource.Designer.cs +++ b/src/Android/Resources/Resource.Designer.cs @@ -2744,8 +2744,8 @@ namespace Bit.Android // aapt resource value: 0x7f0200e4 public const int notification_sm = 2130837732; - // aapt resource value: 0x7f0200f3 - public const int notification_template_icon_bg = 2130837747; + // aapt resource value: 0x7f0200f4 + public const int notification_template_icon_bg = 2130837748; // aapt resource value: 0x7f0200e5 public const int plus = 2130837733; @@ -2789,6 +2789,9 @@ namespace Bit.Android // aapt resource value: 0x7f0200f2 public const int user = 2130837746; + // aapt resource value: 0x7f0200f3 + public const int yubikey = 2130837747; + static Drawable() { global::Android.Runtime.ResourceIdManager.UpdateIdValues(); diff --git a/src/Android/Resources/drawable-hdpi/yubikey.png b/src/Android/Resources/drawable-hdpi/yubikey.png new file mode 100644 index 000000000..ef4d0a992 Binary files /dev/null and b/src/Android/Resources/drawable-hdpi/yubikey.png differ diff --git a/src/Android/Resources/drawable-xhdpi/yubikey.png b/src/Android/Resources/drawable-xhdpi/yubikey.png new file mode 100644 index 000000000..d78442c92 Binary files /dev/null and b/src/Android/Resources/drawable-xhdpi/yubikey.png differ diff --git a/src/Android/Resources/drawable-xxhdpi/yubikey.png b/src/Android/Resources/drawable-xxhdpi/yubikey.png new file mode 100644 index 000000000..364c2d06e Binary files /dev/null and b/src/Android/Resources/drawable-xxhdpi/yubikey.png differ diff --git a/src/Android/Resources/drawable/yubikey.png b/src/Android/Resources/drawable/yubikey.png new file mode 100644 index 000000000..2c1d91cb6 Binary files /dev/null and b/src/Android/Resources/drawable/yubikey.png differ diff --git a/src/Android/Services/DeviceInfoService.cs b/src/Android/Services/DeviceInfoService.cs index 5ac4d09b2..e6514a8b6 100644 --- a/src/Android/Services/DeviceInfoService.cs +++ b/src/Android/Services/DeviceInfoService.cs @@ -1,5 +1,4 @@ using Android.App; -using Android.Nfc; using Android.OS; using Bit.App.Abstractions; @@ -42,14 +41,6 @@ namespace Bit.Android.Services return 1f; } } - public bool NfcEnabled - { - get - { - var manager = (NfcManager)Application.Context.GetSystemService("nfc"); - var adapter = manager.DefaultAdapter; - return adapter != null && adapter.IsEnabled; - } - } + public bool NfcEnabled => Utilities.NfcEnabled(); } } diff --git a/src/Android/Utilities.cs b/src/Android/Utilities.cs index bc1714c18..f7fecc059 100644 --- a/src/Android/Utilities.cs +++ b/src/Android/Utilities.cs @@ -3,11 +3,19 @@ using Android.App; using Android.Content; using Java.Security; using System.IO; +using Android.Nfc; namespace Bit.Android { public static class Utilities { + public static bool NfcEnabled() + { + var manager = (NfcManager)Application.Context.GetSystemService("nfc"); + var adapter = manager.DefaultAdapter; + return adapter != null && adapter.IsEnabled; + } + public static void SendCrashEmail(Exception e, bool includeSecurityProviders = true) { SendCrashEmail(e.Message + "\n\n" + e.StackTrace, includeSecurityProviders); diff --git a/src/App/Pages/LoginTwoFactorPage.cs b/src/App/Pages/LoginTwoFactorPage.cs index 74ff6dae3..6e9439dfc 100644 --- a/src/App/Pages/LoginTwoFactorPage.cs +++ b/src/App/Pages/LoginTwoFactorPage.cs @@ -13,14 +13,17 @@ using Bit.App.Enums; using System.Collections.Generic; using System.Linq; using System.Net; +using FFImageLoading.Forms; namespace Bit.App.Pages { public class LoginTwoFactorPage : ExtendedContentPage { + private DateTime? _lastAction; private IAuthService _authService; private IUserDialogs _userDialogs; private ISyncService _syncService; + private IDeviceInfoService _deviceInfoService; private IGoogleAnalyticsService _googleAnalyticsService; private IPushNotification _pushNotification; private readonly string _email; @@ -33,6 +36,8 @@ namespace Bit.App.Pages public LoginTwoFactorPage(string email, FullLoginResult result, TwoFactorProviderType? type = null) : base(updateActivity: false) { + _deviceInfoService = Resolver.Resolve(); + _email = email; _result = result; _masterPasswordHash = result.MasterPasswordHash; @@ -47,26 +52,54 @@ namespace Bit.App.Pages _pushNotification = Resolver.Resolve(); Init(); + + SubscribeYubiKey(true); } public FormEntryCell TokenCell { get; set; } public ExtendedSwitchCell RememberCell { get; set; } - public HybridWebView WebView { get; set; } private void Init() { var scrollView = new ScrollView(); + var anotherMethodButton = new ExtendedButton + { + Text = "Use another two-step login method", + Style = (Style)Application.Current.Resources["btn-primaryAccent"], + Margin = new Thickness(15, 0, 15, 25), + Command = new Command(() => AnotherMethodAsync()), + Uppercase = false, + BackgroundColor = Color.Transparent + }; + + var instruction = new Label + { + LineBreakMode = LineBreakMode.WordWrap, + Margin = new Thickness(15), + HorizontalTextAlignment = TextAlignment.Center + }; + + RememberCell = new ExtendedSwitchCell + { + Text = "Remember me", + On = false + }; + if(!_providerType.HasValue) { - var noProviderLabel = new Label + instruction.Text = "No providers available."; + + var layout = new StackLayout { - Text = "No provider.", - LineBreakMode = LineBreakMode.WordWrap, - Margin = new Thickness(15), - HorizontalTextAlignment = TextAlignment.Center + Children = { instruction, anotherMethodButton }, + Spacing = 0 }; - scrollView.Content = noProviderLabel; + + scrollView.Content = layout; + + Title = "Login Unavailable"; + Content = scrollView; } else if(_providerType.Value == TwoFactorProviderType.Authenticator || _providerType.Value == TwoFactorProviderType.Email) @@ -88,54 +121,12 @@ namespace Bit.App.Pages TokenCell.Entry.Keyboard = Keyboard.Numeric; TokenCell.Entry.ReturnType = ReturnType.Go; - RememberCell = new ExtendedSwitchCell - { - Text = "Remember me", - On = false - }; - - var table = new ExtendedTableView - { - Intent = TableIntent.Settings, - EnableScrolling = false, - HasUnevenRows = true, - EnableSelection = true, - NoFooter = true, - NoHeader = true, - VerticalOptions = LayoutOptions.Start, - Root = new TableRoot + var table = new TwoFactorTable( + new TableSection(" ") { - new TableSection(" ") - { - TokenCell, - RememberCell - } - } - }; - - if(Device.RuntimePlatform == Device.iOS) - { - table.RowHeight = -1; - table.EstimatedRowHeight = 70; - } - - var instruction = new Label - { - Text = AppResources.EnterVerificationCode, - LineBreakMode = LineBreakMode.WordWrap, - Margin = new Thickness(15), - HorizontalTextAlignment = TextAlignment.Center - }; - - var anotherMethodButton = new ExtendedButton - { - Text = "Use another two-step login method", - Style = (Style)Application.Current.Resources["btn-primaryAccent"], - Margin = new Thickness(15, 0, 15, 25), - Command = new Command(() => AnotherMethodAsync()), - Uppercase = false, - BackgroundColor = Color.Transparent - }; + TokenCell, + RememberCell + }); var layout = new StackLayout { @@ -189,25 +180,57 @@ namespace Bit.App.Pages var host = WebUtility.UrlEncode(duoParams["Host"].ToString()); var req = WebUtility.UrlEncode(duoParams["Signature"].ToString()); - WebView = new HybridWebView + var webView = new HybridWebView { Uri = $"http://192.168.1.6:4001/duo-mobile.html?host={host}&request={req}", HorizontalOptions = LayoutOptions.FillAndExpand, VerticalOptions = LayoutOptions.FillAndExpand }; - WebView.RegisterAction(async (sig) => + webView.RegisterAction(async (sig) => { await LogInAsync(sig, false); }); Title = "Duo"; - Content = WebView; + Content = webView; + } + else if(_providerType == TwoFactorProviderType.YubiKey) + { + instruction.Text = "Hold your YubiKey NEO against the back of the device to continue."; + + var image = new CachedImage + { + Source = "yubikey", + VerticalOptions = LayoutOptions.Start, + HorizontalOptions = LayoutOptions.Center, + WidthRequest = 266, + HeightRequest = 160, + Margin = new Thickness(0, 0, 0, 25) + }; + + var table = new TwoFactorTable( + new TableSection(" ") + { + RememberCell + }); + + var layout = new StackLayout + { + Children = { instruction, image, table, anotherMethodButton }, + Spacing = 0 + }; + + scrollView.Content = layout; + + Title = "YubiKey"; + Content = scrollView; } } protected override void OnAppearing() { base.OnAppearing(); + ListenYubiKey(true); if(TokenCell != null) { @@ -220,6 +243,7 @@ namespace Bit.App.Pages protected override void OnDisappearing() { base.OnDisappearing(); + ListenYubiKey(false); if(TokenCell != null) { @@ -251,6 +275,12 @@ namespace Bit.App.Pages private async Task LogInAsync(string token, bool remember) { + if(_lastAction.LastActionWasRecent()) + { + return; + } + _lastAction = DateTime.UtcNow; + if(string.IsNullOrWhiteSpace(token)) { await DisplayAlert(AppResources.AnErrorHasOccurred, string.Format(AppResources.ValidationFieldRequired, @@ -264,6 +294,7 @@ namespace Bit.App.Pages _userDialogs.HideLoading(); if(!response.Success) { + ListenYubiKey(true); await DisplayAlert(AppResources.AnErrorHasOccurred, response.ErrorMessage, AppResources.Ok); return; } @@ -299,7 +330,7 @@ namespace Bit.App.Pages switch(p.Key) { case TwoFactorProviderType.Authenticator: - if(provider == TwoFactorProviderType.Duo) + if(provider == TwoFactorProviderType.Duo || provider == TwoFactorProviderType.YubiKey) { continue; } @@ -311,6 +342,16 @@ namespace Bit.App.Pages } break; case TwoFactorProviderType.Duo: + if(provider == TwoFactorProviderType.YubiKey) + { + continue; + } + break; + case TwoFactorProviderType.YubiKey: + if(!_deviceInfoService.NfcEnabled) + { + continue; + } break; default: continue; @@ -322,5 +363,76 @@ namespace Bit.App.Pages return provider; } + + private void ListenYubiKey(bool listen) + { + if(_providerType == TwoFactorProviderType.YubiKey) + { + MessagingCenter.Send(Application.Current, "ListenYubiKeyOTP", listen); + } + } + + private void SubscribeYubiKey(bool subscribe) + { + if(_providerType != TwoFactorProviderType.YubiKey) + { + return; + } + + MessagingCenter.Unsubscribe(Application.Current, "GotYubiKeyOTP"); + MessagingCenter.Unsubscribe(Application.Current, "ResumeYubiKey"); + if(!subscribe) + { + return; + } + + MessagingCenter.Subscribe(Application.Current, "GotYubiKeyOTP", async (sender, otp) => + { + MessagingCenter.Unsubscribe(Application.Current, "GotYubiKeyOTP"); + if(_providerType == TwoFactorProviderType.YubiKey) + { + await LogInAsync(otp, RememberCell.On); + } + }); + + SubscribeYubiKeyResume(); + } + + private void SubscribeYubiKeyResume() + { + MessagingCenter.Subscribe(Application.Current, "ResumeYubiKey", (sender) => + { + MessagingCenter.Unsubscribe(Application.Current, "ResumeYubiKey"); + if(_providerType == TwoFactorProviderType.YubiKey) + { + MessagingCenter.Send(Application.Current, "ListenYubiKeyOTP", true); + SubscribeYubiKeyResume(); + } + }); + } + + public class TwoFactorTable : ExtendedTableView + { + public TwoFactorTable(TableSection section) + { + Intent = TableIntent.Settings; + EnableScrolling = false; + HasUnevenRows = true; + EnableSelection = true; + NoFooter = true; + NoHeader = true; + VerticalOptions = LayoutOptions.Start; + Root = Root = new TableRoot + { + section + }; + + if(Device.RuntimePlatform == Device.iOS) + { + RowHeight = -1; + EstimatedRowHeight = 70; + } + } + } } } diff --git a/src/iOS/Resources/yubikey.png b/src/iOS/Resources/yubikey.png new file mode 100644 index 000000000..2c1d91cb6 Binary files /dev/null and b/src/iOS/Resources/yubikey.png differ diff --git a/src/iOS/Resources/yubikey@2x.png b/src/iOS/Resources/yubikey@2x.png new file mode 100644 index 000000000..d78442c92 Binary files /dev/null and b/src/iOS/Resources/yubikey@2x.png differ diff --git a/src/iOS/Resources/yubikey@3x.png b/src/iOS/Resources/yubikey@3x.png new file mode 100644 index 000000000..364c2d06e Binary files /dev/null and b/src/iOS/Resources/yubikey@3x.png differ diff --git a/src/iOS/iOS.csproj b/src/iOS/iOS.csproj index 9f0650c53..fbf076127 100644 --- a/src/iOS/iOS.csproj +++ b/src/iOS/iOS.csproj @@ -716,6 +716,15 @@ + + + + + + + + + diff --git a/test/Android.Test/Android.Test.csproj b/test/Android.Test/Android.Test.csproj index 2d11180f9..4b329b93c 100644 --- a/test/Android.Test/Android.Test.csproj +++ b/test/Android.Test/Android.Test.csproj @@ -57,6 +57,7 @@ + ..\..\packages\PCLCrypto.2.0.147\lib\MonoAndroid23\PCLCrypto.dll