Time-based Secret (TOTP) Generation
In some cases the Bank might not have access to Card Number, therefore unable to provide it in requests to MeaWallet. At the same time, MeaWallet as PCI-DSS approved authority can integrate and fetch card numbers from the bank’s 3rd party vendor (or another instance within the bank). In all such scenarios the bank will use CARD_ID and SECRET digitization type, where the bank provides virtual CARD_ID and SECRET. At the same time, MeaWallet will verify the SECRET and make a request to the 3rd party vendor to receive full card data.
This page explains how the bank can generate a SECRET and how MeaWallet can validate it.
Generation & Verification
SECRET is filled with One Time Password (OTP), which expires in time - TOTP (Time-Based One Time Password). MeaWallet is using algorithm defined in Internet Engineering Task Force (https://ietf.org) standard RFC 6238.
When the MeaWallet’s back-end receives an TOTP from a client, it computes the TOTP on its own, using the shared secret key, CARD_ID and its current Unix timestamp (not the Unix timestamp used by client) and compare the TOTPs: if they are generated within the same time-step they match and the validation succeeds.
If the client sends an TOTP close to the end of a time step, due to network latency or performance issues, the MeaWallet’s back-end may start processing the request in the following time-step, resulting in a validation failure. For this scenario, the MeaWallet’s back-end allow compare TOTPs not only in the current time step but also with the next and previous time-steps.
Setup
MeaWallet and the Bank to exchange 256-bit shared secret key (KEY) in HEX encoding format, and 3-digit KEY ID.
Sample (also key for TEST environment):
KEY =3132333435363738393031323334353637383930313233343536373839303132
KEY_ID =001
MeaWallet and the Bank to agree on SECRET/OTP length (1-8).
Default:
8 (recommended)
Agree on hashing algorithm. Available options are
SHA-256
andSHA-512
.Default:
SHA-512
MeaWallet and the Bank to agree on a TIME interval the OTP is valid.
Default:
60 seconds
Implementation
Whenever a user shows intention to digitize (or push-provision) a card from a mobile app, mobile app calls the Bank's back-end to get TOTP.
The Bank’s back-end prepares FINAL_KEY for TOTP calculation.
The Bank retrieves KEY in HEX and concatenates with CARD_ID in HEX.
FINAL_KEY (hex) = KEY (hex) + CARD_ID (hex)
FINAL_KEY
must not exceed 64 bytes (128 characters in HEX)
Example:
KEY in HEX = 3132333435363738393031323334353637383930313233343536373839303132
CARD_ID = ABCD-EFGH-123
CARD_ID in hex = 414243442D454647482D313233
FINAL_KEY = 3132333435363738393031323334353637383930313233343536373839303132414243442D454647482D313233
Example with too long CARD_ID:
KEY in HEX = 3132333435363738393031323334353637383930313233343536373839303132
CARD_ID = THIS_IS_TOO_LONG_KEY_AND_WE_NEED_TO_TRIM_IT_FROM_THE_LEFT_TO_BE_32_BYTES
CARD_ID in hex = 544849535f49535f544f4f5f4c4f4e475f4b45595f414e445f57455f4e4545445f544f5f5452494d5f49545f46524f4d5f5448455f4c4546545f544f5f42455f33325f4259544553
FINAL_KEY = 3132333435363738393031323334353637383930313233343536373839303132544849535f49535f544f4f5f4c4f4e475f4b45595f414e445f57455f4e4545445f544f5f5452494d5f49545f46524f4d5f5448455f4c4546545f544f5f42455f33325f4259544553
the FINAL_KEY is 104 bytes (208 characters in HEX), this is too long and must be trimmed to 64 bytes (128 characters in HEX)
FINAL_KEY (trimmed) = 3132333435363738393031323334353637383930313233343536373839303132544849535f49535f544f4f5f4c4f4e475f4b45595f414e445f57455f4e454544
Generate TOTP using FINAL_KEY. For that purpose any TOTP/HOTP library can be used. See useful scripts below this description.
- Selected library should use Unix (epoch) time to generate the TOTP. Algorithm can be verified using
Test Vectors
provided down below. An incorrect time is common reason of failure.
- Selected library should use Unix (epoch) time to generate the TOTP. Algorithm can be verified using
Format SECRET value.
SECRET consists of following parts:
Key ID (3 digits) - used by MeaWallet to select the correct shared secret key from its database used to validate the TOTP. This allows us to rotate keys without any downtime and optionally to segregate keys between the card ranges or in any other way.
Separator (1 symbol) - separator of key ID and TOTP. Constant value
#
(ASCII 0x2D).TOTP (8 digits) - Time-based One Time Password. Calculated as described in this document (Remember: the TOTP is calculated combining shared secret key with CARD_ID).
Sample: 001#12345678
- Mobile app sends CARD_ID and SECRET to MeaWallet (might be a method from MeaWallet’s mobile SDK).
- MeaWallet computes the TOTP on its own, using the shared secret key, CARD_ID and its current Unix timestamp (not the Unix timestamp used by client) and compare the TOTPs.
- Time is always UTC.
- Unix time is used in calculations, see https://en.wikipedia.org/wiki/Unix_time.
- If you test with Google Authenticator, remember to convert the
KEY
in Base32 (not Base64).
Samples
- python
- java
- golang
#-- 1. Import necessary libraries
import binascii
import base64
import datetime
#from datetime import timezone
import calendar
# pip install pyotp
import pyotp
import time
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives import hashes, hmac
#-- 2. Define utility functions
def calculateTotp(base32SecretStr, digits=8, interval=60, digest='sha512',time=None):
totp = pyotp.TOTP(base32SecretStr, digits=digits, interval=interval, digest=digest)
if time==None:
return totp.now()
else:
return totp.at(time)
def hex2bytes(hexData):
return bytes(bytearray.fromhex(hexData.replace(" ","")))
def bytes2hex(byteArray, addSpace=False):
hexVal = binascii.hexlify(byteArray).upper().decode("utf-8")
res = ''
if addSpace:
for i in range(int(len(hexVal)/2)):
res += hexVal[i*2:i*2+2]+' '
res = res[:-1]
else:
res = hexVal
return res
def calcHmac512(keyBytes, dataBytes):
h = hmac.HMAC(keyBytes, hashes.SHA512(), backend=default_backend())
h.update(dataBytes)
h512 = h.finalize()
return bytes2hex(h512)
#-- 3. Configure the keys and input data.
TOTP_KEY_HEX = '3132333435363738393031323334353637383930313233343536373839303132'
TOTP_SHA = 'sha512' # Possible values: sha256, sha512
TOTP_LEN = 8 # Digits
TOTP_WINDOW_SIZE = 60 # Seconds
CARD_ID = "335688998"
TIME = datetime.datetime.fromtimestamp(59)
#-- 4. Prepare the inputs.
print('Unix time:', TIME.astimezone(datetime.timezone.utc))
epochSeconds = int(time.mktime(TIME.timetuple()))
print('Epoch seconds:', epochSeconds)
timeStep = int(epochSeconds / TOTP_WINDOW_SIZE)
print('Unix Time /', str(TOTP_WINDOW_SIZE) + ':', timeStep)
keyInHex = TOTP_KEY_HEX
print('Key 128-bit in HEX:', keyInHex)
cardIdInHex = bytes2hex(CARD_ID.encode())
print('CARD_ID:', CARD_ID)
print('CARD_ID in HEX:', cardIdInHex)
cardIdInHex = cardIdInHex[:64]
print("CARD_ID in HEX (trimmed to 64): "+cardIdInHex)
secretKeyHex = keyInHex + cardIdInHex
print('FINAL Key in HEX:', secretKeyHex)
secretKeyInBytes = hex2bytes(secretKeyHex)
keyBytes = base64.b32encode(secretKeyInBytes)
keyBase32Str = keyBytes.decode()
print('FINAL Key in Base32:', keyBase32Str)
#-- 5. Calculate TOTP
timeInBytes = hex2bytes('0000000003f940aa')
h512Base32 = calcHmac512(keyBytes, timeInBytes)
print('HMAC (Base32):', h512Base32)
SECRET = "001#" + calculateTotp(keyBase32Str, digits=TOTP_LEN, interval=TOTP_WINDOW_SIZE, digest=TOTP_SHA, time=None)
print('---------')
print('SECRET =', SECRET, '( current -', datetime.datetime.now(), ')')
SECRET = "001#"+calculateTotp(keyBase32Str, digits=TOTP_LEN, interval=TOTP_WINDOW_SIZE, digest=TOTP_SHA, time=TIME)
print('SECRET =', SECRET, '(', TIME.astimezone(datetime.timezone.utc), ')')
/**
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;
/**
* 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 int[] DIGITS_POWER
// 0 1 2 3 4 5 6 7 8
= {1,10,100,1000,10000,100000,1000000,10000000,100000000 };
/**
* 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);
int otp = binary % DIGITS_POWER[codeDigits];
result = Integer.toString(otp);
while (result.length() < codeDigits) {
result = "0" + result;
}
return result;
}
public static void main(String[] args) {
// Seed/Key for HMAC-SHA512 - 64 bytes
String seed64 = "3132333435363738393031323334353637383930" +
"3132333435363738393031323334353637383930" +
"3132333435363738393031323334353637383930" +
"31323334";
long T0 = 0;
long X = 30;
long testTime[] = {59L, 1111111109L, 1111111111L,
1234567890L, 2000000000L, 20000000000L};
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(seed64, steps, "8",
"HmacSHA512") + "| SHA512 |");
System.out.println(
"+---------------+-----------------------+" +
"------------------+--------+--------+");
}
}catch (final Exception e){
System.out.println("Error : " + e);
}
}
}
// https://tools.ietf.org/html/rfc6238
func getTOTPToken(key []byte, time time.Time, durationSeconds int, digits int) string {
//The TOTP token is just a HOTP token seeded with every 30 seconds.
interval := time.Unix() / int64(durationSeconds)
return getHOTPToken(key, interval, digits)
}
// https://blog.gojekengineering.com/a-diy-two-factor-authenticator-in-golang-32e5641f6ec5
func getHOTPToken(key []byte, seed int64, digits int) string {
// 0 1 2 3 4 5 6 7 8
digitsPower := []int{1, 10, 100, 1000, 10000, 100000, 1000000, 10000000, 100000000}
bs := make([]byte, 8)
binary.BigEndian.PutUint64(bs, uint64(seed))
//Signing the value using HMAC-SHA1 Algorithm
hash := hmac.New(sha512.New, key)
hash.Write(bs)
h := hash.Sum(nil)
// We're going to use a subset of the generated hash.
// Using the last nibble (half-byte) to choose the index to start from.
// This number is always appropriate as it's maximum decimal 15, the hash will
// have the maximum index 19 (20 bytes of SHA1) and we need 4 bytes.
o := (h[len(h)-1] & 15)
var header uint32
//Get 32 bit chunk from hash starting at the o
r := bytes.NewReader(h[o : o+4])
err := binary.Read(r, binary.BigEndian, &header)
if err != nil {
log.Printf("[getHOTPToken] failed to convert to binary\n\t%v", err)
return ""
}
//Ignore most significant bits as per RFC 4226.
//Takes division from one million to generate a remainder less than < 7 digits
h12 := (int(header) & 0x7fffffff) % digitsPower[digits] //1000000
//Converts number as a string
otp := strconv.Itoa(int(h12))
for len(otp) < digits {
otp = "0" + otp
}
return otp
}
Test Vectors
1) SHA256
Timestep window size = 30
Card secret length = 8
CARD_ID (plain text) = 335688998
CARD_ID (hex) = 333335363838393938
Key (hex) = 3132333435363738393031323334353637383930313233343536373839303132
Total Key (hex) = 3132333435363738393031323334353637383930313233343536373839303132333335363838393938
Mode = SHA256
+-------------+----------+----------+
| Time (sec) | Timestep | TOTP |
+-------------+----------+----------+
| 59 | 1 | 66549790 |
| 1111111109 | 37037036 | 52828544 |
| 1234567890 | 41152263 | 88543363 |
| 2000000000 | 66666666 | 58932909 |
+-------------+----------+----------+
2) SHA512
Timestep window size = 60
Card secret length = 8
CARD_ID (plain text) = 516906811
CARD_ID (hex) = 333335363838393938
Key (hex) = 31323334353637383930313233343536373839303132333435363738393031323334353637383930313233343536373839303132333435363738393031323334
Total Key (hex) = 31323334353637383930313233343536373839303132333435363738393031323334353637383930313233343536373839303132333435363738393031323334
Mode = SHA512
Card ID: 115225348
CardId in HEX: 313135323235333438
CardId in HEX (trimmed to 64): 313135323235333438
Key in HEX: 3132333435363738393031323334353637383930313233343536373839303132
FINAL Key in HEX: 3132333435363738393031323334353637383930313233343536373839303132313135323235333438
TOTP Key in Base32: GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQGEZDCMJVGIZDKMZUHA======
+-------------+----------+----------+
| Time (sec) | Timestep | TOTP |
+-------------+----------+----------+
| 1163214254 | 19386904 | 19304652 |
| 1111111109 | 18518638 | 85949906 |
| 1234567890 | 20576251 | 05376914 |
| 2000000000 | 33333513 | 81567743 |
+-------------+----------+----------+
MeaWallet Time-based Secret Library for Backend
MeaWallet provides Java library for Time-based Secret calculations, in case this library is not fit for you, you are free to implement Time-based Secret calculations your self as documented above.
Requirements:
- Java 1.8
repositories {
maven {
url 'https://nexus.ext.meawallet.com/repository/<repository>'
credentials {
username '<user>'
password '<password>'
}
}
}
dependencies {
...
compile 'com.meawallet.commons:mw-totp:<version>'
...
}
Generation
CardSecretKey sha512key = CardSecretKey
.builder()
.sharedKey("shared-key-value")
.cardId("card-id-value")
.algorithm(com.meawallet.commons.totp.HmacHashAlgorithm.SHA_512);
TimestepProvider timestepProvider = new TimestepProvider(Clock.systemUTC());
CardSecretGenerator generator = new CardSecretGenerator(timestepProvider);
CardSecretGenerationConfig config = CardSecretGenerationConfig
.builder(sha512key)
.timestepWindowSize(Duration.ofSeconds(60))
.cardSecretLength(8);
GeneratedCardSecret cardSecret = generator.generate(config);
String result = cardSecret.getSecret();
Verification
CardSecretKey sha512key = CardSecretKey
.builder()
.sharedKey("shared-key-value")
.cardId("card-id-value")
.algorithm(com.meawallet.commons.totp.HmacHashAlgorithm.SHA_512);
CardSecretVerificationConfig config = CardSecretVerificationConfig
.builder(sha512key)
.allowedFutureValidationWindows(futureSteps)
.allowedPastValidationWindows(pastSteps)
.timestepWindowSize(Duration.ofSeconds(60))
.cardSecretLength(8);
TimestepProvider timestepProvider = new TimestepProvider(Clock.systemUTC());
CardSecretGenerator generator = new CardSecretGenerator(timestepProvider);
CardSecretVerifier verifier = new CardSecretVerifier(generator);
VerificationResult result = verifier.verify("card-secret-value", config);
boolean isSuccess = result.isSuccess();