read yubikey and log in
@ -860,6 +860,18 @@
|
||||
<ItemGroup>
|
||||
<AndroidResource Include="Resources\drawable-xxxhdpi\share_tools.png" />
|
||||
</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="..\..\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">
|
||||
|
@ -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<IAppIdService>();
|
||||
var authService = Resolver.Resolve<IAuthService>();
|
||||
@ -105,10 +105,10 @@ namespace Bit.Android
|
||||
LaunchApp(args);
|
||||
});
|
||||
|
||||
MessagingCenter.Subscribe<Xamarin.Forms.Application>(
|
||||
Xamarin.Forms.Application.Current, "ListenYubiKeyOTP", (sender) =>
|
||||
MessagingCenter.Subscribe<Xamarin.Forms.Application, bool>(
|
||||
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,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);
|
||||
intent.AddFlags(ActivityFlags.SingleTop);
|
||||
@ -247,26 +268,30 @@ namespace Bit.Android
|
||||
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
|
||||
var adapter = NfcAdapter.GetDefaultAdapter(this);
|
||||
adapter.EnableForegroundDispatch(this, pendingIntent, new IntentFilter[] { ndef }, null);
|
||||
|
||||
var data = Intent.DataString;
|
||||
if(data != null)
|
||||
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);
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
7
src/Android/Resources/Resource.Designer.cs
generated
@ -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();
|
||||
|
BIN
src/Android/Resources/drawable-hdpi/yubikey.png
Normal file
After Width: | Height: | Size: 108 KiB |
BIN
src/Android/Resources/drawable-xhdpi/yubikey.png
Normal file
After Width: | Height: | Size: 187 KiB |
BIN
src/Android/Resources/drawable-xxhdpi/yubikey.png
Normal file
After Width: | Height: | Size: 381 KiB |
BIN
src/Android/Resources/drawable/yubikey.png
Normal file
After Width: | Height: | Size: 51 KiB |
@ -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();
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
|
@ -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<IDeviceInfoService>();
|
||||
|
||||
_email = email;
|
||||
_result = result;
|
||||
_masterPasswordHash = result.MasterPasswordHash;
|
||||
@ -47,26 +52,54 @@ namespace Bit.App.Pages
|
||||
_pushNotification = Resolver.Resolve<IPushNotification>();
|
||||
|
||||
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();
|
||||
|
||||
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,
|
||||
Margin = new Thickness(15),
|
||||
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 ||
|
||||
_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(" ")
|
||||
{
|
||||
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
|
||||
};
|
||||
});
|
||||
|
||||
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, 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
BIN
src/iOS/Resources/yubikey.png
Normal file
After Width: | Height: | Size: 51 KiB |
BIN
src/iOS/Resources/yubikey@2x.png
Normal file
After Width: | Height: | Size: 187 KiB |
BIN
src/iOS/Resources/yubikey@3x.png
Normal file
After Width: | Height: | Size: 381 KiB |
@ -716,6 +716,15 @@
|
||||
<ItemGroup>
|
||||
<BundleResource Include="Resources\share_tools%403x.png" />
|
||||
</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" />
|
||||
<Target Name="EnsureNuGetPackageBuildImports" BeforeTargets="PrepareForBuild">
|
||||
<PropertyGroup>
|
||||
|
@ -57,6 +57,7 @@
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<Reference Include="Mono.Android" />
|
||||
<Reference Include="Mono.Android.Export" />
|
||||
<Reference Include="mscorlib" />
|
||||
<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>
|
||||
|