/
Totp.cs
132 lines (116 loc) · 5.4 KB
/
Totp.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
// Copyright (c) Arch team. All rights reserved.
using System;
using System.Diagnostics;
using System.Net;
using System.Security.Cryptography;
using System.Text;
namespace Arch.Standard.Utils
{
/// <summary>
/// https://tools.ietf.org/html/rfc6238
/// </summary>
public static class Totp
{
private static readonly DateTime _unixEpoch = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);
private static TimeSpan _timestep = TimeSpan.FromMinutes(3);
private static readonly Encoding _encoding = new UTF8Encoding(false, true);
private 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);
}
/// <summary>
/// Generates code for the specified <paramref name="securityToken"/>.
/// </summary>
/// <param name="securityToken">The security token to generate code.</param>
/// <param name="modifier">The modifier.</param>
/// <returns>The generated code.</returns>
public static int GenerateCode(byte[] securityToken, string modifier = null)
{
if (securityToken == null)
{
throw new ArgumentNullException(nameof(securityToken));
}
// Allow a variance of no greater than 90 seconds in either direction
var currentTimeStep = GetCurrentTimeStepNumber();
using (var hashAlgorithm = new HMACSHA1(securityToken))
{
return ComputeTotp(hashAlgorithm, currentTimeStep, modifier);
}
}
/// <summary>
/// Validates the code for the specified <paramref name="securityToken"/>.
/// </summary>
/// <param name="securityToken">The security token for verifying.</param>
/// <param name="code">The code to validate.</param>
/// <param name="modifier">The modifier</param>
/// <returns><c>True</c> if validate succeed, otherwise, <c>false</c>.</returns>
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 90 seconds 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;
}
/// <summary>
/// Generates code for the specified <paramref name="securityToken"/>.
/// </summary>
/// <param name="securityToken">The security token to generate code.</param>
/// <param name="modifier">The modifier.</param>
/// <returns>The generated code.</returns>
public static int GenerateCode(string securityToken, string modifier = null) => GenerateCode(Encoding.Unicode.GetBytes(securityToken), modifier);
/// <summary>
/// Validates the code for the specified <paramref name="securityToken"/>.
/// </summary>
/// <param name="securityToken">The security token for verifying.</param>
/// <param name="code">The code to validate.</param>
/// <param name="modifier">The modifier</param>
/// <returns><c>True</c> if validate succeed, otherwise, <c>false</c>.</returns>
public static bool ValidateCode(string securityToken, int code, string modifier = null) => ValidateCode(Encoding.Unicode.GetBytes(securityToken), code, modifier);
}
}