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}