totp code generation on view login

This commit is contained in:
Kyle Spearrin 2017-07-13 14:44:02 -04:00
parent 9879f074b4
commit 26c110291e
14 changed files with 251 additions and 19 deletions

View file

@ -18,5 +18,6 @@ namespace Bit.App.Abstractions
string TokenUserId { get; } string TokenUserId { get; }
string TokenEmail { get; } string TokenEmail { get; }
string TokenName { get; } string TokenName { get; }
bool TokenPremium { get; }
} }
} }

View file

@ -294,6 +294,7 @@
<Compile Include="Pages\Vault\VaultEditLoginPage.cs" /> <Compile Include="Pages\Vault\VaultEditLoginPage.cs" />
<Compile Include="Pages\Vault\VaultListLoginsPage.cs" /> <Compile Include="Pages\Vault\VaultListLoginsPage.cs" />
<Compile Include="Services\PasswordGenerationService.cs" /> <Compile Include="Services\PasswordGenerationService.cs" />
<Compile Include="Utilities\Base32.cs" />
<Compile Include="Utilities\Crypto.cs" /> <Compile Include="Utilities\Crypto.cs" />
<Compile Include="Utilities\Helpers.cs" /> <Compile Include="Utilities\Helpers.cs" />
<Compile Include="Utilities\IdentityHttpClient.cs" /> <Compile Include="Utilities\IdentityHttpClient.cs" />

View file

@ -8,7 +8,8 @@ namespace Bit.App.Controls
string labelText = null, string labelText = null,
string valueText = null, string valueText = null,
string button1Text = null, string button1Text = null,
string button2Text = null) string button2Text = null,
string subText = null)
{ {
var containerStackLayout = new StackLayout var containerStackLayout = new StackLayout
{ {
@ -56,6 +57,18 @@ namespace Bit.App.Controls
VerticalOptions = LayoutOptions.CenterAndExpand VerticalOptions = LayoutOptions.CenterAndExpand
}; };
if(subText != null)
{
Sub = new Label
{
FontSize = Device.GetNamedSize(NamedSize.Small, typeof(Label)),
HorizontalOptions = LayoutOptions.End,
VerticalOptions = LayoutOptions.Center
};
buttonStackLayout.Children.Add(Sub);
}
if(button1Text != null) if(button1Text != null)
{ {
Button1 = new ExtendedButton Button1 = new ExtendedButton
@ -100,12 +113,18 @@ namespace Bit.App.Controls
containerStackLayout.AdjustPaddingForDevice(); containerStackLayout.AdjustPaddingForDevice();
} }
if(Sub != null && Button1 != null)
{
Sub.Margin = new Thickness(0, 0, 10, 0);
}
containerStackLayout.Children.Add(buttonStackLayout); containerStackLayout.Children.Add(buttonStackLayout);
View = containerStackLayout; View = containerStackLayout;
} }
public Label Label { get; private set; } public Label Label { get; private set; }
public Label Value { get; private set; } public Label Value { get; private set; }
public Label Sub { get; private set; }
public ExtendedButton Button1 { get; private set; } public ExtendedButton Button1 { get; private set; }
public ExtendedButton Button2 { get; private set; } public ExtendedButton Button2 { get; private set; }
} }

View file

