Generate a 10-digit TOTP password with a certain key
Asked Answered
D

3

24

This problem is related to TOTP as specified in RFC6238 here: https://www.rfc-editor.org/rfc/rfc6238#section-1.2.

I am to implement the RFC6238 to generate a 10-digit TOTP password, which will be used in a POST request later on. The sample input and output for the TOTP is supposed to be like this:

Sample Input:

  • Shared key: "[email protected]" (without double quotes)
  • Hash function used: HMAC-SHA-512
  • T0 = 0, Timestep = 30 seconds (as per specified in RFC6238)
  • Expected TOTP of 10 digits

Sample Output:

Successful TOTP generated: 1773133250, for time of Mon, 17 Mar 2014 15:20:51 GMT

base64 encoded POST Authorization username/password request: bmluamFAZXhhbXBsZS5jb206MTc3MzEzMzI1MA==

(I have decoded the sample POST authorization to be '[email protected]:1773133250' hence why I can say that the sample TOTP output is 1773133250)

After attempting to make my own script according to the rfc6238 specification I cannot get the same output for the sample input as above. I tried using other available online TOTP modules that are available online (mostly in Python), to find that they generate the same output as the script that I created. Finally, I tried the Java code given in the example of RFC6238 and came up with the same result as my script, i.e.:

Attempted input:

Hex encoded seed for HMAC512: "6E696E6A61406578616D706C652E636F6D4844454348414C4C454E4745303033" + "6E696E6A61406578616D706C652E636F6D4844454348414C4C454E4745303033";

Time inputted is 1395069651L, representing the time received in sample output

Result of attempt (same output from custom script, other Python modules, and the Java implementation given in RFC6238 documentation):

Generated TOTP: 0490867067

Here is the code that I first used in attempt to generate the TOTP in 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

Here is the second code in Java which is essentially a modified version of the Java implementation found in 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);
         }
     }
 }

Do note that for the modified RFC Java code, the output would be that of several date/time listed in the testTime[] array, however the targeted GMT from the sample input of the task is also included in here as well. Testing in my Ubuntu showed the same result as that from my Python script.

I believe that I have followed the instructions given by the task. I used the actual RFC given Java code to find it is not generating the same output as the one given in the task. I contacted the provider of the task to ask if there is a bug, but they say that it is correct.

Perhaps I am missing something here, such as the way the task provider actually encrypts the shared key?

Dire answered 2/3, 2017 at 3:47 Comment(14)
Looking at it... had (/have) little timeJoyjoya
Still looking at it, reading the specs. I don't immediately see an issue either. One thing that is immediately irking me is the use of String in the code you posted. This makes it highly possible that this is an encoding bug somewhere. The specs talk about binary strings (byte arrays) all the time.Joyjoya
Man, you need to refactor the code. Everything is stringified, the parameters are in a different order all the time. Furthermore, it seems you input your own email and such twice.Joyjoya
Hey Maarten, sorry for the late reply. Oh I see, so indeed there might have been an encoding bug somewhere eh? You're referring to both the Java and Python code I assume. In that case, can you teach me how to 'refactor' the code as you said? I am not sure on how to do this actually. Also, regarding the input of my own email twice: I thought I needed to do so since '[email protected]' is considered a 32-byte string, and thus I needed to fit it into a 64-byte format that is used for SHA-512 input (from what I read)?Dire
With regard to the refactoring: implement the standard using byte arrays and integers and such. If you follow the SHA-512 specifications then the key should be 512 bits to be able to receive the full security. However duplicating the key obviously doesn't do anything for security and will produce different output. So unless explicitly specified somewhere, duplicating your email and whatnot will just produce a different output. HMAC works with any key and message size.Joyjoya
Hey sorry for the late reply, Maarten. Oh, so even for implementation inside the Java code (the one that is copied straight out of the RFC), I would need to reformat the key into a byte array or something like that? I tried to input the key as a hex string as you can see, with each of the characters being read as bytes. Are you saying that this is not enough of a security for putting in the key itself and thus producing different output? Sorry that I keep asking since I am new to thisDire
Hey sorry for the late reply, Maarten. Oh, so even for implementation inside the Java code (the one that is copied straight out of the RFC), I would need to reformat the key into a byte array or something like that? I tried to input the key as a hex string as you can see, with each of the characters being read as bytes. Are you saying that this is not enough of a security for putting in the key itself and thus producing different output? Sorry that I keep asking since I am new to thisDire
I'm very much strapped for time right now and I'm afraid that the only thing I can do to verify correctness is to implement it correctly myself and then compare. This is the issue with many crypto functions; I cannot tell what goes wrong from the output itself. So apologies if I don't have a direct answer for you, but that's the way it is for now.Joyjoya
Hello Maarten, it's been quite a while. I am sorry for bothering you again, however it seems that the problem that I have been has not been solved. I have now tried multiple codes online, yet they all seem to give me the same answer. So I now have no idea on how the output was generated unfortunately :(. Could you help me by trying to solve the problem and match the output? On a note, it seems the server from the problem provider is actually using a Google Frontend server. Could there be any different totp method that is used by that particular server type that I don't know of? ThanksDire
The code indeed seems OK and gives the correct values for the sample test data tools.ietf.org/html/rfc6238#appendix-B. The only thing you could do is to test your assumption about the successful TOTP value in the beginning. Use the sample shared secret of 1234567890 and check what value will be generated for a given moment. Than compare it with what the sample code is generating for the given time. That will highlight if there is a difference between the algorithm described in the RFC6238 and the actual implementation that you try to use.Milka
Hello zoster! Sorry for the late reply. I have indeed tried the code I'm using with the sample test, and got the same result as the RFC appendix. However when I inputted the challenge provider's sample input into the code, and using SHA-512 (note that since SHA-512 expects 64-byte input, I basically inputted the Hex representation of "[email protected]@example.comHDECHALLENGE003"), I am getting a different output from the sample output for the same unit time provided, which makes me suspect that either a code modification is necessary or the input is formatted differently.Dire
Following my assumption then, is it possible that, to fit into the 64-byte input for SHA-512, instead of expanding the input into Hex representation of "[email protected]@example.comHDECHALLEN‌​GE003", it should be expanded with Hex representation of "[email protected]"? I am clarifying this as from what I read in RFC, I thought expansion is done correctly by repeating the same input till it is 64-bytes long. Also the challenge provider unfortunately didn't indicate any hints on whether I need to change input format or not :(Dire
@NicholasSadjoli Hey, so I know what HDECHALLENGE003 is as I am also looking to solve the questions on this dev test. Can I ask, did you get the job, do you currently work there? If so, could we have a chat over email to talk about the role and company a little more? Thanks.Giovannagiovanni
I have the same issue when I implemented the Algorithm, it gives me the same results in the official doc but when I tried with the given requirements the POST refuse all generated passwords !Cinchonize
C
1

