totp service

This commit is contained in:
Kyle Spearrin 2019-04-17 16:01:07 -04:00
parent f46151bb71
commit f48aa24129
6 changed files with 258 additions and 0 deletions

View file

@ -14,6 +14,9 @@ EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Playground", "test\Playground\Playground.csproj", "{9C8DA5A8-904D-466F-B9B0-1A4AB5A9AFC3}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{D10CA4A9-F866-40E1-B658-F69051236C71}"
ProjectSection(SolutionItems) = preProject
src\Core\Services\ITotpService.cs = src\Core\Services\ITotpService.cs
EndProjectSection
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{8904C536-C67D-420F-9971-51B26574C3AA}"
EndProject

View file

@ -0,0 +1,11 @@
using System.Threading.Tasks;
namespace Bit.Core.Abstractions
{
public interface ITotpService
{
Task<string> GetCodeAsync(string key);
int GetTimeInterval(string key);
Task<bool> IsAutoCopyEnabledAsync();
}
}

View file

@ -7,5 +7,6 @@
public static string LockOptionKey = "lockOption";
public static string PinProtectedKey = "pinProtectedKey";
public static string DefaultUriMatch = "defaultUriMatch";
public static string DisableAutoTotpCopyKey = "disableAutoTotpCopy";
}
}

View file

@ -0,0 +1,142 @@
using Bit.Core.Abstractions;
using Bit.Core.Enums;
using Bit.Core.Utilities;
using System;
using System.Threading.Tasks;
namespace Bit.Core.Services
{
public class TotpService : ITotpService
{
private const string SteamChars = "23456789BCDFGHJKMNPQRTVWXY";
private readonly IStorageService _storageService;
private readonly ICryptoFunctionService _cryptoFunctionService;
public TotpService(
IStorageService storageService,
ICryptoFunctionService cryptoFunctionService)
{
_storageService = storageService;
_cryptoFunctionService = cryptoFunctionService;
}
public async Task<string> GetCodeAsync(string key)
{
if(string.IsNullOrWhiteSpace(key))
{
return null;
}
var period = 30;
var alg = CryptoHashAlgorithm.Sha1;
var digits = 6;
var keyB32 = key;
var isOtpAuth = key?.ToLowerInvariant().StartsWith("otpauth://") ?? false;
var isSteamAuth = key?.ToLowerInvariant().StartsWith("steam://") ?? false;
if(isOtpAuth)
{
var qsParams = CoreHelpers.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)
{
keyB32 = qsParams["secret"];
}
if(qsParams.ContainsKey("algorithm") && qsParams["algorithm"] != null)
{
var algParam = qsParams["algorithm"].ToLowerInvariant();
if(algParam == "sha256")
{
alg = CryptoHashAlgorithm.Sha256;
}
else if(algParam == "sha512")
{
alg = CryptoHashAlgorithm.Sha512;
}
}
}
else if(isSteamAuth)
{
digits = 5;
keyB32 = key.Substring(8);
}
var keyBytes = Base32.FromBase32(keyB32);
if(keyBytes == null || keyBytes.Length == 0)
{
return null;
}
var now = CoreHelpers.EpocUtcNow() / 1000;
var time = now / period;
var timeBytes = BitConverter.GetBytes(time);
if(BitConverter.IsLittleEndian)
{
Array.Reverse(timeBytes, 0, timeBytes.Length);
}
var hash = await _cryptoFunctionService.HmacAsync(timeBytes, keyBytes, alg);
if(hash.Length == 0)
{
return null;
}
var offset = (hash[hash.Length - 1] & 0xf);
var binary = ((hash[offset] & 0x7f) << 24) | ((hash[offset + 1] & 0xff) << 16) |
((hash[offset + 2] & 0xff) << 8) | (hash[offset + 3] & 0xff);
string otp = string.Empty;
if(isSteamAuth)
{
var fullCode = binary & 0x7fffffff;
for(var i = 0; i < digits; i++)
{
otp += SteamChars[fullCode % SteamChars.Length];
fullCode = (int)Math.Truncate(fullCode / (double)SteamChars.Length);
}
}
else
{
var rawOtp = binary % (int)Math.Pow(10, digits);
otp = rawOtp.ToString().PadLeft(digits, '0');
}
return otp;
}
public int GetTimeInterval(string key)
{
var period = 30;
if(key != null && key.ToLowerInvariant().StartsWith("otpauth://"))
{
var qsParams = CoreHelpers.GetQueryParams(key);
if(qsParams.ContainsKey("period") && qsParams["period"] != null &&
int.TryParse(qsParams["period"].Trim(), out var periodParam) && periodParam > 0)
{
period = periodParam;
}
}
return period;
}
public async Task<bool> IsAutoCopyEnabledAsync()
{
var disabled = await _storageService.GetAsync<bool?>(Constants.DisableAutoTotpCopyKey);
return !disabled.GetValueOrDefault();
}
}
}

View file

@ -0,0 +1,72 @@
using System;
namespace Bit.Core.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;
}
}
}

View file

@ -19,6 +19,11 @@ namespace Bit.Core.Utilities
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 bool InDebugMode()
{
#if DEBUG
@ -150,5 +155,29 @@ namespace Bit.Core.Utilities
}
return null;
}
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;
}
}
}