read yubikey and log in

This commit is contained in:
Kyle Spearrin 2017-06-28 22:24:04 -04:00
parent d71bc775d5
commit 56075cb7d9
15 changed files with 262 additions and 101 deletions

View file

@ -860,6 +860,18 @@
<ItemGroup> <ItemGroup>
<AndroidResource Include="Resources\drawable-xxxhdpi\share_tools.png" /> <AndroidResource Include="Resources\drawable-xxxhdpi\share_tools.png" />
</ItemGroup> </ItemGroup>
<ItemGroup>
<AndroidResource Include="Resources\drawable\yubikey.png" />
</ItemGroup>
<ItemGroup>
<AndroidResource Include="Resources\drawable-hdpi\yubikey.png" />
</ItemGroup>
<ItemGroup>
<AndroidResource Include="Resources\drawable-xhdpi\yubikey.png" />
</ItemGroup>
<ItemGroup>
<AndroidResource Include="Resources\drawable-xxhdpi\yubikey.png" />
</ItemGroup>
<Import Project="$(MSBuildExtensionsPath)\Xamarin\Android\Xamarin.Android.CSharp.targets" /> <Import Project="$(MSBuildExtensionsPath)\Xamarin\Android\Xamarin.Android.CSharp.targets" />
<Import Project="..\..\packages\Xamarin.Android.Support.Vector.Drawable.23.3.0\build\Xamarin.Android.Support.Vector.Drawable.targets" Condition="Exists('..\..\packages\Xamarin.Android.Support.Vector.Drawable.23.3.0\build\Xamarin.Android.Support.Vector.Drawable.targets')" /> <Import Project="..\..\packages\Xamarin.Android.Support.Vector.Drawable.23.3.0\build\Xamarin.Android.Support.Vector.Drawable.targets" Condition="Exists('..\..\packages\Xamarin.Android.Support.Vector.Drawable.23.3.0\build\Xamarin.Android.Support.Vector.Drawable.targets')" />
<Target Name="EnsureNuGetPackageBuildImports" BeforeTargets="PrepareForBuild"> <Target Name="EnsureNuGetPackageBuildImports" BeforeTargets="PrepareForBuild">

View file

@ -51,7 +51,7 @@ namespace Bit.Android
Console.WriteLine("A OnCreate"); Console.WriteLine("A OnCreate");
Window.SetSoftInputMode(SoftInput.StateHidden); Window.SetSoftInputMode(SoftInput.StateHidden);
Window.AddFlags(WindowManagerFlags.Secure); //Window.AddFlags(WindowManagerFlags.Secure);
var appIdService = Resolver.Resolve<IAppIdService>(); var appIdService = Resolver.Resolve<IAppIdService>();
var authService = Resolver.Resolve<IAuthService>(); var authService = Resolver.Resolve<IAuthService>();
@ -105,10 +105,10 @@ namespace Bit.Android
LaunchApp(args); LaunchApp(args);
}); });
MessagingCenter.Subscribe<Xamarin.Forms.Application>( MessagingCenter.Subscribe<Xamarin.Forms.Application, bool>(
Xamarin.Forms.Application.Current, "ListenYubiKeyOTP", (sender) => Xamarin.Forms.Application.Current, "ListenYubiKeyOTP", (sender, listen) =>
{ {
ListenYubiKey(); ListenYubiKey(listen);
}); });
} }
@ -142,6 +142,7 @@ namespace Bit.Android
{ {
Console.WriteLine("A OnPause"); Console.WriteLine("A OnPause");
base.OnPause(); base.OnPause();
ListenYubiKey(false);
} }
protected override void OnDestroy() protected override void OnDestroy()
@ -176,6 +177,18 @@ namespace Bit.Android
// workaround for app compat bug // workaround for app compat bug
// ref https://bugzilla.xamarin.com/show_bug.cgi?id=36907 // ref https://bugzilla.xamarin.com/show_bug.cgi?id=36907
Task.Delay(10).Wait(); 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() public void RateApp()
@ -237,7 +250,15 @@ namespace Bit.Android
} }
} }
private void ListenYubiKey() private void ListenYubiKey(bool listen)
{
if(!Utilities.NfcEnabled())
{
return;
}
var adapter = NfcAdapter.GetDefaultAdapter(this);
if(listen)
{ {
var intent = new Intent(this, Class); var intent = new Intent(this, Class);
intent.AddFlags(ActivityFlags.SingleTop); intent.AddFlags(ActivityFlags.SingleTop);
@ -247,26 +268,30 @@ namespace Bit.Android
var ndef = new IntentFilter(NfcAdapter.ActionNdefDiscovered); var ndef = new IntentFilter(NfcAdapter.ActionNdefDiscovered);
ndef.AddDataScheme("http"); ndef.AddDataScheme("http");
ndef.AddDataScheme("https"); ndef.AddDataScheme("https");
var filters = new IntentFilter[] { ndef };
// register for foreground dispatch so we'll receive tags according to our intent filters // register for foreground dispatch so we'll receive tags according to our intent filters
var adapter = NfcAdapter.GetDefaultAdapter(this); adapter.EnableForegroundDispatch(this, pendingIntent, filters, null);
adapter.EnableForegroundDispatch(this, pendingIntent, new IntentFilter[] { ndef }, null); }
else
var data = Intent.DataString;
if(data != null)
{ {
adapter.DisableForegroundDispatch(this);
}
}
private void ParseYubiKey(string data)
{
if(data == null)
{
return;
}
var otpMatch = _otpPattern.Matcher(data); var otpMatch = _otpPattern.Matcher(data);
if(otpMatch.Matches()) if(otpMatch.Matches())
{ {
var otp = otpMatch.Group(1); var otp = otpMatch.Group(1);
Console.WriteLine("Got OTP: " + otp);
MessagingCenter.Send(Xamarin.Forms.Application.Current, "GotYubiKeyOTP", otp); MessagingCenter.Send(Xamarin.Forms.Application.Current, "GotYubiKeyOTP", otp);
} }
else
{
Console.WriteLine("Data from ndef didn't match, it was: " + data);
}
}
} }
} }
} }

