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