使用特定密钥生成 10 位 TOTP 密码

Posted

技术标签:

【中文标题】使用特定密钥生成 10 位 TOTP 密码【英文标题】:Generate a 10-digit TOTP password with a certain key 【发布时间】:2017-07-21 15:38:42 【问题描述】:

此问题与 RFC6238 中指定的 TOTP 相关:https://www.rfc-editor.org/rfc/rfc6238#section-1.2。

我要实现 RFC6238 来生成一个 10 位 TOTP 密码,稍后将在 POST 请求中使用该密码。 TOTP 的示例输入和输出应该是这样的:

示例输入:

共享密钥:“ninja@example.comHDECHALLENGE003”(不带双引号) 使用的哈希函数:HMAC-SHA-512 T0 = 0,时间步长 = 30 秒(按照 RFC6238 中的规定) 预计 TOTP 为 10 位数

样本输出:

成功生成 TOTP:1773133250,时间为 2014 年 3 月 17 日星期一 15:20:51 GMT

base64 编码的 POST 授权用户名/密码请求:bmluamFAZXhhbXBsZS5jb206MTc3MzEzMzI1MA==

(我已将示例 POST 授权解码为“ninja@example.com:1773133250”,因此我可以说示例 TOTP 输出为 1773133250)

尝试根据 rfc6238 规范制作我自己的脚本后,我无法获得与上述示例输入相同的输出。我尝试使用其他在线可用的在线 TOTP 模块(主要在 Python 中),发现它们生成的输出与我创建的脚本相同。最后,我尝试了 RFC6238 示例中给出的 Java 代码,并得出了与我的脚本相同的结果,即:

尝试输入:

HMAC512的HIX编码种子:“6E696E6A6140657803033”+“6E6960303393503030339350303033935303033”+“6E696030339350303033”+“ P>

输入的时间为1395069651L,表示样本输出中接收到的时间

尝试的结果(来自自定义脚本、其他 Python 模块以及 RFC6238 文档中给出的 Java 实现的相同输出):

生成的 TOTP:0490867067

这是我第一次尝试在 Python 中生成 TOTP 的代码:

    # Mission/Task Description:
    # * For the "password", provide an 10-digit time-based one time password conforming to RFC6238 TOTP.
    # 
    # ** You have to read RFC6238 (and the errata too!) and get a correct one time password by yourself.
    # ** TOTP's "Time Step X" is 30 seconds. "T0" is 0.
    # ** Use HMAC-SHA-512 for the hash function, instead of the default HMAC-SHA-1.
    # ** Token shared secret is the userid followed by ASCII string value "HDECHALLENGE003" (not including double quotations).
    # 
    # *** For example, if the userid is "ninja@example.com", the token shared secret is "ninja@example.comHDECHALLENGE003".
    # *** For example, if the userid is "ninjasamuraisumotorishogun@example.com", the token shared secret is "ninjasamuraisumotorishogun@example.comHDECHALLENGE003"
    # 

import hmac
import hashlib
import time
import sys
import struct

userid = "ninja@example.com"
secret_suffix = "HDECHALLENGE003"
shared_secret = userid+secret_suffix

timestep = 30
T0 = 0

def HOTP(K, C, digits=10):
    """HTOP:
    K is the shared key
    C is the counter value
    digits control the response length
    """
    K_bytes = K.encode()
    C_bytes = struct.pack(">Q", C)
    hmac_sha512 = hmac.new(key = K_bytes, msg=C_bytes, digestmod=hashlib.sha512).hexdigest()
    return Truncate(hmac_sha512)[-digits:]

def Truncate(hmac_sha512):
    """truncate sha512 value"""
    offset = int(hmac_sha512[-1], 16)
    binary = int(hmac_sha512[(offset *2):((offset*2)+8)], 16) & 0x7FFFFFFF
    return str(binary)

def TOTP(K, digits=10, timeref = 0, timestep = 30):
    """TOTP, time-based variant of HOTP
    digits control the response length
    the C in HOTP is replaced by ( (currentTime - timeref) / timestep )
    """
    C = int ( 1395069651 - timeref ) // timestep
    return HOTP(K, C, digits = digits)

passwd = TOTP("ninja@example.comHDECHALLENGE003ninja@example.comHDECHALLENGE003", 10, T0, timestep).zfill(10)
print passwd

