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