Created
June 19, 2017 23:11
-
-
Save pjan/3f5dc83532db3cdd8a11b23db158230d to your computer and use it in GitHub Desktop.
Password implementation following latest NIST recommendations
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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