From 26c110291e61780c8ef00ec018e4396f6a416c93 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Thu, 13 Jul 2017 14:44:02 -0400 Subject: [PATCH] totp code generation on view login --- .../Abstractions/Services/ITokenService.cs | 1 + src/App/App.csproj | 1 + src/App/Controls/LabeledValueCell.cs | 21 +++++- .../Models/Page/VaultViewLoginPageModel.cs | 27 +++++++ src/App/Pages/Vault/VaultAddLoginPage.cs | 12 ++-- src/App/Pages/Vault/VaultEditLoginPage.cs | 17 +++-- src/App/Pages/Vault/VaultViewLoginPage.cs | 57 +++++++++++++++ src/App/Repositories/AccountsApiRepository.cs | 5 +- src/App/Resources/AppResources.Designer.cs | 9 +++ src/App/Resources/AppResources.resx | 4 ++ src/App/Services/TokenService.cs | 6 +- src/App/Utilities/Base32.cs | 72 +++++++++++++++++++ src/App/Utilities/Crypto.cs | 31 ++++++++ src/App/Utilities/Helpers.cs | 7 ++ 14 files changed, 251 insertions(+), 19 deletions(-) create mode 100644 src/App/Utilities/Base32.cs diff --git a/src/App/Abstractions/Services/ITokenService.cs b/src/App/Abstractions/Services/ITokenService.cs index 30fcacc7b..931652682 100644 --- a/src/App/Abstractions/Services/ITokenService.cs +++ b/src/App/Abstractions/Services/ITokenService.cs @@ -18,5 +18,6 @@ namespace Bit.App.Abstractions string TokenUserId { get; } string TokenEmail { get; } string TokenName { get; } + bool TokenPremium { get; } } } diff --git a/src/App/App.csproj b/src/App/App.csproj index 145ddb693..31b1ae484 100644 --- a/src/App/App.csproj +++ b/src/App/App.csproj @@ -294,6 +294,7 @@ + diff --git a/src/App/Controls/LabeledValueCell.cs b/src/App/Controls/LabeledValueCell.cs index c84e967e3..6a74e11ed 100644 --- a/src/App/Controls/LabeledValueCell.cs +++ b/src/App/Controls/LabeledValueCell.cs @@ -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; } } diff --git a/src/App/Models/Page/VaultViewLoginPageModel.cs b/src/App/Models/Page/VaultViewLoginPageModel.cs index de1b4cba8..6ebd79e0c 100644 --- a/src/App/Models/Page/VaultViewLoginPageModel.cs +++ b/src/App/Models/Page/VaultViewLoginPageModel.cs @@ -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 _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 Attachments { get { return _attachments; } diff --git a/src/App/Pages/Vault/VaultAddLoginPage.cs b/src/App/Pages/Vault/VaultAddLoginPage.cs index 2836662f2..296e1c95c 100644 --- a/src/App/Pages/Vault/VaultAddLoginPage.cs +++ b/src/App/Pages/Vault/VaultAddLoginPage.cs @@ -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 }; diff --git a/src/App/Pages/Vault/VaultEditLoginPage.cs b/src/App/Pages/Vault/VaultEditLoginPage.cs index c16979356..a945fed5b 100644 --- a/src/App/Pages/Vault/VaultEditLoginPage.cs +++ b/src/App/Pages/Vault/VaultEditLoginPage.cs @@ -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) diff --git a/src/App/Pages/Vault/VaultViewLoginPage.cs b/src/App/Pages/Vault/VaultViewLoginPage.cs index 48363e258..d2843cb91 100644 --- a/src/App/Pages/Vault/VaultViewLoginPage.cs +++ b/src/App/Pages/Vault/VaultViewLoginPage.cs @@ -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(); _userDialogs = Resolver.Resolve(); _deviceActionService = Resolver.Resolve(); + _tokenService = Resolver.Resolve(); 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 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; diff --git a/src/App/Repositories/AccountsApiRepository.cs b/src/App/Repositories/AccountsApiRepository.cs index 414d1e17b..9b55f162a 100644 --- a/src/App/Repositories/AccountsApiRepository.cs +++ b/src/App/Repositories/AccountsApiRepository.cs @@ -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(response).ConfigureAwait(false); } - return ApiResult.Success(_epoc.AddMilliseconds(ms), response.StatusCode); + return ApiResult.Success(Helpers.Epoc.AddMilliseconds(ms), response.StatusCode); } catch { diff --git a/src/App/Resources/AppResources.Designer.cs b/src/App/Resources/AppResources.Designer.cs index 801124198..585281f2b 100644 --- a/src/App/Resources/AppResources.Designer.cs +++ b/src/App/Resources/AppResources.Designer.cs @@ -2122,6 +2122,15 @@ namespace Bit.App.Resources { } } + /// + /// Looks up a localized string similar to Verification Code (TOTP). + /// + public static string VerificationCodeTotp { + get { + return ResourceManager.GetString("VerificationCodeTotp", resourceCulture); + } + } + /// /// Looks up a localized string similar to Could not send verification email. Try again.. /// diff --git a/src/App/Resources/AppResources.resx b/src/App/Resources/AppResources.resx index 27350e1a8..b55e52299 100644 --- a/src/App/Resources/AppResources.resx +++ b/src/App/Resources/AppResources.resx @@ -925,4 +925,8 @@ Authenticator Key (TOTP) + + Verification Code (TOTP) + Totp code label + \ No newline at end of file diff --git a/src/App/Services/TokenService.cs b/src/App/Services/TokenService.cs index ed966f6b1..b9d2a2977 100644 --- a/src/App/Services/TokenService.cs +++ b/src/App/Services/TokenService.cs @@ -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())); + return Helpers.Epoc.AddSeconds(Convert.ToDouble(decoded["exp"].Value())); } } @@ -97,6 +96,7 @@ namespace Bit.App.Services public string TokenUserId => DecodeToken()?["sub"].Value(); public string TokenEmail => DecodeToken()?["email"].Value(); public string TokenName => DecodeToken()?["name"].Value(); + public bool TokenPremium => (DecodeToken()?["premium"].Value()).GetValueOrDefault(false); public string RefreshToken { diff --git a/src/App/Utilities/Base32.cs b/src/App/Utilities/Base32.cs new file mode 100644 index 000000000..9d036360d --- /dev/null +++ b/src/App/Utilities/Base32.cs @@ -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; + } + } +} diff --git a/src/App/Utilities/Crypto.cs b/src/App/Utilities/Crypto.cs index ba6800352..20abfda3e 100644 --- a/src/App/Utilities/Crypto.cs +++ b/src/App/Utilities/Crypto.cs @@ -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'); + } } } diff --git a/src/App/Utilities/Helpers.cs b/src/App/Utilities/Helpers.cs index dcd0894f4..dda103ff1 100644 --- a/src/App/Utilities/Helpers.cs +++ b/src/App/Utilities/Helpers.cs @@ -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 iOS = default(T), T Android = default(T), T WinPhone = default(T), T Windows = default(T)) {