All Downloads are FREE. Search and download functionalities are using the official Maven repository.

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