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 TokenEmail { get; }
string TokenName { get; }
bool TokenPremium { get; }
}
}

View file

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

View file

@ -8,7 +8,8 @@ namespace Bit.App.Controls
string labelText = null,
string valueText = null,
string button1Text = null,
string button2Text = null)
string button2Text = null,
string subText = null)
{
var containerStackLayout = new StackLayout
{
@ -56,6 +57,18 @@ namespace Bit.App.Controls
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)
{
Button1 = new ExtendedButton
@ -100,12 +113,18 @@ namespace Bit.App.Controls
containerStackLayout.AdjustPaddingForDevice();
}
if(Sub != null && Button1 != null)
{
Sub.Margin = new Thickness(0, 0, 10, 0);
}
containerStackLayout.Children.Add(buttonStackLayout);
View = containerStackLayout;
}
public Label Label { get; private set; }
public Label Value { get; private set; }
public Label Sub { get; private set; }
public ExtendedButton Button1 { 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 _uri;
private string _notes;
private string _totpCode;
private int _totpSec = 30;
private bool _revealPassword;
private List<Attachment> _attachments;
@ -194,6 +196,31 @@ namespace Bit.App.Models.Page
public string ShowHideText => RevealPassword ? AppResources.Hide : AppResources.Show;
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
{
get { return _attachments; }

View file

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

View file

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

View file

@ -19,6 +19,7 @@ namespace Bit.App.Pages
private readonly ILoginService _loginService;
private readonly IUserDialogs _userDialogs;
private readonly IDeviceActionService _deviceActionService;
private readonly ITokenService _tokenService;
public VaultViewLoginPage(string loginId)
{
@ -26,6 +27,7 @@ namespace Bit.App.Pages
_loginService = Resolver.Resolve<ILoginService>();
_userDialogs = Resolver.Resolve<IUserDialogs>();
_deviceActionService = Resolver.Resolve<IDeviceActionService>();
_tokenService = Resolver.Resolve<ITokenService>();
Init();
}
@ -39,6 +41,7 @@ namespace Bit.App.Pages
public LabeledValueCell PasswordCell { get; set; }
public LabeledValueCell UriCell { get; set; }
public LabeledValueCell NotesCell { get; set; }
public LabeledValueCell TotpCodeCell { get; set; }
private EditLoginToolBarItem EditItem { 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
NotesCell = new LabeledValueCell();
NotesCell.Value.SetBinding(Label.TextProperty, nameof(VaultViewLoginPageModel.Notes));
@ -129,6 +141,7 @@ namespace Bit.App.Pages
PasswordCell.Button1.WidthRequest = 40;
PasswordCell.Button2.WidthRequest = 59;
UsernameCell.Button1.WidthRequest = 59;
TotpCodeCell.Button1.WidthRequest = 59;
UriCell.Button1.WidthRequest = 75;
}
@ -209,6 +222,38 @@ namespace Bit.App.Pages
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();
}
@ -273,6 +318,18 @@ namespace Bit.App.Pages
_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 readonly VaultViewLoginPage _page;

View file

@ -5,13 +5,12 @@ using Bit.App.Abstractions;
using Bit.App.Models.Api;
using Plugin.Connectivity.Abstractions;
using Newtonsoft.Json;
using Bit.App.Utilities;
namespace Bit.App.Repositories
{
public class AccountsApiRepository : BaseApiRepository, IAccountsApiRepository
{
private static readonly DateTime _epoc = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);
public AccountsApiRepository(
IConnectivity connectivity,
IHttpService httpService,
@ -125,7 +124,7 @@ namespace Bit.App.Repositories
{
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
{

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>
/// Looks up a localized string similar to Could not send verification email. Try again..
/// </summary>

View file

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

View file

@ -2,6 +2,7 @@
using Bit.App.Abstractions;
using System.Text;
using Newtonsoft.Json.Linq;
using Bit.App.Utilities;
namespace Bit.App.Services
{
@ -19,8 +20,6 @@ namespace Bit.App.Services
private string _refreshToken;
private string _authBearer;
private static readonly DateTime _epoc = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);
public TokenService(ISecureStorageService secureStorage)
{
_secureStorage = secureStorage;
@ -73,7 +72,7 @@ namespace Bit.App.Services
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 TokenEmail => DecodeToken()?["email"].Value<string>();
public string TokenName => DecodeToken()?["name"].Value<string>();
public bool TokenPremium => (DecodeToken()?["premium"].Value<bool?>()).GetValueOrDefault(false);
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;
}
// 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 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),
T WinPhone = default(T), T Windows = default(T))
{