Kotlin SHA-256 and PBKDF2 on the JVM
Hash text with MessageDigest and derive a salted password hash with PBKDF2.
On this page
Kotlin bytes, strings, and Java library calls.
- Hash text with MessageDigest and derive a salted password hash with PBKDF2.
- Predict the sample output and explain the state changes.
- Complete the practice task and check the boundary cases.
The concept
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.
Code example
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))}")
}ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad a52d159f262b2c6ddb724a61840befc36eb30c88877a4030b65cbe86298449c9 hash length: 32 bytes right password: true wrong password: false
Follow the code
Open the walkthrough to inspect the example one step at a time.
Show guided walkthrough
import java.security.MessageDigestimport java.security.SecureRandomimport javax.crypto.SecretKeyFactoryimport 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))}")}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
| Step / line | What happens | State after step | Output 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 |
Behind the scenes
How Kotlin processes this code
Kotlin commonly compiles to JVM bytecode, which the JVM executes.
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.