Rfc6238AuthenticationService 时间过期解读
源码
// Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System.Diagnostics; using System.Net; using System.Security.Cryptography; namespace Microsoft.AspNetCore.Identity { using System; using System.Text; internal static class Rfc6238AuthenticationService { private static readonly DateTime _unixEpoch = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc); private static readonly TimeSpan _timestep = TimeSpan.FromMinutes(3); private static readonly Encoding _encoding = new UTF8Encoding(false, true); private static readonly RandomNumberGenerator _rng = RandomNumberGenerator.Create(); // Generates a new 80-bit security token public static byte[] GenerateRandomKey() { byte[] bytes = new byte[20]; _rng.GetBytes(bytes); return bytes; } internal static int ComputeTotp(HashAlgorithm hashAlgorithm, ulong timestepNumber, string modifier) { // # of 0's = length of pin const int Mod = 1000000; // See https://tools.ietf.org/html/rfc4226 // We can add an optional modifier var timestepAsBytes = BitConverter.GetBytes(IPAddress.HostToNetworkOrder((long)timestepNumber)); var hash = hashAlgorithm.ComputeHash(ApplyModifier(timestepAsBytes, modifier)); // Generate DT string var offset = hash[hash.Length - 1] & 0xf; Debug.Assert(offset + 4 < hash.Length); var binaryCode = (hash[offset] & 0x7f) << 24 | (hash[offset + 1] & 0xff) << 16 | (hash[offset + 2] & 0xff) << 8 | (hash[offset + 3] & 0xff); return binaryCode % Mod; } private static byte[] ApplyModifier(byte[] input, string modifier) { if (String.IsNullOrEmpty(modifier)) { return input; } var modifierBytes = _encoding.GetBytes(modifier); var combined = new byte[checked(input.Length + modifierBytes.Length)]; Buffer.BlockCopy(input, 0, combined, 0, input.Length); Buffer.BlockCopy(modifierBytes, 0, combined, input.Length, modifierBytes.Length); return combined; } // More info: https://tools.ietf.org/html/rfc6238#section-4 private static ulong GetCurrentTimeStepNumber() { var delta = DateTime.UtcNow - _unixEpoch; return (ulong)(delta.Ticks / _timestep.Ticks); } public static int GenerateCode(byte[] securityToken, string modifier = null) { if (securityToken == null) { throw new ArgumentNullException(nameof(securityToken)); } // Allow a variance of no greater than 9 minutes in either direction var currentTimeStep = GetCurrentTimeStepNumber(); using (var hashAlgorithm = new HMACSHA1(securityToken)) { return ComputeTotp(hashAlgorithm, currentTimeStep, modifier); } } public static bool ValidateCode(byte[] securityToken, int code, string modifier = null) { if (securityToken == null) { throw new ArgumentNullException(nameof(securityToken)); } // Allow a variance of no greater than 9 minutes in either direction var currentTimeStep = GetCurrentTimeStepNumber(); using (var hashAlgorithm = new HMACSHA1(securityToken)) { for (var i = -2; i <= 2; i++) { var computedTotp = ComputeTotp(hashAlgorithm, (ulong)((long)currentTimeStep + i), modifier); if (computedTotp == code) { return true; } } } // No match return false; } } }
解读:
GenerateRandomKey方法能得到一个3分钟内一致的token
而 for i = -2到2 前后就允许有3个3分钟的间隔,就是9分钟,前面的9分钟忽略,正常的业务就是9分钟内有效
for (var i = -2; i <= 2; i++)
{
var computedTotp = ComputeTotp(hashAlgorithm, (ulong)((long)currentTimeStep + i), modifier);
if (computedTotp == code)
{
return true;
}
}
测试
using System; using Microsoft.VisualStudio.TestTools.UnitTesting; using System.Text; using System.Security.Cryptography; using System.Diagnostics; using System.Net; using System.Threading; namespace UnitTestProject { [TestClass] public class UnitTest { [TestMethod] public void TestMethod() { var token = Rfc6238AuthenticationService.GenerateCode(Encoding.UTF8.GetBytes("1"), "2"); // var code = int.Parse(token); Thread.Sleep(1000*10*4); var @is = Rfc6238AuthenticationService.ValidateCode(Encoding.UTF8.GetBytes("1"), token, "2"); }
40s 得 false,
10s得 true
大于30s是false,小于30s得true,为什么不是10s呢,是因为 i = -2在起作用,懂了吗 } internal static class Rfc6238AuthenticationService { private static readonly DateTime _unixEpoch = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc); private static readonly TimeSpan _timestep = TimeSpan.FromSeconds(10); private static readonly Encoding _encoding = new UTF8Encoding(false, true); private static readonly RandomNumberGenerator _rng = RandomNumberGenerator.Create(); // Generates a new 80-bit security token public static byte[] GenerateRandomKey() { byte[] bytes = new byte[20]; _rng.GetBytes(bytes); return bytes; } internal static int ComputeTotp(HashAlgorithm hashAlgorithm, ulong timestepNumber, string modifier) { // # of 0's = length of pin const int Mod = 1000000; // See https://tools.ietf.org/html/rfc4226 // We can add an optional modifier var timestepAsBytes = BitConverter.GetBytes(IPAddress.HostToNetworkOrder((long)timestepNumber)); var hash = hashAlgorithm.ComputeHash(ApplyModifier(timestepAsBytes, modifier)); // Generate DT string var offset = hash[hash.Length - 1] & 0xf; Debug.Assert(offset + 4 < hash.Length); var binaryCode = (hash[offset] & 0x7f) << 24 | (hash[offset + 1] & 0xff) << 16 | (hash[offset + 2] & 0xff) << 8 | (hash[offset + 3] & 0xff); return binaryCode % Mod; } private static byte[] ApplyModifier(byte[] input, string modifier) { if (String.IsNullOrEmpty(modifier)) { return input; } var modifierBytes = _encoding.GetBytes(modifier); var combined = new byte[checked(input.Length + modifierBytes.Length)]; Buffer.BlockCopy(input, 0, combined, 0, input.Length); Buffer.BlockCopy(modifierBytes, 0, combined, input.Length, modifierBytes.Length); return combined; } // More info: https://tools.ietf.org/html/rfc6238#section-4 private static ulong GetCurrentTimeStepNumber() { var delta = DateTime.UtcNow - _unixEpoch; return (ulong)(delta.Ticks / _timestep.Ticks); } public static int GenerateCode(byte[] securityToken, string modifier = null) { if (securityToken == null) { throw new ArgumentNullException(nameof(securityToken)); } // Allow a variance of no greater than 9 minutes in either direction var currentTimeStep = GetCurrentTimeStepNumber(); using (var hashAlgorithm = new HMACSHA1(securityToken)) { return ComputeTotp(hashAlgorithm, currentTimeStep, modifier); } } public static bool ValidateCode(byte[] securityToken, int code, string modifier = null) { if (securityToken == null) { throw new ArgumentNullException(nameof(securityToken)); } // Allow a variance of no greater than 9 minutes in either direction var currentTimeStep = GetCurrentTimeStepNumber(); using (var hashAlgorithm = new HMACSHA1(securityToken)) { for (var i = -2; i <= 2; i++) { var computedTotp = ComputeTotp(hashAlgorithm, (ulong)((long)currentTimeStep + i), modifier); if (computedTotp == code) { return true; } } } // No match return false; } } }