Kotlin SHA-256 and PBKDF2 on the JVM
Hash text with MessageDigest and derive a salted password hash with PBKDF2.
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
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
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 |
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.