From b8d9ca2ead9db249c3f55af494a378c5e68a2f8a Mon Sep 17 00:00:00 2001 From: Valera V Harseko Date: Thu, 2 Jul 2026 17:58:30 +0300 Subject: [PATCH 1/2] Remove global digestLock serialization in digest password storage schemes Every digest-based password storage scheme (and the CRAM-MD5 SASL handler) shared a single MessageDigest instance guarded by synchronized(digestLock). Schemes are server-wide singletons, so all concurrent bind password verifications for a given scheme were serialized on one monitor, with the CPU-bound hashing executed under the lock (JFR: ~19s of blocking per 80s window at 16 worker threads under concurrent {SSHA} bind load). Replace the shared instance with ThreadLocal. Initialization still fails fast with InitializationException if the algorithm is unavailable. Password scrubbing (Arrays.fill in finally) is preserved. --- .../CRAMMD5SASLMechanismHandler.java | 82 ++++---- .../extensions/MD5PasswordStorageScheme.java | 134 ++++++------ .../extensions/SHA1PasswordStorageScheme.java | 130 ++++++------ .../SaltedMD5PasswordStorageScheme.java | 194 +++++++++-------- .../SaltedSHA1PasswordStorageScheme.java | 194 +++++++++-------- .../SaltedSHA256PasswordStorageScheme.java | 195 +++++++++--------- .../SaltedSHA384PasswordStorageScheme.java | 195 +++++++++--------- .../SaltedSHA512PasswordStorageScheme.java | 195 +++++++++--------- 8 files changed, 663 insertions(+), 656 deletions(-) diff --git a/opendj-server-legacy/src/main/java/org/opends/server/extensions/CRAMMD5SASLMechanismHandler.java b/opendj-server-legacy/src/main/java/org/opends/server/extensions/CRAMMD5SASLMechanismHandler.java index 8149ec5b5a..400c3a02e6 100644 --- a/opendj-server-legacy/src/main/java/org/opends/server/extensions/CRAMMD5SASLMechanismHandler.java +++ b/opendj-server-legacy/src/main/java/org/opends/server/extensions/CRAMMD5SASLMechanismHandler.java @@ -13,6 +13,7 @@ * * Copyright 2006-2009 Sun Microsystems, Inc. * Portions Copyright 2011-2016 ForgeRock AS. + * Portions Copyright 2026 3A Systems, LLC. */ package org.opends.server.extensions; @@ -81,11 +82,13 @@ public class CRAMMD5SASLMechanismHandler /** The identity mapper that will be used to map ID strings to user entries. */ private IdentityMapper identityMapper; - /** The message digest engine that will be used to create the MD5 digests. */ - private MessageDigest md5Digest; - - /** The lock that will be used to provide threadsafe access to the message digest. */ - private Object digestLock; + /** + * The message digest engines that will be used to create the MD5 digests. + * MessageDigest is not thread-safe, so a per-thread instance is used + * instead of a shared instance guarded by a lock: hashing under a global + * lock serializes all concurrent CRAM-MD5 binds. + */ + private ThreadLocal md5Digest; /** The random number generator that we will use to create the server challenge. */ private SecureRandom randomGenerator; @@ -109,12 +112,12 @@ public void initializeSASLMechanismHandler( currentConfig = configuration; // Initialize the variables needed for the MD5 digest creation. - digestLock = new Object(); randomGenerator = new SecureRandom(); try { - md5Digest = MessageDigest.getInstance("MD5"); + // Fail fast at initialization time if the algorithm is unavailable. + MessageDigest.getInstance("MD5"); } catch (Exception e) { @@ -125,6 +128,17 @@ public void initializeSASLMechanismHandler( throw new InitializationException(message, e); } + md5Digest = ThreadLocal.withInitial(() -> { + try + { + return MessageDigest.getInstance("MD5"); + } + catch (Exception e) + { + throw new IllegalStateException(e); + } + }); + // Create and fill the iPad and oPad arrays. iPad = new byte[HMAC_MD5_BLOCK_LENGTH]; oPad = new byte[HMAC_MD5_BLOCK_LENGTH]; @@ -427,40 +441,38 @@ private byte[] generateDigest(ByteString password, ByteString challenge) byte[] p = password.toByteArray(); byte[] c = challenge.toByteArray(); - // Grab a lock to protect the MD5 digest generation. - synchronized (digestLock) + MessageDigest md5Digest = this.md5Digest.get(); + + // If the password is longer than the HMAC-MD5 block length, then use an + // MD5 digest of the password rather than the password itself. + if (p.length > HMAC_MD5_BLOCK_LENGTH) { - // If the password is longer than the HMAC-MD5 block length, then use an - // MD5 digest of the password rather than the password itself. - if (p.length > HMAC_MD5_BLOCK_LENGTH) - { - p = md5Digest.digest(p); - } + p = md5Digest.digest(p); + } - // Create byte arrays with data needed for the hash generation. - byte[] iPadAndData = new byte[HMAC_MD5_BLOCK_LENGTH + c.length]; - System.arraycopy(iPad, 0, iPadAndData, 0, HMAC_MD5_BLOCK_LENGTH); - System.arraycopy(c, 0, iPadAndData, HMAC_MD5_BLOCK_LENGTH, c.length); + // Create byte arrays with data needed for the hash generation. + byte[] iPadAndData = new byte[HMAC_MD5_BLOCK_LENGTH + c.length]; + System.arraycopy(iPad, 0, iPadAndData, 0, HMAC_MD5_BLOCK_LENGTH); + System.arraycopy(c, 0, iPadAndData, HMAC_MD5_BLOCK_LENGTH, c.length); - byte[] oPadAndHash = new byte[HMAC_MD5_BLOCK_LENGTH + MD5_DIGEST_LENGTH]; - System.arraycopy(oPad, 0, oPadAndHash, 0, HMAC_MD5_BLOCK_LENGTH); + byte[] oPadAndHash = new byte[HMAC_MD5_BLOCK_LENGTH + MD5_DIGEST_LENGTH]; + System.arraycopy(oPad, 0, oPadAndHash, 0, HMAC_MD5_BLOCK_LENGTH); - // Iterate through the bytes in the key and XOR them with the iPad and - // oPad as appropriate. - for (int i=0; i < p.length; i++) - { - iPadAndData[i] ^= p[i]; - oPadAndHash[i] ^= p[i]; - } + // Iterate through the bytes in the key and XOR them with the iPad and + // oPad as appropriate. + for (int i=0; i < p.length; i++) + { + iPadAndData[i] ^= p[i]; + oPadAndHash[i] ^= p[i]; + } - // Copy an MD5 digest of the iPad-XORed key and the data into the array to - // be hashed. - System.arraycopy(md5Digest.digest(iPadAndData), 0, oPadAndHash, - HMAC_MD5_BLOCK_LENGTH, MD5_DIGEST_LENGTH); + // Copy an MD5 digest of the iPad-XORed key and the data into the array to + // be hashed. + System.arraycopy(md5Digest.digest(iPadAndData), 0, oPadAndHash, + HMAC_MD5_BLOCK_LENGTH, MD5_DIGEST_LENGTH); - // Return an MD5 digest of the resulting array. - return md5Digest.digest(oPadAndHash); - } + // Return an MD5 digest of the resulting array. + return md5Digest.digest(oPadAndHash); } @Override diff --git a/opendj-server-legacy/src/main/java/org/opends/server/extensions/MD5PasswordStorageScheme.java b/opendj-server-legacy/src/main/java/org/opends/server/extensions/MD5PasswordStorageScheme.java index 8d639c974d..8518405d55 100644 --- a/opendj-server-legacy/src/main/java/org/opends/server/extensions/MD5PasswordStorageScheme.java +++ b/opendj-server-legacy/src/main/java/org/opends/server/extensions/MD5PasswordStorageScheme.java @@ -13,6 +13,7 @@ * * Copyright 2006-2008 Sun Microsystems, Inc. * Portions Copyright 2013-2016 ForgeRock AS. + * Portions Copyright 2026 3A Systems, LLC. */ package org.opends.server.extensions; @@ -53,11 +54,13 @@ public class MD5PasswordStorageScheme private static final String CLASS_NAME = "org.opends.server.extensions.MD5PasswordStorageScheme"; - /** The message digest that will actually be used to generate the MD5 hashes. */ - private MessageDigest messageDigest; - - /** The lock used to provide threadsafe access to the message digest. */ - private Object digestLock; + /** + * The message digests used to generate the MD5 hashes. + * MessageDigest is not thread-safe, so a per-thread instance is used + * instead of a shared instance guarded by a lock: hashing under a global + * lock serializes all concurrent bind password verifications. + */ + private ThreadLocal messageDigest; /** * Creates a new instance of this password storage scheme. Note that no @@ -76,7 +79,8 @@ public void initializePasswordStorageScheme( { try { - messageDigest = MessageDigest.getInstance(MESSAGE_DIGEST_ALGORITHM_MD5); + // Fail fast at initialization time if the algorithm is unavailable. + MessageDigest.getInstance(MESSAGE_DIGEST_ALGORITHM_MD5); } catch (Exception e) { @@ -87,7 +91,16 @@ public void initializePasswordStorageScheme( throw new InitializationException(message, e); } - digestLock = new Object(); + messageDigest = ThreadLocal.withInitial(() -> { + try + { + return MessageDigest.getInstance(MESSAGE_DIGEST_ALGORITHM_MD5); + } + catch (Exception e) + { + throw new IllegalStateException(e); + } + }); } @Override @@ -103,29 +116,26 @@ public ByteString encodePassword(ByteSequence plaintext) byte[] digestBytes; byte[] plaintextBytes = null; - synchronized (digestLock) + try { - try - { - // TODO: Can we avoid this copy? - plaintextBytes = plaintext.toByteArray(); - digestBytes = messageDigest.digest(plaintextBytes); - } - catch (Exception e) - { - logger.traceException(e); + // TODO: Can we avoid this copy? + plaintextBytes = plaintext.toByteArray(); + digestBytes = messageDigest.get().digest(plaintextBytes); + } + catch (Exception e) + { + logger.traceException(e); - LocalizableMessage message = ERR_PWSCHEME_CANNOT_ENCODE_PASSWORD.get( - CLASS_NAME, getExceptionMessage(e)); - throw new DirectoryException(DirectoryServer.getCoreConfigManager().getServerErrorResultCode(), - message, e); - } - finally + LocalizableMessage message = ERR_PWSCHEME_CANNOT_ENCODE_PASSWORD.get( + CLASS_NAME, getExceptionMessage(e)); + throw new DirectoryException(DirectoryServer.getCoreConfigManager().getServerErrorResultCode(), + message, e); + } + finally + { + if (plaintextBytes != null) { - if (plaintextBytes != null) - { - Arrays.fill(plaintextBytes, (byte) 0); - } + Arrays.fill(plaintextBytes, (byte) 0); } } @@ -144,29 +154,26 @@ public ByteString encodePasswordWithScheme(ByteSequence plaintext) byte[] plaintextBytes = null; byte[] digestBytes; - synchronized (digestLock) + try { - try - { - // TODO: Can we avoid this copy? - plaintextBytes = plaintext.toByteArray(); - digestBytes = messageDigest.digest(plaintextBytes); - } - catch (Exception e) - { - logger.traceException(e); + // TODO: Can we avoid this copy? + plaintextBytes = plaintext.toByteArray(); + digestBytes = messageDigest.get().digest(plaintextBytes); + } + catch (Exception e) + { + logger.traceException(e); - LocalizableMessage message = ERR_PWSCHEME_CANNOT_ENCODE_PASSWORD.get( - CLASS_NAME, getExceptionMessage(e)); - throw new DirectoryException(DirectoryServer.getCoreConfigManager().getServerErrorResultCode(), - message, e); - } - finally + LocalizableMessage message = ERR_PWSCHEME_CANNOT_ENCODE_PASSWORD.get( + CLASS_NAME, getExceptionMessage(e)); + throw new DirectoryException(DirectoryServer.getCoreConfigManager().getServerErrorResultCode(), + message, e); + } + finally + { + if (plaintextBytes != null) { - if (plaintextBytes != null) - { - Arrays.fill(plaintextBytes, (byte) 0); - } + Arrays.fill(plaintextBytes, (byte) 0); } } @@ -182,27 +189,24 @@ public boolean passwordMatches(ByteSequence plaintextPassword, byte[] plaintextPasswordBytes = null; ByteString userPWDigestBytes; - synchronized (digestLock) + try { - try - { - // TODO: Can we avoid this copy? - plaintextPasswordBytes = plaintextPassword.toByteArray(); - userPWDigestBytes = - ByteString.wrap(messageDigest.digest(plaintextPasswordBytes)); - } - catch (Exception e) - { - logger.traceException(e); + // TODO: Can we avoid this copy? + plaintextPasswordBytes = plaintextPassword.toByteArray(); + userPWDigestBytes = + ByteString.wrap(messageDigest.get().digest(plaintextPasswordBytes)); + } + catch (Exception e) + { + logger.traceException(e); - return false; - } - finally + return false; + } + finally + { + if (plaintextPasswordBytes != null) { - if (plaintextPasswordBytes != null) - { - Arrays.fill(plaintextPasswordBytes, (byte) 0); - } + Arrays.fill(plaintextPasswordBytes, (byte) 0); } } diff --git a/opendj-server-legacy/src/main/java/org/opends/server/extensions/SHA1PasswordStorageScheme.java b/opendj-server-legacy/src/main/java/org/opends/server/extensions/SHA1PasswordStorageScheme.java index b3843bf9a8..f16a88f962 100644 --- a/opendj-server-legacy/src/main/java/org/opends/server/extensions/SHA1PasswordStorageScheme.java +++ b/opendj-server-legacy/src/main/java/org/opends/server/extensions/SHA1PasswordStorageScheme.java @@ -13,6 +13,7 @@ * * Copyright 2006-2008 Sun Microsystems, Inc. * Portions Copyright 2013-2016 ForgeRock AS. + * Portions Copyright 2026 3A Systems, LLC. */ package org.opends.server.extensions; @@ -53,11 +54,13 @@ public class SHA1PasswordStorageScheme private static final String CLASS_NAME = "org.opends.server.extensions.SHA1PasswordStorageScheme"; - /** The message digest that will actually be used to generate the SHA-1 hashes. */ - private MessageDigest messageDigest; - - /** The lock used to provide threadsafe access to the message digest. */ - private Object digestLock; + /** + * The message digests used to generate the SHA-1 hashes. + * MessageDigest is not thread-safe, so a per-thread instance is used + * instead of a shared instance guarded by a lock: hashing under a global + * lock serializes all concurrent bind password verifications. + */ + private ThreadLocal messageDigest; /** * Creates a new instance of this password storage scheme. Note that no @@ -76,7 +79,8 @@ public void initializePasswordStorageScheme( { try { - messageDigest = MessageDigest.getInstance(MESSAGE_DIGEST_ALGORITHM_SHA_1); + // Fail fast at initialization time if the algorithm is unavailable. + MessageDigest.getInstance(MESSAGE_DIGEST_ALGORITHM_SHA_1); } catch (Exception e) { @@ -87,7 +91,16 @@ public void initializePasswordStorageScheme( throw new InitializationException(message, e); } - digestLock = new Object(); + messageDigest = ThreadLocal.withInitial(() -> { + try + { + return MessageDigest.getInstance(MESSAGE_DIGEST_ALGORITHM_SHA_1); + } + catch (Exception e) + { + throw new IllegalStateException(e); + } + }); } @Override @@ -103,29 +116,26 @@ public ByteString encodePassword(ByteSequence plaintext) byte[] digestBytes; byte[] plaintextBytes = null; - synchronized (digestLock) + try { - try - { - // TODO: Can we avoid this copy? - plaintextBytes = plaintext.toByteArray(); - digestBytes = messageDigest.digest(plaintextBytes); - } - catch (Exception e) - { - logger.traceException(e); + // TODO: Can we avoid this copy? + plaintextBytes = plaintext.toByteArray(); + digestBytes = messageDigest.get().digest(plaintextBytes); + } + catch (Exception e) + { + logger.traceException(e); - LocalizableMessage message = ERR_PWSCHEME_CANNOT_ENCODE_PASSWORD.get( - CLASS_NAME, getExceptionMessage(e)); - throw new DirectoryException(DirectoryServer.getCoreConfigManager().getServerErrorResultCode(), - message, e); - } - finally + LocalizableMessage message = ERR_PWSCHEME_CANNOT_ENCODE_PASSWORD.get( + CLASS_NAME, getExceptionMessage(e)); + throw new DirectoryException(DirectoryServer.getCoreConfigManager().getServerErrorResultCode(), + message, e); + } + finally + { + if (plaintextBytes != null) { - if (plaintextBytes != null) - { - Arrays.fill(plaintextBytes, (byte) 0); - } + Arrays.fill(plaintextBytes, (byte) 0); } } @@ -145,28 +155,25 @@ public ByteString encodePasswordWithScheme(ByteSequence plaintext) byte[] plaintextBytes = null; byte[] digestBytes; - synchronized (digestLock) + try { - try - { - plaintextBytes = plaintext.toByteArray(); - digestBytes = messageDigest.digest(plaintextBytes); - } - catch (Exception e) - { - logger.traceException(e); + plaintextBytes = plaintext.toByteArray(); + digestBytes = messageDigest.get().digest(plaintextBytes); + } + catch (Exception e) + { + logger.traceException(e); - LocalizableMessage message = ERR_PWSCHEME_CANNOT_ENCODE_PASSWORD.get( - CLASS_NAME, getExceptionMessage(e)); - throw new DirectoryException(DirectoryServer.getCoreConfigManager().getServerErrorResultCode(), - message, e); - } - finally + LocalizableMessage message = ERR_PWSCHEME_CANNOT_ENCODE_PASSWORD.get( + CLASS_NAME, getExceptionMessage(e)); + throw new DirectoryException(DirectoryServer.getCoreConfigManager().getServerErrorResultCode(), + message, e); + } + finally + { + if (plaintextBytes != null) { - if (plaintextBytes != null) - { - Arrays.fill(plaintextBytes, (byte) 0); - } + Arrays.fill(plaintextBytes, (byte) 0); } } @@ -183,26 +190,23 @@ public boolean passwordMatches(ByteSequence plaintextPassword, byte[] plaintextPasswordBytes = null; ByteString userPWDigestBytes; - synchronized (digestLock) + try { - try - { - plaintextPasswordBytes = plaintextPassword.toByteArray(); - userPWDigestBytes = - ByteString.wrap(messageDigest.digest(plaintextPasswordBytes)); - } - catch (Exception e) - { - logger.traceException(e); + plaintextPasswordBytes = plaintextPassword.toByteArray(); + userPWDigestBytes = + ByteString.wrap(messageDigest.get().digest(plaintextPasswordBytes)); + } + catch (Exception e) + { + logger.traceException(e); - return false; - } - finally + return false; + } + finally + { + if (plaintextPasswordBytes != null) { - if (plaintextPasswordBytes != null) - { - Arrays.fill(plaintextPasswordBytes, (byte) 0); - } + Arrays.fill(plaintextPasswordBytes, (byte) 0); } } diff --git a/opendj-server-legacy/src/main/java/org/opends/server/extensions/SaltedMD5PasswordStorageScheme.java b/opendj-server-legacy/src/main/java/org/opends/server/extensions/SaltedMD5PasswordStorageScheme.java index b02e865769..def7f1e4fe 100644 --- a/opendj-server-legacy/src/main/java/org/opends/server/extensions/SaltedMD5PasswordStorageScheme.java +++ b/opendj-server-legacy/src/main/java/org/opends/server/extensions/SaltedMD5PasswordStorageScheme.java @@ -13,6 +13,7 @@ * * Copyright 2006-2008 Sun Microsystems, Inc. * Portions Copyright 2013-2016 ForgeRock AS. + * Portions Copyright 2026 3A Systems, LLC. */ package org.opends.server.extensions; @@ -62,11 +63,13 @@ public class SaltedMD5PasswordStorageScheme /** The number of bytes MD5 algorithm produces. */ private static final int MD5_LENGTH = 16; - /** The message digest that will actually be used to generate the MD5 hashes. */ - private MessageDigest messageDigest; - - /** The lock used to provide threadsafe access to the message digest. */ - private Object digestLock; + /** + * The message digests used to generate the MD5 hashes. + * MessageDigest is not thread-safe, so a per-thread instance is used + * instead of a shared instance guarded by a lock: hashing under a global + * lock serializes all concurrent bind password verifications. + */ + private ThreadLocal messageDigest; /** The secure random number generator to use to generate the salt values. */ private Random random; @@ -88,7 +91,8 @@ public void initializePasswordStorageScheme( { try { - messageDigest = MessageDigest.getInstance(MESSAGE_DIGEST_ALGORITHM_MD5); + // Fail fast at initialization time if the algorithm is unavailable. + MessageDigest.getInstance(MESSAGE_DIGEST_ALGORITHM_MD5); } catch (Exception e) { @@ -98,8 +102,17 @@ public void initializePasswordStorageScheme( throw new InitializationException(message, e); } - digestLock = new Object(); - random = new Random(); + messageDigest = ThreadLocal.withInitial(() -> { + try + { + return MessageDigest.getInstance(MESSAGE_DIGEST_ALGORITHM_MD5); + } + catch (Exception e) + { + throw new IllegalStateException(e); + } + }); + random = new Random(); } @Override @@ -120,31 +133,28 @@ public ByteString encodePassword(ByteSequence plaintext) byte[] digestBytes; - synchronized (digestLock) + try { - try - { - // Generate the salt and put in the plain+salt array. - random.nextBytes(saltBytes); - System.arraycopy(saltBytes,0, plainPlusSalt, plainBytesLength, - NUM_SALT_BYTES); + // Generate the salt and put in the plain+salt array. + random.nextBytes(saltBytes); + System.arraycopy(saltBytes,0, plainPlusSalt, plainBytesLength, + NUM_SALT_BYTES); - // Create the hash from the concatenated value. - digestBytes = messageDigest.digest(plainPlusSalt); - } - catch (Exception e) - { - logger.traceException(e); + // Create the hash from the concatenated value. + digestBytes = messageDigest.get().digest(plainPlusSalt); + } + catch (Exception e) + { + logger.traceException(e); - LocalizableMessage message = ERR_PWSCHEME_CANNOT_ENCODE_PASSWORD.get( - CLASS_NAME, getExceptionMessage(e)); - throw new DirectoryException(DirectoryServer.getCoreConfigManager().getServerErrorResultCode(), - message, e); - } - finally - { - Arrays.fill(plainPlusSalt, (byte) 0); - } + LocalizableMessage message = ERR_PWSCHEME_CANNOT_ENCODE_PASSWORD.get( + CLASS_NAME, getExceptionMessage(e)); + throw new DirectoryException(DirectoryServer.getCoreConfigManager().getServerErrorResultCode(), + message, e); + } + finally + { + Arrays.fill(plainPlusSalt, (byte) 0); } // Append the salt to the hashed value and base64-the whole thing. @@ -174,31 +184,28 @@ public ByteString encodePasswordWithScheme(ByteSequence plaintext) byte[] digestBytes; - synchronized (digestLock) + try { - try - { - // Generate the salt and put in the plain+salt array. - random.nextBytes(saltBytes); - System.arraycopy(saltBytes,0, plainPlusSalt, plainBytesLength, - NUM_SALT_BYTES); + // Generate the salt and put in the plain+salt array. + random.nextBytes(saltBytes); + System.arraycopy(saltBytes,0, plainPlusSalt, plainBytesLength, + NUM_SALT_BYTES); - // Create the hash from the concatenated value. - digestBytes = messageDigest.digest(plainPlusSalt); - } - catch (Exception e) - { - logger.traceException(e); + // Create the hash from the concatenated value. + digestBytes = messageDigest.get().digest(plainPlusSalt); + } + catch (Exception e) + { + logger.traceException(e); - LocalizableMessage message = ERR_PWSCHEME_CANNOT_ENCODE_PASSWORD.get( - CLASS_NAME, getExceptionMessage(e)); - throw new DirectoryException(DirectoryServer.getCoreConfigManager().getServerErrorResultCode(), - message, e); - } - finally - { - Arrays.fill(plainPlusSalt, (byte) 0); - } + LocalizableMessage message = ERR_PWSCHEME_CANNOT_ENCODE_PASSWORD.get( + CLASS_NAME, getExceptionMessage(e)); + throw new DirectoryException(DirectoryServer.getCoreConfigManager().getServerErrorResultCode(), + message, e); + } + finally + { + Arrays.fill(plainPlusSalt, (byte) 0); } // Append the salt to the hashed value and base64-the whole thing. @@ -251,22 +258,19 @@ public boolean passwordMatches(ByteSequence plaintextPassword, byte[] userDigestBytes; - synchronized (digestLock) + try { - try - { - userDigestBytes = messageDigest.digest(plainPlusSalt); - } - catch (Exception e) - { - logger.traceException(e); + userDigestBytes = messageDigest.get().digest(plainPlusSalt); + } + catch (Exception e) + { + logger.traceException(e); - return false; - } - finally - { - Arrays.fill(plainPlusSalt, (byte) 0); - } + return false; + } + finally + { + Arrays.fill(plainPlusSalt, (byte) 0); } return Arrays.equals(digestBytes, userDigestBytes); @@ -297,31 +301,28 @@ public ByteString encodeAuthPassword(ByteSequence plaintext) byte[] digestBytes; - synchronized (digestLock) + try { - try - { - // Generate the salt and put in the plain+salt array. - random.nextBytes(saltBytes); - System.arraycopy(saltBytes,0, plainPlusSalt, plaintextLength, - NUM_SALT_BYTES); + // Generate the salt and put in the plain+salt array. + random.nextBytes(saltBytes); + System.arraycopy(saltBytes,0, plainPlusSalt, plaintextLength, + NUM_SALT_BYTES); - // Create the hash from the concatenated value. - digestBytes = messageDigest.digest(plainPlusSalt); - } - catch (Exception e) - { - logger.traceException(e); + // Create the hash from the concatenated value. + digestBytes = messageDigest.get().digest(plainPlusSalt); + } + catch (Exception e) + { + logger.traceException(e); - LocalizableMessage message = ERR_PWSCHEME_CANNOT_ENCODE_PASSWORD.get( - CLASS_NAME, getExceptionMessage(e)); - throw new DirectoryException(DirectoryServer.getCoreConfigManager().getServerErrorResultCode(), - message, e); - } - finally - { - Arrays.fill(plainPlusSalt, (byte) 0); - } + LocalizableMessage message = ERR_PWSCHEME_CANNOT_ENCODE_PASSWORD.get( + CLASS_NAME, getExceptionMessage(e)); + throw new DirectoryException(DirectoryServer.getCoreConfigManager().getServerErrorResultCode(), + message, e); + } + finally + { + Arrays.fill(plainPlusSalt, (byte) 0); } // Encode and return the value. @@ -359,17 +360,14 @@ public boolean authPasswordMatches(ByteSequence plaintextPassword, System.arraycopy(saltBytes, 0, plainPlusSaltBytes, plainBytesLength, saltBytes.length); - synchronized (digestLock) + try { - try - { - return Arrays.equals(digestBytes, - messageDigest.digest(plainPlusSaltBytes)); - } - finally - { - Arrays.fill(plainPlusSaltBytes, (byte) 0); - } + return Arrays.equals(digestBytes, + messageDigest.get().digest(plainPlusSaltBytes)); + } + finally + { + Arrays.fill(plainPlusSaltBytes, (byte) 0); } } diff --git a/opendj-server-legacy/src/main/java/org/opends/server/extensions/SaltedSHA1PasswordStorageScheme.java b/opendj-server-legacy/src/main/java/org/opends/server/extensions/SaltedSHA1PasswordStorageScheme.java index c03357c488..07e68abfcf 100644 --- a/opendj-server-legacy/src/main/java/org/opends/server/extensions/SaltedSHA1PasswordStorageScheme.java +++ b/opendj-server-legacy/src/main/java/org/opends/server/extensions/SaltedSHA1PasswordStorageScheme.java @@ -13,6 +13,7 @@ * * Copyright 2006-2010 Sun Microsystems, Inc. * Portions Copyright 2010-2016 ForgeRock AS. + * Portions Copyright 2026 3A Systems, LLC. */ package org.opends.server.extensions; @@ -62,11 +63,13 @@ public class SaltedSHA1PasswordStorageScheme /** The number of bytes SHA algorithm produces. */ private static final int SHA1_LENGTH = 20; - /** The message digest that will actually be used to generate the SHA-1 hashes. */ - private MessageDigest messageDigest; - - /** The lock used to provide threadsafe access to the message digest. */ - private Object digestLock; + /** + * The message digests used to generate the SHA-1 hashes. + * MessageDigest is not thread-safe, so a per-thread instance is used + * instead of a shared instance guarded by a lock: hashing under a global + * lock serializes all concurrent bind password verifications. + */ + private ThreadLocal messageDigest; /** The secure random number generator to use to generate the salt values. */ private Random random; @@ -88,7 +91,8 @@ public void initializePasswordStorageScheme( { try { - messageDigest = MessageDigest.getInstance(MESSAGE_DIGEST_ALGORITHM_SHA_1); + // Fail fast at initialization time if the algorithm is unavailable. + MessageDigest.getInstance(MESSAGE_DIGEST_ALGORITHM_SHA_1); } catch (Exception e) { @@ -98,8 +102,17 @@ public void initializePasswordStorageScheme( throw new InitializationException(message, e); } - digestLock = new Object(); - random = new Random(); + messageDigest = ThreadLocal.withInitial(() -> { + try + { + return MessageDigest.getInstance(MESSAGE_DIGEST_ALGORITHM_SHA_1); + } + catch (Exception e) + { + throw new IllegalStateException(e); + } + }); + random = new Random(); } @Override @@ -120,31 +133,28 @@ public ByteString encodePassword(ByteSequence plaintext) byte[] digestBytes; - synchronized (digestLock) + try { - try - { - // Generate the salt and put in the plain+salt array. - random.nextBytes(saltBytes); - System.arraycopy(saltBytes,0, plainPlusSalt, plainBytesLength, - NUM_SALT_BYTES); + // Generate the salt and put in the plain+salt array. + random.nextBytes(saltBytes); + System.arraycopy(saltBytes,0, plainPlusSalt, plainBytesLength, + NUM_SALT_BYTES); - // Create the hash from the concatenated value. - digestBytes = messageDigest.digest(plainPlusSalt); - } - catch (Exception e) - { - logger.traceException(e); + // Create the hash from the concatenated value. + digestBytes = messageDigest.get().digest(plainPlusSalt); + } + catch (Exception e) + { + logger.traceException(e); - LocalizableMessage message = ERR_PWSCHEME_CANNOT_ENCODE_PASSWORD.get( - CLASS_NAME, getExceptionMessage(e)); - throw new DirectoryException(DirectoryServer.getCoreConfigManager().getServerErrorResultCode(), - message, e); - } - finally - { - Arrays.fill(plainPlusSalt, (byte) 0); - } + LocalizableMessage message = ERR_PWSCHEME_CANNOT_ENCODE_PASSWORD.get( + CLASS_NAME, getExceptionMessage(e)); + throw new DirectoryException(DirectoryServer.getCoreConfigManager().getServerErrorResultCode(), + message, e); + } + finally + { + Arrays.fill(plainPlusSalt, (byte) 0); } // Append the salt to the hashed value and base64-the whole thing. @@ -174,31 +184,28 @@ public ByteString encodePasswordWithScheme(ByteSequence plaintext) byte[] digestBytes; - synchronized (digestLock) + try { - try - { - // Generate the salt and put in the plain+salt array. - random.nextBytes(saltBytes); - System.arraycopy(saltBytes,0, plainPlusSalt, plainBytesLength, - NUM_SALT_BYTES); + // Generate the salt and put in the plain+salt array. + random.nextBytes(saltBytes); + System.arraycopy(saltBytes,0, plainPlusSalt, plainBytesLength, + NUM_SALT_BYTES); - // Create the hash from the concatenated value. - digestBytes = messageDigest.digest(plainPlusSalt); - } - catch (Exception e) - { - logger.traceException(e); + // Create the hash from the concatenated value. + digestBytes = messageDigest.get().digest(plainPlusSalt); + } + catch (Exception e) + { + logger.traceException(e); - LocalizableMessage message = ERR_PWSCHEME_CANNOT_ENCODE_PASSWORD.get( - CLASS_NAME, getExceptionMessage(e)); - throw new DirectoryException(DirectoryServer.getCoreConfigManager().getServerErrorResultCode(), - message, e); - } - finally - { - Arrays.fill(plainPlusSalt, (byte) 0); - } + LocalizableMessage message = ERR_PWSCHEME_CANNOT_ENCODE_PASSWORD.get( + CLASS_NAME, getExceptionMessage(e)); + throw new DirectoryException(DirectoryServer.getCoreConfigManager().getServerErrorResultCode(), + message, e); + } + finally + { + Arrays.fill(plainPlusSalt, (byte) 0); } // Append the salt to the hashed value and base64-the whole thing. @@ -251,22 +258,19 @@ public boolean passwordMatches(ByteSequence plaintextPassword, byte[] userDigestBytes; - synchronized (digestLock) + try { - try - { - userDigestBytes = messageDigest.digest(plainPlusSalt); - } - catch (Exception e) - { - logger.traceException(e); + userDigestBytes = messageDigest.get().digest(plainPlusSalt); + } + catch (Exception e) + { + logger.traceException(e); - return false; - } - finally - { - Arrays.fill(plainPlusSalt, (byte) 0); - } + return false; + } + finally + { + Arrays.fill(plainPlusSalt, (byte) 0); } return Arrays.equals(digestBytes, userDigestBytes); @@ -297,31 +301,28 @@ public ByteString encodeAuthPassword(ByteSequence plaintext) byte[] digestBytes; - synchronized (digestLock) + try { - try - { - // Generate the salt and put in the plain+salt array. - random.nextBytes(saltBytes); - System.arraycopy(saltBytes,0, plainPlusSalt, plaintextLength, - NUM_SALT_BYTES); + // Generate the salt and put in the plain+salt array. + random.nextBytes(saltBytes); + System.arraycopy(saltBytes,0, plainPlusSalt, plaintextLength, + NUM_SALT_BYTES); - // Create the hash from the concatenated value. - digestBytes = messageDigest.digest(plainPlusSalt); - } - catch (Exception e) - { - logger.traceException(e); + // Create the hash from the concatenated value. + digestBytes = messageDigest.get().digest(plainPlusSalt); + } + catch (Exception e) + { + logger.traceException(e); - LocalizableMessage message = ERR_PWSCHEME_CANNOT_ENCODE_PASSWORD.get( - CLASS_NAME, getExceptionMessage(e)); - throw new DirectoryException(DirectoryServer.getCoreConfigManager().getServerErrorResultCode(), - message, e); - } - finally - { - Arrays.fill(plainPlusSalt, (byte) 0); - } + LocalizableMessage message = ERR_PWSCHEME_CANNOT_ENCODE_PASSWORD.get( + CLASS_NAME, getExceptionMessage(e)); + throw new DirectoryException(DirectoryServer.getCoreConfigManager().getServerErrorResultCode(), + message, e); + } + finally + { + Arrays.fill(plainPlusSalt, (byte) 0); } // Encode and return the value. @@ -359,17 +360,14 @@ public boolean authPasswordMatches(ByteSequence plaintextPassword, System.arraycopy(saltBytes, 0, plainPlusSaltBytes, plainBytesLength, saltBytes.length); - synchronized (digestLock) + try { - try - { - return Arrays.equals(digestBytes, - messageDigest.digest(plainPlusSaltBytes)); - } - finally - { - Arrays.fill(plainPlusSaltBytes, (byte) 0); - } + return Arrays.equals(digestBytes, + messageDigest.get().digest(plainPlusSaltBytes)); + } + finally + { + Arrays.fill(plainPlusSaltBytes, (byte) 0); } } diff --git a/opendj-server-legacy/src/main/java/org/opends/server/extensions/SaltedSHA256PasswordStorageScheme.java b/opendj-server-legacy/src/main/java/org/opends/server/extensions/SaltedSHA256PasswordStorageScheme.java index d6780baa7b..adca96909c 100644 --- a/opendj-server-legacy/src/main/java/org/opends/server/extensions/SaltedSHA256PasswordStorageScheme.java +++ b/opendj-server-legacy/src/main/java/org/opends/server/extensions/SaltedSHA256PasswordStorageScheme.java @@ -13,6 +13,7 @@ * * Copyright 2006-2008 Sun Microsystems, Inc. * Portions Copyright 2010-2016 ForgeRock AS. + * Portions Copyright 2026 3A Systems, LLC. */ package org.opends.server.extensions; @@ -62,11 +63,13 @@ public class SaltedSHA256PasswordStorageScheme /** Size of the dgiest in bytes. */ private static final int SHA256_LENGTH = 256 / 8; - /** The message digest that will actually be used to generate the 256-bit SHA-2 hashes. */ - private MessageDigest messageDigest; - - /** The lock used to provide threadsafe access to the message digest. */ - private Object digestLock; + /** + * The message digests used to generate the 256-bit SHA-2 hashes. + * MessageDigest is not thread-safe, so a per-thread instance is used + * instead of a shared instance guarded by a lock: hashing under a global + * lock serializes all concurrent bind password verifications. + */ + private ThreadLocal messageDigest; /** The secure random number generator to use to generate the salt values. */ private Random random; @@ -88,8 +91,8 @@ public void initializePasswordStorageScheme( { try { - messageDigest = - MessageDigest.getInstance(MESSAGE_DIGEST_ALGORITHM_SHA_256); + // Fail fast at initialization time if the algorithm is unavailable. + MessageDigest.getInstance(MESSAGE_DIGEST_ALGORITHM_SHA_256); } catch (Exception e) { @@ -100,8 +103,17 @@ public void initializePasswordStorageScheme( throw new InitializationException(message, e); } - digestLock = new Object(); - random = new Random(); + messageDigest = ThreadLocal.withInitial(() -> { + try + { + return MessageDigest.getInstance(MESSAGE_DIGEST_ALGORITHM_SHA_256); + } + catch (Exception e) + { + throw new IllegalStateException(e); + } + }); + random = new Random(); } @Override @@ -122,31 +134,28 @@ public ByteString encodePassword(ByteSequence plaintext) byte[] digestBytes; - synchronized (digestLock) + try { - try - { - // Generate the salt and put in the plain+salt array. - random.nextBytes(saltBytes); - System.arraycopy(saltBytes,0, plainPlusSalt, plainBytesLength, - NUM_SALT_BYTES); + // Generate the salt and put in the plain+salt array. + random.nextBytes(saltBytes); + System.arraycopy(saltBytes,0, plainPlusSalt, plainBytesLength, + NUM_SALT_BYTES); - // Create the hash from the concatenated value. - digestBytes = messageDigest.digest(plainPlusSalt); - } - catch (Exception e) - { - logger.traceException(e); + // Create the hash from the concatenated value. + digestBytes = messageDigest.get().digest(plainPlusSalt); + } + catch (Exception e) + { + logger.traceException(e); - LocalizableMessage message = ERR_PWSCHEME_CANNOT_ENCODE_PASSWORD.get( - CLASS_NAME, getExceptionMessage(e)); - throw new DirectoryException(DirectoryServer.getCoreConfigManager().getServerErrorResultCode(), - message, e); - } - finally - { - Arrays.fill(plainPlusSalt, (byte) 0); - } + LocalizableMessage message = ERR_PWSCHEME_CANNOT_ENCODE_PASSWORD.get( + CLASS_NAME, getExceptionMessage(e)); + throw new DirectoryException(DirectoryServer.getCoreConfigManager().getServerErrorResultCode(), + message, e); + } + finally + { + Arrays.fill(plainPlusSalt, (byte) 0); } // Append the salt to the hashed value and base64-the whole thing. @@ -176,31 +185,28 @@ public ByteString encodePasswordWithScheme(ByteSequence plaintext) byte[] digestBytes; - synchronized (digestLock) + try { - try - { - // Generate the salt and put in the plain+salt array. - random.nextBytes(saltBytes); - System.arraycopy(saltBytes,0, plainPlusSalt, plainBytesLength, - NUM_SALT_BYTES); + // Generate the salt and put in the plain+salt array. + random.nextBytes(saltBytes); + System.arraycopy(saltBytes,0, plainPlusSalt, plainBytesLength, + NUM_SALT_BYTES); - // Create the hash from the concatenated value. - digestBytes = messageDigest.digest(plainPlusSalt); - } - catch (Exception e) - { - logger.traceException(e); + // Create the hash from the concatenated value. + digestBytes = messageDigest.get().digest(plainPlusSalt); + } + catch (Exception e) + { + logger.traceException(e); - LocalizableMessage message = ERR_PWSCHEME_CANNOT_ENCODE_PASSWORD.get( - CLASS_NAME, getExceptionMessage(e)); - throw new DirectoryException(DirectoryServer.getCoreConfigManager().getServerErrorResultCode(), - message, e); - } - finally - { - Arrays.fill(plainPlusSalt, (byte) 0); - } + LocalizableMessage message = ERR_PWSCHEME_CANNOT_ENCODE_PASSWORD.get( + CLASS_NAME, getExceptionMessage(e)); + throw new DirectoryException(DirectoryServer.getCoreConfigManager().getServerErrorResultCode(), + message, e); + } + finally + { + Arrays.fill(plainPlusSalt, (byte) 0); } // Append the salt to the hashed value and base64-the whole thing. @@ -255,22 +261,19 @@ public boolean passwordMatches(ByteSequence plaintextPassword, byte[] userDigestBytes; - synchronized (digestLock) + try { - try - { - userDigestBytes = messageDigest.digest(plainPlusSalt); - } - catch (Exception e) - { - logger.traceException(e); + userDigestBytes = messageDigest.get().digest(plainPlusSalt); + } + catch (Exception e) + { + logger.traceException(e); - return false; - } - finally - { - Arrays.fill(plainPlusSalt, (byte) 0); - } + return false; + } + finally + { + Arrays.fill(plainPlusSalt, (byte) 0); } return Arrays.equals(digestBytes, userDigestBytes); @@ -301,31 +304,28 @@ public ByteString encodeAuthPassword(ByteSequence plaintext) byte[] digestBytes; - synchronized (digestLock) + try { - try - { - // Generate the salt and put in the plain+salt array. - random.nextBytes(saltBytes); - System.arraycopy(saltBytes,0, plainPlusSalt, plaintextLength, - NUM_SALT_BYTES); + // Generate the salt and put in the plain+salt array. + random.nextBytes(saltBytes); + System.arraycopy(saltBytes,0, plainPlusSalt, plaintextLength, + NUM_SALT_BYTES); - // Create the hash from the concatenated value. - digestBytes = messageDigest.digest(plainPlusSalt); - } - catch (Exception e) - { - logger.traceException(e); + // Create the hash from the concatenated value. + digestBytes = messageDigest.get().digest(plainPlusSalt); + } + catch (Exception e) + { + logger.traceException(e); - LocalizableMessage message = ERR_PWSCHEME_CANNOT_ENCODE_PASSWORD.get( - CLASS_NAME, getExceptionMessage(e)); - throw new DirectoryException(DirectoryServer.getCoreConfigManager().getServerErrorResultCode(), - message, e); - } - finally - { - Arrays.fill(plainPlusSalt, (byte) 0); - } + LocalizableMessage message = ERR_PWSCHEME_CANNOT_ENCODE_PASSWORD.get( + CLASS_NAME, getExceptionMessage(e)); + throw new DirectoryException(DirectoryServer.getCoreConfigManager().getServerErrorResultCode(), + message, e); + } + finally + { + Arrays.fill(plainPlusSalt, (byte) 0); } // Encode and return the value. @@ -363,17 +363,14 @@ public boolean authPasswordMatches(ByteSequence plaintextPassword, System.arraycopy(saltBytes, 0, plainPlusSaltBytes, plainBytesLength, saltBytes.length); - synchronized (digestLock) + try { - try - { - return Arrays.equals(digestBytes, - messageDigest.digest(plainPlusSaltBytes)); - } - finally - { - Arrays.fill(plainPlusSaltBytes, (byte) 0); - } + return Arrays.equals(digestBytes, + messageDigest.get().digest(plainPlusSaltBytes)); + } + finally + { + Arrays.fill(plainPlusSaltBytes, (byte) 0); } } diff --git a/opendj-server-legacy/src/main/java/org/opends/server/extensions/SaltedSHA384PasswordStorageScheme.java b/opendj-server-legacy/src/main/java/org/opends/server/extensions/SaltedSHA384PasswordStorageScheme.java index 187f211c2e..50a5096bdc 100644 --- a/opendj-server-legacy/src/main/java/org/opends/server/extensions/SaltedSHA384PasswordStorageScheme.java +++ b/opendj-server-legacy/src/main/java/org/opends/server/extensions/SaltedSHA384PasswordStorageScheme.java @@ -13,6 +13,7 @@ * * Copyright 2006-2008 Sun Microsystems, Inc. * Portions Copyright 2010-2016 ForgeRock AS. + * Portions Copyright 2026 3A Systems, LLC. */ package org.opends.server.extensions; @@ -62,11 +63,13 @@ public class SaltedSHA384PasswordStorageScheme /** The size of the digest in bytes. */ private static final int SHA384_LENGTH = 384 / 8; - /** The message digest that will actually be used to generate the 384-bit SHA-2 hashes. */ - private MessageDigest messageDigest; - - /** The lock used to provide threadsafe access to the message digest. */ - private Object digestLock; + /** + * The message digests used to generate the 384-bit SHA-2 hashes. + * MessageDigest is not thread-safe, so a per-thread instance is used + * instead of a shared instance guarded by a lock: hashing under a global + * lock serializes all concurrent bind password verifications. + */ + private ThreadLocal messageDigest; /** The secure random number generator to use to generate the salt values. */ private Random random; @@ -88,8 +91,8 @@ public void initializePasswordStorageScheme( { try { - messageDigest = - MessageDigest.getInstance(MESSAGE_DIGEST_ALGORITHM_SHA_384); + // Fail fast at initialization time if the algorithm is unavailable. + MessageDigest.getInstance(MESSAGE_DIGEST_ALGORITHM_SHA_384); } catch (Exception e) { @@ -100,8 +103,17 @@ public void initializePasswordStorageScheme( throw new InitializationException(message, e); } - digestLock = new Object(); - random = new Random(); + messageDigest = ThreadLocal.withInitial(() -> { + try + { + return MessageDigest.getInstance(MESSAGE_DIGEST_ALGORITHM_SHA_384); + } + catch (Exception e) + { + throw new IllegalStateException(e); + } + }); + random = new Random(); } @Override @@ -122,31 +134,28 @@ public ByteString encodePassword(ByteSequence plaintext) byte[] digestBytes; - synchronized (digestLock) + try { - try - { - // Generate the salt and put in the plain+salt array. - random.nextBytes(saltBytes); - System.arraycopy(saltBytes,0, plainPlusSalt, plainBytesLength, - NUM_SALT_BYTES); + // Generate the salt and put in the plain+salt array. + random.nextBytes(saltBytes); + System.arraycopy(saltBytes,0, plainPlusSalt, plainBytesLength, + NUM_SALT_BYTES); - // Create the hash from the concatenated value. - digestBytes = messageDigest.digest(plainPlusSalt); - } - catch (Exception e) - { - logger.traceException(e); + // Create the hash from the concatenated value. + digestBytes = messageDigest.get().digest(plainPlusSalt); + } + catch (Exception e) + { + logger.traceException(e); - LocalizableMessage message = ERR_PWSCHEME_CANNOT_ENCODE_PASSWORD.get( - CLASS_NAME, getExceptionMessage(e)); - throw new DirectoryException(DirectoryServer.getCoreConfigManager().getServerErrorResultCode(), - message, e); - } - finally - { - Arrays.fill(plainPlusSalt, (byte) 0); - } + LocalizableMessage message = ERR_PWSCHEME_CANNOT_ENCODE_PASSWORD.get( + CLASS_NAME, getExceptionMessage(e)); + throw new DirectoryException(DirectoryServer.getCoreConfigManager().getServerErrorResultCode(), + message, e); + } + finally + { + Arrays.fill(plainPlusSalt, (byte) 0); } // Append the salt to the hashed value and base64-the whole thing. @@ -176,31 +185,28 @@ public ByteString encodePasswordWithScheme(ByteSequence plaintext) byte[] digestBytes; - synchronized (digestLock) + try { - try - { - // Generate the salt and put in the plain+salt array. - random.nextBytes(saltBytes); - System.arraycopy(saltBytes,0, plainPlusSalt, plainBytesLength, - NUM_SALT_BYTES); + // Generate the salt and put in the plain+salt array. + random.nextBytes(saltBytes); + System.arraycopy(saltBytes,0, plainPlusSalt, plainBytesLength, + NUM_SALT_BYTES); - // Create the hash from the concatenated value. - digestBytes = messageDigest.digest(plainPlusSalt); - } - catch (Exception e) - { - logger.traceException(e); + // Create the hash from the concatenated value. + digestBytes = messageDigest.get().digest(plainPlusSalt); + } + catch (Exception e) + { + logger.traceException(e); - LocalizableMessage message = ERR_PWSCHEME_CANNOT_ENCODE_PASSWORD.get( - CLASS_NAME, getExceptionMessage(e)); - throw new DirectoryException(DirectoryServer.getCoreConfigManager().getServerErrorResultCode(), - message, e); - } - finally - { - Arrays.fill(plainPlusSalt, (byte) 0); - } + LocalizableMessage message = ERR_PWSCHEME_CANNOT_ENCODE_PASSWORD.get( + CLASS_NAME, getExceptionMessage(e)); + throw new DirectoryException(DirectoryServer.getCoreConfigManager().getServerErrorResultCode(), + message, e); + } + finally + { + Arrays.fill(plainPlusSalt, (byte) 0); } // Append the salt to the hashed value and base64-the whole thing. @@ -255,22 +261,19 @@ public boolean passwordMatches(ByteSequence plaintextPassword, byte[] userDigestBytes; - synchronized (digestLock) + try { - try - { - userDigestBytes = messageDigest.digest(plainPlusSalt); - } - catch (Exception e) - { - logger.traceException(e); + userDigestBytes = messageDigest.get().digest(plainPlusSalt); + } + catch (Exception e) + { + logger.traceException(e); - return false; - } - finally - { - Arrays.fill(plainPlusSalt, (byte) 0); - } + return false; + } + finally + { + Arrays.fill(plainPlusSalt, (byte) 0); } return Arrays.equals(digestBytes, userDigestBytes); @@ -301,31 +304,28 @@ public ByteString encodeAuthPassword(ByteSequence plaintext) byte[] digestBytes; - synchronized (digestLock) + try { - try - { - // Generate the salt and put in the plain+salt array. - random.nextBytes(saltBytes); - System.arraycopy(saltBytes,0, plainPlusSalt, plaintextLength, - NUM_SALT_BYTES); + // Generate the salt and put in the plain+salt array. + random.nextBytes(saltBytes); + System.arraycopy(saltBytes,0, plainPlusSalt, plaintextLength, + NUM_SALT_BYTES); - // Create the hash from the concatenated value. - digestBytes = messageDigest.digest(plainPlusSalt); - } - catch (Exception e) - { - logger.traceException(e); + // Create the hash from the concatenated value. + digestBytes = messageDigest.get().digest(plainPlusSalt); + } + catch (Exception e) + { + logger.traceException(e); - LocalizableMessage message = ERR_PWSCHEME_CANNOT_ENCODE_PASSWORD.get( - CLASS_NAME, getExceptionMessage(e)); - throw new DirectoryException(DirectoryServer.getCoreConfigManager().getServerErrorResultCode(), - message, e); - } - finally - { - Arrays.fill(plainPlusSalt, (byte) 0); - } + LocalizableMessage message = ERR_PWSCHEME_CANNOT_ENCODE_PASSWORD.get( + CLASS_NAME, getExceptionMessage(e)); + throw new DirectoryException(DirectoryServer.getCoreConfigManager().getServerErrorResultCode(), + message, e); + } + finally + { + Arrays.fill(plainPlusSalt, (byte) 0); } // Encode and return the value. @@ -363,17 +363,14 @@ public boolean authPasswordMatches(ByteSequence plaintextPassword, System.arraycopy(saltBytes, 0, plainPlusSaltBytes, plainBytesLength, saltBytes.length); - synchronized (digestLock) + try { - try - { - return Arrays.equals(digestBytes, - messageDigest.digest(plainPlusSaltBytes)); - } - finally - { - Arrays.fill(plainPlusSaltBytes, (byte) 0); - } + return Arrays.equals(digestBytes, + messageDigest.get().digest(plainPlusSaltBytes)); + } + finally + { + Arrays.fill(plainPlusSaltBytes, (byte) 0); } } diff --git a/opendj-server-legacy/src/main/java/org/opends/server/extensions/SaltedSHA512PasswordStorageScheme.java b/opendj-server-legacy/src/main/java/org/opends/server/extensions/SaltedSHA512PasswordStorageScheme.java index 7e2f683dde..1337bcff17 100644 --- a/opendj-server-legacy/src/main/java/org/opends/server/extensions/SaltedSHA512PasswordStorageScheme.java +++ b/opendj-server-legacy/src/main/java/org/opends/server/extensions/SaltedSHA512PasswordStorageScheme.java @@ -13,6 +13,7 @@ * * Copyright 2006-2008 Sun Microsystems, Inc. * Portions Copyright 2010-2016 ForgeRock AS. + * Portions Copyright 2026 3A Systems, LLC. */ package org.opends.server.extensions; @@ -62,11 +63,13 @@ public class SaltedSHA512PasswordStorageScheme /** The size of the digest in bytes. */ private static final int SHA512_LENGTH = 512 / 8; - /** The message digest that will actually be used to generate the 512-bit SHA-2 hashes. */ - private MessageDigest messageDigest; - - /** The lock used to provide threadsafe access to the message digest. */ - private Object digestLock; + /** + * The message digests used to generate the 512-bit SHA-2 hashes. + * MessageDigest is not thread-safe, so a per-thread instance is used + * instead of a shared instance guarded by a lock: hashing under a global + * lock serializes all concurrent bind password verifications. + */ + private ThreadLocal messageDigest; /** The secure random number generator to use to generate the salt values. */ private Random random; @@ -88,8 +91,8 @@ public void initializePasswordStorageScheme( { try { - messageDigest = - MessageDigest.getInstance(MESSAGE_DIGEST_ALGORITHM_SHA_512); + // Fail fast at initialization time if the algorithm is unavailable. + MessageDigest.getInstance(MESSAGE_DIGEST_ALGORITHM_SHA_512); } catch (Exception e) { @@ -100,8 +103,17 @@ public void initializePasswordStorageScheme( throw new InitializationException(message, e); } - digestLock = new Object(); - random = new Random(); + messageDigest = ThreadLocal.withInitial(() -> { + try + { + return MessageDigest.getInstance(MESSAGE_DIGEST_ALGORITHM_SHA_512); + } + catch (Exception e) + { + throw new IllegalStateException(e); + } + }); + random = new Random(); } @Override @@ -122,31 +134,28 @@ public ByteString encodePassword(ByteSequence plaintext) byte[] digestBytes; - synchronized (digestLock) + try { - try - { - // Generate the salt and put in the plain+salt array. - random.nextBytes(saltBytes); - System.arraycopy(saltBytes,0, plainPlusSalt, plainBytesLength, - NUM_SALT_BYTES); + // Generate the salt and put in the plain+salt array. + random.nextBytes(saltBytes); + System.arraycopy(saltBytes,0, plainPlusSalt, plainBytesLength, + NUM_SALT_BYTES); - // Create the hash from the concatenated value. - digestBytes = messageDigest.digest(plainPlusSalt); - } - catch (Exception e) - { - logger.traceException(e); + // Create the hash from the concatenated value. + digestBytes = messageDigest.get().digest(plainPlusSalt); + } + catch (Exception e) + { + logger.traceException(e); - LocalizableMessage message = ERR_PWSCHEME_CANNOT_ENCODE_PASSWORD.get( - CLASS_NAME, getExceptionMessage(e)); - throw new DirectoryException(DirectoryServer.getCoreConfigManager().getServerErrorResultCode(), - message, e); - } - finally - { - Arrays.fill(plainPlusSalt, (byte) 0); - } + LocalizableMessage message = ERR_PWSCHEME_CANNOT_ENCODE_PASSWORD.get( + CLASS_NAME, getExceptionMessage(e)); + throw new DirectoryException(DirectoryServer.getCoreConfigManager().getServerErrorResultCode(), + message, e); + } + finally + { + Arrays.fill(plainPlusSalt, (byte) 0); } // Append the salt to the hashed value and base64-the whole thing. @@ -176,31 +185,28 @@ public ByteString encodePasswordWithScheme(ByteSequence plaintext) byte[] digestBytes; - synchronized (digestLock) + try { - try - { - // Generate the salt and put in the plain+salt array. - random.nextBytes(saltBytes); - System.arraycopy(saltBytes,0, plainPlusSalt, plainBytesLength, - NUM_SALT_BYTES); + // Generate the salt and put in the plain+salt array. + random.nextBytes(saltBytes); + System.arraycopy(saltBytes,0, plainPlusSalt, plainBytesLength, + NUM_SALT_BYTES); - // Create the hash from the concatenated value. - digestBytes = messageDigest.digest(plainPlusSalt); - } - catch (Exception e) - { - logger.traceException(e); + // Create the hash from the concatenated value. + digestBytes = messageDigest.get().digest(plainPlusSalt); + } + catch (Exception e) + { + logger.traceException(e); - LocalizableMessage message = ERR_PWSCHEME_CANNOT_ENCODE_PASSWORD.get( - CLASS_NAME, getExceptionMessage(e)); - throw new DirectoryException(DirectoryServer.getCoreConfigManager().getServerErrorResultCode(), - message, e); - } - finally - { - Arrays.fill(plainPlusSalt, (byte) 0); - } + LocalizableMessage message = ERR_PWSCHEME_CANNOT_ENCODE_PASSWORD.get( + CLASS_NAME, getExceptionMessage(e)); + throw new DirectoryException(DirectoryServer.getCoreConfigManager().getServerErrorResultCode(), + message, e); + } + finally + { + Arrays.fill(plainPlusSalt, (byte) 0); } // Append the salt to the hashed value and base64-the whole thing. @@ -255,22 +261,19 @@ public boolean passwordMatches(ByteSequence plaintextPassword, byte[] userDigestBytes; - synchronized (digestLock) + try { - try - { - userDigestBytes = messageDigest.digest(plainPlusSalt); - } - catch (Exception e) - { - logger.traceException(e); + userDigestBytes = messageDigest.get().digest(plainPlusSalt); + } + catch (Exception e) + { + logger.traceException(e); - return false; - } - finally - { - Arrays.fill(plainPlusSalt, (byte) 0); - } + return false; + } + finally + { + Arrays.fill(plainPlusSalt, (byte) 0); } return Arrays.equals(digestBytes, userDigestBytes); @@ -301,31 +304,28 @@ public ByteString encodeAuthPassword(ByteSequence plaintext) byte[] digestBytes; - synchronized (digestLock) + try { - try - { - // Generate the salt and put in the plain+salt array. - random.nextBytes(saltBytes); - System.arraycopy(saltBytes,0, plainPlusSalt, plaintextLength, - NUM_SALT_BYTES); + // Generate the salt and put in the plain+salt array. + random.nextBytes(saltBytes); + System.arraycopy(saltBytes,0, plainPlusSalt, plaintextLength, + NUM_SALT_BYTES); - // Create the hash from the concatenated value. - digestBytes = messageDigest.digest(plainPlusSalt); - } - catch (Exception e) - { - logger.traceException(e); + // Create the hash from the concatenated value. + digestBytes = messageDigest.get().digest(plainPlusSalt); + } + catch (Exception e) + { + logger.traceException(e); - LocalizableMessage message = ERR_PWSCHEME_CANNOT_ENCODE_PASSWORD.get( - CLASS_NAME, getExceptionMessage(e)); - throw new DirectoryException(DirectoryServer.getCoreConfigManager().getServerErrorResultCode(), - message, e); - } - finally - { - Arrays.fill(plainPlusSalt, (byte) 0); - } + LocalizableMessage message = ERR_PWSCHEME_CANNOT_ENCODE_PASSWORD.get( + CLASS_NAME, getExceptionMessage(e)); + throw new DirectoryException(DirectoryServer.getCoreConfigManager().getServerErrorResultCode(), + message, e); + } + finally + { + Arrays.fill(plainPlusSalt, (byte) 0); } // Encode and return the value. @@ -363,17 +363,14 @@ public boolean authPasswordMatches(ByteSequence plaintextPassword, System.arraycopy(saltBytes, 0, plainPlusSaltBytes, plainBytesLength, saltBytes.length); - synchronized (digestLock) + try { - try - { - return Arrays.equals(digestBytes, - messageDigest.digest(plainPlusSaltBytes)); - } - finally - { - Arrays.fill(plainPlusSaltBytes, (byte) 0); - } + return Arrays.equals(digestBytes, + messageDigest.get().digest(plainPlusSaltBytes)); + } + finally + { + Arrays.fill(plainPlusSaltBytes, (byte) 0); } } From d9724fd7cf73d4cac87b6b366d75bc765fd201ff Mon Sep 17 00:00:00 2001 From: Valera V Harseko Date: Thu, 2 Jul 2026 18:12:06 +0300 Subject: [PATCH 2/2] Remove global lock serialization in UNIX crypt password operations Crypt (the unix crypt(3)/DES implementation behind the {CRYPT} storage scheme's unix mode) kept all of its mutable working buffers in a single shared SubCrypt instance guarded by synchronized(digestLock). The scheme is a server-wide singleton, so all concurrent {CRYPT} password encodings and verifications serialized on one monitor, with the DES work executed under the lock. Replace the shared SubCrypt with a ThreadLocal. This also fixes a pre-existing race: _crypt() returns a reference to the shared _iobuf buffer, and crypt() converted it to bytes after releasing the lock, so a concurrent call could overwrite the result while it was being read. --- .../java/org/opends/server/util/Crypt.java | 33 ++++++++++--------- 1 file changed, 18 insertions(+), 15 deletions(-) diff --git a/opendj-server-legacy/src/main/java/org/opends/server/util/Crypt.java b/opendj-server-legacy/src/main/java/org/opends/server/util/Crypt.java index 1391b2e038..078f849235 100644 --- a/opendj-server-legacy/src/main/java/org/opends/server/util/Crypt.java +++ b/opendj-server-legacy/src/main/java/org/opends/server/util/Crypt.java @@ -13,6 +13,7 @@ * * Copyright 2008 Sun Microsystems, Inc. * Portions Copyright 2015 ForgeRock AS. + * Portions Copyright 2026 3A Systems, LLC */ /* * Copyright 2005 Sun Microsystems, Inc. All rights reserved. @@ -136,18 +137,26 @@ private static class SubCrypt int _iobuf[] = new int[16]; } - private final SubCrypt _crypt; + /** + * The working state of the algorithm. setkey(), encrypt() and _crypt() all + * scribble on these buffers (and _crypt() returns a reference to _iobuf), + * so a per-thread instance is used instead of a shared instance guarded by + * a lock: encrypting under a global lock serializes all concurrent {CRYPT} + * password operations. + */ + private final ThreadLocal _crypt = ThreadLocal.withInitial(() -> { + SubCrypt c = new SubCrypt(); + copy(e, c._E); + return c; + }); /** * Constructor. */ public Crypt() { - _crypt = new SubCrypt(); - - copy(e, _crypt._E); } - private void copy(byte[] src, int[] dest) { + private static void copy(byte[] src, int[] dest) { for (int i = 0; i < dest.length; i++) { dest[i] = src[i]; } @@ -158,7 +167,7 @@ private void copy(byte[] src, int[] dest) { */ private void setkey(int[] key) { - SubCrypt _c = _crypt; + SubCrypt _c = _crypt.get(); /* * if (_c == null) { _cryptinit(); _c = __crypt; } @@ -270,7 +279,7 @@ private void rotate(int[] array) */ private final void encrypt(int block[], int edflag) { - SubCrypt _c = _crypt; + SubCrypt _c = _crypt.get(); /* * First, permute the bits in the input @@ -369,8 +378,6 @@ private final void encrypt(int block[], int edflag) } } - private Object digestLock = new Object(); - /** * Encode the supplied password in unix crypt form with the provided * salt. @@ -382,11 +389,7 @@ private final void encrypt(int block[], int edflag) */ public byte[] crypt(byte[] pw, byte[] salt) { - int[] r; - synchronized (digestLock) - { - r = _crypt(pw, salt); - } + int[] r = _crypt(pw, salt); //TODO: crypt always returns same size array? So don't mess // around calculating the number of zeros at the end. @@ -416,7 +419,7 @@ public byte[] crypt(byte[] pw, byte[] salt) private int[] _crypt(byte[] pw, byte[] salt) { - SubCrypt _c = _crypt; + SubCrypt _c = _crypt.get(); Arrays.fill(_c._ablock, 0);