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 048 * Nokia/Alcatel-Lucent 8661 server products. These classes provide support 049 * for proprietary functionality or for external specifications that are not 050 * considered stable or mature enough to be guaranteed to work in an 051 * interoperable way with 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 = 1_000_000; 167 decimalFormat = new DecimalFormat("000000"); 168 break; 169 case 7: 170 modulus = 10_000_000; 171 decimalFormat = new DecimalFormat("0000000"); 172 break; 173 case 8: 174 modulus = 100_000_000; 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}