这是 Java 中的第二个代码,它本质上是 RFC6238 中的 Java 实现的修改版本:

 /**
 Copyright (c) 2011 IETF Trust and the persons identified as
 authors of the code. All rights reserved.

 Redistribution and use in source and binary forms, with or without
 modification, is permitted pursuant to, and subject to the license
 terms contained in, the Simplified BSD License set forth in Section
 4.c of the IETF Trust's Legal Provisions Relating to IETF Documents
 (http://trustee.ietf.org/license-info).
 */

 import java.lang.reflect.UndeclaredThrowableException;
 import java.security.GeneralSecurityException;
 import java.text.DateFormat;
 import java.text.SimpleDateFormat;
 import java.util.Date;
 import javax.crypto.Mac;
 import javax.crypto.spec.SecretKeySpec;
 import java.math.BigInteger;
 import java.util.TimeZone;
 import java.util.Calendar;


 /**
  * This is an example implementation of the OATH
  * TOTP algorithm.
  * Visit www.openauthentication.org for more information.
  *
  * @author Johan Rydell, PortWise, Inc.
  */

 public class TOTP 

     private TOTP() 

     /**
      * This method uses the JCE to provide the crypto algorithm.
      * HMAC computes a Hashed Message Authentication Code with the
      * crypto hash algorithm as a parameter.
      *
      * @param crypto: the crypto algorithm (HmacSHA1, HmacSHA256,
      *                             HmacSHA512)
      * @param keyBytes: the bytes to use for the HMAC key
      * @param text: the message or text to be authenticated
      */


     private static byte[] hmac_sha(String crypto, byte[] keyBytes,
             byte[] text)
         try 
             Mac hmac;
             hmac = Mac.getInstance(crypto);
             SecretKeySpec macKey =
                 new SecretKeySpec(keyBytes, "RAW");
             hmac.init(macKey);
             return hmac.doFinal(text);
          catch (GeneralSecurityException gse) 
             throw new UndeclaredThrowableException(gse);
         
     


     /**
      * This method converts a HEX string to Byte[]
      *
      * @param hex: the HEX string
      *
      * @return: a byte array
      */

     private static byte[] hexStr2Bytes(String hex)
         // Adding one byte to get the right conversion
         // Values starting with "0" can be converted
         byte[] bArray = new BigInteger("10" + hex,16).toByteArray();

         // Copy all the REAL bytes, not the "first"
         byte[] ret = new byte[bArray.length - 1];
         for (int i = 0; i < ret.length; i++)
             ret[i] = bArray[i+1];
         return ret;
     

     private static final long[] DIGITS_POWER
     // 0 1  2   3    4     5      6       7        8         9          10
     = 1,10,100,1000,10000,100000,1000000,10000000,100000000,1000000000,10000000000L;

     /**
      * This method generates a TOTP value for the given
      * set of parameters.
      *
      * @param key: the shared secret, HEX encoded
      * @param time: a value that reflects a time
      * @param returnDigits: number of digits to return
      *
      * @return: a numeric String in base 10 that includes
      *              @link truncationDigits digits
      */

     public static String generateTOTP(String key,
             String time,
             String returnDigits)
         return generateTOTP(key, time, returnDigits, "HmacSHA1");
     


     /**
      * This method generates a TOTP value for the given
      * set of parameters.
      *
      * @param key: the shared secret, HEX encoded
      * @param time: a value that reflects a time
      * @param returnDigits: number of digits to return
      *
      * @return: a numeric String in base 10 that includes
      *              @link truncationDigits digits
      */

     public static String generateTOTP256(String key,
             String time,
             String returnDigits)
         return generateTOTP(key, time, returnDigits, "HmacSHA256");
     

     /**
      * This method generates a TOTP value for the given
      * set of parameters.
      *
      * @param key: the shared secret, HEX encoded
      * @param time: a value that reflects a time
      * @param returnDigits: number of digits to return
      *
      * @return: a numeric String in base 10 that includes
      *              @link truncationDigits digits
      */

     public static String generateTOTP512(String key,
             String time,
             String returnDigits)
         return generateTOTP(key, time, returnDigits, "HmacSHA512");
     


     /**
      * This method generates a TOTP value for the given
      * set of parameters.
      *
      * @param key: the shared secret, HEX encoded
      * @param time: a value that reflects a time
      * @param returnDigits: number of digits to return
      * @param crypto: the crypto function to use
      *
      * @return: a numeric String in base 10 that includes
      *              @link truncationDigits digits
      */

     public static String generateTOTP(String key,
             String time,
             String returnDigits,
             String crypto)
         int codeDigits = Integer.decode(returnDigits).intValue();
         String result = null;

         // Using the counter
         // First 8 bytes are for the movingFactor
         // Compliant with base RFC 4226 (HOTP)
         while (time.length() < 16 )
             time = "0" + time;

         // Get the HEX in a Byte[]
         byte[] msg = hexStr2Bytes(time);
         byte[] k = hexStr2Bytes(key);

         byte[] hash = hmac_sha(crypto, k, msg);

         // put selected bytes into result int
         int offset = hash[hash.length - 1] & 0xf;

         int binary =
             ((hash[offset] & 0x7f) << 24) |
             ((hash[offset + 1] & 0xff) << 16) |
             ((hash[offset + 2] & 0xff) << 8) |
             (hash[offset + 3] & 0xff);

         long otp = binary % DIGITS_POWER[codeDigits];

         result = Long.toString(otp);
         while (result.length() < codeDigits) 
             result = "0" + result;
         
         return result;
     

     public static void main(String[] args) 
         // Seed for HMAC-SHA1 - 20 bytes
         String seed = "3132333435363738393031323334353637383930";
         // Seed for HMAC-SHA256 - 32 bytes
         String seed32 = "3132333435363738393031323334353637383930" +
         "313233343536373839303132";
         // Seed for HMAC-SHA512 - 64 bytes
         String seed64 = "6E696E6A61406578616D706C652E636F6D4844454348414C4C454E4745303033";

         //NOTE: this is the 16-bit/hex encoded representation of "ninja@example.comHDECHALLENGE003"
         String seednew = "6E696E6A61406578616D706C652E636F6D4844454348414C4C454E4745303033" +
         "6E696E6A61406578616D706C652E636F6D4844454348414C4C454E4745303033"; 
         long T0 = 0;
         long X = 30;
         long current = System.currentTimeMillis()/1000;
         System.out.println(current);
         long testTime[] = 59L, 1234567890L,1395069651L;

         String steps = "0";
         DateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
         df.setTimeZone(TimeZone.getTimeZone("UTC"));
         try 
             System.out.println(
                     "+---------------+-----------------------+" +
             "------------------+--------+--------+");
             System.out.println(
                     "|  Time(sec)    |   Time (UTC format)   " +
             "| Value of T(Hex)  |  TOTP  | Mode   |");
             System.out.println(
                     "+---------------+-----------------------+" +
             "------------------+--------+--------+");

             for (int i=0; i<testTime.length; i++) 
                 long T = (testTime[i] - T0)/X;
                 steps = Long.toHexString(T).toUpperCase();
                 while (steps.length() < 16) steps = "0" + steps;
                 String fmtTime = String.format("%1$-11s", testTime[i]);
                 String utcTime = df.format(new Date(testTime[i]*1000));
                 System.out.print("|  " + fmtTime + "  |  " + utcTime +
                         "  | " + steps + " |");
                 System.out.println(generateTOTP(seed, steps, "8",
                 "HmacSHA1") + "| SHA1   |");
                 System.out.print("|  " + fmtTime + "  |  " + utcTime +
                         "  | " + steps + " |");
                 System.out.println(generateTOTP(seed32, steps, "8",
                 "HmacSHA256") + "| SHA256 |");
                 System.out.print("|  " + fmtTime + "  |  " + utcTime +
                         "  | " + steps + " |");
                 System.out.println(generateTOTP(seed64, steps, "10",
                 "HmacSHA256") + "| SHA256 |");
                 System.out.print("|  " + fmtTime + "  |  " + utcTime +
                         "  | " + steps + " |");
                 System.out.println(generateTOTP(seednew, steps, "10",
                  "HmacSHA512") + "| SHA512 |");
                 System.out.println(
                         "+---------------+-----------------------+" +
                 "------------------+--------+--------+");
             
         catch (final Exception e)
             System.out.println("Error : " + e);
         
     
 

