mirror of
https://github.com/bitwarden/mobile.git
synced 2024-11-27 12:26:31 +01:00
totp code generation on view login
This commit is contained in:
parent
9879f074b4
commit
26c110291e
@ -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; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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" />
|
||||||
|
@ -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; }
|
||||||
}
|
}
|
||||||
|
@ -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; }
|
||||||
|
@ -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
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -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)
|
||||||
|
@ -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;
|
||||||
|
@ -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
|
||||||
{
|
{
|
||||||
|
9
src/App/Resources/AppResources.Designer.cs
generated
9
src/App/Resources/AppResources.Designer.cs
generated
@ -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>
|
||||||
|
@ -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>
|
@ -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
|
||||||
{
|
{
|
||||||
|
72
src/App/Utilities/Base32.cs
Normal file
72
src/App/Utilities/Base32.cs
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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))
|
||||||
{
|
{
|
||||||
|
Loading…
Reference in New Issue
Block a user