使用特定密钥生成一个10位数的TOTP密码。

24
这个问题与TOTP有关,其在RFC6238中进行了规定,链接如下: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规定) - 预期的10位TOTP
示例输出:
成功生成TOTP: 1773133250,时间为Mon, 17 Mar 2014 15:20:51 GMT
base64编码的POST授权用户名/密码请求:bmluamFAZXhhbXBsZS5jb206MTc3MzEzMzI1MA==
(我已解码样本POST授权为“ninja@example.com:1773133250”,因此我可以说样本TOTP输出为1773133250)
尝试根据rfc6238规范编写自己的脚本后,我无法获得与上面示例输入相同的输出。 我尝试使用其他可用的在线TOTP模块(大多数是Python),发现它们生成与我创建的脚本相同的输出。 最后,我尝试了RFC6238示例中给出的Java代码,并得到了与我的脚本相同的结果,即:
尝试的输入:
- HMAC512的十六进制编码种子:“6E696E6A61406578616D706C652E636F6D4844454348414C4C454E4745303033”+“6E696E6A61406578616D706C652E636F6D4844454348414C4C454E4745303033”; - 输入时间为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代码,输出将是测试时间[]数组中列出的几个日期/时间,但目标GMT也包含在其中。在我的Ubuntu上进行测试显示与我的Python脚本相同的结果。
我相信我已经按照任务给出的说明进行了操作。我使用了实际的RFC给定Java代码,发现它没有生成与任务中给出的代码相同的输出。我联系了任务提供者询问是否存在错误,但他们说这是正确的。
也许我在这里漏掉了什么,例如任务提供者实际加密共享密钥的方式?

1
兄弟,你需要重构代码。所有东西都被字符串化了,参数每次都是不同的顺序。此外,似乎你输入自己的电子邮件等信息两次 - Maarten Bodewes
嘿,Maarten,抱歉回复晚了。哦,所以即使是在Java代码内部实现(直接从RFC复制的代码),我也需要将密钥重新格式化为字节数组或类似的东西吗?正如您所看到的,我尝试将密钥作为十六进制字符串输入,每个字符都被读取为字节。您是说这对于放入密钥本身并因此产生不同的输出来说不足够安全吗?抱歉我一直在问,因为我是新手。 - Nicholas Sadjoli
你好Maarten,好久不见了。很抱歉再次打扰你,但是似乎我一直遇到的问题还没有解决。我现在已经尝试了多个在线代码,但它们似乎都给我相同的答案。所以我现在不知道输出是如何生成的 :(. 你能否通过尝试解决这个问题并匹配输出来帮助我?另外,问题提供者的服务器似乎实际上正在使用Google前端服务器。那种特定服务器是否有任何不同的totp方法,我不知道吗?谢谢。 - Nicholas Sadjoli
1
代码看起来确实没问题,并且对于样本测试数据 https://tools.ietf.org/html/rfc6238#appendix-B 给出了正确的值。你唯一能做的就是测试你关于成功 TOTP 值的假设。使用共享密钥的示例 1234567890,并检查在给定时刻将生成什么值。然后将其与样本代码为给定时间生成的值进行比较。这将突出显示 RFC6238 中描述的算法和您尝试使用的实际实现之间是否存在差异。 - zloster
你好zoster!很抱歉回复晚了。我确实已经尝试过我正在使用的代码与样例测试,并得到了与RFC附录相同的结果。然而,当我将挑战提供者的样例输入输入到代码中,并使用SHA-512(请注意,由于SHA-512期望64字节的输入,因此我基本上输入了“ninja@example.comHDECHALLENGE003ninja@example.comHDECHALLENGE003”的十六进制表示),我得到了与提供的相同时间单位的样例输出不同的输出,这让我怀疑是否需要进行代码修改或输入格式不同。 - Nicholas Sadjoli
显示剩余9条评论
2个回答

1

这段Python代码还需要一些修改。

  • 首先,为了在Python3上运行代码,你需要将print passwd替换为print(passwd)
  • 在倒数第二行,将"ninja@example.comHDECHALLENGE003ninja@example.comHDECHALLENGE003"替换为shared_secret。更新在顶部声明的变量的密钥。
  • 最后,最重要的部分是在C = int ( 1395069651 - timeref ) // timestep行。不要使用静态时间戳。取而代之,用time.time()替换1395069651。你已经在顶部导入了time

进行这些更改后,它对我起作用了。感谢你的代码,它为我节省了大量时间。


嘿,用 time.time 替换会出错。这是指当前时间吗?如果是的话,那么是以秒为单位,对吗? - Rishab Mamgai
是的,时间单位应该是秒。我在答案中打了一个错字。请检查更新版本。应该是time.time()。感谢你指出错误。 - KhairulBashar
没问题。你也在做 HENNGECHALLENGE003 吗?因为 POST 请求仍然无法接受生成的密码。 - Rishab Mamgai
它起作用了吗?是的,我试过了。但不幸的是,我没有成功。祝你好运! - KhairulBashar
只是提醒您,totp 有效期为30秒。您需要在生成后尽快提交,这样当他们检查时也在这30秒内。否则它会更改并出现错误。 - KhairulBashar
显示剩余3条评论

1

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

在你的代码中,你将你的32个字节的密钥连接在一起,以获得64个字节。

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

我已经阅读了 RFC,但我并不清楚实现者是否需要将密钥扩展到特定的字节数。

可能是你的代码是正确的,而 1773133250 是一个误导。

下面是我的测试代码:

@Test
public void stackOverflow_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。 - Menai Ala Eddine - Aladdin
@MenaiAlaEddine 上面的代码是我的结果,使用了64字节的密钥,得到了0490867067。这是测试链接:https://github.com/FusionAuth/fusionauth-2fa/blob/c24e92a83ab46371fb3ad3b5935a8e4df9927c6b/src/test/java/io/fusionauth/twofactor/TwoFactorTest.java#L63 - robotdan

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接