@ -13,6 +13,8 @@ namespace Bit.App.Models.Page
private string _password; private string _password;
private string _uri; private string _uri;
private string _notes; private string _notes;
private string _totpCode;
private int _totpSec = 30;
private bool _revealPassword; private bool _revealPassword;
private List<Attachment> _attachments; private List<Attachment> _attachments;
@ -194,6 +196,31 @@ namespace Bit.App.Models.Page
public string ShowHideText => RevealPassword ? AppResources.Hide : AppResources.Show; public string ShowHideText => RevealPassword ? AppResources.Hide : AppResources.Show;
public ImageSource ShowHideImage => RevealPassword ? ImageSource.FromFile("eye_slash") : ImageSource.FromFile("eye"); public ImageSource ShowHideImage => RevealPassword ? ImageSource.FromFile("eye_slash") : ImageSource.FromFile("eye");
public string TotpCode
{
get { return _totpCode; }
set
{
_totpCode = value;
PropertyChanged(this, new PropertyChangedEventArgs(nameof(TotpCode)));
PropertyChanged(this, new PropertyChangedEventArgs(nameof(TotpCodeFormatted)));
}
}
public int TotpSecond
{
get { return _totpSec; }
set
{
_totpSec = value;
PropertyChanged(this, new PropertyChangedEventArgs(nameof(TotpSecond)));
PropertyChanged(this, new PropertyChangedEventArgs(nameof(TotpColor)));
}
}
public bool TotpLow => TotpSecond <= 7;
public Color TotpColor => !string.IsNullOrWhiteSpace(TotpCode) && TotpLow ? Color.Red : Color.Black;
public string TotpCodeFormatted => !string.IsNullOrWhiteSpace(TotpCode) ?
string.Format("{0} {1}", TotpCode.Substring(0, 3), TotpCode.Substring(3)) : null;
public List<Attachment> Attachments public List<Attachment> Attachments
{ {
get { return _attachments; } get { return _attachments; }

View file

@ -168,12 +168,12 @@ namespace Bit.App.Pages
var login = new Login var login = new Login
{ {
Uri = UriCell.Entry.Text?.Encrypt(), Name = NameCell.Entry.Text.Encrypt(),
Name = NameCell.Entry.Text?.Encrypt(), Uri = string.IsNullOrWhiteSpace(UriCell.Entry.Text) ? null : UriCell.Entry.Text.Encrypt(),
Username = UsernameCell.Entry.Text?.Encrypt(), Username = string.IsNullOrWhiteSpace(UsernameCell.Entry.Text) ? null : UsernameCell.Entry.Text.Encrypt(),
Password = PasswordCell.Entry.Text?.Encrypt(), Password = string.IsNullOrWhiteSpace(PasswordCell.Entry.Text) ? null : PasswordCell.Entry.Text.Encrypt(),
Notes = NotesCell.Editor.Text?.Encrypt(), Notes = string.IsNullOrWhiteSpace(NotesCell.Editor.Text) ? null : NotesCell.Editor.Text.Encrypt(),
Totp = TotpCell.Entry.Text?.Encrypt(), Totp = string.IsNullOrWhiteSpace(TotpCell.Entry.Text) ? null : TotpCell.Entry.Text.Encrypt(),
Favorite = favoriteCell.On Favorite = favoriteCell.On
}; };

View file

@ -177,12 +177,17 @@ namespace Bit.App.Pages
return; return;
} }
login.Uri = UriCell.Entry.Text?.Encrypt(login.OrganizationId); login.Name = NameCell.Entry.Text.Encrypt(login.OrganizationId);
login.Name = NameCell.Entry.Text?.Encrypt(login.OrganizationId); login.Uri = string.IsNullOrWhiteSpace(UriCell.Entry.Text) ? null :
login.Username = UsernameCell.Entry.Text?.Encrypt(login.OrganizationId); UriCell.Entry.Text.Encrypt(login.OrganizationId);
login.Password = PasswordCell.Entry.Text?.Encrypt(login.OrganizationId); login.Username = string.IsNullOrWhiteSpace(UsernameCell.Entry.Text) ? null :
login.Notes = NotesCell.Editor.Text?.Encrypt(login.OrganizationId); UsernameCell.Entry.Text.Encrypt(login.OrganizationId);
login.Totp = TotpCell.Entry.Text?.Encrypt(login.OrganizationId); login.Password = string.IsNullOrWhiteSpace(PasswordCell.Entry.Text) ? null :
PasswordCell.Entry.Text.Encrypt(login.OrganizationId);
login.Notes = string.IsNullOrWhiteSpace(NotesCell.Editor.Text) ? null :
NotesCell.Editor.Text.Encrypt(login.OrganizationId);
login.Totp = string.IsNullOrWhiteSpace(TotpCell.Entry.Text) ? null :
TotpCell.Entry.Text.Encrypt(login.OrganizationId);
login.Favorite = favoriteCell.On; login.Favorite = favoriteCell.On;
if(FolderCell.Picker.SelectedIndex > 0) if(FolderCell.Picker.SelectedIndex > 0)

View file

