Rewrite the HOTP/TOTP code to be more compact and support more digits

This commit is contained in:
Alexander Bakker 2019-03-25 23:51:10 +01:00
parent 066a7447c0
commit 29aa81765b
7 changed files with 101 additions and 364 deletions

View file

@ -1,189 +1,47 @@
package com.beemdevelopment.aegis.crypto.otp;
/*
* OneTimePasswordAlgorithm.java
* OATH Initiative,
* HOTP one-time password algorithm
*
*/
/* Copyright (C) 2004, OATH. All rights reserved.
*
* License to copy and use this software is granted provided that it
* is identified as the "OATH HOTP Algorithm" in all material
* mentioning or referencing this software or this function.
*
* License is also granted to make and use derivative works provided
* that such works are identified as
* "derived from OATH HOTP algorithm"
* in all material mentioning or referencing the derived work.
*
* OATH (Open AuTHentication) and its members make no
* representations concerning either the merchantability of this
* software or the suitability of this software for any particular
* purpose.
*
* It is provided "as is" without express or implied warranty
* of any kind and OATH AND ITS MEMBERS EXPRESSaLY DISCLAIMS
* ANY WARRANTY OR LIABILITY OF ANY KIND relating to this software.
*
* These notices must be retained in any copies of any part of this
* documentation and/or software.
*/
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.security.NoSuchAlgorithmException;
import java.security.InvalidKeyException;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
/**
* This class contains static methods that are used to calculate the
* One-Time Password (OTP) using
* JCE to provide the HMAC-SHA-1.
*
* @author Loren Hart
* @version 1.0
*/
public class HOTP {
private HOTP() {
}
// These are used to calculate the check-sum digits.
// 0 1 2 3 4 5 6 7 8 9
private static final int[] doubleDigits =
{0, 2, 4, 6, 8, 1, 3, 5, 7, 9};
/**
* Calculates the checksum using the credit card algorithm.
* This algorithm has the advantage that it detects any single
* mistyped digit and any single transposition of
* adjacent digits.
*
* @param num the number to calculate the checksum for
* @param digits number of significant places in the number
* @return the checksum of num
*/
public static int calcChecksum(long num, int digits) {
boolean doubleDigit = true;
int total = 0;
while (0 < digits--) {
int digit = (int) (num % 10);
num /= 10;
if (doubleDigit) {
digit = doubleDigits[digit];
}
total += digit;
doubleDigit = !doubleDigit;
}
int result = total % 10;
if (result > 0) {
result = 10 - result;
}
return result;
}
/**
* This method uses the JCE to provide the HMAC-SHA-1
* algorithm.
* HMAC computes a Hashed Message Authentication Code and
* in this case SHA1 is the hash algorithm used.
*
* @param keyBytes the bytes to use for the HMAC-SHA-1 key
* @param text the message or text to be authenticated.
* @throws NoSuchAlgorithmException if no provider makes
* either HmacSHA1 or HMAC-SHA-1
* digest algorithms available.
* @throws InvalidKeyException The secret provided was not a valid HMAC-SHA-1 key.
*/
public static byte[] hmac_sha1(byte[] keyBytes, byte[] text)
public static String generateOTP(byte[] secret, String algo, int digits, long counter)
throws NoSuchAlgorithmException, InvalidKeyException {
// try {
Mac hmacSha1;
try {
hmacSha1 = Mac.getInstance("HmacSHA1");
} catch (NoSuchAlgorithmException nsae) {
hmacSha1 = Mac.getInstance("HMAC-SHA-1");
}
SecretKeySpec macKey =
new SecretKeySpec(keyBytes, "RAW");
hmacSha1.init(macKey);
return hmacSha1.doFinal(text);
// } catch (GeneralSecurityException gse) {
// throw new UndeclaredThrowableException(gse);
// }
}
SecretKeySpec key = new SecretKeySpec(secret, "RAW");
private static final int[] DIGITS_POWER
// 0 1 2 3 4 5 6 7 8
= {1, 10, 100, 1000, 10000, 100000, 1000000, 10000000, 100000000};
// encode counter in big endian
byte[] counterBytes = ByteBuffer.allocate(8)
.order(ByteOrder.BIG_ENDIAN)
.putLong(counter)
.array();
/**
* This method generates an OTP value for the given
* set of parameters.
*
* @param secret the shared secret
* @param movingFactor the counter, time, or other value that
* changes on a per use basis.
* @param codeDigits the number of digits in the OTP, not
* including the checksum, if any.
* @param addChecksum a flag that indicates if a checksum digit
* should be appended to the OTP.
* @param truncationOffset the offset into the MAC result to
* begin truncation. If this value is out of
* the range of 0 ... 15, then dynamic
* truncation will be used.
* Dynamic truncation is when the last 4
* bits of the last byte of the MAC are
* used to determine the start offset.
* @return A numeric String in base 10 that includes
* {@link codeDigits} digits plus the optional checksum
* digit if requested.
* @throws NoSuchAlgorithmException if no provider makes
* either HmacSHA1 or HMAC-SHA-1
* digest algorithms available.
* @throws InvalidKeyException The secret provided was not
* a valid HMAC-SHA-1 key.
*/
static public String generateOTP(byte[] secret,
long movingFactor,
int codeDigits,
boolean addChecksum,
int truncationOffset)
throws NoSuchAlgorithmException, InvalidKeyException {
// put movingFactor value into text byte array
String result = null;
int digits = addChecksum ? (codeDigits + 1) : codeDigits;
byte[] text = new byte[8];
for (int i = text.length - 1; i >= 0; i--) {
text[i] = (byte) (movingFactor & 0xff);
movingFactor >>= 8;
}
// calculate the hash of the counter
Mac mac = Mac.getInstance(algo);
mac.init(key);
byte[] hash = mac.doFinal(counterBytes);
// compute hmac hash
byte[] hash = hmac_sha1(secret, text);
// put selected bytes into result int
// truncate hash to get the HTOP value
// http://tools.ietf.org/html/rfc4226#section-5.4
int offset = hash[hash.length - 1] & 0xf;
if ((0 <= truncationOffset) &&
(truncationOffset < (hash.length - 4))) {
offset = truncationOffset;
}
int binary =
((hash[offset] & 0x7f) << 24)
| ((hash[offset + 1] & 0xff) << 16)
| ((hash[offset + 2] & 0xff) << 8)
| (hash[offset + 3] & 0xff);
long bin = ((hash[offset] & 0x7f) << 24)
| ((hash[offset + 1] & 0xff) << 16)
| ((hash[offset + 2] & 0xff) << 8)
| (hash[offset + 3] & 0xff);
long otp = bin % (long) Math.pow(10, digits);
int otp = binary % DIGITS_POWER[codeDigits];
if (addChecksum) {
otp = (otp * 10) + calcChecksum(otp, codeDigits);
// prepend zeroes if needed
StringBuilder res = new StringBuilder(Long.toString(otp));
while (res.length() < digits) {
res.insert(0, "0");
}
result = Integer.toString(otp);
while (result.length() < digits) {
result = "0" + result;
}
return result;
return res.toString();
}
}

View file

@ -1,170 +1,21 @@
package com.beemdevelopment.aegis.crypto.otp;
/**
Copyright (c) 2011 IETF Trust and the persons identified as
authors of the code. All rights reserved.
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
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 javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.math.BigInteger;
/**
* 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);
}
public static String generateOTP(byte[] secret, String algo, int digits, long period, long seconds)
throws InvalidKeyException, NoSuchAlgorithmException {
long counter = (long) Math.floor((double) seconds / period);
return HOTP.generateOTP(secret, algo, digits, counter);
}
/**
* 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 generateTOTP(byte[] key,
String time,
int 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(byte[] key,
String time,
int 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(byte[] key,
String time,
int 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(byte[] key,
String time,
int returnDigits,
String crypto) {
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[] hash = hmac_sha(crypto, key, 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[returnDigits];
result = Integer.toString(otp);
while (result.length() < returnDigits) {
result = "0" + result;
}
return result;
public static String generateOTP(byte[] secret, String algo, int digits, long period)
throws InvalidKeyException, NoSuchAlgorithmException {
return generateOTP(secret, algo, digits, period, System.currentTimeMillis() / 1000);
}
}

View file

@ -28,7 +28,7 @@ public class HotpInfo extends OtpInfo {
@Override
public String getOtp() {
try {
return HOTP.generateOTP(getSecret(), getCounter(), getDigits(), false, -1);
return HOTP.generateOTP(getSecret(), getAlgorithm(true), getDigits(), getCounter());
} catch (NoSuchAlgorithmException | InvalidKeyException e) {
throw new RuntimeException();
}

View file

@ -5,6 +5,9 @@ import com.beemdevelopment.aegis.crypto.otp.TOTP;
import org.json.JSONException;
import org.json.JSONObject;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
public class TotpInfo extends OtpInfo {
private int _period;
@ -20,8 +23,11 @@ public class TotpInfo extends OtpInfo {
@Override
public String getOtp() {
String time = Long.toHexString(System.currentTimeMillis() / 1000 / getPeriod());
return TOTP.generateTOTP(getSecret(), time, getDigits(), getAlgorithm(true));
try {
return TOTP.generateOTP(getSecret(), getAlgorithm(true), getDigits(), getPeriod());
} catch (InvalidKeyException | NoSuchAlgorithmException e) {
throw new RuntimeException(e);
}
}
@Override

View file

@ -134,7 +134,11 @@ public class EntryHolder extends RecyclerView.ViewHolder {
private void updateCode() {
String otp = _entry.getInfo().getOtp();
_profileCode.setText(otp.substring(0, otp.length() / 2) + " " + otp.substring(otp.length() / 2));
int offset = 0;
if (otp.length() % 2 != 0) {
offset = 1;
}
_profileCode.setText(otp.substring(0, (otp.length() / 2) + offset) + " " + otp.substring(otp.length() / 2));
}
public void hideCode() {

View file

@ -12,22 +12,22 @@ import static org.junit.Assert.*;
public class HOTPTest {
// https://tools.ietf.org/html/rfc4226#page-32
private final String[] _vectors = {
"755224", "287082",
"359152", "969429",
"338314", "254676",
"287922", "162583",
"399871", "520489"
"755224", "287082",
"359152", "969429",
"338314", "254676",
"287922", "162583",
"399871", "520489"
};
private final byte[] _secret = new byte[] {
0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x30,
0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x30
private final byte[] _secret = new byte[]{
0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x30,
0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x30
};
@Test
public void vectorsMatch() throws InvalidKeyException, NoSuchAlgorithmException {
for (int i = 0; i < _vectors.length; i++) {
String otp = HOTP.generateOTP(_secret, i, 6, false, -1);
String otp = HOTP.generateOTP(_secret, "HmacSHA1", 6, i);
assertEquals(_vectors[i], otp);
}
}

View file

@ -3,31 +3,49 @@ package com.beemdevelopment.aegis;
import org.junit.Test;
import com.beemdevelopment.aegis.crypto.otp.TOTP;
import com.beemdevelopment.aegis.encoding.Hex;
import com.beemdevelopment.aegis.encoding.HexException;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import static org.junit.Assert.*;
public class TOTPTest {
private static class Vector {
public long Time;
public String OTP;
public String Algo;
public Vector(long time, String otp, String algo) {
Time = time;
OTP = otp;
Algo = algo;
}
}
// https://tools.ietf.org/html/rfc6238#appendix-B
private final String[][] _vectors = {
// time, OPT, algorithm
{"0000000000000001", "94287082", "HmacSHA1"},
{"0000000000000001", "46119246", "HmacSHA256"},
{"0000000000000001", "90693936", "HmacSHA512"},
{"00000000023523EC", "07081804", "HmacSHA1"},
{"00000000023523EC", "68084774", "HmacSHA256"},
{"00000000023523EC", "25091201", "HmacSHA512"},
{"00000000023523ED", "14050471", "HmacSHA1"},
{"00000000023523ED", "67062674", "HmacSHA256"},
{"00000000023523ED", "99943326", "HmacSHA512"},
{"000000000273EF07", "89005924", "HmacSHA1"},
{"000000000273EF07", "91819424", "HmacSHA256"},
{"000000000273EF07", "93441116", "HmacSHA512"},
{"0000000003F940AA", "69279037", "HmacSHA1"},
{"0000000003F940AA", "90698825", "HmacSHA256"},
{"0000000003F940AA", "38618901", "HmacSHA512"},
{"0000000027BC86AA", "65353130", "HmacSHA1"},
{"0000000027BC86AA", "77737706", "HmacSHA256"},
{"0000000027BC86AA", "47863826", "HmacSHA512"}
private final Vector[] _vectors = {
new Vector(59, "94287082", "HmacSHA1"),
new Vector(59, "46119246", "HmacSHA256"),
new Vector(59, "90693936", "HmacSHA512"),
new Vector(1111111109, "07081804", "HmacSHA1"),
new Vector(1111111109, "68084774", "HmacSHA256"),
new Vector(1111111109, "25091201", "HmacSHA512"),
new Vector(1111111111, "14050471", "HmacSHA1"),
new Vector(1111111111, "67062674", "HmacSHA256"),
new Vector(1111111111, "99943326", "HmacSHA512"),
new Vector(1234567890, "89005924", "HmacSHA1"),
new Vector(1234567890, "91819424", "HmacSHA256"),
new Vector(1234567890, "93441116", "HmacSHA512"),
new Vector(2000000000, "69279037", "HmacSHA1"),
new Vector(2000000000, "90698825", "HmacSHA256"),
new Vector(2000000000, "38618901", "HmacSHA512"),
new Vector(20000000000L, "65353130", "HmacSHA1"),
new Vector(20000000000L, "77737706", "HmacSHA256"),
new Vector(20000000000L, "47863826", "HmacSHA512")
};
private final byte[] _seed = new byte[]{
@ -50,11 +68,11 @@ public class TOTPTest {
};
@Test
public void vectorsMatch() {
for (String[] vector : _vectors) {
public void vectorsMatch() throws NoSuchAlgorithmException, InvalidKeyException, HexException {
for (Vector vector : _vectors) {
byte[] seed;
switch (vector[2]) {
switch (vector.Algo) {
case "HmacSHA1":
seed = _seed;
break;
@ -69,8 +87,8 @@ public class TOTPTest {
return;
}
String otp = TOTP.generateTOTP(seed, vector[0], 8, vector[2]);
assertEquals(vector[1], otp);
String otp = TOTP.generateOTP(seed, vector.Algo, 8, 30, vector.Time);
assertEquals(vector.OTP, otp);
}
}
}