001/*
002 * Copyright 2012-2018 Ping Identity Corporation
003 * All Rights Reserved.
004 */
005/*
006 * Copyright (C) 2015-2018 Ping Identity Corporation
007 *
008 * This program is free software; you can redistribute it and/or modify
009 * it under the terms of the GNU General Public License (GPLv2 only)
010 * or the terms of the GNU Lesser General Public License (LGPLv2.1 only)
011 * as published by the Free Software Foundation.
012 *
013 * This program is distributed in the hope that it will be useful,
014 * but WITHOUT ANY WARRANTY; without even the implied warranty of
015 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
016 * GNU General Public License for more details.
017 *
018 * You should have received a copy of the GNU General Public License
019 * along with this program; if not, see <http://www.gnu.org/licenses>.
020 */
021package com.unboundid.ldap.sdk.unboundidds;
022
023
024
025import java.text.DecimalFormat;
026import javax.crypto.Mac;
027import javax.crypto.SecretKey;
028import javax.crypto.spec.SecretKeySpec;
029
030import com.unboundid.ldap.sdk.LDAPException;
031import com.unboundid.ldap.sdk.ResultCode;
032import com.unboundid.util.Debug;
033import com.unboundid.util.StaticUtils;
034import com.unboundid.util.ThreadSafety;
035import com.unboundid.util.ThreadSafetyLevel;
036
037import static com.unboundid.ldap.sdk.unboundidds.UnboundIDDSMessages.*;
038
039
040
041/**
042 * This class provides support for a number of one-time password algorithms.
043 * <BR>
044 * <BLOCKQUOTE>
045 *   <B>NOTE:</B>  This class, and other classes within the
046 *   {@code com.unboundid.ldap.sdk.unboundidds} package structure, are only
047 *   supported for use against Ping Identity, UnboundID, and Alcatel-Lucent 8661
048 *   server products.  These classes provide support for proprietary
049 *   functionality or for external specifications that are not considered stable
050 *   or mature enough to be guaranteed to work in an interoperable way with
051 *   other types of LDAP servers.
052 * </BLOCKQUOTE>
053 * <BR>
054 * Supported algorithms include:
055 * <UL>
056 *   <LI>HOTP -- The HMAC-based one-time password algorithm described in
057 *       <A HREF="http://www.ietf.org/rfc/rfc4226.txt">RFC 4226</A>.</LI>
058 *   <LI>TOTP -- The time-based one-time password algorithm described in
059 *       <A HREF="http://www.ietf.org/rfc/rfc6238.txt">RFC 6238</A>.</LI>
060 * </UL>
061 */
062@ThreadSafety(level=ThreadSafetyLevel.COMPLETELY_THREADSAFE)
063public final class OneTimePassword
064{
065  /**
066   * The default number of digits to include in generated HOTP passwords.
067   */
068  public static final int DEFAULT_HOTP_NUM_DIGITS = 6;
069
070
071
072  /**
073   * The default time interval (in seconds) to use when generating TOTP
074   * passwords.
075   */
076  public static final int DEFAULT_TOTP_INTERVAL_DURATION_SECONDS = 30;
077
078
079
080  /**
081   * The default number of digits to include in generated TOTP passwords.
082   */
083  public static final int DEFAULT_TOTP_NUM_DIGITS = 6;
084
085
086
087  /**
088   * The name of the MAC algorithm that will be used to perform HMAC-SHA-1
089   * processing.
090   */
091  private static final String HMAC_ALGORITHM_SHA_1 = "HmacSHA1";
092
093
094
095  /**
096   * The name of the secret key spec algorithm that will be used to construct a
097   * secret key from the raw bytes that comprise it.
098   */
099  private static final String KEY_ALGORITHM_RAW = "RAW";
100
101
102
103  /**
104   * Prevent this utility class from being instantiated.
105   */
106  private OneTimePassword()
107  {
108    // No implementation required.
109  }
110
111
112
113  /**
114   * Generates a six-digit HMAC-based one-time-password using the provided
115   * information.
116   *
117   * @param  sharedSecret  The secret key shared by both parties that will be
118   *                       using the generated one-time password.
119   * @param  counter       The counter value that will be used in the course of
120   *                       generating the one-time password.
121   *
122   * @return  The zero-padded string representation of the resulting HMAC-based
123   *          one-time password.
124   *
125   * @throws  LDAPException  If an unexpected problem is encountered while
126   *                         attempting to generate the one-time password.
127   */
128  public static String hotp(final byte[] sharedSecret, final long counter)
129         throws LDAPException
130  {
131    return hotp(sharedSecret, counter, DEFAULT_HOTP_NUM_DIGITS);
132  }
133
134
135
136  /**
137   * Generates an HMAC-based one-time-password using the provided information.
138   *
139   * @param  sharedSecret  The secret key shared by both parties that will be
140   *                       using the generated one-time password.
141   * @param  counter       The counter value that will be used in the course of
142   *                       generating the one-time password.
143   * @param  numDigits     The number of digits that should be included in the
144   *                       generated one-time password.  It must be greater than
145   *                       or equal to six and less than or equal to eight.
146   *
147   * @return  The zero-padded string representation of the resulting HMAC-based
148   *          one-time password.
149   *
150   * @throws  LDAPException  If an unexpected problem is encountered while
151   *                         attempting to generate the one-time password.
152   */
153  public static String hotp(final byte[] sharedSecret, final long counter,
154                            final int numDigits)
155         throws LDAPException
156  {
157    try
158    {
159      // Ensure that the number of digits is between 6 and 8, inclusive, and
160      // get the appropriate modulus and decimal formatters to use.
161      final int modulus;
162      final DecimalFormat decimalFormat;
163      switch (numDigits)
164      {
165        case 6:
166          modulus = 1000000;
167          decimalFormat = new DecimalFormat("000000");
168          break;
169        case 7:
170          modulus = 10000000;
171          decimalFormat = new DecimalFormat("0000000");
172          break;
173        case 8:
174          modulus = 100000000;
175          decimalFormat = new DecimalFormat("00000000");
176          break;
177        default:
178          throw new LDAPException(ResultCode.PARAM_ERROR,
179               ERR_HOTP_INVALID_NUM_DIGITS.get(numDigits));
180      }
181
182
183      // Convert the provided counter to a 64-bit value.
184      final byte[] counterBytes = new byte[8];
185      counterBytes[0] = (byte) ((counter >> 56) & 0xFFL);
186      counterBytes[1] = (byte) ((counter >> 48) & 0xFFL);
187      counterBytes[2] = (byte) ((counter >> 40) & 0xFFL);
188      counterBytes[3] = (byte) ((counter >> 32) & 0xFFL);
189      counterBytes[4] = (byte) ((counter >> 24) & 0xFFL);
190      counterBytes[5] = (byte) ((counter >> 16) & 0xFFL);
191      counterBytes[6] = (byte) ((counter >> 8) & 0xFFL);
192      counterBytes[7] = (byte) (counter & 0xFFL);
193
194
195      // Generate an HMAC-SHA-1 of the given counter using the provided key.
196      final SecretKey k = new SecretKeySpec(sharedSecret, KEY_ALGORITHM_RAW);
197      final Mac m = Mac.getInstance(HMAC_ALGORITHM_SHA_1);
198      m.init(k);
199      final byte[] hmacBytes = m.doFinal(counterBytes);
200
201
202      // Generate a dynamic truncation of the resulting HMAC-SHA-1.
203      final int dtOffset = hmacBytes[19] & 0x0F;
204      final int dtValue  = (((hmacBytes[dtOffset] & 0x7F) << 24) |
205           ((hmacBytes[dtOffset+1] & 0xFF) << 16) |
206           ((hmacBytes[dtOffset+2] & 0xFF) << 8) |
207           (hmacBytes[dtOffset+3] & 0xFF));
208
209
210      // Use a modulus operation to convert the value into one that has at most
211      // the desired number of digits.
212      return decimalFormat.format(dtValue % modulus);
213    }
214    catch (final Exception e)
215    {
216      Debug.debugException(e);
217      throw new LDAPException(ResultCode.LOCAL_ERROR,
218           ERR_HOTP_ERROR_GENERATING_PW.get(StaticUtils.getExceptionMessage(e)),
219           e);
220    }
221  }
222
223
224
225  /**
226   * Generates a six-digit time-based one-time-password using the provided
227   * information and a 30-second time interval.
228   *
229   * @param  sharedSecret  The secret key shared by both parties that will be
230   *                       using the generated one-time password.
231   *
232   * @return  The zero-padded string representation of the resulting time-based
233   *          one-time password.
234   *
235   * @throws  LDAPException  If an unexpected problem is encountered while
236   *                         attempting to generate the one-time password.
237   */
238  public static String totp(final byte[] sharedSecret)
239         throws LDAPException
240  {
241    return totp(sharedSecret, System.currentTimeMillis(),
242         DEFAULT_TOTP_INTERVAL_DURATION_SECONDS, DEFAULT_TOTP_NUM_DIGITS);
243  }
244
245
246
247  /**
248   * Generates a six-digit time-based one-time-password using the provided
249   * information.
250   *
251   * @param  sharedSecret             The secret key shared by both parties that
252   *                                  will be using the generated one-time
253   *                                  password.
254   * @param  authTime                 The time (in milliseconds since the epoch,
255   *                                  as reported by
256   *                                  {@code System.currentTimeMillis} or
257   *                                  {@code Date.getTime}) at which the
258   *                                  authentication attempt occurred.
259   * @param  intervalDurationSeconds  The duration of the time interval, in
260   *                                  seconds, that should be used when
261   *                                  performing the computation.
262   * @param  numDigits                The number of digits that should be
263   *                                  included in the generated one-time
264   *                                  password.  It must be greater than or
265   *                                  equal to six and less than or equal to
266   *                                  eight.
267   *
268   * @return  The zero-padded string representation of the resulting time-based
269   *          one-time password.
270   *
271   * @throws  LDAPException  If an unexpected problem is encountered while
272   *                         attempting to generate the one-time password.
273   */
274  public static String totp(final byte[] sharedSecret, final long authTime,
275                            final int intervalDurationSeconds,
276                            final int numDigits)
277         throws LDAPException
278  {
279    // Make sure that the specified number of digits is between 6 and 8,
280    // inclusive.
281    if ((numDigits < 6) || (numDigits > 8))
282    {
283      throw new LDAPException(ResultCode.PARAM_ERROR,
284           ERR_TOTP_INVALID_NUM_DIGITS.get(numDigits));
285    }
286
287    try
288    {
289      final long timeIntervalNumber = authTime / 1000 / intervalDurationSeconds;
290      return hotp(sharedSecret, timeIntervalNumber, numDigits);
291    }
292    catch (final Exception e)
293    {
294      Debug.debugException(e);
295      throw new LDAPException(ResultCode.LOCAL_ERROR,
296           ERR_TOTP_ERROR_GENERATING_PW.get(StaticUtils.getExceptionMessage(e)),
297           e);
298    }
299  }
300}