mirror of
https://github.com/bitwarden/android.git
synced 2024-12-24 01:48:25 +03:00
totp code generation on view login
This commit is contained in:
parent
9879f074b4
commit
26c110291e
14 changed files with 251 additions and 19 deletions
|
@ -18,5 +18,6 @@ namespace Bit.App.Abstractions
|
|||
string TokenUserId { get; }
|
||||
string TokenEmail { get; }
|
||||
string TokenName { get; }
|
||||
bool TokenPremium { get; }
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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" />
|
||||
|
|
|
@ -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; }
|
||||
}
|
||||
|
|
|
@ -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; }
|
||||
|
|
|
@ -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
|
||||
};
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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
|
||||
{
|
||||
|
|
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>
|
||||
/// Looks up a localized string similar to Could not send verification email. Try again..
|
||||
/// </summary>
|
||||
|
|
|
@ -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>
|
|
@ -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
|
||||
{
|
||||
|
|
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;
|
||||
}
|
||||
|
||||
// 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 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))
|
||||
{
|
||||
|
|
Loading…
Reference in a new issue