Add some types to password hasher
And fix tests
This commit is contained in:
parent
a545324172
commit
d15881f799
|
@ -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]
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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")
|
||||
}
|
||||
}
|
|
@ -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
|
||||
|
|
Loading…
Reference in a new issue