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.SaltedMD5PasswordStorageSchemeCfg;
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 * MD5 algorithm defined in RFC 1321. This is a one-way digest algorithm so
060 * 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 MD5 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 SaltedMD5PasswordStorageScheme
069 extends PasswordStorageScheme<SaltedMD5PasswordStorageSchemeCfg>
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.SaltedMD5PasswordStorageScheme";
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 MD5 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 SaltedMD5PasswordStorageScheme()
109 {
110 super();
111
112 }
113
114
115
116 /**
117 * {@inheritDoc}
118 */
119 @Override()
120 public void initializePasswordStorageScheme(
121 SaltedMD5PasswordStorageSchemeCfg configuration)
122 throws ConfigException, InitializationException
123 {
124 try
125 {
126 messageDigest = MessageDigest.getInstance(MESSAGE_DIGEST_ALGORITHM_MD5);
127 }
128 catch (Exception e)
129 {
130 if (debugEnabled())
131 {
132 TRACER.debugCaught(DebugLogLevel.ERROR, e);
133 }
134
135 Message message = ERR_PWSCHEME_CANNOT_INITIALIZE_MESSAGE_DIGEST.get(
136 MESSAGE_DIGEST_ALGORITHM_MD5, String.valueOf(e));
137 throw new InitializationException(message, e);
138 }
139
140
141 digestLock = new Object();
142 random = new Random();
143 }
144
145
146
147 /**
148 * {@inheritDoc}
149 */
150 @Override()
151 public String getStorageSchemeName()
152 {
153 return STORAGE_SCHEME_NAME_SALTED_MD5;
154 }
155
156
157
158 /**
159 * {@inheritDoc}
160 */
161 @Override()
162 public ByteString encodePassword(ByteString plaintext)
163 throws DirectoryException
164 {
165 byte[] plainBytes = plaintext.value();
166 byte[] saltBytes = new byte[NUM_SALT_BYTES];
167 byte[] plainPlusSalt = new byte[plainBytes.length + NUM_SALT_BYTES];
168
169 System.arraycopy(plainBytes, 0, plainPlusSalt, 0, plainBytes.length);
170
171 byte[] digestBytes;
172
173 synchronized (digestLock)
174 {
175 try
176 {
177 // Generate the salt and put in the plain+salt array.
178 random.nextBytes(saltBytes);
179 System.arraycopy(saltBytes,0, plainPlusSalt, plainBytes.length,
180 NUM_SALT_BYTES);
181
182 // Create the hash from the concatenated value.
183 digestBytes = messageDigest.digest(plainPlusSalt);
184 }
185 catch (Exception e)
186 {
187 if (debugEnabled())
188 {
189 TRACER.debugCaught(DebugLogLevel.ERROR, e);
190 }
191
192 Message message = ERR_PWSCHEME_CANNOT_ENCODE_PASSWORD.get(
193 CLASS_NAME, getExceptionMessage(e));
194 throw new DirectoryException(DirectoryServer.getServerErrorResultCode(),
195 message, e);
196 }
197 }
198
199 // Append the salt to the hashed value and base64-the whole thing.
200 byte[] hashPlusSalt = new byte[digestBytes.length + NUM_SALT_BYTES];
201
202 System.arraycopy(digestBytes, 0, hashPlusSalt, 0, digestBytes.length);
203 System.arraycopy(saltBytes, 0, hashPlusSalt, digestBytes.length,
204 NUM_SALT_BYTES);
205
206 return ByteStringFactory.create(Base64.encode(hashPlusSalt));
207 }
208
209
210
211 /**
212 * {@inheritDoc}
213 */
214 @Override()
215 public ByteString encodePasswordWithScheme(ByteString plaintext)
216 throws DirectoryException
217 {
218 StringBuilder buffer = new StringBuilder();
219 buffer.append('{');
220 buffer.append(STORAGE_SCHEME_NAME_SALTED_MD5);
221 buffer.append('}');
222
223 byte[] plainBytes = plaintext.value();
224 byte[] saltBytes = new byte[NUM_SALT_BYTES];
225 byte[] plainPlusSalt = new byte[plainBytes.length + NUM_SALT_BYTES];
226
227 System.arraycopy(plainBytes, 0, plainPlusSalt, 0, plainBytes.length);
228
229 byte[] digestBytes;
230
231 synchronized (digestLock)
232 {
233 try
234 {
235 // Generate the salt and put in the plain+salt array.
236 random.nextBytes(saltBytes);
237 System.arraycopy(saltBytes,0, plainPlusSalt, plainBytes.length,
238 NUM_SALT_BYTES);
239
240 // Create the hash from the concatenated value.
241 digestBytes = messageDigest.digest(plainPlusSalt);
242 }
243 catch (Exception e)
244 {
245 if (debugEnabled())
246 {
247 TRACER.debugCaught(DebugLogLevel.ERROR, e);
248 }
249
250 Message message = ERR_PWSCHEME_CANNOT_ENCODE_PASSWORD.get(
251 CLASS_NAME, getExceptionMessage(e));
252 throw new DirectoryException(DirectoryServer.getServerErrorResultCode(),
253 message, e);
254 }
255 }
256
257 // Append the salt to the hashed value and base64-the whole thing.
258 byte[] hashPlusSalt = new byte[digestBytes.length + NUM_SALT_BYTES];
259
260 System.arraycopy(digestBytes, 0, hashPlusSalt, 0, digestBytes.length);
261 System.arraycopy(saltBytes, 0, hashPlusSalt, digestBytes.length,
262 NUM_SALT_BYTES);
263 buffer.append(Base64.encode(hashPlusSalt));
264
265 return ByteStringFactory.create(buffer.toString());
266 }
267
268
269
270 /**
271 * {@inheritDoc}
272 */
273 @Override()
274 public boolean passwordMatches(ByteString plaintextPassword,
275 ByteString storedPassword)
276 {
277 // Base64-decode the stored value and take the last 8 bytes as the salt.
278 byte[] saltBytes = new byte[NUM_SALT_BYTES];
279 byte[] digestBytes;
280 try
281 {
282 byte[] decodedBytes = Base64.decode(storedPassword.stringValue());
283
284 int digestLength = decodedBytes.length - NUM_SALT_BYTES;
285 digestBytes = new byte[digestLength];
286 System.arraycopy(decodedBytes, 0, digestBytes, 0, digestLength);
287 System.arraycopy(decodedBytes, digestLength, saltBytes, 0,
288 NUM_SALT_BYTES);
289 }
290 catch (Exception e)
291 {
292 if (debugEnabled())
293 {
294 TRACER.debugCaught(DebugLogLevel.ERROR, e);
295 }
296
297 Message message = ERR_PWSCHEME_CANNOT_BASE64_DECODE_STORED_PASSWORD.get(
298 storedPassword.stringValue(), String.valueOf(e));
299 ErrorLogger.logError(message);
300 return false;
301 }
302
303
304 // Use the salt to generate a digest based on the provided plain-text value.
305 byte[] plainBytes = plaintextPassword.value();
306 byte[] plainPlusSalt = new byte[plainBytes.length + NUM_SALT_BYTES];
307 System.arraycopy(plainBytes, 0, plainPlusSalt, 0, plainBytes.length);
308 System.arraycopy(saltBytes, 0,plainPlusSalt, plainBytes.length,
309 NUM_SALT_BYTES);
310
311 byte[] userDigestBytes;
312
313 synchronized (digestLock)
314 {
315 try
316 {
317 userDigestBytes = messageDigest.digest(plainPlusSalt);
318 }
319 catch (Exception e)
320 {
321 if (debugEnabled())
322 {
323 TRACER.debugCaught(DebugLogLevel.ERROR, e);
324 }
325
326 return false;
327 }
328 }
329
330 return Arrays.equals(digestBytes, userDigestBytes);
331 }
332
333
334
335 /**
336 * {@inheritDoc}
337 */
338 @Override()
339 public boolean supportsAuthPasswordSyntax()
340 {
341 // This storage scheme does support the authentication password syntax.
342 return true;
343 }
344
345
346
347 /**
348 * {@inheritDoc}
349 */
350 @Override()
351 public String getAuthPasswordSchemeName()
352 {
353 return AUTH_PASSWORD_SCHEME_NAME_SALTED_MD5;
354 }
355
356
357
358 /**
359 * {@inheritDoc}
360 */
361 @Override()
362 public ByteString encodeAuthPassword(ByteString plaintext)
363 throws DirectoryException
364 {
365 byte[] plainBytes = plaintext.value();
366 byte[] saltBytes = new byte[NUM_SALT_BYTES];
367 byte[] plainPlusSalt = new byte[plainBytes.length + NUM_SALT_BYTES];
368
369 System.arraycopy(plainBytes, 0, plainPlusSalt, 0, plainBytes.length);
370
371 byte[] digestBytes;
372
373 synchronized (digestLock)
374 {
375 try
376 {
377 // Generate the salt and put in the plain+salt array.
378 random.nextBytes(saltBytes);
379 System.arraycopy(saltBytes,0, plainPlusSalt, plainBytes.length,
380 NUM_SALT_BYTES);
381
382 // Create the hash from the concatenated value.
383 digestBytes = messageDigest.digest(plainPlusSalt);
384 }
385 catch (Exception e)
386 {
387 if (debugEnabled())
388 {
389 TRACER.debugCaught(DebugLogLevel.ERROR, e);
390 }
391
392 Message message = ERR_PWSCHEME_CANNOT_ENCODE_PASSWORD.get(
393 CLASS_NAME, getExceptionMessage(e));
394 throw new DirectoryException(DirectoryServer.getServerErrorResultCode(),
395 message, e);
396 }
397 }
398
399
400 // Encode and return the value.
401 StringBuilder authPWValue = new StringBuilder();
402 authPWValue.append(AUTH_PASSWORD_SCHEME_NAME_SALTED_MD5);
403 authPWValue.append('$');
404 authPWValue.append(Base64.encode(saltBytes));
405 authPWValue.append('$');
406 authPWValue.append(Base64.encode(digestBytes));
407
408 return ByteStringFactory.create(authPWValue.toString());
409 }
410
411
412
413 /**
414 * {@inheritDoc}
415 */
416 @Override()
417 public boolean authPasswordMatches(ByteString plaintextPassword,
418 String authInfo, String authValue)
419 {
420 byte[] saltBytes;
421 byte[] digestBytes;
422 try
423 {
424 saltBytes = Base64.decode(authInfo);
425 digestBytes = Base64.decode(authValue);
426 }
427 catch (Exception e)
428 {
429 if (debugEnabled())
430 {
431 TRACER.debugCaught(DebugLogLevel.ERROR, e);
432 }
433
434 return false;
435 }
436
437
438 byte[] plainBytes = plaintextPassword.value();
439 byte[] plainPlusSaltBytes = new byte[plainBytes.length + saltBytes.length];
440 System.arraycopy(plainBytes, 0, plainPlusSaltBytes, 0, plainBytes.length);
441 System.arraycopy(saltBytes, 0, plainPlusSaltBytes, plainBytes.length,
442 saltBytes.length);
443
444 synchronized (digestLock)
445 {
446 return Arrays.equals(digestBytes,
447 messageDigest.digest(plainPlusSaltBytes));
448 }
449 }
450
451
452
453 /**
454 * {@inheritDoc}
455 */
456 @Override()
457 public boolean isReversible()
458 {
459 return false;
460 }
461
462
463
464 /**
465 * {@inheritDoc}
466 */
467 @Override()
468 public ByteString getPlaintextValue(ByteString storedPassword)
469 throws DirectoryException
470 {
471 Message message =
472 ERR_PWSCHEME_NOT_REVERSIBLE.get(STORAGE_SCHEME_NAME_SALTED_MD5);
473 throw new DirectoryException(ResultCode.CONSTRAINT_VIOLATION, message);
474 }
475
476
477
478 /**
479 * {@inheritDoc}
480 */
481 @Override()
482 public ByteString getAuthPasswordPlaintextValue(String authInfo,
483 String authValue)
484 throws DirectoryException
485 {
486 Message message =
487 ERR_PWSCHEME_NOT_REVERSIBLE.get(AUTH_PASSWORD_SCHEME_NAME_SALTED_MD5);
488 throw new DirectoryException(ResultCode.CONSTRAINT_VIOLATION, message);
489 }
490
491
492
493 /**
494 * {@inheritDoc}
495 */
496 @Override()
497 public boolean isStorageSchemeSecure()
498 {
499 // MD5 may be considered reasonably secure for this purpose.
500 return true;
501 }
502 }
503