Kotlin SHA-256 and PBKDF2 on the JVM

Hash text with MessageDigest and derive a salted password hash with PBKDF2.

Kotlin
import java.security.MessageDigest
import java.security.SecureRandom
import javax.crypto.SecretKeyFactory
import javax.crypto.spec.PBEKeySpec

fun hex(bytes: ByteArray): String = bytes.joinToString("") { "%02x".format(it) }

fun derive(password: String, salt: ByteArray): ByteArray {
    val spec = PBEKeySpec(password.toCharArray(), salt, 600_000, 256)
    return SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256").generateSecret(spec).encoded
}

fun main() {
    val sha = MessageDigest.getInstance("SHA-256")
    println(hex(sha.digest("abc".toByteArray(Charsets.UTF_8))))
    println(hex(sha.digest("abd".toByteArray(Charsets.UTF_8))))

    val salt = ByteArray(16).also { SecureRandom().nextBytes(it) }
    val stored = derive("correct horse", salt)
    println("hash length: ${stored.size} bytes")
    println("right password: ${MessageDigest.isEqual(stored, derive("correct horse", salt))}")
    println("wrong password: ${MessageDigest.isEqual(stored, derive("wrong guess", salt))}")
}
Output
ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad
a52d159f262b2c6ddb724a61840befc36eb30c88877a4030b65cbe86298449c9
hash length: 32 bytes
right password: true
wrong password: false

Explanation

Kotlin on the JVM can use the Java security libraries directly. MessageDigest computes hashes such as SHA-256. A hash is deterministic and one-way, and a tiny change in the input produces a completely different digest. Hashes are used to check integrity. Convert text to bytes with an explicit charset before hashing, so that the result does not depend on the platform.

A password needs more than a fast hash. PBKDF2 repeats an HMAC many times, and a random salt, stored with the hash, makes equal passwords produce different hashes. The iteration count controls how costly each guess is. The OWASP Password Storage Cheat Sheet recommends hundreds of thousands of iterations for PBKDF2 with HMAC-SHA-256. MessageDigest.isEqual compares two byte arrays in constant time.

Explore the dry run
Guided walkthroughReady
1import java.security.MessageDigest
2import java.security.SecureRandom
3import javax.crypto.SecretKeyFactory
4import javax.crypto.spec.PBEKeySpec
5
6fun hex(bytes: ByteArray): String = bytes.joinToString("") { "%02x".format(it) }
7
8fun derive(password: String, salt: ByteArray): ByteArray {
9 val spec = PBEKeySpec(password.toCharArray(), salt, 600_000, 256)
10 return SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256").generateSecret(spec).encoded
11}
12
13fun main() {
14 val sha = MessageDigest.getInstance("SHA-256")
15 println(hex(sha.digest("abc".toByteArray(Charsets.UTF_8))))
16 println(hex(sha.digest("abd".toByteArray(Charsets.UTF_8))))
17
18 val salt = ByteArray(16).also { SecureRandom().nextBytes(it) }
19 val stored = derive("correct horse", salt)
20 println("hash length: ${stored.size} bytes")
21 println("right password: ${MessageDigest.isEqual(stored, derive("correct horse", salt))}")
22 println("wrong password: ${MessageDigest.isEqual(stored, derive("wrong guess", salt))}")
23}

Follow the example

Start to inspect the source, values, and output. These prepared steps explain the published sample; grouped loop steps describe multiple iterations.

View the complete dry-run table
Prepared trace of the published program. Some rows group repeated iterations.
Step / lineWhat happensState after stepOutput so far
Create a SHA-256 hasher from the Java library.—
—
A standard SHA-256 test vector.—
ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad
One changed letter gives an unrelated digest.—
ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad
a52d159f262b2c6ddb724a61840befc36eb30c88877a4030b65cbe86298449c9
A random 16-byte salt from a secure random source.—
ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad
a52d159f262b2c6ddb724a61840befc36eb30c88877a4030b65cbe86298449c9
The 256-bit PBKDF2 hash to store together with the salt and the iteration count.—
ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad
a52d159f262b2c6ddb724a61840befc36eb30c88877a4030b65cbe86298449c9
256 bits is 32 bytes.—
ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad
a52d159f262b2c6ddb724a61840befc36eb30c88877a4030b65cbe86298449c9
hash length: 32 bytes
Deriving again with the same salt gives the same bytes.—
ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad
a52d159f262b2c6ddb724a61840befc36eb30c88877a4030b65cbe86298449c9
hash length: 32 bytes
right password: true
A different password gives a different hash.—
ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad
a52d159f262b2c6ddb724a61840befc36eb30c88877a4030b65cbe86298449c9
hash length: 32 bytes
right password: true
wrong password: false

Cost and common mistakes

PBKDF2 costs O(iterations) per hash, chosen to be slow on purpose.

  • Use SecureRandom for salts and tokens. kotlin.random.Random and java.util.Random are not secure.
  • toByteArray() without a charset uses UTF-8 in Kotlin, but being explicit documents the intention.
  • Store the algorithm name, iteration count, salt, and hash together so the cost can be raised later.

Try it yourself

Why store the salt next to the hash?

Reveal answer and reasoning

The salt is not secret. Its job is to make each hash unique, and it is needed again to verify the password at login.

Further reading