一次性密码HOTP/TOTP

OTP一次性密码
OTP是One Time Password的简写,即一次性密码。在平时生活中,我们接触一次性密码的场景非常多,比如在登录账号、找回密码,更改密码和转账操作等等这些场景,其中一些常用到的方式有:

手机短信+短信验证码;

邮件+邮件验证码;

认证器软件+验证码,比如Microsoft Authenticator App,Google Authenticator App等等;

硬件+验证码:比如网银的电子密码器;

这些场景的流程一般都是在用户提供了账号+密码的基础上,让用户再提供一个一次性的验证码来提供一层额外的安全防护。通常情况下,这个验证码是一个6-8位的数字,只能使用一次或者仅在很短的时间内可用(比如5分钟以内)。

HOTP基于消息认证码的一次性密码
HOTP是HMAC-Based One Time Password的缩写,即是基于HMAC(基于Hash的消息认证码)实现的一次性密码。算法细节定义在RFC4226(https://tools.ietf.org/html/rfc4226),算法公式为: HOTP(Key,Counter)  ,拆开是 Truncate(HMAC-SHA-1(Key,Counter)) 。

Key:密钥;

Counter:一个计数器;

HMAC-SHA-1:基于SHA1的HMAC算法的一个函数,返回MAC的值,MAC是一个20bytes(160bits)的字节数组;

Truncate:一个截取数字的函数,以3中的MAC为参数,按照指定规则,得到一个6位或者8位数字

 

TOTP基于时间的一次性密码
TOTP是Time-Based One Time Password的缩写。TOTP是在HOTP的基础上扩展的一个算法,算法细节定义在RFC6238(https://tools.ietf.org/html/rfc6238),其核心在于把HOTP中的counter换成了时间T,可以简单的理解为一个当前时间的时间戳(unixtime)。一般实际应用中会固定一个时间的步长,比如30秒,60秒,120秒等等,也就是说再这个步长的时间内,基于TOTP算法算出的OTP值是一样的。

 

举例:

Nuget:

System.Security.Cryptography.Algorithms

Rfc6238AuthenticationService.cs 

来自:https://github.com/aspnet/AspNetCore  AspNetCore/src/Identity/Extensions.Core/src/Rfc6238AuthenticationService.cs

// 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 OTP
{
    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;
        }
    }
}

Program.cs

using System;
using System.Security.Cryptography;
using System.Text;
using System.Threading;

namespace OTP
{
    class Program
    {
        static void Main(string[] args)
        {
            var code = Rfc6238AuthenticationService.GenerateCode(UTF8Encoding.UTF8.GetBytes("nii"));
            var validateResult = Rfc6238AuthenticationService.ValidateCode(UTF8Encoding.UTF8.GetBytes("nii"), code);


            //HOTP基于消息认证码的一次性密码
            //密钥key
            var key = UTF8Encoding.UTF8.GetBytes("xx_key");
            //计数器
            var counter = UTF8Encoding.UTF8.GetBytes("xx_counter");
            //otp6=068878
            var otp6 = HOTP(key, counter, 6);
            var otp5 = HOTP(key, counter, 6);
            //otp8=33068878
            var otp8 = HOTP(key, counter, 8);

            //TOTP基于时间的一次性密码
            //密钥keyvar
            key = UTF8Encoding.UTF8.GetBytes("xx_key");
            //在10秒内生成,otp是一样的
            for (var i = 0; i < 100000; i++)
            {
                var otp = TOTP(key, 10, 6);
                Console.WriteLine(otp);
                Thread.Sleep(1000);
            }

            Console.WriteLine("Hello World!");

        }

        /// 
        /// HOTP基于消息认证码的一次性密码
        /// 
        /// 
        /// 
        /// 
        /// 
        public static string HOTP(byte[] key, byte[] counter, int length = 6)
        {
            using (var hashAlgorithm = new HMACSHA1(key))
            {
                var hmac = hashAlgorithm.ComputeHash(counter);

                var offset = hmac[hmac.Length - 1] & 0xF;

                var b1 = (hmac[offset] & 0x7F) << 24;
                var b2 = (hmac[offset + 1] & 0xFF) << 16;
                var b3 = (hmac[offset + 2] & 0xFF) << 8;
                var b4 = (hmac[offset + 3] & 0xFF);

                var code = b1 | b2 | b3 | b4;

                var value = code % (int)Math.Pow(10, length);

                return value.ToString().PadLeft(length, '0');
            }
        }

        /// 
        /// TOTP基于时间的一次性密码
        /// 
        /// 
        /// 
        /// 
        /// 
        public static string TOTP(byte[] key, int step = 60, int length = 6)
        {
            var unixTime = (DateTime.UtcNow - new DateTime(1970, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc)).TotalSeconds;
            var counter = ((int)unixTime) / step;
            var counterBytes = BitConverter.GetBytes(counter);
            return HOTP(key, counterBytes, length);
        }
    }
}

 


 

 

你可能感兴趣的:(ASP.NET,Core)