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