mirror of
https://github.com/bitwarden/mobile.git
synced 2024-12-24 16:38:22 +01:00
support otpath:// totp secrets
This commit is contained in:
parent
4e4b56d7fe
commit
acdfce7e88
58
src/App/Models/OtpAuth.cs
Normal file
58
src/App/Models/OtpAuth.cs
Normal 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; }
|
||||
}
|
||||
}
|
@ -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
|
||||
|
@ -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)
|
||||
{
|
||||
|
@ -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
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user