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}