io.github.nremond.SecureHash.scala Maven / Gradle / Ivy
/**
* Copyright 2012-2014 Nicolas Rémond (@nremond)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.github.nremond
import java.nio.charset.StandardCharsets.UTF_8
import java.security.SecureRandom
import java.util.Base64
import scala.util.Try
/**
* Implements functionality to create and validate password hashes using [[PBKDF2]]
*/
object SecureHash {
import internals._
/**
* Creates a hashed password using [[PBKDF2]]
*
* this function output a string in the modified MCF format :
*
* p0\$params\$salt\$key
*
* - p0 : version 0 of the format
*
* - params: 8 digit hexadecimal representation of the number of iterations concatenated with the algo name
*
* - salt : Base64 encoded salt
*
* - key : Base64 encoded derived key
*
* Example :
*
* p0\$00004e20HmacSHA256\$mOCtN/Scjry0uIALe4bCCrL9eL8aWEA/\$hDxtqCnBF1MS5qIOxHeDAZ23QEmqdL7796I0pVJ2yvQ
*
* @param password the password to hash
* @param iterations the number of encryption iterations, default to 20000
* @param dkLength derived-key length, default to 32
* @param cryptoAlgo HMAC+SHA512 is the default as HMAC+SHA1 is now considered weak
* @param saltLength length of the salt, default to 24
*/
def createHash(password: String, iterations: Int = 20000,
dkLength: Int = 32, cryptoAlgo: String = "HmacSHA512", saltLength: Int = 24): String = {
val salt = {
val b = new Array[Byte](saltLength)
(new SecureRandom).nextBytes(b)
b
}
val key = PBKDF2(password.getBytes(UTF_8), salt, iterations, dkLength, cryptoAlgo)
encode(salt, key, iterations, cryptoAlgo)
}
/**
* Tests two byte arrays for value equality in constant time.
*
* @note This function leaks information about the length of each byte array as well as
* whether the two byte arrays have the same length.
* @see [[http://codahale.com/a-lesson-in-timing-attacks]]
*/
private[this] def secure_==(a1: Array[Byte], a2: Array[Byte]): Boolean =
a1.length == a2.length && a1.zip(a2).foldLeft(0) { case (r, (x1, x2)) => r | x1 ^ x2 } == 0
/**
* Validate a password against a password hash
*
* @param password the password to validate
* @param hashedPassword the password hash. This should be in the same format as generated by [[SecureHash.createHash]]
* @return true is the password is valid
*/
def validatePassword(password: String, hashedPassword: String): Boolean = decode(hashedPassword) match {
case Some(Decoded(_, iterations, algo, salt, key)) =>
val hash = PBKDF2(password.getBytes(UTF_8), salt, iterations, key.length, algo)
secure_==(key, hash)
case _ => false
}
private[nremond] object internals {
def encode(salt: Array[Byte], key: Array[Byte], iterations: Int, algo: String): String = {
val iters = iterations.toString
// use hash name compatible with PassLib (https://pythonhosted.org/passlib/index.html)
val compAlgo = javaAlgoToPassLibAlgo.getOrElse(algo, algo)
s"$$pbkdf2-$compAlgo$$$iters$$${b64Encoder(salt)}$$${b64Encoder(key)}"
}
case class Decoded(version: String, iterations: Int, algo: String, salt: Array[Byte], key: Array[Byte])
def decode(s: String): Option[Decoded] = Try {
s match {
case rx(a, i, s, h) => Some(Decoded("pbkdf2", i.toInt, passLibAlgoToJava.getOrElse(a, a), b64Decoder(s), b64Decoder(h)))
case _ => None
}
}.toOption.flatten
private[nremond] val javaAlgoToPassLibAlgo = Map("HmacSHA1" -> "sha1", "HmacSHA256" -> "sha256", "HmacSHA512" -> "sha512")
private[nremond] val passLibAlgoToJava = javaAlgoToPassLibAlgo.map(_.swap)
private[this] val rx = "\\$pbkdf2-([^\\$]+)\\$(\\d+)\\$([^\\$]*)\\$([^\\$]*)".r
private[this] def b64Decoder(s: String) =
Base64.getDecoder.decode(s.replace(".", "+"))
private[this] def b64Encoder(ba: Array[Byte]) =
Base64.getEncoder.withoutPadding.encodeToString(ba).replace("+", ".")
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy