Introduce
TOTP: Time-Based One-Time Password Algorithm
HOTP(K,C) = Truncate(HMAC-SHA-1(K,C))
Source Code
Java Source Code
TOTP.java
:
package me.hatter.tools.commons.totp; import java.lang.reflect.UndeclaredThrowableException; import java.math.BigInteger; import java.security.GeneralSecurityException; import java.util.Arrays; import java.util.List; import javax.crypto.Mac; import javax.crypto.spec.SecretKeySpec; import me.hatter.tools.commons.totp.Base32String.DecodingException; // otpauth://totp/Example:alice@google.com?secret=JBSWY3DPEHPK3PXP&issuer=Example // https://code.google.com/p/google-authenticator/ public class TOTP { 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); } } 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 }; public static String generateTOTP(String key, String time, String returnDigits) { return generateTOTP(key, time, returnDigits, "HmacSHA1"); } public static String generateTOTP256(String key, String time, String returnDigits) { return generateTOTP(key, time, returnDigits, "HmacSHA256"); } public static String generateTOTP512(String key, String time, String returnDigits) { return generateTOTP(key, time, returnDigits, "HmacSHA512"); } public static String generateTOTP(String key, String time, String returnDigits, String crypto) { return generateTOTP(hexStr2Bytes(key), time, returnDigits, crypto); } public static String generateTOTP(byte[] 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[] 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[codeDigits]; result = Integer.toString(otp); while (result.length() < codeDigits) { result = "0" + result; } return result; } public static String generateTOTP(byte[] key, long sec30OffSet) { long nowIn30Sec = System.currentTimeMillis() / 1000 / 30; return generateTOTP(key, Long.toHexString(nowIn30Sec + sec30OffSet), "6", "HmacSHA1"); } public static String generateTOTP(byte[] key) { return generateTOTP(key, 0L); } public static String generateTOTP(String base32Key) { try { return generateTOTP(Base32String.decode(base32Key), 0L); } catch (DecodingException e) { throw new RuntimeException(e); } } public static List<String> generateTOTP3(byte[] key) { return Arrays.asList(generateTOTP(key, -1L), generateTOTP(key, 0L), generateTOTP(key, 1L)); } public static List<String> generateTOTP3(String base32Key) { try { return generateTOTP3(Base32String.decode(base32Key)); } catch (DecodingException e) { throw new RuntimeException(e); } } }
Base32String.java
:
/* * Copyright 2009 Google Inc. All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package me.hatter.tools.commons.totp; import java.util.HashMap; import java.util.Locale; /** * Encodes arbitrary byte arrays as case-insensitive base-32 strings. * <p> * The implementation is slightly different than in RFC 4648. During encoding, * padding is not added, and during decoding the last incomplete chunk is not * taken into account. The result is that multiple strings decode to the same * byte array, for example, string of sixteen 7s ("7...7") and seventeen 7s both * decode to the same byte array. * TODO(sarvar): Revisit this encoding and whether this ambiguity needs fixing. * * @author sweis@google.com (Steve Weis) * @author Neal Gafter */ public class Base32String { // singleton private static final Base32String INSTANCE = new Base32String("ABCDEFGHIJKLMNOPQRSTUVWXYZ234567"); // RFC 4648/3548 static Base32String getInstance() { return INSTANCE; } // 32 alpha-numeric characters. private String ALPHABET; private char[] DIGITS; private int MASK; private int SHIFT; private HashMap<Character, Integer> CHAR_MAP; static final String SEPARATOR = "-"; protected Base32String(String alphabet) { this.ALPHABET = alphabet; DIGITS = ALPHABET.toCharArray(); MASK = DIGITS.length - 1; SHIFT = Integer.numberOfTrailingZeros(DIGITS.length); CHAR_MAP = new HashMap<Character, Integer>(); for (int i = 0; i < DIGITS.length; i++) { CHAR_MAP.put(DIGITS[i], i); } } public static byte[] decode(String encoded) throws DecodingException { return getInstance().decodeInternal(encoded); } protected byte[] decodeInternal(String encoded) throws DecodingException { // Remove whitespace and separators encoded = encoded.trim().replaceAll(SEPARATOR, "").replaceAll(" ", ""); // Remove padding. Note: the padding is used as hint to determine how many // bits to decode from the last incomplete chunk (which is commented out // below, so this may have been wrong to start with). encoded = encoded.replaceFirst("[=]*$", ""); // Canonicalize to all upper case encoded = encoded.toUpperCase(Locale.US); if (encoded.length() == 0) { return new byte[0]; } int encodedLength = encoded.length(); int outLength = encodedLength * SHIFT / 8; byte[] result = new byte[outLength]; int buffer = 0; int next = 0; int bitsLeft = 0; for (char c : encoded.toCharArray()) { if (!CHAR_MAP.containsKey(c)) { throw new DecodingException("Illegal character: " + c); } buffer <<= SHIFT; buffer |= CHAR_MAP.get(c) & MASK; bitsLeft += SHIFT; if (bitsLeft >= 8) { result[next++] = (byte) (buffer >> (bitsLeft - 8)); bitsLeft -= 8; } } // We'll ignore leftover bits for now. // // if (next != outLength || bitsLeft >= SHIFT) { // throw new DecodingException("Bits left: " + bitsLeft); // } return result; } public static String encode(byte[] data) { return getInstance().encodeInternal(data); } protected String encodeInternal(byte[] data) { if (data.length == 0) { return ""; } // SHIFT is the number of bits per output character, so the length of the // output is the length of the input multiplied by 8/SHIFT, rounded up. if (data.length >= (1 << 28)) { // The computation below will fail, so don't do it. throw new IllegalArgumentException(); } int outputLength = (data.length * 8 + SHIFT - 1) / SHIFT; StringBuilder result = new StringBuilder(outputLength); int buffer = data[0]; int next = 1; int bitsLeft = 8; while (bitsLeft > 0 || next < data.length) { if (bitsLeft < SHIFT) { if (next < data.length) { buffer <<= 8; buffer |= (data[next++] & 0xff); bitsLeft += 8; } else { int pad = SHIFT - bitsLeft; buffer <<= pad; bitsLeft += pad; } } int index = MASK & (buffer >> (bitsLeft - SHIFT)); bitsLeft -= SHIFT; result.append(DIGITS[index]); } return result.toString(); } @Override // enforce that this class is a singleton public Object clone() throws CloneNotSupportedException { throw new CloneNotSupportedException(); } public static class DecodingException extends Exception { private static final long serialVersionUID = 8330091713817140208L; public DecodingException(String message) { super(message); } } }
Authenticators
Reference
- https://github.com/google/google-authenticator
- Open source version of Google Authenticator (except the Android app)
- https://tools.ietf.org/html/rfc6238 - TOTP: Time-Based One-Time Password Algorithm