001 /*
002 * CDDL HEADER START
003 *
004 * The contents of this file are subject to the terms of the
005 * Common Development and Distribution License, Version 1.0 only
006 * (the "License"). You may not use this file except in compliance
007 * with the License.
008 *
009 * You can obtain a copy of the license at
010 * trunk/opends/resource/legal-notices/OpenDS.LICENSE
011 * or https://OpenDS.dev.java.net/OpenDS.LICENSE.
012 * See the License for the specific language governing permissions
013 * and limitations under the License.
014 *
015 * When distributing Covered Code, include this CDDL HEADER in each
016 * file and include the License file at
017 * trunk/opends/resource/legal-notices/OpenDS.LICENSE. If applicable,
018 * add the following below this CDDL HEADER, with the fields enclosed
019 * by brackets "[]" replaced with your own identifying information:
020 * Portions Copyright [yyyy] [name of copyright owner]
021 *
022 * CDDL HEADER END
023 *
024 *
025 * Copyright 2006-2008 Sun Microsystems, Inc.
026 */
027 package org.opends.server.extensions;
028
029
030
031 import java.security.MessageDigest;
032 import java.util.Arrays;
033 import java.util.Random;
034
035 import org.opends.messages.Message;
036 import org.opends.server.admin.std.server.SaltedSHA384PasswordStorageSchemeCfg;
037 import org.opends.server.api.PasswordStorageScheme;
038 import org.opends.server.config.ConfigException;
039 import org.opends.server.core.DirectoryServer;
040 import org.opends.server.loggers.ErrorLogger;
041 import org.opends.server.loggers.debug.DebugTracer;
042 import org.opends.server.types.ByteString;
043 import org.opends.server.types.ByteStringFactory;
044 import org.opends.server.types.DebugLogLevel;
045 import org.opends.server.types.DirectoryException;
046 import org.opends.server.types.InitializationException;
047 import org.opends.server.types.ResultCode;
048 import org.opends.server.util.Base64;
049
050 import static org.opends.messages.ExtensionMessages.*;
051 import static org.opends.server.extensions.ExtensionsConstants.*;
052 import static org.opends.server.loggers.debug.DebugLogger.*;
053 import static org.opends.server.util.StaticUtils.*;
054
055
056
057 /**
058 * This class defines a Directory Server password storage scheme based on the
059 * 384-bit SHA-2 algorithm defined in FIPS 180-2. This is a one-way digest
060 * algorithm so there is no way to retrieve the original clear-text version of
061 * the password from the hashed value (although this means that it is not
062 * suitable for things that need the clear-text password like DIGEST-MD5). The
063 * values that it generates are also salted, which protects against dictionary
064 * attacks. It does this by generating a 64-bit random salt which is appended to
065 * the clear-text value. A SHA-2 hash is then generated based on this, the salt
066 * is appended to the hash, and then the entire value is base64-encoded.
067 */
068 public class SaltedSHA384PasswordStorageScheme
069 extends PasswordStorageScheme<SaltedSHA384PasswordStorageSchemeCfg>
070 {
071 /**
072 * The tracer object for the debug logger.
073 */
074 private static final DebugTracer TRACER = getTracer();
075
076 /**
077 * The fully-qualified name of this class.
078 */
079 private static final String CLASS_NAME =
080 "org.opends.server.extensions.SaltedSHA384PasswordStorageScheme";
081
082
083
084 /**
085 * The number of bytes of random data to use as the salt when generating the
086 * hashes.
087 */
088 private static final int NUM_SALT_BYTES = 8;
089
090
091
092 // The message digest that will actually be used to generate the 384-bit SHA-2
093 // hashes.
094 private MessageDigest messageDigest;
095
096 // The lock used to provide threadsafe access to the message digest.
097 private Object digestLock;
098
099 // The secure random number generator to use to generate the salt values.
100 private Random random;
101
102
103
104 /**
105 * Creates a new instance of this password storage scheme. Note that no
106 * initialization should be performed here, as all initialization should be
107 * done in the <CODE>initializePasswordStorageScheme</CODE> method.
108 */
109 public SaltedSHA384PasswordStorageScheme()
110 {
111 super();
112 }
113
114
115
116 /**
117 * {@inheritDoc}
118 */
119 @Override()
120 public void initializePasswordStorageScheme(
121 SaltedSHA384PasswordStorageSchemeCfg configuration)
122 throws ConfigException, InitializationException
123 {
124 try
125 {
126 messageDigest =
127 MessageDigest.getInstance(MESSAGE_DIGEST_ALGORITHM_SHA_384);
128 }
129 catch (Exception e)
130 {
131 if (debugEnabled())
132 {
133 TRACER.debugCaught(DebugLogLevel.ERROR, e);
134 }
135
136 Message message = ERR_PWSCHEME_CANNOT_INITIALIZE_MESSAGE_DIGEST.get(
137 MESSAGE_DIGEST_ALGORITHM_SHA_384, String.valueOf(e));
138 throw new InitializationException(message, e);
139 }
140
141
142 digestLock = new Object();
143 random = new Random();
144 }
145
146
147
148 /**
149 * {@inheritDoc}
150 */
151 @Override()
152 public String getStorageSchemeName()
153 {
154 return STORAGE_SCHEME_NAME_SALTED_SHA_384;
155 }
156
157
158
159 /**
160 * {@inheritDoc}
161 */
162 @Override()
163 public ByteString encodePassword(ByteString plaintext)
164 throws DirectoryException
165 {
166 byte[] plainBytes = plaintext.value();
167 byte[] saltBytes = new byte[NUM_SALT_BYTES];
168 byte[] plainPlusSalt = new byte[plainBytes.length + NUM_SALT_BYTES];
169
170 System.arraycopy(plainBytes, 0, plainPlusSalt,0,plainBytes.length);
171
172 byte[] digestBytes;
173
174 synchronized (digestLock)
175 {
176 try
177 {
178 // Generate the salt and put in the plain+salt array.
179 random.nextBytes(saltBytes);
180 System.arraycopy(saltBytes,0, plainPlusSalt, plainBytes.length,
181 NUM_SALT_BYTES);
182
183 // Create the hash from the concatenated value.
184 digestBytes = messageDigest.digest(plainPlusSalt);
185 }
186 catch (Exception e)
187 {
188 if (debugEnabled())
189 {
190 TRACER.debugCaught(DebugLogLevel.ERROR, e);
191 }
192
193 Message message = ERR_PWSCHEME_CANNOT_ENCODE_PASSWORD.get(
194 CLASS_NAME, getExceptionMessage(e));
195 throw new DirectoryException(DirectoryServer.getServerErrorResultCode(),
196 message, e);
197 }
198 }
199
200 // Append the salt to the hashed value and base64-the whole thing.
201 byte[] hashPlusSalt = new byte[digestBytes.length + NUM_SALT_BYTES];
202
203 System.arraycopy(digestBytes, 0, hashPlusSalt, 0, digestBytes.length);
204 System.arraycopy(saltBytes, 0, hashPlusSalt, digestBytes.length,
205 NUM_SALT_BYTES);
206
207 return ByteStringFactory.create(Base64.encode(hashPlusSalt));
208 }
209
210
211
212 /**
213 * {@inheritDoc}
214 */
215 @Override()
216 public ByteString encodePasswordWithScheme(ByteString plaintext)
217 throws DirectoryException
218 {
219 StringBuilder buffer = new StringBuilder();
220 buffer.append('{');
221 buffer.append(STORAGE_SCHEME_NAME_SALTED_SHA_384);
222 buffer.append('}');
223
224 byte[] plainBytes = plaintext.value();
225 byte[] saltBytes = new byte[NUM_SALT_BYTES];
226 byte[] plainPlusSalt = new byte[plainBytes.length + NUM_SALT_BYTES];
227
228 System.arraycopy(plainBytes, 0, plainPlusSalt,0,plainBytes.length);
229
230 byte[] digestBytes;
231
232 synchronized (digestLock)
233 {
234 try
235 {
236 // Generate the salt and put in the plain+salt array.
237 random.nextBytes(saltBytes);
238 System.arraycopy(saltBytes,0, plainPlusSalt, plainBytes.length,
239 NUM_SALT_BYTES);
240
241 // Create the hash from the concatenated value.
242 digestBytes = messageDigest.digest(plainPlusSalt);
243 }
244 catch (Exception e)
245 {
246 if (debugEnabled())
247 {
248 TRACER.debugCaught(DebugLogLevel.ERROR, e);
249 }
250
251 Message message = ERR_PWSCHEME_CANNOT_ENCODE_PASSWORD.get(
252 CLASS_NAME, getExceptionMessage(e));
253 throw new DirectoryException(DirectoryServer.getServerErrorResultCode(),
254 message, e);
255 }
256 }
257
258 // Append the salt to the hashed value and base64-the whole thing.
259 byte[] hashPlusSalt = new byte[digestBytes.length + NUM_SALT_BYTES];
260
261 System.arraycopy(digestBytes, 0, hashPlusSalt, 0, digestBytes.length);
262 System.arraycopy(saltBytes, 0, hashPlusSalt, digestBytes.length,
263 NUM_SALT_BYTES);
264 buffer.append(Base64.encode(hashPlusSalt));
265
266 return ByteStringFactory.create(buffer.toString());
267 }
268
269
270
271 /**
272 * {@inheritDoc}
273 */
274 @Override()
275 public boolean passwordMatches(ByteString plaintextPassword,
276 ByteString storedPassword)
277 {
278 // Base64-decode the stored value and take the last 8 bytes as the salt.
279 byte[] saltBytes = new byte[NUM_SALT_BYTES];
280 byte[] digestBytes;
281 try
282 {
283 byte[] decodedBytes = Base64.decode(storedPassword.stringValue());
284
285 int digestLength = decodedBytes.length - NUM_SALT_BYTES;
286 digestBytes = new byte[digestLength];
287 System.arraycopy(decodedBytes, 0, digestBytes, 0, digestLength);
288 System.arraycopy(decodedBytes, digestLength, saltBytes, 0,
289 NUM_SALT_BYTES);
290 }
291 catch (Exception e)
292 {
293 if (debugEnabled())
294 {
295 TRACER.debugCaught(DebugLogLevel.ERROR, e);
296 }
297
298 Message message = ERR_PWSCHEME_CANNOT_BASE64_DECODE_STORED_PASSWORD.get(
299 storedPassword.stringValue(), String.valueOf(e));
300 ErrorLogger.logError(message);
301 return false;
302 }
303
304
305 // Use the salt to generate a digest based on the provided plain-text value.
306 byte[] plainBytes = plaintextPassword.value();
307 byte[] plainPlusSalt = new byte[plainBytes.length + NUM_SALT_BYTES];
308 System.arraycopy(plainBytes, 0, plainPlusSalt, 0, plainBytes.length);
309 System.arraycopy(saltBytes, 0,plainPlusSalt, plainBytes.length,
310 NUM_SALT_BYTES);
311
312 byte[] userDigestBytes;
313
314 synchronized (digestLock)
315 {
316 try
317 {
318 userDigestBytes = messageDigest.digest(plainPlusSalt);
319 }
320 catch (Exception e)
321 {
322 if (debugEnabled())
323 {
324 TRACER.debugCaught(DebugLogLevel.ERROR, e);
325 }
326
327 return false;
328 }
329 }
330
331 return Arrays.equals(digestBytes, userDigestBytes);
332 }
333
334
335
336 /**
337 * {@inheritDoc}
338 */
339 @Override()
340 public boolean supportsAuthPasswordSyntax()
341 {
342 // This storage scheme does support the authentication password syntax.
343 return true;
344 }
345
346
347
348 /**
349 * {@inheritDoc}
350 */
351 @Override()
352 public String getAuthPasswordSchemeName()
353 {
354 return AUTH_PASSWORD_SCHEME_NAME_SALTED_SHA_384;
355 }
356
357
358
359 /**
360 * {@inheritDoc}
361 */
362 @Override()
363 public ByteString encodeAuthPassword(ByteString plaintext)
364 throws DirectoryException
365 {
366 byte[] plainBytes = plaintext.value();
367 byte[] saltBytes = new byte[NUM_SALT_BYTES];
368 byte[] plainPlusSalt = new byte[plainBytes.length + NUM_SALT_BYTES];
369
370 System.arraycopy(plainBytes, 0, plainPlusSalt, 0, plainBytes.length);
371
372 byte[] digestBytes;
373
374 synchronized (digestLock)
375 {
376 try
377 {
378 // Generate the salt and put in the plain+salt array.
379 random.nextBytes(saltBytes);
380 System.arraycopy(saltBytes,0, plainPlusSalt, plainBytes.length,
381 NUM_SALT_BYTES);
382
383 // Create the hash from the concatenated value.
384 digestBytes = messageDigest.digest(plainPlusSalt);
385 }
386 catch (Exception e)
387 {
388 if (debugEnabled())
389 {
390 TRACER.debugCaught(DebugLogLevel.ERROR, e);
391 }
392
393 Message message = ERR_PWSCHEME_CANNOT_ENCODE_PASSWORD.get(
394 CLASS_NAME, getExceptionMessage(e));
395 throw new DirectoryException(DirectoryServer.getServerErrorResultCode(),
396 message, e);
397 }
398 }
399
400
401 // Encode and return the value.
402 StringBuilder authPWValue = new StringBuilder();
403 authPWValue.append(AUTH_PASSWORD_SCHEME_NAME_SALTED_SHA_384);
404 authPWValue.append('$');
405 authPWValue.append(Base64.encode(saltBytes));
406 authPWValue.append('$');
407 authPWValue.append(Base64.encode(digestBytes));
408
409 return ByteStringFactory.create(authPWValue.toString());
410 }
411
412
413
414 /**
415 * {@inheritDoc}
416 */
417 @Override()
418 public boolean authPasswordMatches(ByteString plaintextPassword,
419 String authInfo, String authValue)
420 {
421 byte[] saltBytes;
422 byte[] digestBytes;
423 try
424 {
425 saltBytes = Base64.decode(authInfo);
426 digestBytes = Base64.decode(authValue);
427 }
428 catch (Exception e)
429 {
430 if (debugEnabled())
431 {
432 TRACER.debugCaught(DebugLogLevel.ERROR, e);
433 }
434
435 return false;
436 }
437
438
439 byte[] plainBytes = plaintextPassword.value();
440 byte[] plainPlusSaltBytes = new byte[plainBytes.length + saltBytes.length];
441 System.arraycopy(plainBytes, 0, plainPlusSaltBytes, 0, plainBytes.length);
442 System.arraycopy(saltBytes, 0, plainPlusSaltBytes, plainBytes.length,
443 saltBytes.length);
444
445 synchronized (digestLock)
446 {
447 return Arrays.equals(digestBytes,
448 messageDigest.digest(plainPlusSaltBytes));
449 }
450 }
451
452
453
454 /**
455 * {@inheritDoc}
456 */
457 @Override()
458 public boolean isReversible()
459 {
460 return false;
461 }
462
463
464
465 /**
466 * {@inheritDoc}
467 */
468 @Override()
469 public ByteString getPlaintextValue(ByteString storedPassword)
470 throws DirectoryException
471 {
472 Message message =
473 ERR_PWSCHEME_NOT_REVERSIBLE.get(STORAGE_SCHEME_NAME_SALTED_SHA_384);
474 throw new DirectoryException(ResultCode.CONSTRAINT_VIOLATION, message);
475 }
476
477
478
479 /**
480 * {@inheritDoc}
481 */
482 @Override()
483 public ByteString getAuthPasswordPlaintextValue(String authInfo,
484 String authValue)
485 throws DirectoryException
486 {
487 Message message = ERR_PWSCHEME_NOT_REVERSIBLE.get(
488 AUTH_PASSWORD_SCHEME_NAME_SALTED_SHA_384);
489 throw new DirectoryException(ResultCode.CONSTRAINT_VIOLATION, message);
490 }
491
492
493
494 /**
495 * {@inheritDoc}
496 */
497 @Override()
498 public boolean isStorageSchemeSecure()
499 {
500 // SHA-2 should be considered secure.
501 return true;
502 }
503 }
504