请注意,对于修改后的 RFC Java 代码,输出将是 testTime[] 数组中列出的几个日期/时间的输出,但是此处也包含来自任务示例输入的目标 GMT。在我的 Ubuntu 中进行的测试显示了与我的 Python 脚本相同的结果。

我相信我已经遵循了任务给出的指示。我使用给定 Java 代码的实际 RFC 发现它没有生成与任务中给定的输出相同的输出。我联系了任务的提供者询问是否有错误,但他们说是正确的。

也许我在这里遗漏了一些东西,例如任务提供者实际加密共享密钥的方式?

【问题讨论】:

看着它......有(/有)一点时间 仍在看它,阅读规格。我也没有立即看到问题。立即惹恼我的一件事是在您发布的代码中使用 String 。这很可能是某个地方的编码错误。规范一直在谈论二进制字符串(字节数组)。 伙计,你需要重构代码。一切都是字符串化的,参数始终处于不同的顺序。此外,您似乎输入了自己的电子邮件等两次 嗨 Maarten,抱歉回复晚了。哦,我明白了,所以确实可能在某处存在编码错误,嗯?您指的是我假设的 Java 和 Python 代码。在那种情况下,你能教我如何按照你说的“重构”代码吗?我不确定如何实际执行此操作。此外,关于我自己的电子邮件的两次输入:我认为我需要这样做,因为 'ninja@example.comHDECHALLENGE003' 被认为是 32 字节字符串,因此我需要将其调整为使用的 64 字节格式对于 SHA-512 输入(从我读到的)? 代码确实看起来不错,并为示例测试数据tools.ietf.org/html/rfc6238#appendix-B 提供了正确的值。您唯一能做的就是在开始时测试您对成功 TOTP 值的假设。使用1234567890 的示例共享密钥并检查给定时刻将生成什么值。然后将其与示例代码在给定时间内生成的内容进行比较。这将突出显示 RFC6238 中描述的算法与您尝试使用的实际实现之间是否存在差异。 【参考方案1】:

