mirror of
https://github.com/bitwarden/mobile
synced 2025-02-05 12:58:51 +01:00
totp service
This commit is contained in:
parent
f46151bb71
commit
f48aa24129
@ -14,6 +14,9 @@ EndProject
|
|||||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Playground", "test\Playground\Playground.csproj", "{9C8DA5A8-904D-466F-B9B0-1A4AB5A9AFC3}"
|
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Playground", "test\Playground\Playground.csproj", "{9C8DA5A8-904D-466F-B9B0-1A4AB5A9AFC3}"
|
||||||
EndProject
|
EndProject
|
||||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{D10CA4A9-F866-40E1-B658-F69051236C71}"
|
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
|
EndProject
|
||||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{8904C536-C67D-420F-9971-51B26574C3AA}"
|
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{8904C536-C67D-420F-9971-51B26574C3AA}"
|
||||||
EndProject
|
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 LockOptionKey = "lockOption";
|
||||||
public static string PinProtectedKey = "pinProtectedKey";
|
public static string PinProtectedKey = "pinProtectedKey";
|
||||||
public static string DefaultUriMatch = "defaultUriMatch";
|
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 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()
|
public static bool InDebugMode()
|
||||||
{
|
{
|
||||||
#if DEBUG
|
#if DEBUG
|
||||||
@ -150,5 +155,29 @@ namespace Bit.Core.Utilities
|
|||||||
}
|
}
|
||||||
return null;
|
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…
x
Reference in New Issue
Block a user