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.SaltedSHA1PasswordStorageSchemeCfg;
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 * SHA-1 algorithm defined in FIPS 180-1. This is a one-way digest algorithm
060 * so there is no way to retrieve the original clear-text version of the
061 * password from the hashed value (although this means that it is not suitable
062 * for things that need the clear-text password like DIGEST-MD5). The values
063 * that it generates are also salted, which protects against dictionary attacks.
064 * It does this by generating a 64-bit random salt which is appended to the
065 * clear-text value. A SHA-1 hash is then generated based on this, the salt is
066 * appended to the hash, and then the entire value is base64-encoded.
067 */
068 public class SaltedSHA1PasswordStorageScheme
069 extends PasswordStorageScheme<SaltedSHA1PasswordStorageSchemeCfg>
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.SaltedSHA1PasswordStorageScheme";
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 SHA-1 hashes.
093 private MessageDigest messageDigest;
094
095 // The lock used to provide threadsafe access to the message digest.
096 private Object digestLock;
097
098 // The secure random number generator to use to generate the salt values.
099 private Random random;
100
101
102
103 /**
104 * Creates a new instance of this password storage scheme. Note that no
105 * initialization should be performed here, as all initialization should be
106 * done in the <CODE>initializePasswordStorageScheme</CODE> method.
107 */
108 public SaltedSHA1PasswordStorageScheme()
109 {
110 super();
111 }
112
113
114
115 /**
116 * {@inheritDoc}
117 */
118 @Override()
119 public void initializePasswordStorageScheme(
120 SaltedSHA1PasswordStorageSchemeCfg configuration)
121 throws ConfigException, InitializationException
122 {
123 try
124 {
125 messageDigest = MessageDigest.getInstance(MESSAGE_DIGEST_ALGORITHM_SHA_1);
126 }
127 catch (Exception e)
128 {
129 if (debugEnabled())
130 {
131 TRACER.debugCaught(DebugLogLevel.ERROR, e);
132 }
133
134 Message message = ERR_PWSCHEME_CANNOT_INITIALIZE_MESSAGE_DIGEST.get(
135 MESSAGE_DIGEST_ALGORITHM_SHA_1, String.valueOf(e));
136 throw new InitializationException(message, e);
137 }
138
139 digestLock = new Object();
140 random = new Random();
141 }
142
143
144
145 /**
146 * {@inheritDoc}
147 */
148 @Override()
149 public String getStorageSchemeName()
150 {
151 return STORAGE_SCHEME_NAME_SALTED_SHA_1;
152 }
153
154
155
156 /**
157 * {@inheritDoc}
158 */
159 @Override()
160 public ByteString encodePassword(ByteString plaintext)
161 throws DirectoryException
162 {
163 byte[] plainBytes = plaintext.value();
164 byte[] saltBytes = new byte[NUM_SALT_BYTES];
165 byte[] plainPlusSalt = new byte[plainBytes.length + NUM_SALT_BYTES];
166
167 System.arraycopy(plainBytes, 0, plainPlusSalt,0,plainBytes.length);
168
169 byte[] digestBytes;
170
171 synchronized (digestLock)
172 {
173 try
174 {
175 // Generate the salt and put in the plain+salt array.
176 random.nextBytes(saltBytes);
177 System.arraycopy(saltBytes,0, plainPlusSalt, plainBytes.length,
178 NUM_SALT_BYTES);
179
180 // Create the hash from the concatenated value.
181 digestBytes = messageDigest.digest(plainPlusSalt);
182 }
183 catch (Exception e)
184 {
185 if (debugEnabled())
186 {
187 TRACER.debugCaught(DebugLogLevel.ERROR, e);
188 }
189
190 Message message = ERR_PWSCHEME_CANNOT_ENCODE_PASSWORD.get(
191 CLASS_NAME, getExceptionMessage(e));
192 throw new DirectoryException(DirectoryServer.getServerErrorResultCode(),
193 message, e);
194 }
195 }
196
197 // Append the salt to the hashed value and base64-the whole thing.
198 byte[] hashPlusSalt = new byte[digestBytes.length + NUM_SALT_BYTES];
199
200 System.arraycopy(digestBytes, 0, hashPlusSalt, 0, digestBytes.length);
201 System.arraycopy(saltBytes, 0, hashPlusSalt, digestBytes.length,
202 NUM_SALT_BYTES);
203
204 return ByteStringFactory.create(Base64.encode(hashPlusSalt));
205 }
206
207
208
209 /**
210 * {@inheritDoc}
211 */
212 @Override()
213 public ByteString encodePasswordWithScheme(ByteString plaintext)
214 throws DirectoryException
215 {
216 StringBuilder buffer = new StringBuilder();
217 buffer.append('{');
218 buffer.append(STORAGE_SCHEME_NAME_SALTED_SHA_1);
219 buffer.append('}');
220
221 byte[] plainBytes = plaintext.value();
222 byte[] saltBytes = new byte[NUM_SALT_BYTES];
223 byte[] plainPlusSalt = new byte[plainBytes.length + NUM_SALT_BYTES];
224
225 System.arraycopy(plainBytes, 0, plainPlusSalt,0,plainBytes.length);
226
227 byte[] digestBytes;
228
229 synchronized (digestLock)
230 {
231 try
232 {
233 // Generate the salt and put in the plain+salt array.
234 random.nextBytes(saltBytes);
235 System.arraycopy(saltBytes,0, plainPlusSalt, plainBytes.length,
236 NUM_SALT_BYTES);
237
238 // Create the hash from the concatenated value.
239 digestBytes = messageDigest.digest(plainPlusSalt);
240 }
241 catch (Exception e)
242 {
243 if (debugEnabled())
244 {
245 TRACER.debugCaught(DebugLogLevel.ERROR, e);
246 }
247
248 Message message = ERR_PWSCHEME_CANNOT_ENCODE_PASSWORD.get(
249 CLASS_NAME, getExceptionMessage(e));
250 throw new DirectoryException(DirectoryServer.getServerErrorResultCode(),
251 message, e);
252 }
253 }
254
255 // Append the salt to the hashed value and base64-the whole thing.
256 byte[] hashPlusSalt = new byte[digestBytes.length + NUM_SALT_BYTES];
257
258 System.arraycopy(digestBytes, 0, hashPlusSalt, 0, digestBytes.length);
259 System.arraycopy(saltBytes, 0, hashPlusSalt, digestBytes.length,
260 NUM_SALT_BYTES);
261 buffer.append(Base64.encode(hashPlusSalt));
262
263 return ByteStringFactory.create(buffer.toString());
264 }
265
266
267
268 /**
269 * {@inheritDoc}
270 */
271 @Override()
272 public boolean passwordMatches(ByteString plaintextPassword,
273 ByteString storedPassword)
274 {
275 // Base64-decode the stored value and take the last 8 bytes as the salt.
276 byte[] saltBytes = new byte[NUM_SALT_BYTES];
277 byte[] digestBytes;
278 try
279 {
280 byte[] decodedBytes = Base64.decode(storedPassword.stringValue());
281
282 int digestLength = decodedBytes.length - NUM_SALT_BYTES;
283 digestBytes = new byte[digestLength];
284 System.arraycopy(decodedBytes, 0, digestBytes, 0, digestLength);
285 System.arraycopy(decodedBytes, digestLength, saltBytes, 0,
286 NUM_SALT_BYTES);
287 }
288 catch (Exception e)
289 {
290 if (debugEnabled())
291 {
292 TRACER.debugCaught(DebugLogLevel.ERROR, e);
293 }
294
295 Message message = ERR_PWSCHEME_CANNOT_BASE64_DECODE_STORED_PASSWORD.get(
296 storedPassword.stringValue(), String.valueOf(e));
297 ErrorLogger.logError(message);
298 return false;
299 }
300
301
302 // Use the salt to generate a digest based on the provided plain-text value.
303 byte[] plainBytes = plaintextPassword.value();
304 byte[] plainPlusSalt = new byte[plainBytes.length + NUM_SALT_BYTES];
305 System.arraycopy(plainBytes, 0, plainPlusSalt, 0, plainBytes.length);
306 System.arraycopy(saltBytes, 0,plainPlusSalt, plainBytes.length,
307 NUM_SALT_BYTES);
308
309 byte[] userDigestBytes;
310
311 synchronized (digestLock)
312 {
313 try
314 {
315 userDigestBytes = messageDigest.digest(plainPlusSalt);
316 }
317 catch (Exception e)
318 {
319 if (debugEnabled())
320 {
321 TRACER.debugCaught(DebugLogLevel.ERROR, e);
322 }
323
324 return false;
325 }
326 }
327
328 return Arrays.equals(digestBytes, userDigestBytes);
329 }
330
331
332
333 /**
334 * {@inheritDoc}
335 */
336 @Override()
337 public boolean supportsAuthPasswordSyntax()
338 {
339 // This storage scheme does support the authentication password syntax.
340 return true;
341 }
342
343
344
345 /**
346 * {@inheritDoc}
347 */
348 @Override()
349 public String getAuthPasswordSchemeName()
350 {
351 return AUTH_PASSWORD_SCHEME_NAME_SALTED_SHA_1;
352 }
353
354
355
356 /**
357 * {@inheritDoc}
358 */
359 @Override()
360 public ByteString encodeAuthPassword(ByteString plaintext)
361 throws DirectoryException
362 {
363 byte[] plainBytes = plaintext.value();
364 byte[] saltBytes = new byte[NUM_SALT_BYTES];
365 byte[] plainPlusSalt = new byte[plainBytes.length + NUM_SALT_BYTES];
366
367 System.arraycopy(plainBytes, 0, plainPlusSalt, 0, plainBytes.length);
368
369 byte[] digestBytes;
370
371 synchronized (digestLock)
372 {
373 try
374 {
375 // Generate the salt and put in the plain+salt array.
376 random.nextBytes(saltBytes);
377 System.arraycopy(saltBytes,0, plainPlusSalt, plainBytes.length,
378 NUM_SALT_BYTES);
379
380 // Create the hash from the concatenated value.
381 digestBytes = messageDigest.digest(plainPlusSalt);
382 }
383 catch (Exception e)
384 {
385 if (debugEnabled())
386 {
387 TRACER.debugCaught(DebugLogLevel.ERROR, e);
388 }
389
390 Message message = ERR_PWSCHEME_CANNOT_ENCODE_PASSWORD.get(
391 CLASS_NAME, getExceptionMessage(e));
392 throw new DirectoryException(DirectoryServer.getServerErrorResultCode(),
393 message, e);
394 }
395 }
396
397
398 // Encode and return the value.
399 StringBuilder authPWValue = new StringBuilder();
400 authPWValue.append(AUTH_PASSWORD_SCHEME_NAME_SALTED_SHA_1);
401 authPWValue.append('$');
402 authPWValue.append(Base64.encode(saltBytes));
403 authPWValue.append('$');
404 authPWValue.append(Base64.encode(digestBytes));
405
406 return ByteStringFactory.create(authPWValue.toString());
407 }
408
409
410
411 /**
412 * {@inheritDoc}
413 */
414 @Override()
415 public boolean authPasswordMatches(ByteString plaintextPassword,
416 String authInfo, String authValue)
417 {
418 byte[] saltBytes;
419 byte[] digestBytes;
420 try
421 {
422 saltBytes = Base64.decode(authInfo);
423 digestBytes = Base64.decode(authValue);
424 }
425 catch (Exception e)
426 {
427 if (debugEnabled())
428 {
429 TRACER.debugCaught(DebugLogLevel.ERROR, e);
430 }
431
432 return false;
433 }
434
435
436 byte[] plainBytes = plaintextPassword.value();
437 byte[] plainPlusSaltBytes = new byte[plainBytes.length + saltBytes.length];
438 System.arraycopy(plainBytes, 0, plainPlusSaltBytes, 0, plainBytes.length);
439 System.arraycopy(saltBytes, 0, plainPlusSaltBytes, plainBytes.length,
440 saltBytes.length);
441
442 synchronized (digestLock)
443 {
444 return Arrays.equals(digestBytes,
445 messageDigest.digest(plainPlusSaltBytes));
446 }
447 }
448
449
450
451 /**
452 * {@inheritDoc}
453 */
454 @Override()
455 public boolean isReversible()
456 {
457 return false;
458 }
459
460
461
462 /**
463 * {@inheritDoc}
464 */
465 @Override()
466 public ByteString getPlaintextValue(ByteString storedPassword)
467 throws DirectoryException
468 {
469 Message message =
470 ERR_PWSCHEME_NOT_REVERSIBLE.get(STORAGE_SCHEME_NAME_SALTED_SHA_1);
471 throw new DirectoryException(ResultCode.CONSTRAINT_VIOLATION, message);
472 }
473
474
475
476 /**
477 * {@inheritDoc}
478 */
479 @Override()
480 public ByteString getAuthPasswordPlaintextValue(String authInfo,
481 String authValue)
482 throws DirectoryException
483 {
484 Message message =
485 ERR_PWSCHEME_NOT_REVERSIBLE.get(AUTH_PASSWORD_SCHEME_NAME_SALTED_SHA_1);
486 throw new DirectoryException(ResultCode.CONSTRAINT_VIOLATION, message);
487 }
488
489
490
491 /**
492 * {@inheritDoc}
493 */
494 @Override()
495 public boolean isStorageSchemeSecure()
496 {
497 // SHA-1 should be considered secure.
498 return true;
499 }
500
501
502
503 /**
504 * Generates an encoded password string from the given clear-text password.
505 * This method is primarily intended for use when it is necessary to generate
506 * a password with the server offline (e.g., when setting the initial root
507 * user password).
508 *
509 * @param passwordBytes The bytes that make up the clear-text password.
510 *
511 * @return The encoded password string, including the scheme name in curly
512 * braces.
513 *
514 * @throws DirectoryException If a problem occurs during processing.
515 */
516 public static String encodeOffline(byte[] passwordBytes)
517 throws DirectoryException
518 {
519 byte[] saltBytes = new byte[NUM_SALT_BYTES];
520 new Random().nextBytes(saltBytes);
521
522 byte[] passwordPlusSalt = new byte[passwordBytes.length + NUM_SALT_BYTES];
523 System.arraycopy(passwordBytes, 0, passwordPlusSalt, 0,
524 passwordBytes.length);
525 System.arraycopy(saltBytes, 0, passwordPlusSalt, passwordBytes.length,
526 NUM_SALT_BYTES);
527
528 MessageDigest messageDigest;
529 try
530 {
531 messageDigest = MessageDigest.getInstance(MESSAGE_DIGEST_ALGORITHM_SHA_1);
532 }
533 catch (Exception e)
534 {
535 Message message = ERR_PWSCHEME_CANNOT_INITIALIZE_MESSAGE_DIGEST.get(
536 MESSAGE_DIGEST_ALGORITHM_SHA_1, String.valueOf(e));
537 throw new DirectoryException(ResultCode.OTHER, message, e);
538 }
539
540
541 byte[] digestBytes = messageDigest.digest(passwordPlusSalt);
542 byte[] digestPlusSalt = new byte[digestBytes.length + NUM_SALT_BYTES];
543 System.arraycopy(digestBytes, 0, digestPlusSalt, 0, digestBytes.length);
544 System.arraycopy(saltBytes, 0, digestPlusSalt, digestBytes.length,
545 NUM_SALT_BYTES);
546
547 return "{" + STORAGE_SCHEME_NAME_SALTED_SHA_1 + "}" +
548 Base64.encode(digestPlusSalt);
549 }
550 }
551