title: Spring password encoder for the default Django password format (pbkdf2_sha256)
body: <p>I am working on a Spring Boot application using Spring Security connected to a database originally generated by Django, in which all of the user passwords are stored in Django's <code>pbkdf2_sha256</code> <a href="https://docs.djangoproject.com/en/3.1/topics/auth/passwords/" rel="nofollow noreferrer">format</a>.</p>
  <pre><code>&lt;algorithm&gt;$&lt;iterations&gt;$&lt;salt&gt;$&lt;hash&gt;
  </code></pre>
  <p>Looking at Spring Security's <code>Pbkdf2PasswordEncoder</code>, I was able to mostly figure it out and create an encoder that is able to encode and match Django's format.</p>
  <p>After successfully unit testing it, I tried it on a sample value generated by Django... and it failed.</p>
  <p>Why is this encoder not matching Django-generated values? Is Django performing some ~magic~ under the hood? I have also tried multiple replacements for Java's <code>SecurityFactory</code> implementation. This <a href="https://github.com/django/django/blob/9ee693bd6cf4074f04ec51c6f3cfe87cad392f12/django/contrib/auth/hashers.py#L276" rel="nofollow noreferrer">line</a> in Django's source code caught my attention as well, but writing the equivalent Kotlin did not fix the issue.</p>
  <pre class="lang-kotlin prettyprint-override"><code>class DjangoPbkdf2PasswordEncoder : PasswordEncoder {
  
      companion object {
          private const val PREFIX = &quot;pbkdf2_sha256&quot;
          private const val SEPARATOR = &quot;\$&quot;
          private const val ITERATIONS = 180000
          private const val HASH_WIDTH = 256
          private const val ALGORITHM = &quot;PBKDF2WithHmacSHA256&quot;
      }
  
      private val saltGenerator: BytesKeyGenerator = KeyGenerators.secureRandom()
  
      private fun base64Decode(string: String): ByteArray {
          return Base64.getDecoder().decode(string)
      }
  
      private fun base64Encode(bytes: ByteArray): String {
          return Base64.getEncoder().encodeToString(bytes)
      }
  
      override fun encode(rawPassword: CharSequence): String {
          val salt = saltGenerator.generateKey()
          val hash = encodeWithSalt(rawPassword, salt)
  
          val encodedHash = base64Encode(hash)
          val encodedSalt = base64Encode(salt)
  
          return listOf(PREFIX, ITERATIONS, encodedSalt, encodedHash).joinToString(SEPARATOR)
      }
  
      private fun encodeWithSalt(rawPassword: CharSequence, salt: ByteArray): ByteArray {
          return encodeWithSaltAndIterations(rawPassword, salt, ITERATIONS)
      }
  
      private fun encodeWithSaltAndIterations(rawPassword: CharSequence, salt: ByteArray, iterations: Int): ByteArray {
          val keySpec = PBEKeySpec(
              rawPassword.toString().toCharArray(),
              salt,
              iterations,
              HASH_WIDTH
          )
  
          return try {
              SecretKeyFactory.getInstance(ALGORITHM)
                  .generateSecret(keySpec)
                  .encoded
          } catch (e: GeneralSecurityException) {
              throw IllegalStateException(&quot;Could not create hash&quot;, e)
          }
      }
  
      override fun matches(rawPassword: CharSequence, partsEncodedPassword: String): Boolean {
          if (!partsEncodedPassword.startsWith(PREFIX)) {
              throw IllegalArgumentException(&quot;Encoded password does not start with: $PREFIX&quot;)
          }
  
          val parts = partsEncodedPassword.split(SEPARATOR)
          if (parts.size != 4) {
              throw IllegalArgumentException(&quot;The encoded password format does not have 4 parts&quot;)
          }
  
          val iterations = parts[1].toInt()
          val salt = base64Decode(parts[2])
          val hash = base64Decode(parts[3])
  
          return MessageDigest.isEqual(
              hash,
              encodeWithSaltAndIterations(rawPassword, salt, iterations)
          )
      }
  }
  </code></pre>