support otpath:// totp secrets

This commit is contained in:
Kyle Spearrin 2018-07-31 12:34:07 -04:00
parent 4e4b56d7fe
commit acdfce7e88
5 changed files with 111 additions and 16 deletions

58
src/App/Models/OtpAuth.cs Normal file
View file

@ -0,0 +1,58 @@
using Bit.App.Utilities;
using PCLCrypto;
namespace Bit.App.Models
{
public class OtpAuth
{
public OtpAuth(string key)
{
if(key?.ToLowerInvariant().StartsWith("otpauth://") ?? false)
{
var qsParams = Helpers.GetQueryParams(key);
if(qsParams.ContainsKey("digits") && qsParams["digits"] != null &&
int.TryParse(qsParams["digits"].Trim(), out var digitParam))
{
if(digitParam > 10)
{
Digits = 10;
}
else if(digitParam > 0)
{
Digits = digitParam;
}
}
if(qsParams.ContainsKey("period") && qsParams["period"] != null &&
int.TryParse(qsParams["period"].Trim(), out var periodParam) && periodParam > 0)
{
Period = periodParam;
}
if(qsParams.ContainsKey("secret") && qsParams["secret"] != null)
{
Secret = qsParams["secret"];
}
if(qsParams.ContainsKey("algorithm") && qsParams["algorithm"] != null)
{
var algParam = qsParams["algorithm"].ToLowerInvariant();
if(algParam == "sha256")
{
Algorithm = MacAlgorithm.HmacSha256;
}
else if(algParam == "sha512")
{
Algorithm = MacAlgorithm.HmacSha512;
}
}
}
else
{
Secret = key;
}
}
public int Period { get; set; } = 30;
public int Digits { get; set; } = 6;
public MacAlgorithm Algorithm { get; set; } = MacAlgorithm.HmacSha1;
public string Secret { get; set; }
}
}

View file

@ -178,8 +178,18 @@ namespace Bit.App.Models.Page
public bool LoginTotpLow => LoginTotpSecond <= 7;
public Color LoginTotpColor => !string.IsNullOrWhiteSpace(LoginTotpCode) && LoginTotpLow ?
Color.Red : Color.Black;
public string LoginTotpCodeFormatted => !string.IsNullOrWhiteSpace(LoginTotpCode) ?
string.Format("{0} {1}", LoginTotpCode.Substring(0, 3), LoginTotpCode.Substring(3)) : null;
public string LoginTotpCodeFormatted
{
get
{
if(string.IsNullOrWhiteSpace(LoginTotpCode) || LoginTotpCode.Length < 5)
{
return LoginTotpCode;
}
var half = (int)Math.Floor((double)LoginTotpCode.Length / 2);
return string.Format("{0} {1}", LoginTotpCode.Substring(0, half), LoginTotpCode.Substring(half));
}
}
// Card
public string CardName

View file

@ -412,10 +412,11 @@ namespace Bit.App.Pages
var totpKey = cipher.Login?.Totp.Decrypt(cipher.OrganizationId);
if(!string.IsNullOrWhiteSpace(totpKey))
{
var otpParams = new OtpAuth(totpKey);
Model.LoginTotpCode = Crypto.Totp(totpKey);
if(!string.IsNullOrWhiteSpace(Model.LoginTotpCode))
{
TotpTick(totpKey);
TotpTick(totpKey, otpParams.Period);
_timerStarted = DateTime.Now;
Device.StartTimer(new TimeSpan(0, 0, 1), () =>
{
@ -424,7 +425,7 @@ namespace Bit.App.Pages
return false;
}
TotpTick(totpKey);
TotpTick(totpKey, otpParams.Period);
return true;
});
@ -529,11 +530,11 @@ namespace Bit.App.Pages
_deviceActionService.Toast(string.Format(AppResources.ValueHasBeenCopied, alertLabel));
}
private void TotpTick(string totpKey)
private void TotpTick(string totpKey, int interval)
{
var now = Helpers.EpocUtcNow() / 1000;
var mod = now % 30;
Model.LoginTotpSecond = (int)(30 - mod);
var mod = now % interval;
Model.LoginTotpSecond = (int)(interval - mod);
if(mod == 0)
{

View file

@ -177,16 +177,17 @@ namespace Bit.App.Utilities
}
// ref: https://github.com/mirthas/totp-net/blob/master/TOTP/Totp.cs
public static string Totp(string b32Key)
public static string Totp(string key)
{
var key = Base32.FromBase32(b32Key);
if(key == null || key.Length == 0)
var otpParams = new OtpAuth(key);
var b32Key = Base32.FromBase32(otpParams.Secret);
if(b32Key == null || b32Key.Length == 0)
{
return null;
}
var now = Helpers.EpocUtcNow() / 1000;
var sec = now / 30;
var sec = now / otpParams.Period;
var secBytes = BitConverter.GetBytes(sec);
if(BitConverter.IsLittleEndian)
@ -194,17 +195,17 @@ namespace Bit.App.Utilities
Array.Reverse(secBytes, 0, secBytes.Length);
}
var algorithm = WinRTCrypto.MacAlgorithmProvider.OpenAlgorithm(MacAlgorithm.HmacSha1);
var hasher = algorithm.CreateHash(key);
var algorithm = WinRTCrypto.MacAlgorithmProvider.OpenAlgorithm(otpParams.Algorithm);
var hasher = algorithm.CreateHash(b32Key);
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) |
var binary = ((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);
var otp = binary % (int)Math.Pow(10, otpParams.Digits);
return code.ToString().PadLeft(6, '0');
return otp.ToString().PadLeft(otpParams.Digits, '0');
}
// ref: https://tools.ietf.org/html/rfc5869

View file

@ -533,5 +533,30 @@ namespace Bit.App.Utilities
page.DisplayAlert(AppResources.InternetConnectionRequiredTitle,
AppResources.InternetConnectionRequiredMessage, AppResources.Ok);
}
public static Dictionary<string, string> GetQueryParams(string urlString)
{
var dict = new Dictionary<string, string>();
if(!Uri.TryCreate(urlString, UriKind.Absolute, out var uri) || string.IsNullOrWhiteSpace(uri.Query))
{
return dict;
}
var pairs = uri.Query.Substring(1).Split('&');
foreach(var pair in pairs)
{
var parts = pair.Split('=');
if(parts.Length < 1)
{
continue;
}
var key = System.Net.WebUtility.UrlDecode(parts[0]).ToLower();
if(!dict.ContainsKey(key))
{
dict.Add(key, parts[1] == null ? string.Empty : System.Net.WebUtility.UrlDecode(parts[1]));
}
}
return dict;
}
}
}