diff --git a/src/App/Models/OtpAuth.cs b/src/App/Models/OtpAuth.cs new file mode 100644 index 000000000..01f2e0b5d --- /dev/null +++ b/src/App/Models/OtpAuth.cs @@ -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; } + } +} diff --git a/src/App/Models/Page/VaultViewCipherPageModel.cs b/src/App/Models/Page/VaultViewCipherPageModel.cs index 6cf4f2776..9c7f82be7 100644 --- a/src/App/Models/Page/VaultViewCipherPageModel.cs +++ b/src/App/Models/Page/VaultViewCipherPageModel.cs @@ -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 diff --git a/src/App/Pages/Vault/VaultViewCipherPage.cs b/src/App/Pages/Vault/VaultViewCipherPage.cs index 82ad89c68..477e3bd16 100644 --- a/src/App/Pages/Vault/VaultViewCipherPage.cs +++ b/src/App/Pages/Vault/VaultViewCipherPage.cs @@ -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) { diff --git a/src/App/Utilities/Crypto.cs b/src/App/Utilities/Crypto.cs index 3cf6b9a6a..3e2b748cc 100644 --- a/src/App/Utilities/Crypto.cs +++ b/src/App/Utilities/Crypto.cs @@ -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 diff --git a/src/App/Utilities/Helpers.cs b/src/App/Utilities/Helpers.cs index 5bac1cce3..9c2605457 100644 --- a/src/App/Utilities/Helpers.cs +++ b/src/App/Utilities/Helpers.cs @@ -533,5 +533,30 @@ namespace Bit.App.Utilities page.DisplayAlert(AppResources.InternetConnectionRequiredTitle, AppResources.InternetConnectionRequiredMessage, AppResources.Ok); } + + public static Dictionary GetQueryParams(string urlString) + { + var dict = new Dictionary(); + 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; + } } }