Skip to content

Instantly share code, notes, and snippets.

@soujiro32167
Created November 14, 2025 15:48
Show Gist options
  • Select an option

  • Save soujiro32167/6ad9bd85eea50e9586f03b52283b19b3 to your computer and use it in GitHub Desktop.

Select an option

Save soujiro32167/6ad9bd85eea50e9586f03b52283b19b3 to your computer and use it in GitHub Desktop.
Argon2 password hashing with bouncy castle
//> using scala 3.7
//> using dep "org.bouncycastle:bcprov-jdk18on:1.82"
import java.security.SecureRandom
import org.bouncycastle.crypto.params.Argon2Parameters
import org.bouncycastle.crypto.generators.Argon2BytesGenerator
import java.util.Base64
import scala.util.Try
import BouncyCastleArgon2PasswordHasher.Config
case class StoredArgon2Hash (`type`: Int, version: Int, memory: Int, iterations: Int, parallelism: Int, salt: Array[Byte], hash: Array[Byte]){
def format: String = {
val stringType = `type` match {
case Argon2Parameters.ARGON2_d => "argon2d"
case Argon2Parameters.ARGON2_i => "argon2i"
case Argon2Parameters.ARGON2_id => "argon2id"
case _ => "unknown"
}
val versionString = s"v=$version"
val mtp = s"m=$memory,t=$iterations,p=$parallelism"
val saltBase64 = Base64.getEncoder.encodeToString(salt)
val digestBase64 = Base64.getEncoder.encodeToString(hash)
"$" + List(stringType, versionString, mtp, saltBase64, digestBase64).mkString("$")
}
def toParameters: Argon2Parameters.Builder = {
new Argon2Parameters.Builder(`type`)
.withVersion(version)
.withMemoryAsKB(memory)
.withIterations(iterations)
.withParallelism(parallelism)
.withSalt(salt)
}
}
object StoredArgon2Hash:
def fromParameters(params: Argon2Parameters, hash: Array[Byte]): StoredArgon2Hash =
StoredArgon2Hash(
params.getType,
params.getVersion,
params.getMemory,
params.getIterations,
params.getLanes,
params.getSalt,
hash
)
def parse(stored: String): Either[String, StoredArgon2Hash] = {
stored.split("\\$") match {
case Array(_, stringType, versionPart, mtpPart, saltBase64, hashBase64) =>
for {
intType <- stringType match {
case "argon2d" => Right(Argon2Parameters.ARGON2_d)
case "argon2i" => Right(Argon2Parameters.ARGON2_i)
case "argon2id" => Right(Argon2Parameters.ARGON2_id)
case _ => Left(s"Unknown Argon2 type: $stringType")
}
version <- Try(versionPart.stripPrefix("v=").toInt).toEither.left.map(_ => s"Invalid version value: $versionPart")
(memory, iterations, parallelism) <- mtpPart.split(",") match {
case Array(mPart, tPart, pPart) =>
for {
m <- Try(mPart.stripPrefix("m=").toInt).toEither.left.map(_ => s"Invalid memory value: $mPart")
t <- Try(tPart.stripPrefix("t=").toInt).toEither.left.map(_ => s"Invalid iterations value: $tPart")
p <- Try(pPart.stripPrefix("p=").toInt).toEither.left.map(_ => s"Invalid parallelism value: $pPart")
} yield (m, t, p)
case _ => Left(s"Invalid memory/iterations/parallelism part: $mtpPart")
}
salt = Base64.getDecoder.decode(saltBase64)
hash = Base64.getDecoder.decode(hashBase64)
result = StoredArgon2Hash(
intType,
version,
memory,
iterations,
parallelism,
salt,
hash
)
} yield result
case _ => Left(s"Invalid stored Argon2 hash format: $stored")
}
}
trait Argon2PasswordHasher {
def hashPassword(password: String): StoredArgon2Hash
def verifyPassword(password: String, stored: StoredArgon2Hash): Boolean
}
case class BouncyCastleArgon2PasswordHasher(config: Config) extends Argon2PasswordHasher {
private def generateSalt(length: Int): Array[Byte] = {
val salt = new Array[Byte](length)
val random = SecureRandom.getInstanceStrong()
random.nextBytes(salt)
salt
}
override def hashPassword(password: String): StoredArgon2Hash = {
import config.*
val salt = generateSalt(config.saltLength)
val params = new Argon2Parameters.Builder(`type`)
.withVersion(version)
.withIterations(iterations)
.withMemoryAsKB(memory)
.withParallelism(parallelism)
.withSalt(salt)
.withSecret(pepper)
.build()
// hash a password
val generator = new Argon2BytesGenerator()
generator.init(params)
val hash = new Array[Byte](hashLength)
generator.generateBytes(password.getBytes, hash)
StoredArgon2Hash.fromParameters(params, hash)
}
override def verifyPassword(password: String, stored: StoredArgon2Hash): Boolean = {
val verifier = new Argon2BytesGenerator()
verifier.init(stored.toParameters.withSecret(config.pepper).build())
val testHash = new Array[Byte](config.hashLength)
verifier.generateBytes(password.getBytes, testHash)
stored.hash.sameElements(testHash)
}
}
object BouncyCastleArgon2PasswordHasher {
case class Config(pepperBase64: String, iterations: Int, memory: Int, parallelism: Int, hashLength: Int, `type`: Int, version: Int, saltLength: Int) {
val pepper: Array[Byte] = Base64.getDecoder.decode(pepperBase64)
}
object Config {
// see https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html
def default(pepperBase64: String): Config = Config(
pepperBase64 = pepperBase64,
iterations = 2,
memory = 65536,
parallelism = 1,
hashLength = 32,
`type` = Argon2Parameters.ARGON2_id,
version = Argon2Parameters.ARGON2_VERSION_13,
saltLength = 16
)
}
}
@main def run = {
val password = "some properly L0ng P@ssw0rd"
val pepper = Base64.getEncoder().encodeToString("le poivre".getBytes())
val hasher = BouncyCastleArgon2PasswordHasher(
BouncyCastleArgon2PasswordHasher.Config.default(pepper)
)
val stored = hasher.hashPassword(password)
println(s"Stored hash: ${stored.format}")
val restored = StoredArgon2Hash.parse(stored.format).getOrElse(throw new Exception("Failed to parse stored hash"))
val verified = hasher.verifyPassword(password, restored)
assert(verified, "Password verification failed")
assert(restored.format == stored.format, "Hash was not restored correctly")
println("Password verified successfully")
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment