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