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