View file

@ -2744,8 +2744,8 @@ namespace Bit.Android
// aapt resource value: 0x7f0200e4 // aapt resource value: 0x7f0200e4
public const int notification_sm = 2130837732; public const int notification_sm = 2130837732;
// aapt resource value: 0x7f0200f3 // aapt resource value: 0x7f0200f4
public const int notification_template_icon_bg = 2130837747; public const int notification_template_icon_bg = 2130837748;
// aapt resource value: 0x7f0200e5 // aapt resource value: 0x7f0200e5
public const int plus = 2130837733; public const int plus = 2130837733;
@ -2789,6 +2789,9 @@ namespace Bit.Android
// aapt resource value: 0x7f0200f2 // aapt resource value: 0x7f0200f2
public const int user = 2130837746; public const int user = 2130837746;
// aapt resource value: 0x7f0200f3
public const int yubikey = 2130837747;
static Drawable() static Drawable()
{ {
global::Android.Runtime.ResourceIdManager.UpdateIdValues(); global::Android.Runtime.ResourceIdManager.UpdateIdValues();

Binary file not shown.

After

Width:  |  Height:  |  Size: 108 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 187 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 381 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

View file

@ -1,5 +1,4 @@
using Android.App; using Android.App;
using Android.Nfc;
using Android.OS; using Android.OS;
using Bit.App.Abstractions; using Bit.App.Abstractions;
@ -42,14 +41,6 @@ namespace Bit.Android.Services
return 1f; return 1f;
} }
} }
public bool NfcEnabled public bool NfcEnabled => Utilities.NfcEnabled();
{
get
{
var manager = (NfcManager)Application.Context.GetSystemService("nfc");
var adapter = manager.DefaultAdapter;
return adapter != null && adapter.IsEnabled;
}
}
} }
} }

View file

