mirror of
https://github.com/bitwarden/android.git
synced 2024-12-24 01:48:25 +03:00
totp service
This commit is contained in:
parent
f46151bb71
commit
f48aa24129
6 changed files with 258 additions and 0 deletions
|
@ -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
|
||||
|
|
11
src/Core/Abstractions/ITotpService.cs
Normal file
11
src/Core/Abstractions/ITotpService.cs
Normal 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();
|
||||
}
|
||||
}
|
|
@ -7,5 +7,6 @@
|
|||
public static string LockOptionKey = "lockOption";
|
||||
public static string PinProtectedKey = "pinProtectedKey";
|
||||
public static string DefaultUriMatch = "defaultUriMatch";
|
||||
public static string DisableAutoTotpCopyKey = "disableAutoTotpCopy";
|
||||
}
|
||||
}
|
||||
|
|
142
src/Core/Services/TotpService.cs
Normal file
142
src/Core/Services/TotpService.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
72
src/Core/Utilities/Base32.cs
Normal file
72
src/Core/Utilities/Base32.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue