From 1bda529e3cf685d5108832ecac7f433c85acb6fd Mon Sep 17 00:00:00 2001
From: Valery Kharseko <vharseko@3a-systems.ru>
Date: Thu, 06 Aug 2026 11:31:31 +0000
Subject: [PATCH] Remove global digestLock serialization in digest password storage schemes (#667)

---
 opendj-server-legacy/src/main/java/org/opends/server/util/Crypt.java                                   |   33 
 opendj-server-legacy/src/main/java/org/opends/server/extensions/SaltedSHA512PasswordStorageScheme.java |  196 ++++----
 opendj-server-legacy/src/main/java/org/opends/server/extensions/MD5PasswordStorageScheme.java          |  134 +++--
 opendj-server-legacy/src/main/java/org/opends/server/extensions/SHA1PasswordStorageScheme.java         |  130 ++--
 opendj-server-legacy/src/main/java/org/opends/server/extensions/SaltedSHA384PasswordStorageScheme.java |  195 ++++----
 opendj-server-legacy/src/main/java/org/opends/server/extensions/SaltedSHA256PasswordStorageScheme.java |  195 ++++----
 opendj-server-legacy/src/main/java/org/opends/server/extensions/CRAMMD5SASLMechanismHandler.java       |   90 ++-
 opendj-server-legacy/src/main/java/org/opends/server/extensions/SaltedMD5PasswordStorageScheme.java    |  194 ++++----
 opendj-server-legacy/src/main/java/org/opends/server/extensions/SaltedSHA1PasswordStorageScheme.java   |  195 ++++----
 9 files changed, 685 insertions(+), 677 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 8149ec5..400c3a0 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 @@
   /** 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<MessageDigest> md5Digest;
 
   /** The random number generator that we will use to create the server challenge. */
   private SecureRandom randomGenerator;
@@ -109,12 +112,12 @@
     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 @@
       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 @@
     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);
-      }
-
-      // 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);
-
-      // 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);
-
-      // Return an MD5 digest of the resulting array.
-      return md5Digest.digest(oPadAndHash);
+      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);
+
+    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];
+    }
+
+    // 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);
   }
 
   @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 8d639c9..8518405 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 @@
   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> messageDigest;
 
   /**
    * Creates a new instance of this password storage scheme.  Note that no
@@ -76,7 +79,8 @@
   {
     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 @@
       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 @@
     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 @@
     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 @@
     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 b3843bf..f16a88f 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 @@
   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> messageDigest;
 
   /**
    * Creates a new instance of this password storage scheme.  Note that no
@@ -76,7 +79,8 @@
   {
     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 @@
       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 @@
     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 @@
     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 @@
     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 b02e865..def7f1e 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 @@
   /** 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> messageDigest;
 
   /** The secure random number generator to use to generate the salt values. */
   private Random random;
@@ -88,7 +91,8 @@
   {
     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 @@
       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 @@
 
     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 @@
 
     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 @@
 
     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 @@
 
     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 @@
     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 e417e1c..d82a994 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,7 +13,7 @@
  *
  * Copyright 2006-2010 Sun Microsystems, Inc.
  * Portions Copyright 2010-2016 ForgeRock AS.
- * Portions Copyrighted 2026 3A Systems, LLC.
+ * Portions Copyright 2026 3A Systems, LLC.
  */
 package org.opends.server.extensions;
 
@@ -70,11 +70,13 @@
   /** 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> messageDigest;
 
   /** The secure random number generator to use to generate the salt values. */
   private SecureRandom random;
@@ -96,7 +98,8 @@
   {
     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)
     {
@@ -106,8 +109,17 @@
       throw new InitializationException(message, e);
     }
 
-    digestLock = new Object();
-    random     = new SecureRandom();
+    messageDigest = ThreadLocal.withInitial(() -> {
+      try
+      {
+        return MessageDigest.getInstance(MESSAGE_DIGEST_ALGORITHM_SHA_1);
+      }
+      catch (Exception e)
+      {
+        throw new IllegalStateException(e);
+      }
+    });
+    random = new SecureRandom();
   }
 
   @Override
@@ -128,31 +140,28 @@
 
     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.
@@ -182,31 +191,28 @@
 
     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.
@@ -259,22 +265,19 @@
 
     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);
@@ -305,31 +308,28 @@
 
     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.
@@ -367,17 +367,14 @@
     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 d6780ba..adca969 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 @@
   /** 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> messageDigest;
 
   /** The secure random number generator to use to generate the salt values. */
   private Random random;
@@ -88,8 +91,8 @@
   {
     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 @@
       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 @@
 
     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 @@
 
     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 @@
 
     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 @@
 
     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 @@
     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 187f211..50a5096 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 @@
   /** 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> messageDigest;
 
   /** The secure random number generator to use to generate the salt values. */
   private Random random;
@@ -88,8 +91,8 @@
   {
     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 @@
       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 @@
 
     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 @@
 
     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 @@
 
     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 @@
 
     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 @@
     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 23d969b..58512d4 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,7 +13,7 @@
  *
  * Copyright 2006-2008 Sun Microsystems, Inc.
  * Portions Copyright 2010-2016 ForgeRock AS.
- * Portions Copyrighted 2026 3A Systems, LLC.
+ * Portions Copyright 2026 3A Systems, LLC.
  */
 package org.opends.server.extensions;
 
@@ -70,11 +70,13 @@
   /** 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> messageDigest;
 
   /** The secure random number generator to use to generate the salt values. */
   private SecureRandom random;
@@ -96,8 +98,8 @@
   {
     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)
     {
@@ -108,8 +110,17 @@
       throw new InitializationException(message, e);
     }
 
-    digestLock = new Object();
-    random     = new SecureRandom();
+    messageDigest = ThreadLocal.withInitial(() -> {
+      try
+      {
+        return MessageDigest.getInstance(MESSAGE_DIGEST_ALGORITHM_SHA_512);
+      }
+      catch (Exception e)
+      {
+        throw new IllegalStateException(e);
+      }
+    });
+    random = new SecureRandom();
   }
 
   @Override
@@ -130,31 +141,28 @@
 
     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.
@@ -184,31 +192,28 @@
 
     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.
@@ -263,22 +268,19 @@
 
     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);
@@ -309,31 +311,28 @@
 
     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.
@@ -371,17 +370,14 @@
     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/util/Crypt.java b/opendj-server-legacy/src/main/java/org/opends/server/util/Crypt.java
index 1391b2e..078f849 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 @@
     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<SubCrypt> _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 setkey(int[] key)
   {
-    SubCrypt _c = _crypt;
+    SubCrypt _c = _crypt.get();
 
     /*
      * if (_c == null) { _cryptinit(); _c = __crypt; }
@@ -270,7 +279,7 @@
    */
   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 Object digestLock = new Object();
-
   /**
    * Encode the supplied password in unix crypt form with the provided
    * salt.
@@ -382,11 +389,7 @@
    */
   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 @@
 
   private int[] _crypt(byte[] pw, byte[] salt)
   {
-    SubCrypt _c = _crypt;
+    SubCrypt _c = _crypt.get();
 
     Arrays.fill(_c._ablock, 0);
 

--
Gitblit v1.10.0