您确定 TOTP 1773133250 是正确的吗?由于您的密码只有 32 字节,您是否确定返回 1773133250 的提供者正在构建与您相同的 64 字节密码?

在您的代码中,您获取 32 字节的密钥并将其连接在一起以获得 64 字节。

我正在使用FusionAuth-2FA Java 库,如果我将 32 字节密码连接在一起得到 64 字节密码,我会得到相同的结果。

我已阅读 RFC,但我不清楚实现者是否需要将密钥扩展为特定字节大小。

这可能是您的代码是正确的,而1773133250 是一个红鲱鱼。

这是我的测试代码:

@Test
public void ***_42546493() 
  // Mon, 17 Mar 2014 15:20:51 GMT
  ZonedDateTime date = ZonedDateTime.of(2014, 3, 17, 15, 20, 51, 0, ZoneId.of("GMT"));
  long seconds = date.toEpochSecond();
  assert seconds == 1395069651L; 
  long timeStep = seconds / 30;

  // Your shared key in a 32-byte string  
  String rawSecret = "ninja@example.comHDECHALLENGE003";
  String rawSecret64 = rawSecret + rawSecret; // 64 bytes
    
  // Using 32 byte secret
  String code = TwoFactor.calculateVerificationCode(rawSecret, timeStep, Algorithm.HmacSHA512, 10);
  assert code.equals("1264436375");

  // Using 64 byte secret
  String code = TwoFactor.calculateVerificationCode(rawSecret64, timeStep, Algorithm.HmacSHA512, 10);
  assert code.equals("0490867067");

【讨论】:

你在样本中得到相同的结果吗?我的意思是:1773133250 @MenaiAlaEddine 上面的代码是我的结果,我使用 64 字节秘密得到了0490867067。这是测试github.com/FusionAuth/fusionauth-2fa/blob/…

以上是关于使用特定密钥生成 10 位 TOTP 密码的主要内容,如果未能解决你的问题,请参考以下文章

使用 MD5 从密码生成加密密钥?

动态令牌-(OTP,HOTP,TOTP)-基本原理

动态密码卡TOTP算法

OTP,HOTP,TOTP基本原理

银行在其密码生成器令牌上使用哪种 OTP(一次性密码)算法? [关闭]

国密算法密码长度才256位二进制,为啥能说是安全的?