Are you certain the TOTP 1773133250 is correct? Since your secret is only 32 bytes, do you know for sure that the provider that returned the 1773133250 is building the same 64-byte secret you are?

In your code, you take your 32-byte secret and concatenate it together in order to get 64 bytes.

I am using FusionAuth-2FA Java library, and I get the same result you do if I concatenate your 32-byte secret together to get a 64-byte secret.

I've read the RFC and it isn't clear to me that there is any requirement for an implementor to expand a secret to a particular byte size.

It could be your code is correct, and 1773133250 is a red herring.

Here is my test code:

@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");
}
Cooper answered 21/12, 2018 at 20:27 Comment(2)
Did you get the same results in the sample I mean: 1773133250Cinchonize
@MenaiAlaEddine The above code is my result, I got 0490867067 using the 64 byte secret. Here is the test github.com/FusionAuth/fusionauth-2fa/blob/…Cooper
D
1

The python code is okay. Just needs some modification.

  • Firstly for running the code on python3 you need to replace print passwd with print(passwd).
  • On the second last line, replace "[email protected]@example.comHDECHALLENGE003" with shared_secret. Update the secrets on the variables declared at top.
  • Finally the most important part is on the C = int ( 1395069651 - timeref ) // timestep line. Do not use a static timestamp. Instead replace 1395069651 with time.time(). You already imported time at the top.

After these changes it worked for me. And thank you for the code. It saved me a huge time.

Divisionism answered 9/3, 2022 at 17:4 Comment(8)
Hey, replacing with time.time gives an error. Is it meant for the current time? If yes, then in secs, right?Aludel
Yes it should be in seconds. And I did a typo in the answer. Kindly check the updated version. It should be time.time(). Thank you for pointing out.Divisionism
No problem. Were you also doing HENNGECHALLENGE003 because the POST request is still not accepting the passwords generated.Aludel
Did it work? And yes I was. But unfortunately I didn't make it. Best of luck to you!Divisionism
Still getting 401. Thanks btw!Aludel
Just to remind you that the totp is valid for 30 seconds. You need to submit it as soon as it's generated, so that when they check it's also in that 30 secs. Or else it'll change and give an error.Divisionism
Yeah I have read about that. I am making a post request immediately after generating TOTP.Aludel
I don't know what else could cause the problem. What I can confirm is that this code worked for me.Divisionism
N
0

For anyone still stuck on this, you need to change the time so it reflects the current time. I verified this with another (confirmed) working Python lib that does the same and gives the same answer within a short time difference.

Instead of:

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

Try:

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

(I also modified some other parts of the code so it doesn't loop, etc...)

Along with the changes mentioned in the original question (change the 'returnDigits' to 10, append 'DIGITS_POWER' with 2 more, longer values, change it to long, etc...) it should work as intended.

Nash answered 28/3 at 3:2 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.