Add some types to password hasher

And fix tests
This commit is contained in:
Isaac Levy 2017-09-27 22:41:20 -04:00
parent a545324172
commit d15881f799
4 changed files with 62 additions and 46 deletions

View file

@ -15,14 +15,14 @@ final class Authenticator(
) {
import Authenticator._
def passEnc(pass: String): Array[Byte] = passHasher.hash(pass)
def passEnc(pass: String): HashedPass = passHasher.hash(pass)
def compare(auth: AuthData, p: String): Boolean = {
val newP = auth.salt.fold(p) { s =>
val salted = s"$p{$s}" // BC
(~auth.sha512).fold(salted.sha512, salted.sha1).hex
}
auth.bpass match {
auth.bcryptHash match {
// Deprecated fallback. Log & fail after DB migration.
case None => auth.password ?? { p => onShaLogin(); p == newP }
case Some(bHash) => passHasher.check(bHash, newP)
@ -38,9 +38,9 @@ final class Authenticator(
// This creates a bcrypt hash using the existing sha as input,
// allowing us to migrate all users in bulk.
def upgradePassword(a: AuthData) = (a.bpass, a.password) match {
case (None, Some(pass)) => Some(userRepo.coll.update(
case (None, Some(shaHash)) => Some(userRepo.coll.update(
$id(a._id),
$set(F.bpass -> passEnc(pass)) ++ $unset(F.password)
$set(F.bpass -> passEnc(shaHash).bytes) ++ $unset(F.password)
).void >>- lila.mon.user.auth.shaBcUpgrade())
case _ => None
@ -58,7 +58,7 @@ final class Authenticator(
def setPassword(id: User.ID, pass: String): Funit =
userRepo.coll.update(
$id(id),
$set(F.bpass -> passEnc(pass)) ++ $unset(F.salt, F.password, F.sha512)
$set(F.bpass -> passEnc(pass).bytes) ++ $unset(F.salt, F.password, F.sha512)
).void
private def authWithBenefits(auth: AuthData)(p: String): Boolean = {
@ -86,6 +86,8 @@ object Authenticator {
sha512: Option[Boolean] = None
) {
def bcryptHash = bpass map HashedPass
def hashToken: String = bpass.fold(~password) { _.sha512.hex }
}
implicit val AuthDataBSONHandler = Macros.handler[AuthData]

View file

@ -14,7 +14,6 @@ import com.roundeights.hasher.Implicits._
* this application.
*/
private[user] final class Aes(secret: String) {
private val sKey = {
val sk = Base64.getDecoder.decode(secret)
val kBits = sk.length * 8
@ -26,15 +25,25 @@ private[user] final class Aes(secret: String) {
new SecretKeySpec(sk, "AES")
}
@inline private def run(mode: Int, iv: Array[Byte], b: Array[Byte]) = {
@inline private def run(mode: Int, iv: Aes.InitVector, b: Array[Byte]) = {
val c = Cipher.getInstance("AES/CTS/NoPadding")
c.init(mode, sKey, new IvParameterSpec(iv))
c.init(mode, sKey, iv)
c.doFinal(b)
}
import Cipher.{ ENCRYPT_MODE, DECRYPT_MODE }
def encrypt(iv: Array[Byte], b: Array[Byte]) = run(ENCRYPT_MODE, iv, b)
def decrypt(iv: Array[Byte], b: Array[Byte]) = run(DECRYPT_MODE, iv, b)
def encrypt(iv: Aes.InitVector, b: Array[Byte]) = run(ENCRYPT_MODE, iv, b)
def decrypt(iv: Aes.InitVector, b: Array[Byte]) = run(DECRYPT_MODE, iv, b)
}
private[user] object Aes {
type InitVector = IvParameterSpec
def iv(bytes: Array[Byte]): InitVector = new IvParameterSpec(bytes)
}
case class HashedPass(bytes: Array[Byte]) extends AnyVal {
def parse = bytes.size == 39 option bytes.splitAt(16)
}
final class PasswordHasher(
@ -45,19 +54,19 @@ final class PasswordHasher(
import org.mindrot.BCrypt
private val aes = new Aes(secret)
private def bHash(salt: Array[Byte], pass: String) =
hashTimer(BCrypt.hashpwRaw(pass.sha512, 'a', logRounds, salt))
private def bHash(salt: Array[Byte], p: String) =
hashTimer(BCrypt.hashpwRaw(p.sha512, 'a', logRounds, salt))
def hash(pass: String): Array[Byte] = {
def hash(p: String): HashedPass = {
val salt = new Array[Byte](16)
new SecureRandom().nextBytes(salt)
salt ++ aes.encrypt(salt, bHash(salt, pass))
HashedPass(salt ++ aes.encrypt(Aes.iv(salt), bHash(salt, p)))
}
def check(bytes: Array[Byte], pass: String): Boolean = bytes.size == 39 && {
val (salt, encHash) = bytes.splitAt(16)
val hash = aes.decrypt(salt, encHash)
BCrypt.bytesEqualSecure(hash, bHash(salt, pass))
def check(bytes: HashedPass, p: String): Boolean = bytes.parse ?? {
case (salt, encHash) =>
val hash = aes.decrypt(Aes.iv(salt), encHash)
BCrypt.bytesEqualSecure(hash, bHash(salt, p))
}
}

View file

@ -2,12 +2,19 @@ package lila.user
import org.specs2.mutable.Specification
import java.util.Base64
import Authenticator.AuthData
class AuthTest extends Specification {
val secret = Array.fill(32)(1.toByte).toBase64
val authWrapper = new Authenticator(new PasswordHasher(secret, 2), ())
import authWrapper.{ passEnc, AuthData }
def getAuth(passHasher: PasswordHasher) = new Authenticator(
passHasher = passHasher,
userRepo = null,
upgradeShaPasswords = false,
onShaLogin = () => ()
)
val auth = getAuth(new PasswordHasher(secret, 2))
// Extracted from mongo
val shaUser = AuthData(
@ -20,11 +27,11 @@ class AuthTest extends Specification {
// Mongo after password change
val shaUserWithKey = shaUser.copy(sha512 = Some(false))
"correct1" >> shaUser.compare("password")
"correct2" >> shaUserWithKey.compare("password")
"wrong1" >> !shaUser.compare("")
"wrong2" >> !shaUser.compare("")
"wrong sha" >> !shaUser.copy(sha512 = Some(true)).compare("password")
"correct1" >> auth.compare(shaUser, "password")
"correct2" >> auth.compare(shaUserWithKey, "password")
"wrong1" >> !auth.compare(shaUser, "")
"wrong2" >> !auth.compare(shaUser, "")
"wrong sha" >> !auth.compare(shaUser.copy(sha512 = Some(true)), "password")
}
"bcrypt checks" in {
@ -34,46 +41,44 @@ class AuthTest extends Specification {
"+p7ysDb8OU9yMQ/LuFxFNgJ0HBKH7iJy8tkowG65NWjPC3Y6CzYV"
))
)
"correct" >> bCryptUser.compare("password")
"wrong pass" >> !bCryptUser.compare("")
"correct" >> auth.compare(bCryptUser, "password")
"wrong pass" >> !auth.compare(bCryptUser, "")
// sanity check of aes encryption
"wrong secret" >> !{
val badHasher = new PasswordHasher((new Array[Byte](32)).toBase64, 2)
new Authenticator(badHasher, ()).AuthData(
"",
bpass = bCryptUser.bpass
).compare("password")
getAuth(new PasswordHasher((new Array[Byte](32)).toBase64, 2)).compare(
bCryptUser, "password"
)
}
"very long password" in {
val longPass = "a" * 100
val user = AuthData("", bpass = Some(passEnc(longPass)))
"correct" >> user.compare(longPass)
"wrong fails" >> !user.compare("a" * 99)
val user = AuthData("", bpass = Some(auth.passEnc(longPass).bytes))
"correct" >> auth.compare(user, longPass)
"wrong fails" >> !auth.compare(user, "a" * 99)
}
"handle crazy passwords" in {
val abcUser = AuthData("", bpass = Some(passEnc("abc")))
val abcUser = AuthData("", bpass = Some(auth.passEnc("abc").bytes))
"test eq" >> abcUser.compare("abc")
"vs null bytes" >> !abcUser.compare("abc\u0000")
"vs unicode" >> !abcUser.compare("abc\uD83D\uDE01")
"vs empty" >> !abcUser.compare("")
"test eq" >> auth.compare(abcUser, "abc")
"vs null bytes" >> !auth.compare(abcUser, "abc\u0000")
"vs unicode" >> !auth.compare(abcUser, "abc\uD83D\uDE01")
"vs empty" >> !auth.compare(abcUser, "")
}
}
"migrated user" in {
val shaToBcrypt = shaUser.copy(
// generated purely from stored data
bpass = shaUser.password map { passEnc(_) }
bpass = shaUser.password map { auth.passEnc(_).bytes }
)
val shaToBcryptNoPass = shaToBcrypt.copy(password = None)
"correct" >> shaToBcrypt.compare("password")
"wrong pass" >> !shaToBcrypt.compare("")
"no pass" >> shaToBcryptNoPass.compare("password")
"wrong sha" >> !shaToBcryptNoPass.copy(sha512 = Some(true)).compare("password")
"correct" >> auth.compare(shaToBcrypt, "password")
"wrong pass" >> !auth.compare(shaToBcrypt, "")
"no pass" >> auth.compare(shaToBcryptNoPass, "password")
"wrong sha" >> !auth.compare(shaToBcryptNoPass.copy(sha512 = Some(true)), "password")
}
}

View file

@ -17,7 +17,7 @@ class PasswordHasherTest extends Specification {
def emptyArr(i: Int) = new Array[Byte](i)
val aes = new Aes(secret)
val iv = emptyArr(16)
val iv = Aes.iv(emptyArr(16))
"preserve size" in {
aes.encrypt(iv, emptyArr(20)).size must_== 20