mirror of
https://github.com/bitwarden/mobile.git
synced 2024-12-25 16:47:55 +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 bool LoginTotpLow => LoginTotpSecond <= 7;
|
||||||
public Color LoginTotpColor => !string.IsNullOrWhiteSpace(LoginTotpCode) && LoginTotpLow ?
|
public Color LoginTotpColor => !string.IsNullOrWhiteSpace(LoginTotpCode) && LoginTotpLow ?
|
||||||
Color.Red : Color.Black;
|
Color.Red : Color.Black;
|
||||||
public string LoginTotpCodeFormatted => !string.IsNullOrWhiteSpace(LoginTotpCode) ?
|
public string LoginTotpCodeFormatted
|
||||||
string.Format("{0} {1}", LoginTotpCode.Substring(0, 3), LoginTotpCode.Substring(3)) : null;
|
{
|
||||||
|
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
|
// Card
|
||||||
public string CardName
|
public string CardName
|
||||||
|
@ -412,10 +412,11 @@ namespace Bit.App.Pages
|
|||||||
var totpKey = cipher.Login?.Totp.Decrypt(cipher.OrganizationId);
|
var totpKey = cipher.Login?.Totp.Decrypt(cipher.OrganizationId);
|
||||||
if(!string.IsNullOrWhiteSpace(totpKey))
|
if(!string.IsNullOrWhiteSpace(totpKey))
|
||||||
{
|
{
|
||||||
|
var otpParams = new OtpAuth(totpKey);
|
||||||
Model.LoginTotpCode = Crypto.Totp(totpKey);
|
Model.LoginTotpCode = Crypto.Totp(totpKey);
|
||||||
if(!string.IsNullOrWhiteSpace(Model.LoginTotpCode))
|
if(!string.IsNullOrWhiteSpace(Model.LoginTotpCode))
|
||||||
{
|
{
|
||||||
TotpTick(totpKey);
|
TotpTick(totpKey, otpParams.Period);
|
||||||
_timerStarted = DateTime.Now;
|
_timerStarted = DateTime.Now;
|
||||||
Device.StartTimer(new TimeSpan(0, 0, 1), () =>
|
Device.StartTimer(new TimeSpan(0, 0, 1), () =>
|
||||||
{
|
{
|
||||||
@ -424,7 +425,7 @@ namespace Bit.App.Pages
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
TotpTick(totpKey);
|
TotpTick(totpKey, otpParams.Period);
|
||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -529,11 +530,11 @@ namespace Bit.App.Pages
|
|||||||
_deviceActionService.Toast(string.Format(AppResources.ValueHasBeenCopied, alertLabel));
|
_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 now = Helpers.EpocUtcNow() / 1000;
|
||||||
var mod = now % 30;
|
var mod = now % interval;
|
||||||
Model.LoginTotpSecond = (int)(30 - mod);
|
Model.LoginTotpSecond = (int)(interval - mod);
|
||||||
|
|
||||||
if(mod == 0)
|
if(mod == 0)
|
||||||
{
|
{
|
||||||
|
@ -177,16 +177,17 @@ namespace Bit.App.Utilities
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ref: https://github.com/mirthas/totp-net/blob/master/TOTP/Totp.cs
|
// 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);
|
var otpParams = new OtpAuth(key);
|
||||||
if(key == null || key.Length == 0)
|
var b32Key = Base32.FromBase32(otpParams.Secret);
|
||||||
|
if(b32Key == null || b32Key.Length == 0)
|
||||||
{
|
{
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
var now = Helpers.EpocUtcNow() / 1000;
|
var now = Helpers.EpocUtcNow() / 1000;
|
||||||
var sec = now / 30;
|
var sec = now / otpParams.Period;
|
||||||
|
|
||||||
var secBytes = BitConverter.GetBytes(sec);
|
var secBytes = BitConverter.GetBytes(sec);
|
||||||
if(BitConverter.IsLittleEndian)
|
if(BitConverter.IsLittleEndian)
|
||||||
@ -194,17 +195,17 @@ namespace Bit.App.Utilities
|
|||||||
Array.Reverse(secBytes, 0, secBytes.Length);
|
Array.Reverse(secBytes, 0, secBytes.Length);
|
||||||
}
|
}
|
||||||
|
|
||||||
var algorithm = WinRTCrypto.MacAlgorithmProvider.OpenAlgorithm(MacAlgorithm.HmacSha1);
|
var algorithm = WinRTCrypto.MacAlgorithmProvider.OpenAlgorithm(otpParams.Algorithm);
|
||||||
var hasher = algorithm.CreateHash(key);
|
var hasher = algorithm.CreateHash(b32Key);
|
||||||
hasher.Append(secBytes);
|
hasher.Append(secBytes);
|
||||||
var hash = hasher.GetValueAndReset();
|
var hash = hasher.GetValueAndReset();
|
||||||
|
|
||||||
var offset = (hash[hash.Length - 1] & 0xf);
|
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);
|
((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
|
// ref: https://tools.ietf.org/html/rfc5869
|
||||||
|
@ -533,5 +533,30 @@ namespace Bit.App.Utilities
|
|||||||
page.DisplayAlert(AppResources.InternetConnectionRequiredTitle,
|
page.DisplayAlert(AppResources.InternetConnectionRequiredTitle,
|
||||||
AppResources.InternetConnectionRequiredMessage, AppResources.Ok);
|
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