/* * Password Management Servlets (PWM) * http://www.pwm-project.org * * Copyright (c) 2006-2009 Novell, Inc. * Copyright (c) 2009-2017 The PWM Project * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ package password.pwm.util.operations.otp; import javax.crypto.Mac; import java.io.ByteArrayInputStream; import java.io.DataInput; import java.io.DataInputStream; import java.io.IOException; import java.nio.ByteBuffer; import java.security.GeneralSecurityException; /** * 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. * * The default passcode is a 6-digit decimal code and the default timeout * period is 5 minutes. * * @author sweis@google.com (Steve Weis) * * http://code.google.com/p/google-authenticator/ * */ public class PasscodeGenerator { /** Default decimal passcode length */ private static final int PASS_CODE_LENGTH = 6; /** Default passcode timeout period (in seconds) */ private static final int INTERVAL = 30; /** The number of previous and future intervals to check */ private static final int ADJACENT_INTERVALS = 1; private static final int PIN_MODULO = (int) Math.pow(10, PASS_CODE_LENGTH); private final Signer signer; private final int codeLength; private final int intervalPeriod; /* * Using an interface to allow us to inject different signature * implementations. */ interface Signer { byte[] sign(byte[] data) throws GeneralSecurityException; } /** * @param mac A {@link Mac} used to generate passcodes */ public PasscodeGenerator(final Mac mac) { this(mac, PASS_CODE_LENGTH, INTERVAL); } /** * @param mac A {@link Mac} used to generate passcodes * @param passCodeLength The length of the decimal passcode * @param interval The interval that a passcode is valid for */ public PasscodeGenerator(final Mac mac, final int passCodeLength, final int interval) { this(new Signer() { public byte[] sign(final byte[] data){ return mac.doFinal(data); } }, passCodeLength, interval); } public PasscodeGenerator(final Signer signer, final int passCodeLength, final int interval) { this.signer = signer; this.codeLength = passCodeLength; this.intervalPeriod = interval; } private String padOutput(final int value) { String result = Integer.toString(value); for (int i = result.length(); i < codeLength; i++) { result = "0" + result; } return result; } /** * @return A decimal timeout code * */ public String generateTimeoutCode() throws GeneralSecurityException { return generateResponseCode(clock.getCurrentInterval()); } /** * @param challenge A long-valued challenge * @return A decimal response code * @throws GeneralSecurityException If a JCE exception occur */ public String generateResponseCode(final long challenge) throws GeneralSecurityException { final byte[] value = ByteBuffer.allocate(8).putLong(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(final byte[] challenge) throws GeneralSecurityException { final byte[] hash = signer.sign(challenge); // Dynamically truncate the hash // OffsetBits are the low order bits of the last byte of the hash final int offset = hash[hash.length - 1] & 0xF; // Grab a positive integer value starting at the given offset. final int truncatedHash = hashToInt(hash, offset) & 0x7FFFFFFF; final int pinValue = truncatedHash % PIN_MODULO; 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(final byte[] bytes, final int start) { final DataInput input = new DataInputStream( new ByteArrayInputStream(bytes, start, bytes.length - start)); final 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(final long challenge, final String response) throws GeneralSecurityException { final String expectedResponse = generateResponseCode(challenge); 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(final String timeoutCode) throws GeneralSecurityException { return verifyTimeoutCode(timeoutCode, 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(final String timeoutCode, final int pastIntervals, final int futureIntervals) throws GeneralSecurityException { final long currentInterval = clock.getCurrentInterval(); final String expectedResponse = generateResponseCode(currentInterval); if (expectedResponse.equals(timeoutCode)) { return true; } for (int i = 1; i <= pastIntervals; i++) { final String pastResponse = generateResponseCode(currentInterval - i); if (pastResponse.equals(timeoutCode)) { return true; } } for (int i = 1; i <= futureIntervals; i++) { final String futureResponse = generateResponseCode(currentInterval + i); if (futureResponse.equals(timeoutCode)) { return true; } } return false; } private IntervalClock clock = new IntervalClock() { /* * @return The current interval */ public long getCurrentInterval() { final long currentTimeSeconds = System.currentTimeMillis() / 1000; return currentTimeSeconds / getIntervalPeriod(); } public int getIntervalPeriod() { return intervalPeriod; } }; // To facilitate injecting a mock clock interface IntervalClock { int getIntervalPeriod(); long getCurrentInterval(); } }