目录
移动应用程序允许您向Microsoft/Google或任何其他TOTP身份验证器应用程序(通过专门生成的二维码)注册您的帐户。成功注册后,验证器应用程序将每30秒生成一个新代码,该代码可用于实现基于MFA的登录。要使其成为完整的MFA,需要将PIN作为前缀添加到应用程序生成的代码中。登录密码或称为密码的人将是PIN+代码。
介绍
基于时间的一次性密码算法(RFC-6238、TOTP——基于HMAC的一次性密码算法)的令牌,由Microsoft或Google Authenticator移动应用程序等实现。移动应用程序允许您向Microsoft/Google或任何其他TOTP身份验证器应用程序(通过专门生成的二维码)注册您的帐户。成功注册后,验证器应用程序将每30秒生成一个新代码,该代码可用于实现基于MFA的登录。要使其成为完整的MFA,需要将PIN作为前缀添加到应用程序生成的代码中。登录密码或称为密码的人将是PIN +代码。
背景
为了保护对任何C#、Java或C++(Windows或Linux)Web或普通应用程序的访问,MFA是最佳且简单的选择,无需创建您自己的自定义移动应用程序。它完成了场景,即你知道的东西和你拥有的东西。在这里,您知道的是您的PIN,您拥有的是您的移动应用程序和由 Microsoft Authenticator 等身份验证器移动应用程序强制执行的生物功能。
注册二维码
身份验证器应用程序(Microsoft和Google)遵循一个标准。但是,只有Google定义了向Authenticator应用程序注册帐户所需的 URI和参数。
从逻辑上讲,第一步是能够生成QR码,以便向身份验证器应用程序注册所需的用户。这里的神奇成分是TOTP种子、用户所属的公司/Web应用程序以及用户的UPN或电子邮件地址。
下面的代码使用GUID生成种子(我使用GUID,因为重新生成同一GUID的几率是20亿分之一):
/**
* Converts Hex string to Unsigned Bytes (0 to 256)
*/
public static Byte[] HexToByte(string hexStr)
{
byte[] bArray = new byte[hexStr.Length / 2];
for (int i = 0; i < (hexStr.Length / 2); i++)
{
byte firstNibble = Byte.Parse(hexStr.Substring((2 * i), 1), System.Globalization.NumberStyles.HexNumber); // [x,y)
byte secondNibble = Byte.Parse(hexStr.Substring((2 * i) + 1, 1), System.Globalization.NumberStyles.HexNumber);
int finalByte = (secondNibble) | (firstNibble << 4); // bit-operations only with numbers, not bytes.
bArray[i] = (byte)finalByte;
}
return bArray;
}
/*
* Generates GUID as a string and remove brackets
*/
public static string getNewId()
{
string sR = Guid.NewGuid().ToString().ToUpper();
sR = sR.Replace("{", "");
sR = sR.Replace("}", "");
return sR;
}
/*
* Generates the QR code for authenticator as a base64 encoded svg image
* You must use something like
* <img runat="server" id="qrCode" name="qrCode" src="javascript:" alt="Scan this QR code with your mobile application" style="height:300px;width:300px"/>
*/
private void generateQRCode()
{
//create new key based on hash to be used
string seed = getNewId() + getNewId();
seed = seed.Replace("-", "");
seed = seed.Substring(0, 40);
byte[] byteSeed = HexToByte(seed);
//Must save this seed to be able to validate the TOTP
var KeyString = Base32.ToBase32String(byteSeed);
string orgDomain = "elogic.synology.me";
string orgName = "eLogic Builders Inc.";
string userUPN = "Kashif" + '@' + orgDomain;
const string AuthenticatorUriFormat = "otpauth://totp/{0}:{1}?secret={2}&issuer={0}&algorithm=SHA1&digits=6&period=30";
string tokenURI = string.Format(
AuthenticatorUriFormat,
HttpUtility.UrlEncode(orgDomain),
HttpUtility.UrlEncode(userUPN),
KeyString);
var qr = QrCode.EncodeText(tokenURI, QrCode.Ecc.High);
string base64EncodedImage = Convert.ToBase64String(Encoding.UTF8.GetBytes(qr.ToSvgString(4)));
string imageSrc = "data:image/svg+xml;base64," + base64EncodedImage;
//Assign image here in your ASP application
//this.qrCode.Src = imageSrc;
}
必须保存KeyString(TOTP种子)并将其链接到正在验证的用户。相同的种子将用于验证用户输入的TOTP。为了生成QR码,我使用了Net.Codecrete.QrCodeGenerator nuget.org包。这有利于在Windows和Linux上生成QR码(使用Mono框架)。您可以使用适合您应用程序的其他实现。
以下是我用于发送注册的注册链接示例:
当用户点击链接时,QR码生成和注册序列将启动。以下是呈现给用户的内容:
用户使用Microsoft或Google或任何其他符合RFC-6238标准的TOTP身份验证器应用程序扫描QR码。应用程序应注册种子和用户的UPN,并应开始生成TOTP:
使用下面的 RFC-6238 兼容类,您可以验证生成的TOTP(它是经过少量修改的Microsoft代码示例版本):
using System;
using System.Diagnostics;
using System.Net;
using System.Security.Cryptography;
using System.Text;
class SecurityToken
{
private readonly byte[] _data;
public SecurityToken(byte[] data)
{
_data = (byte[])data.Clone();
}
internal byte[] GetDataNoClone()
{
return _data;
}
}
public 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);
public 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);
}
private static int GenerateCode(SecurityToken securityToken, string modifier = null)
{
if (securityToken == null)
{
throw new ArgumentNullException("securityToken");
}
// Allow a variance of no greater than 9 minutes in either direction
var currentTimeStep = GetCurrentTimeStepNumber();
using (var hashAlgorithm = new HMACSHA1(securityToken.GetDataNoClone()))
{
return ComputeTotp(hashAlgorithm, currentTimeStep, modifier);
}
}
private static bool ValidateCode(SecurityToken securityToken, int code, string modifier = null)
{
if (securityToken == null)
{
throw new ArgumentNullException("securityToken");
}
// Allow a variance of no greater than 9 minutes in either direction
var currentTimeStep = GetCurrentTimeStepNumber();
using (var hashAlgorithm = new HMACSHA1(securityToken.GetDataNoClone()))
{
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;
}
}
以下是可用于验证生成的TOTP的函数:
public bool CheckTimeBasedOTP_Rfc6238(byte[] byteSeed, string incomingOTP)
{
bool bR = false;
int IntIncomingCode = int.Parse(incomingOTP);
var hash = new HMACSHA1(byteSeed);
var unixTimestamp = Convert.ToInt64(Math.Round((DateTime.UtcNow - new DateTime(1970, 1, 1, 0, 0, 0)).TotalSeconds));
var timestep = Convert.ToInt64(unixTimestamp / 30);
// Allow codes from 90s in each direction (we could make this configurable?)
for (long i = -2; i <= 2; i++)
{
var expectedCode = Rfc6238AuthenticationService.ComputeTotp(hash, (ulong)(timestep + i), modifier: null);
if (expectedCode == IntIncomingCode)
{
bR = true;
break;
}
}
return bR;
}
byteSeed是一个字节数组,您可以从Base32编码和保存的种子转换而来。
Base32编码器/解码器:
using System;
using System.Collections.Generic;
using System.Text;
using System.Text.RegularExpressions;
public static class Base32
{
private static readonly char[] _digits = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567".ToCharArray();
private const int _mask = 31;
private const int _shift = 5;
private static int CharToInt(char c)
{
switch (c)
{
case 'A': return 0;
case 'B': return 1;
case 'C': return 2;
case 'D': return 3;
case 'E': return 4;
case 'F': return 5;
case 'G': return 6;
case 'H': return 7;
case 'I': return 8;
case 'J': return 9;
case 'K': return 10;
case 'L': return 11;
case 'M': return 12;
case 'N': return 13;
case 'O': return 14;
case 'P': return 15;
case 'Q': return 16;
case 'R': return 17;
case 'S': return 18;
case 'T': return 19;
case 'U': return 20;
case 'V': return 21;
case 'W': return 22;
case 'X': return 23;
case 'Y': return 24;
case 'Z': return 25;
case '2': return 26;
case '3': return 27;
case '4': return 28;
case '5': return 29;
case '6': return 30;
case '7': return 31;
}
return -1;
}
public static byte[] FromBase32String(string encoded)
{
if (encoded == null)
throw new ArgumentNullException(nameof(encoded));
// Remove whitespace and padding. Note: the padding is used as hint
// to determine how many bits to decode from the last incomplete chunk
// Also, canonicalize to all upper case
encoded = encoded.Trim().TrimEnd('=').ToUpper();
if (encoded.Length == 0)
return new byte[0];
var outLength = encoded.Length * _shift / 8;
var result = new byte[outLength];
var buffer = 0;
var next = 0;
var bitsLeft = 0;
var charValue = 0;
foreach (var c in encoded)
{
charValue = CharToInt(c);
if (charValue < 0)
throw new FormatException("Illegal character: `" + c + "`");
buffer <<= _shift;
buffer |= charValue & _mask;
bitsLeft += _shift;
if (bitsLeft >= 8)
{
result[next++] = (byte)(buffer >> (bitsLeft - 8));
bitsLeft -= 8;
}
}
return result;
}
public static string ToBase32String(byte[] data, bool padOutput = false)
{
return ToBase32String(data, 0, data.Length, padOutput);
}
public static string ToBase32String(byte[] data, int offset, int length, bool padOutput = false)
{
if (data == null)
throw new ArgumentNullException(nameof(data));
if (offset < 0)
throw new ArgumentOutOfRangeException(nameof(offset));
if (length < 0)
throw new ArgumentOutOfRangeException(nameof(length));
if ((offset + length) > data.Length)
throw new ArgumentOutOfRangeException();
if (length == 0)
return "";
// SHIFT is the number of bits per output character, so the length of the
// output is the length of the input multiplied by 8/SHIFT, rounded up.
// The computation below will fail, so don't do it.
if (length >= (1 << 28))
throw new ArgumentOutOfRangeException(nameof(data));
var outputLength = (length * 8 + _shift - 1) / _shift;
var result = new StringBuilder(outputLength);
var last = offset + length;
int buffer = data[offset++];
var bitsLeft = 8;
while (bitsLeft > 0 || offset < last)
{
if (bitsLeft < _shift)
{
if (offset < last)
{
buffer <<= 8;
buffer |= (data[offset++] & 0xff);
bitsLeft += 8;
}
else
{
int pad = _shift - bitsLeft;
buffer <<= pad;
bitsLeft += pad;
}
}
int index = _mask & (buffer >> (bitsLeft - _shift));
bitsLeft -= _shift;
result.Append(_digits[index]);
}
if (padOutput)
{
int padding = 8 - (result.Length % 8);
if (padding > 0) result.Append('=', padding == 8 ? 0 : padding);
}
return result.ToString();
}
}
https://www.codeproject.com/Tips/5384961/How-to-add-a-Multifactor-Authentication-using-Micr