Skip to content

Instantly share code, notes, and snippets.

@pjan
Created June 19, 2017 23:11
Show Gist options
  • Save pjan/3f5dc83532db3cdd8a11b23db158230d to your computer and use it in GitHub Desktop.
Save pjan/3f5dc83532db3cdd8a11b23db158230d to your computer and use it in GitHub Desktop.
Password implementation following latest NIST recommendations
import java.security._
import java.util.Base64
import javax.crypto._
import javax.crypto.spec._
sealed trait Password
object Password {
private val Random = new SecureRandom()
private val Base64Encoder = Base64.getUrlEncoder
private val Base64Decoder = Base64.getUrlDecoder
private val DefaultNrOfIterations = 40000
private val SizeOfPasswordSaltInBytes = 16
private val SizeOfPasswordHashInBytes = 32
case class Clear(value: String) extends AnyVal {
def hashed(nrOfIterations: Int = DefaultNrOfIterations): Hash = {
val salt = randomBytes(SizeOfPasswordSaltInBytes)
val hash = pbkdf2(value, salt, nrOfIterations)
Hash(nrOfIterations, salt, hash)
}
def validate(passwordHash: Hash): Boolean = {
/** Compares two byte arrays in length-constant time to prevent timing attacks. */
def slowEquals(a: Array[Byte], b: Array[Byte]): Boolean = {
var diff = a.length ^ b.length
for { i 0 until math.min(a.length, b.length) } {
diff |= a(i) ^ b(i)
}
diff == 0
}
val calculatedHash = pbkdf2(this.value, passwordHash.salt, passwordHash.nrOfIterations)
slowEquals(calculatedHash, passwordHash.hash)
}
}
case class Hash(private[Password] val nrOfIterations: Int, private[Password] val salt: Array[Byte], private[Password] val hash: Array[Byte]) {
def hashString: String = {
val salt64 = new String(Base64Encoder.encode(salt))
val hash64 = new String(Base64Encoder.encode(hash))
s"$nrOfIterations:$salt64:$hash64"
}
override def toString: String =
s"Hash($hashString)"
}
object Hash {
def parse(hashString: String): Either[IllegalArgumentException, Password.Hash] = {
val hashParts = hashString.split(":")
if (hashParts.length != 3) {
Left(new IllegalArgumentException("Incorrect number of parts in hash string"))
} else if (!hashParts(0).forall(_.isDigit)) {
Left(new IllegalArgumentException("First part of hash string is not a number"))
} else {
val nrOfIterations = hashParts(0).toInt // this will throw a NumberFormatException for non-Int numbers...
val salt = Base64Decoder.decode(hashParts(1))
val hash = Base64Decoder.decode(hashParts(2))
Right(Hash(nrOfIterations, salt, hash))
}
}
def unapply(s: String): Option[Hash] = parse(s).toOption
}
private def pbkdf2(password: String, salt: Array[Byte], nrOfIterations: Int): Array[Byte] = {
val keySpec = new PBEKeySpec(password.toCharArray, salt, nrOfIterations, SizeOfPasswordHashInBytes * 8)
val keyFactory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256")
keyFactory.generateSecret(keySpec).getEncoded
}
private def randomBytes(length: Int): Array[Byte] = {
val keyData = new Array[Byte](length)
Random.nextBytes(keyData)
keyData
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment