Сгенерируйте 10-значный пароль TOTP с определенным ключом

Эта проблема связана с TOTP, как указано в RFC6238 здесь: https://tools.ietf.org/html/rfc6238#section-1.2.

Я должен реализовать RFC6238 для генерации 10-значного пароля TOTP, который позже будет использоваться в запросе POST. Пример ввода и вывода для TOTP должен быть таким:

Пример ввода:

  • Общий ключ: [email protected] (без двойных кавычек)
  • Используемая хеш-функция: HMAC-SHA-512
  • T0 = ​​0, временной шаг = 30 секунд (согласно RFC6238)
  • Ожидаемый TOTP из 10 цифр

Пример вывода:

Успешно сгенерированный TOTP: 1773133250, на время понедельника, 17 марта 2014 г., 15:20:51 GMT

Запрос имени пользователя / пароля авторизации POST в кодировке base64: bmluamFAZXhhbXBsZS5jb206MTc3MzEzMzI1MA ==

(Я декодировал образец авторизации POST как '[email protected]: 1773133250', поэтому я могу сказать, что образец вывода TOTP - 1773133250)

После попытки создать свой собственный сценарий в соответствии со спецификацией rfc6238 я не могу получить тот же результат для образца ввода, что и выше. Я попытался использовать другие доступные онлайн-модули TOTP, доступные онлайн (в основном на Python), чтобы обнаружить, что они генерируют тот же результат, что и созданный мной скрипт. Наконец, я попробовал код Java, приведенный в примере RFC6238, и получил тот же результат, что и мой скрипт, а именно:

Попытка ввода:

Hex-кодированное начальное число для HMAC512: 6E696E6A61406578616D706C652E636F6D4844454348414C4C454E4745303033 + 6E696E6A61406578616D706C652E636F6D4844454348414C30C30334 ;E4745

Введено время 1395069651L, что соответствует времени, полученному в виде выходных данных.

Результат попытки (такие же выходные данные пользовательского скрипта, других модулей Python и реализации Java, указанные в документации RFC6238):

Создан TOTP: 0490867067

Вот код, который я впервые использовал при попытке сгенерировать TOTP в Python:

    # 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, который по сути является модифицированной версией реализации Java, найденной в RFC6238:

 /**
 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);
         }
     }
 }

Обратите внимание, что для измененного кода Java RFC на выходе будет несколько дат / времени, перечисленных в массиве testTime [], однако целевое время по Гринвичу из образца ввода задачи также включено сюда. Тестирование в моем Ubuntu показало тот же результат, что и в моем скрипте Python.

Я считаю, что выполнил инструкции, данные в задании. Я использовал настоящий RFC-код Java, чтобы обнаружить, что он не генерирует такой же результат, как тот, который указан в задаче. Я связался с провайдером задачи, чтобы спросить, есть ли ошибка, но они говорят, что это правильно.

Возможно, мне здесь что-то не хватает, например, как поставщик задач на самом деле шифрует общий ключ?


person Nicholas Sadjoli    schedule 02.03.2017    source источник
comment
Глядя на это ... мало времени   -  person Maarten Bodewes    schedule 09.03.2017
comment
Все еще смотрю на нее, читаю спецификации. Я тоже не сразу вижу проблему. Меня сразу раздражает использование String в опубликованном вами коде. Это делает весьма вероятным, что это где-то ошибка кодирования. В спецификациях постоянно говорится о двоичных строках (байтовых массивах).   -  person Maarten Bodewes    schedule 10.03.2017
comment
Вам нужно провести рефакторинг кода. Все структурировано, параметры всегда в разном порядке. Кроме того, похоже, вы вводите свой собственный адрес электронной почты и т. Д. дважды.   -  person Maarten Bodewes    schedule 10.03.2017
comment
Привет, Мартен, извини за поздний ответ. О, понятно, значит, где-то действительно могла быть ошибка кодирования, а? Я полагаю, вы имеете в виду код Java и Python. В таком случае, не могли бы вы научить меня, как «реорганизовать» код, как вы сказали? На самом деле я не знаю, как это сделать. Кроме того, что касается ввода моего собственного электронного письма дважды: я подумал, что мне нужно это сделать, поскольку '[email protected]' считается 32-байтовой строкой, и поэтому мне нужно было уместить ее в 64-байтовый формат, который используется для ввода SHA-512 (из того, что я читал)?   -  person Nicholas Sadjoli    schedule 11.03.2017
comment
Что касается рефакторинга: реализуйте стандарт, используя массивы байтов, целые числа и тому подобное. Если вы следуете спецификациям SHA-512, тогда ключ должен быть 512 бит, чтобы иметь возможность получить полную безопасность. Однако дублирование ключа, очевидно, ничего не делает для безопасности и приведет к другому результату. Так что, если явно где-то не указано, дублирование вашего электронного письма и еще много чего просто приведет к другому выводу. HMAC работает с любым ключом и размером сообщения.   -  person Maarten Bodewes    schedule 11.03.2017
comment
Привет, Мартен, извини за поздний ответ. О, так что даже для реализации внутри кода Java (того, который скопирован прямо из RFC), мне нужно было бы переформатировать ключ в массив байтов или что-то в этом роде? Я попытался ввести ключ как шестнадцатеричную строку, как вы можете видеть, причем каждый из символов читается как байты. Вы говорите, что этого недостаточно для того, чтобы ввести сам ключ и, таким образом, получить другой результат? Извините, что я продолжаю спрашивать, так как я новичок в этом   -  person Nicholas Sadjoli    schedule 14.03.2017
comment
Привет, Мартен, извини за поздний ответ. О, так что даже для реализации внутри кода Java (того, который скопирован прямо из RFC), мне нужно было бы переформатировать ключ в массив байтов или что-то в этом роде? Я попытался ввести ключ как шестнадцатеричную строку, как вы можете видеть, причем каждый из символов читается как байты. Вы говорите, что этого недостаточно для того, чтобы ввести сам ключ и, таким образом, получить другой результат? Извините, что я продолжаю спрашивать, так как я новичок в этом   -  person Nicholas Sadjoli    schedule 15.03.2017
comment
У меня сейчас очень мало времени, и я боюсь, что единственное, что я могу сделать, чтобы проверить правильность, - это самому правильно реализовать это, а затем сравнить. Это проблема многих криптографических функций; Я не могу сказать, что пошло не так, по самому выводу. Так что извиняюсь, если у меня нет прямого ответа, но пока это так.   -  person Maarten Bodewes    schedule 15.03.2017
comment
Привет, Мартен, это было давно. Прошу прощения за то, что снова беспокою вас, но похоже, что проблема, с которой я столкнулся, не решена. Сейчас я пробовал несколько кодов в Интернете, но все они, кажется, дают мне один и тот же ответ. Так что я теперь не знаю, как был сгенерирован вывод :(. Не могли бы вы помочь мне, попытавшись решить проблему и сопоставить вывод? Обратите внимание: похоже, что сервер от поставщика проблемы на самом деле использует сервер Google Frontend . Может ли быть какой-то другой метод totp, который используется этим конкретным типом сервера, о котором я не знаю? Спасибо   -  person Nicholas Sadjoli    schedule 06.04.2017
comment
Код действительно выглядит нормально и дает правильные значения для образцов тестовых данных tools.ietf.org / html / rfc6238 # приложение-B. Единственное, что вы могли сделать, - это сначала проверить свое предположение об успешном значении TOTP. Используйте образец общего секрета 1234567890 и проверьте, какое значение будет сгенерировано в данный момент. Затем сравните его с тем, что образец кода генерирует в данное время. Это покажет, есть ли разница между алгоритмом, описанным в RFC6238, и фактической реализацией, которую вы пытаетесь использовать.   -  person zloster    schedule 06.04.2017
comment
Привет, опоясывающий лишай! Извините за задержку с ответом. Я действительно пробовал код, который использую с образцом теста, и получил тот же результат, что и приложение RFC. Однако, когда я ввел в код образец ввода поставщика задачи и использовал SHA-512 (обратите внимание, что, поскольку SHA-512 ожидает 64-байтового ввода, я в основном ввел шестнадцатеричное представление ninja @ example.comHDECHALLENGE003ninja @ example.comHDECHALLENGE003), я я получаю другой вывод из образца вывода за то же предоставленное единичное время, что заставляет меня подозревать, что либо необходима модификация кода, либо ввод отформатирован по-другому.   -  person Nicholas Sadjoli    schedule 08.04.2017
comment
Следуя моему предположению, возможно ли, что для того, чтобы вписаться в 64-байтовый ввод для SHA-512, вместо расширения ввода в шестнадцатеричное представление ninja @ example.comHDECHALLENGE003ninja @ example.comHDECHALLEN‌ GE003, он должен быть расширен шестнадцатеричным представительство [email protected]? Я проясняю это, поскольку из того, что я читал в RFC, я думал, что расширение выполняется правильно, повторяя один и тот же ввод, пока он не станет длиной 64 байта. Также провайдер задач, к сожалению, не указал никаких намеков на то, нужно ли мне изменять формат ввода или нет :(   -  person Nicholas Sadjoli    schedule 08.04.2017
comment
@NicholasSadjoli Привет, я знаю, что такое HDECHALLENGE003, так как я также ищу ответы на вопросы этого теста разработчика. Могу я спросить, вы получили работу, работаете ли вы сейчас? Если да, можем ли мы поговорить по электронной почте, чтобы немного подробнее поговорить о роли и компании? Спасибо.   -  person JamesG    schedule 19.09.2019
comment
У меня такая же проблема, когда я реализовал алгоритм, он дает мне те же результаты в официальном документе, но когда я попытался с указанными требованиями, POST отклонил все сгенерированные пароли!   -  person Menai Ala Eddine - Aladdin    schedule 27.04.2020


Ответы (1)


Вы уверены, что TOTP 1773133250 правильный? Поскольку ваш секрет составляет всего 32 байта, знаете ли вы, что провайдер, вернувший 1773133250, создает тот же 64-байтовый секрет, что и вы?

В вашем коде вы берете свой 32-байтовый секрет и объединяете его вместе, чтобы получить 64 байта.

Я использую библиотеку Java FusionAuth-2FA и получаю тот же результат, что и вы, если я объединяю ваш 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");
}
person robotdan    schedule 21.12.2018
comment
Вы получили те же результаты в образце, я имею в виду: 1773133250 - person Menai Ala Eddine - Aladdin; 27.04.2020
comment
@MenaiAlaEddine Приведенный выше код - мой результат, я получил 0490867067, используя 64-байтовый секрет. Вот тест github.com/FusionAuth/fusionauth-2fa/blob/ - person robotdan; 28.04.2020