/* * 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 net.coding.program.login.auth; import java.io.ByteArrayInputStream; import java.io.DataInput; import java.io.DataInputStream; import java.io.IOException; import java.nio.ByteBuffer; import java.security.GeneralSecurityException; import javax.crypto.Mac; /** * An implementation of the HOTP generator specified by RFC 4226. Generates * short passcodes that may be used in challenge-response protocols or as * timeout passcodes that are only valid for a short period. * <p> * The default passcode is a 6-digit decimal code. The maximum passcode length is 9 digits. * * @author sweis@google.com (Steve Weis) */ public class PasscodeGenerator { /** * Default time interval */ public static final int INTERVAL = 30; private static final int MAX_PASSCODE_LENGTH = 9; /** * Default decimal passcode length */ private static final int PASS_CODE_LENGTH = 6; /** * The number of previous and future intervals to check */ private static final int ADJACENT_INTERVALS = 1; /** * Powers of 10 used to shorten the pin to the desired number of digits */ private static final int[] DIGITS_POWER // 0 1 2 3 4 5 6 7 8 9 = {1, 10, 100, 1000, 10000, 100000, 1000000, 10000000, 100000000, 1000000000}; private final Signer signer; private final int codeLength; /** * @param mac A {@link Mac} used to generate passcodes */ public PasscodeGenerator(Mac mac) { this(mac, PASS_CODE_LENGTH); } public PasscodeGenerator(Signer signer) { this(signer, PASS_CODE_LENGTH); } /** * @param mac A {@link Mac} used to generate passcodes * @param passCodeLength The length of the decimal passcode */ public PasscodeGenerator(final Mac mac, int passCodeLength) { this(new Signer() { @Override public byte[] sign(byte[] data) { return mac.doFinal(data); } }, passCodeLength); } public PasscodeGenerator(Signer signer, int passCodeLength) { if ((passCodeLength < 0) || (passCodeLength > MAX_PASSCODE_LENGTH)) { throw new IllegalArgumentException( "PassCodeLength must be between 1 and " + MAX_PASSCODE_LENGTH + " digits."); } this.signer = signer; this.codeLength = passCodeLength; } private String padOutput(int value) { String result = Integer.toString(value); for (int i = result.length(); i < codeLength; i++) { result = "0" + result; } return result; } /** * @param state 8-byte integer value representing internal OTP state. * @return A decimal response code * @throws GeneralSecurityException If a JCE exception occur */ public String generateResponseCode(long state) throws GeneralSecurityException { byte[] value = ByteBuffer.allocate(8).putLong(state).array(); return generateResponseCode(value); } /** * @param state 8-byte integer value representing internal OTP state. * @param challenge Optional challenge as array of bytes. * @return A decimal response code * @throws GeneralSecurityException If a JCE exception occur */ public String generateResponseCode(long state, byte[] challenge) throws GeneralSecurityException { if (challenge == null) { return generateResponseCode(state); } else { // Allocate space for combination and store. byte value[] = ByteBuffer.allocate(8 + challenge.length) .putLong(state) // Write out OTP state .put(challenge, 0, challenge.length) // Concatenate with challenge. .array(); return generateResponseCode(value); } } /** * @param challenge An arbitrary byte array used as a challenge * @return A decimal response code * @throws GeneralSecurityException If a JCE exception occur */ public String generateResponseCode(byte[] challenge) throws GeneralSecurityException { byte[] hash = signer.sign(challenge); // Dynamically truncate the hash // OffsetBits are the low order bits of the last byte of the hash int offset = hash[hash.length - 1] & 0xF; // Grab a positive integer value starting at the given offset. int truncatedHash = hashToInt(hash, offset) & 0x7FFFFFFF; int pinValue = truncatedHash % DIGITS_POWER[codeLength]; return padOutput(pinValue); } /** * Grabs a positive integer value from the input array starting at * the given offset. * * @param bytes the array of bytes * @param start the index into the array to start grabbing bytes * @return the integer constructed from the four bytes in the array */ private int hashToInt(byte[] bytes, int start) { DataInput input = new DataInputStream( new ByteArrayInputStream(bytes, start, bytes.length - start)); int val; try { val = input.readInt(); } catch (IOException e) { throw new IllegalStateException(e); } return val; } /** * @param challenge A challenge to check a response against * @param response A response to verify * @return True if the response is valid */ public boolean verifyResponseCode(long challenge, String response) throws GeneralSecurityException { String expectedResponse = generateResponseCode(challenge, null); return expectedResponse.equals(response); } /** * Verify a timeout code. The timeout code will be valid for a time * determined by the interval period and the number of adjacent intervals * checked. * * @param timeoutCode The timeout code * @return True if the timeout code is valid */ public boolean verifyTimeoutCode(long currentInterval, String timeoutCode) throws GeneralSecurityException { return verifyTimeoutCode(timeoutCode, currentInterval, ADJACENT_INTERVALS, ADJACENT_INTERVALS); } /** * Verify a timeout code. The timeout code will be valid for a time * determined by the interval period and the number of adjacent intervals * checked. * * @param timeoutCode The timeout code * @param pastIntervals The number of past intervals to check * @param futureIntervals The number of future intervals to check * @return True if the timeout code is valid */ public boolean verifyTimeoutCode(String timeoutCode, long currentInterval, int pastIntervals, int futureIntervals) throws GeneralSecurityException { // Ensure that look-ahead and look-back counts are not negative. pastIntervals = Math.max(pastIntervals, 0); futureIntervals = Math.max(futureIntervals, 0); // Try upto "pastIntervals" before current time, and upto "futureIntervals" after. for (int i = -pastIntervals; i <= futureIntervals; ++i) { String candidate = generateResponseCode(currentInterval - i, null); if (candidate.equals(timeoutCode)) { return true; } } return false; } }