@ -19,6 +19,7 @@ namespace Bit.App.Pages
private readonly ILoginService _loginService; private readonly ILoginService _loginService;
private readonly IUserDialogs _userDialogs; private readonly IUserDialogs _userDialogs;
private readonly IDeviceActionService _deviceActionService; private readonly IDeviceActionService _deviceActionService;
private readonly ITokenService _tokenService;
public VaultViewLoginPage(string loginId) public VaultViewLoginPage(string loginId)
{ {
@ -26,6 +27,7 @@ namespace Bit.App.Pages
_loginService = Resolver.Resolve<ILoginService>(); _loginService = Resolver.Resolve<ILoginService>();
_userDialogs = Resolver.Resolve<IUserDialogs>(); _userDialogs = Resolver.Resolve<IUserDialogs>();
_deviceActionService = Resolver.Resolve<IDeviceActionService>(); _deviceActionService = Resolver.Resolve<IDeviceActionService>();
_tokenService = Resolver.Resolve<ITokenService>();
Init(); Init();
} }
@ -39,6 +41,7 @@ namespace Bit.App.Pages
public LabeledValueCell PasswordCell { get; set; } public LabeledValueCell PasswordCell { get; set; }
public LabeledValueCell UriCell { get; set; } public LabeledValueCell UriCell { get; set; }
public LabeledValueCell NotesCell { get; set; } public LabeledValueCell NotesCell { get; set; }
public LabeledValueCell TotpCodeCell { get; set; }
private EditLoginToolBarItem EditItem { get; set; } private EditLoginToolBarItem EditItem { get; set; }
public List<AttachmentViewCell> AttachmentCells { get; set; } public List<AttachmentViewCell> AttachmentCells { get; set; }
@ -91,6 +94,15 @@ namespace Bit.App.Pages
} }
}); });
// Totp
TotpCodeCell = new LabeledValueCell(AppResources.VerificationCodeTotp, button1Text: AppResources.Copy, subText: "--");
TotpCodeCell.Value.SetBinding(Label.TextProperty, nameof(VaultViewLoginPageModel.TotpCodeFormatted));
TotpCodeCell.Value.SetBinding(Label.TextColorProperty, nameof(VaultViewLoginPageModel.TotpColor));
TotpCodeCell.Button1.Command = new Command(() => Copy(Model.TotpCode, AppResources.VerificationCodeTotp));
TotpCodeCell.Sub.SetBinding(Label.TextProperty, nameof(VaultViewLoginPageModel.TotpSecond));
TotpCodeCell.Sub.SetBinding(Label.TextColorProperty, nameof(VaultViewLoginPageModel.TotpColor));
TotpCodeCell.Value.FontFamily = Helpers.OnPlatform(iOS: "Courier", Android: "monospace", WinPhone: "Courier");
// Notes // Notes
NotesCell = new LabeledValueCell(); NotesCell = new LabeledValueCell();
NotesCell.Value.SetBinding(Label.TextProperty, nameof(VaultViewLoginPageModel.Notes)); NotesCell.Value.SetBinding(Label.TextProperty, nameof(VaultViewLoginPageModel.Notes));
@ -129,6 +141,7 @@ namespace Bit.App.Pages
PasswordCell.Button1.WidthRequest = 40; PasswordCell.Button1.WidthRequest = 40;
PasswordCell.Button2.WidthRequest = 59; PasswordCell.Button2.WidthRequest = 59;
UsernameCell.Button1.WidthRequest = 59; UsernameCell.Button1.WidthRequest = 59;
TotpCodeCell.Button1.WidthRequest = 59;
UriCell.Button1.WidthRequest = 75; UriCell.Button1.WidthRequest = 75;
} }
@ -209,6 +222,38 @@ namespace Bit.App.Pages
Table.Root.Add(AttachmentsSection); Table.Root.Add(AttachmentsSection);
} }
// Totp
var removeTotp = login.Totp == null || (!_tokenService.TokenPremium && !login.OrganizationUseTotp);
if(!removeTotp)
{
var totpKey = login.Totp.Decrypt(login.OrganizationId);
removeTotp = string.IsNullOrWhiteSpace(totpKey);
if(!removeTotp)
{
Model.TotpCode = Crypto.Totp(totpKey);
removeTotp = string.IsNullOrWhiteSpace(Model.TotpCode);
if(!removeTotp)
{
TotpTick(totpKey);
Device.StartTimer(new TimeSpan(0, 0, 1), () =>
{
TotpTick(totpKey);
return true;
});
if(!LoginInformationSection.Contains(TotpCodeCell))
{
LoginInformationSection.Add(TotpCodeCell);
}
}
}
}
if(removeTotp && LoginInformationSection.Contains(TotpCodeCell))
{
LoginInformationSection.Remove(TotpCodeCell);
}
base.OnAppearing(); base.OnAppearing();
} }
@ -273,6 +318,18 @@ namespace Bit.App.Pages
_userDialogs.Toast(string.Format(AppResources.ValueHasBeenCopied, alertLabel)); _userDialogs.Toast(string.Format(AppResources.ValueHasBeenCopied, alertLabel));
} }
private void TotpTick(string totpKey)
{
var now = Helpers.EpocUtcNow() / 1000;
var mod = now % 30;
Model.TotpSecond = (int)(30 - mod);
if(mod == 0)
{
Model.TotpCode = Crypto.Totp(totpKey);
}
}
private class EditLoginToolBarItem : ExtendedToolbarItem private class EditLoginToolBarItem : ExtendedToolbarItem
{ {
private readonly VaultViewLoginPage _page; private readonly VaultViewLoginPage _page;

View file

@ -5,13 +5,12 @@ using Bit.App.Abstractions;
using Bit.App.Models.Api; using Bit.App.Models.Api;
using Plugin.Connectivity.Abstractions; using Plugin.Connectivity.Abstractions;
using Newtonsoft.Json; using Newtonsoft.Json;
using Bit.App.Utilities;
namespace Bit.App.Repositories namespace Bit.App.Repositories
{ {
public class AccountsApiRepository : BaseApiRepository, IAccountsApiRepository public class AccountsApiRepository : BaseApiRepository, IAccountsApiRepository
{ {
private static readonly DateTime _epoc = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);
public AccountsApiRepository( public AccountsApiRepository(
IConnectivity connectivity, IConnectivity connectivity,
IHttpService httpService, IHttpService httpService,
@ -125,7 +124,7 @@ namespace Bit.App.Repositories
{ {
return await HandleErrorAsync<DateTime?>(response).ConfigureAwait(false); return await HandleErrorAsync<DateTime?>(response).ConfigureAwait(false);
} }
return ApiResult<DateTime?>.Success(_epoc.AddMilliseconds(ms), response.StatusCode); return ApiResult<DateTime?>.Success(Helpers.Epoc.AddMilliseconds(ms), response.StatusCode);
} }
catch catch
{ {

View file

@ -2122,6 +2122,15 @@ namespace Bit.App.Resources {
} }
} }
/// <summary>
/// Looks up a localized string similar to Verification Code (TOTP).
/// </summary>
public static string VerificationCodeTotp {
get {
return ResourceManager.GetString("VerificationCodeTotp", resourceCulture);
}
}
/// <summary> /// <summary>
/// Looks up a localized string similar to Could not send verification email. Try again.. /// Looks up a localized string similar to Could not send verification email. Try again..
/// </summary> /// </summary>

View file

@ -925,4 +925,8 @@
<data name="AuthenticatorKey" xml:space="preserve"> <data name="AuthenticatorKey" xml:space="preserve">
<value>Authenticator Key (TOTP)</value> <value>Authenticator Key (TOTP)</value>
</data> </data>
<data name="VerificationCodeTotp" xml:space="preserve">
<value>Verification Code (TOTP)</value>
<comment>Totp code label</comment>
</data>
</root> </root>

View file

@ -2,6 +2,7 @@
using Bit.App.Abstractions; using Bit.App.Abstractions;
using System.Text; using System.Text;
using Newtonsoft.Json.Linq; using Newtonsoft.Json.Linq;
using Bit.App.Utilities;
namespace Bit.App.Services namespace Bit.App.Services
{ {
@ -19,8 +20,6 @@ namespace Bit.App.Services
private string _refreshToken; private string _refreshToken;
private string _authBearer; private string _authBearer;
private static readonly DateTime _epoc = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);
public TokenService(ISecureStorageService secureStorage) public TokenService(ISecureStorageService secureStorage)
{ {
_secureStorage = secureStorage; _secureStorage = secureStorage;
@ -73,7 +72,7 @@ namespace Bit.App.Services
throw new InvalidOperationException("No exp in token."); throw new InvalidOperationException("No exp in token.");
} }
return _epoc.AddSeconds(Convert.ToDouble(decoded["exp"].Value<long>())); return Helpers.Epoc.AddSeconds(Convert.ToDouble(decoded["exp"].Value<long>()));
} }
} }
@ -97,6 +96,7 @@ namespace Bit.App.Services
public string TokenUserId => DecodeToken()?["sub"].Value<string>(); public string TokenUserId => DecodeToken()?["sub"].Value<string>();
public string TokenEmail => DecodeToken()?["email"].Value<string>(); public string TokenEmail => DecodeToken()?["email"].Value<string>();
public string TokenName => DecodeToken()?["name"].Value<string>(); public string TokenName => DecodeToken()?["name"].Value<string>();
public bool TokenPremium => (DecodeToken()?["premium"].Value<bool?>()).GetValueOrDefault(false);
public string RefreshToken public string RefreshToken
{ {

View file

@ -0,0 +1,72 @@
using System;
namespace Bit.App.Utilities
{
// ref: https://github.com/aspnet/Identity/blob/dev/src/Microsoft.Extensions.Identity.Core/Base32.cs
// with some modifications for cleaning input
public static class Base32
{
private static readonly string _base32Chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
public static byte[] FromBase32(string input)
{
if(input == null)
{
throw new ArgumentNullException(nameof(input));
}
input = input.ToUpperInvariant();
var cleanedInput = string.Empty;
foreach(var c in input)
{
if(_base32Chars.IndexOf(c) < 0)
{
continue;
}
cleanedInput += c;
}
input = cleanedInput;
if(input.Length == 0)
{
return new byte[0];
}
var output = new byte[input.Length * 5 / 8];
var bitIndex = 0;
var inputIndex = 0;
var outputBits = 0;
var outputIndex = 0;
while(outputIndex < output.Length)
{
var byteIndex = _base32Chars.IndexOf(input[inputIndex]);
if(byteIndex < 0)
{
throw new FormatException();
}
var bits = Math.Min(5 - bitIndex, 8 - outputBits);
output[outputIndex] <<= bits;
output[outputIndex] |= (byte)(byteIndex >> (5 - (bitIndex + bits)));
bitIndex += bits;
if(bitIndex >= 5)
{
inputIndex++;
bitIndex = 0;
}
outputBits += bits;
if(outputBits >= 8)
{
outputIndex++;
outputBits = 0;
}
}
return output;
}
}
}

View file

@ -152,5 +152,36 @@ namespace Bit.App.Utilities
return true; return true;
} }
// ref: https://github.com/mirthas/totp-net/blob/master/TOTP/Totp.cs
public static string Totp(string b32Key)
{
var key = Base32.FromBase32(b32Key);
if(key == null || key.Length == 0)
{
return null;
}
var now = Helpers.EpocUtcNow() / 1000;
var sec = now / 30;
var secBytes = BitConverter.GetBytes(sec);
if(BitConverter.IsLittleEndian)
{
Array.Reverse(secBytes, 0, secBytes.Length);
}
var algorithm = WinRTCrypto.MacAlgorithmProvider.OpenAlgorithm(MacAlgorithm.HmacSha1);
var hasher = algorithm.CreateHash(key);
hasher.Append(secBytes);
var hash = hasher.GetValueAndReset();
var offset = (hash[hash.Length - 1] & 0xf);
var i = ((hash[offset] & 0x7f) << 24) | ((hash[offset + 1] & 0xff) << 16) |
((hash[offset + 2] & 0xff) << 8) | (hash[offset + 3] & 0xff);
var code = i % (int)Math.Pow(10, 6);
return code.ToString().PadLeft(6, '0');
}
} }
} }

View file

@ -5,6 +5,13 @@ namespace Bit.App.Utilities
{ {
public static class Helpers public static class Helpers
{ {
public static readonly DateTime Epoc = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);
public static long EpocUtcNow()
{
return (long)(DateTime.UtcNow - Epoc).TotalMilliseconds;
}
public static T OnPlatform<T>(T iOS = default(T), T Android = default(T), public static T OnPlatform<T>(T iOS = default(T), T Android = default(T),
T WinPhone = default(T), T Windows = default(T)) T WinPhone = default(T), T Windows = default(T))
{ {