@ -3,11 +3,19 @@ using Android.App;
using Android.Content; using Android.Content;
using Java.Security; using Java.Security;
using System.IO; using System.IO;
using Android.Nfc;
namespace Bit.Android namespace Bit.Android
{ {
public static class Utilities 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) public static void SendCrashEmail(Exception e, bool includeSecurityProviders = true)
{ {
SendCrashEmail(e.Message + "\n\n" + e.StackTrace, includeSecurityProviders); SendCrashEmail(e.Message + "\n\n" + e.StackTrace, includeSecurityProviders);

View file

@ -13,14 +13,17 @@ using Bit.App.Enums;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Net; using System.Net;
using FFImageLoading.Forms;
namespace Bit.App.Pages namespace Bit.App.Pages
{ {
public class LoginTwoFactorPage : ExtendedContentPage public class LoginTwoFactorPage : ExtendedContentPage
{ {
private DateTime? _lastAction;
private IAuthService _authService; private IAuthService _authService;
private IUserDialogs _userDialogs; private IUserDialogs _userDialogs;
private ISyncService _syncService; private ISyncService _syncService;
private IDeviceInfoService _deviceInfoService;
private IGoogleAnalyticsService _googleAnalyticsService; private IGoogleAnalyticsService _googleAnalyticsService;
private IPushNotification _pushNotification; private IPushNotification _pushNotification;
private readonly string _email; private readonly string _email;
@ -33,6 +36,8 @@ namespace Bit.App.Pages
public LoginTwoFactorPage(string email, FullLoginResult result, TwoFactorProviderType? type = null) public LoginTwoFactorPage(string email, FullLoginResult result, TwoFactorProviderType? type = null)
: base(updateActivity: false) : base(updateActivity: false)
{ {
_deviceInfoService = Resolver.Resolve<IDeviceInfoService>();
_email = email; _email = email;
_result = result; _result = result;
_masterPasswordHash = result.MasterPasswordHash; _masterPasswordHash = result.MasterPasswordHash;
@ -47,26 +52,54 @@ namespace Bit.App.Pages
_pushNotification = Resolver.Resolve<IPushNotification>(); _pushNotification = Resolver.Resolve<IPushNotification>();
Init(); Init();
SubscribeYubiKey(true);
} }
public FormEntryCell TokenCell { get; set; } public FormEntryCell TokenCell { get; set; }
public ExtendedSwitchCell RememberCell { get; set; } public ExtendedSwitchCell RememberCell { get; set; }
public HybridWebView WebView { get; set; }
private void Init() private void Init()
{ {
var scrollView = new ScrollView(); var scrollView = new ScrollView();
if(!_providerType.HasValue) var anotherMethodButton = new ExtendedButton
{ {
var noProviderLabel = new Label 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
{ {
Text = "No provider.",
LineBreakMode = LineBreakMode.WordWrap, LineBreakMode = LineBreakMode.WordWrap,
Margin = new Thickness(15), Margin = new Thickness(15),
HorizontalTextAlignment = TextAlignment.Center HorizontalTextAlignment = TextAlignment.Center
}; };
scrollView.Content = noProviderLabel;
RememberCell = new ExtendedSwitchCell
{
Text = "Remember me",
On = false
};
if(!_providerType.HasValue)
{
instruction.Text = "No providers available.";
var layout = new StackLayout
{
Children = { instruction, anotherMethodButton },
Spacing = 0
};
scrollView.Content = layout;
Title = "Login Unavailable";
Content = scrollView;
} }
else if(_providerType.Value == TwoFactorProviderType.Authenticator || else if(_providerType.Value == TwoFactorProviderType.Authenticator ||
_providerType.Value == TwoFactorProviderType.Email) _providerType.Value == TwoFactorProviderType.Email)
@ -88,54 +121,12 @@ namespace Bit.App.Pages
TokenCell.Entry.Keyboard = Keyboard.Numeric; TokenCell.Entry.Keyboard = Keyboard.Numeric;
TokenCell.Entry.ReturnType = ReturnType.Go; TokenCell.Entry.ReturnType = ReturnType.Go;
RememberCell = new ExtendedSwitchCell var table = new TwoFactorTable(
{
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
{
new TableSection(" ") new TableSection(" ")
{ {
TokenCell, TokenCell,
RememberCell 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
};
var layout = new StackLayout var layout = new StackLayout
{ {
@ -189,25 +180,57 @@ namespace Bit.App.Pages
var host = WebUtility.UrlEncode(duoParams["Host"].ToString()); var host = WebUtility.UrlEncode(duoParams["Host"].ToString());
var req = WebUtility.UrlEncode(duoParams["Signature"].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}", Uri = $"http://192.168.1.6:4001/duo-mobile.html?host={host}&request={req}",
HorizontalOptions = LayoutOptions.FillAndExpand, HorizontalOptions = LayoutOptions.FillAndExpand,
VerticalOptions = LayoutOptions.FillAndExpand VerticalOptions = LayoutOptions.FillAndExpand
}; };
WebView.RegisterAction(async (sig) => webView.RegisterAction(async (sig) =>
{ {
await LogInAsync(sig, false); await LogInAsync(sig, false);
}); });
Title = "Duo"; 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() protected override void OnAppearing()
{ {
base.OnAppearing(); base.OnAppearing();
ListenYubiKey(true);
if(TokenCell != null) if(TokenCell != null)
{ {
@ -220,6 +243,7 @@ namespace Bit.App.Pages
protected override void OnDisappearing() protected override void OnDisappearing()
{ {
base.OnDisappearing(); base.OnDisappearing();
ListenYubiKey(false);
if(TokenCell != null) if(TokenCell != null)
{ {
@ -251,6 +275,12 @@ namespace Bit.App.Pages
private async Task LogInAsync(string token, bool remember) private async Task LogInAsync(string token, bool remember)
{ {
if(_lastAction.LastActionWasRecent())
{
return;
}
_lastAction = DateTime.UtcNow;
if(string.IsNullOrWhiteSpace(token)) if(string.IsNullOrWhiteSpace(token))
{ {
await DisplayAlert(AppResources.AnErrorHasOccurred, string.Format(AppResources.ValidationFieldRequired, await DisplayAlert(AppResources.AnErrorHasOccurred, string.Format(AppResources.ValidationFieldRequired,
@ -264,6 +294,7 @@ namespace Bit.App.Pages
_userDialogs.HideLoading(); _userDialogs.HideLoading();
if(!response.Success) if(!response.Success)
{ {
ListenYubiKey(true);
await DisplayAlert(AppResources.AnErrorHasOccurred, response.ErrorMessage, AppResources.Ok); await DisplayAlert(AppResources.AnErrorHasOccurred, response.ErrorMessage, AppResources.Ok);
return; return;
} }
@ -299,7 +330,7 @@ namespace Bit.App.Pages
switch(p.Key) switch(p.Key)
{ {
case TwoFactorProviderType.Authenticator: case TwoFactorProviderType.Authenticator:
if(provider == TwoFactorProviderType.Duo) if(provider == TwoFactorProviderType.Duo || provider == TwoFactorProviderType.YubiKey)
{ {
continue; continue;
} }
@ -311,6 +342,16 @@ namespace Bit.App.Pages
} }
break; break;
case TwoFactorProviderType.Duo: case TwoFactorProviderType.Duo:
if(provider == TwoFactorProviderType.YubiKey)
{
continue;
}
break;
case TwoFactorProviderType.YubiKey:
if(!_deviceInfoService.NfcEnabled)
{
continue;
}
break; break;
default: default:
continue; continue;
@ -322,5 +363,76 @@ namespace Bit.App.Pages
return provider; 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, string>(Application.Current, "GotYubiKeyOTP");
MessagingCenter.Unsubscribe<Application>(Application.Current, "ResumeYubiKey");
if(!subscribe)
{
return;
}
MessagingCenter.Subscribe<Application, string>(Application.Current, "GotYubiKeyOTP", async (sender, otp) =>
{
MessagingCenter.Unsubscribe<Application, string>(Application.Current, "GotYubiKeyOTP");
if(_providerType == TwoFactorProviderType.YubiKey)
{
await LogInAsync(otp, RememberCell.On);
}
});
SubscribeYubiKeyResume();
}
private void SubscribeYubiKeyResume()
{
MessagingCenter.Subscribe<Application>(Application.Current, "ResumeYubiKey", (sender) =>
{
MessagingCenter.Unsubscribe<Application>(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;
}
}
}
} }
} }

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 187 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 381 KiB

View file

@ -716,6 +716,15 @@
<ItemGroup> <ItemGroup>
<BundleResource Include="Resources\share_tools%403x.png" /> <BundleResource Include="Resources\share_tools%403x.png" />
</ItemGroup> </ItemGroup>
<ItemGroup>
<BundleResource Include="Resources\yubikey.png" />
</ItemGroup>
<ItemGroup>
<BundleResource Include="Resources\yubikey%402x.png" />
</ItemGroup>
<ItemGroup>
<BundleResource Include="Resources\yubikey%403x.png" />
</ItemGroup>
<Import Project="$(MSBuildExtensionsPath)\Xamarin\iOS\Xamarin.iOS.CSharp.targets" /> <Import Project="$(MSBuildExtensionsPath)\Xamarin\iOS\Xamarin.iOS.CSharp.targets" />
<Target Name="EnsureNuGetPackageBuildImports" BeforeTargets="PrepareForBuild"> <Target Name="EnsureNuGetPackageBuildImports" BeforeTargets="PrepareForBuild">
<PropertyGroup> <PropertyGroup>

View file

@ -57,6 +57,7 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<Reference Include="Mono.Android" /> <Reference Include="Mono.Android" />
<Reference Include="Mono.Android.Export" />
<Reference Include="mscorlib" /> <Reference Include="mscorlib" />
<Reference Include="PCLCrypto, Version=2.0.0.0, Culture=neutral, PublicKeyToken=d4421c8a4786956c, processorArchitecture=MSIL"> <Reference Include="PCLCrypto, Version=2.0.0.0, Culture=neutral, PublicKeyToken=d4421c8a4786956c, processorArchitecture=MSIL">
<HintPath>..\..\packages\PCLCrypto.2.0.147\lib\MonoAndroid23\PCLCrypto.dll</HintPath> <HintPath>..\..\packages\PCLCrypto.2.0.147\lib\MonoAndroid23\PCLCrypto.dll</HintPath>