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

问题描述 投票:0回答:3

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

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

示例输入:

  • 共享密钥:“[电子邮件受保护]”(不带双引号)
  • 使用的哈希函数:
    HMAC-SHA-512
  • T0 = 0,时间步长 = 30 秒(按照 RFC6238 中指定)
  • 预计 TOTP 为 10 位数字

示例输出:

成功生成 TOTP:

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

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

(我已将示例 POST 授权解码为“[电子邮件受保护]:1773133250”,因此我可以说示例 TOTP 输出为 1773133250)

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

尝试输入:

HMAC512 的十六进制编码种子:“6E696E6A61406578616D706C652E636F6D4844454348414C4C454E4745303033”+“6E696E6A61406578616D706C652E636F6D4844454 348414C4C454E4745303033";

输入的时间为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 "[email protected]", the token shared secret is "[email protected]".
    # *** For example, if the userid is "[email protected]", the token shared secret is "[email protected]"
    # 

import hmac
import hashlib
import time
import sys
import struct

userid = "[email protected]"
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("[email protected]@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 "[email protected]"
         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 脚本相同的结果。

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

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

java encryption hmac password-encryption one-time-password
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 = "[email protected]";
  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");
}

1
投票

Python代码没问题。只需要一些修改。

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

这些改变之后,它对我有用。并感谢您的代码。它节省了我很多时间。


0
投票

对于仍然坚持这个问题的人,您需要更改时间以反映当前时间。我用另一个(已确认的)工作 Python 库验证了这一点,该库执行相同的操作并在短时间内给出相同的答案。

代替:

long testTime[] = {59L, 1111111109L, 1111111111L,
             1234567890L, 2000000000L, 20000000000L};

尝试:

long testTime = System.currentTimeMillis() / 1000l; // ms to s

(我还修改了代码的其他一些部分,这样它就不会循环,等等......)

连同原始问题中提到的更改(将 'returnDigits' 更改为 10,在 'DIGITS_POWER' 后面附加 2 个更长的值,将其更改为 long 等...

© www.soinside.com 2019 - 2024. All rights reserved.