Add ClearPassword wrapper

This commit is contained in:
Isaac Levy 2017-09-28 16:11:59 -04:00
parent 14f5a341a3
commit 81754ca72b
6 changed files with 31 additions and 23 deletions

View file

@ -6,6 +6,7 @@ import play.api.data.validation.Constraints
import lila.common.{ LameName, EmailAddress }
import lila.user.{ User, UserRepo }
import User.ClearPassword
final class DataForm(
val captcher: akka.actor.ActorSelection,
@ -84,7 +85,7 @@ final class DataForm(
def changeEmail(u: User, old: Option[EmailAddress]) = authenticator loginCandidate u map { candidate =>
Form(mapping(
"passwd" -> nonEmptyText.verifying("incorrectPassword", candidate.check),
"passwd" -> nonEmptyText.verifying("incorrectPassword", p => candidate.check(ClearPassword(p))),
"email" -> acceptableUniqueEmail(candidate.user.some).verifying(emailValidator differentConstraint old)
)(ChangeEmail.apply)(ChangeEmail.unapply)).fill(ChangeEmail(
passwd = "",

View file

@ -47,7 +47,7 @@ final class SecurityApi(
} map loadedLoginForm _
private def authenticateCandidate(candidate: Option[User.LoginCandidate])(username: String, password: String): Option[User] =
candidate ?? { _(password) }
candidate ?? { _(User.ClearPassword(password)) }
def saveAuthentication(userId: User.ID, apiVersion: Option[ApiVersion])(implicit req: RequestHeader): Fu[String] =
UserRepo mustConfirmEmail userId flatMap {

View file

@ -5,7 +5,7 @@ import reactivemongo.bson._
import lila.common.EmailAddress
import lila.db.dsl._
import lila.user.User.{ BSONFields => F }
import lila.user.User.{ ClearPassword, BSONFields => F }
final class Authenticator(
passHasher: PasswordHasher,
@ -15,24 +15,24 @@ final class Authenticator(
) {
import Authenticator._
def passEnc(pass: String): HashedPassword = passHasher.hash(pass)
def passEnc(p: ClearPassword): HashedPassword = passHasher.hash(p)
def compare(auth: AuthData, p: String): Boolean = {
def compare(auth: AuthData, p: ClearPassword): Boolean = {
val newP = auth.salt.fold(p) { s =>
val salted = s"$p{$s}" // BC
(~auth.sha512).fold(salted.sha512, salted.sha1).hex
val salted = s"${p.value}{$s}" // BC
ClearPassword((~auth.sha512).fold(salted.sha512, salted.sha1))
}
auth.bpass match {
// Deprecated fallback. Log & fail after DB migration.
case None => auth.password ?? { p => onShaLogin(); p == newP }
case None => auth.password ?? { sHash => onShaLogin(); sHash == newP.value }
case Some(bpass) => passHasher.check(bpass, newP)
}
}
def authenticateById(id: User.ID, password: String): Fu[Option[User]] =
def authenticateById(id: User.ID, password: ClearPassword): Fu[Option[User]] =
loginCandidateById(id) map { _ flatMap { _(password) } }
def authenticateByEmail(email: EmailAddress, password: String): Fu[Option[User]] =
def authenticateByEmail(email: EmailAddress, password: ClearPassword): Fu[Option[User]] =
loginCandidateByEmail(email) map { _ flatMap { _(password) } }
// This creates a bcrypt hash using the existing sha as input,
@ -40,7 +40,7 @@ final class Authenticator(
def upgradePassword(a: AuthData) = (a.bpass, a.password) match {
case (None, Some(shaHash)) => Some(userRepo.coll.update(
$id(a._id),
$set(F.bpass -> passEnc(shaHash).bytes) ++ $unset(F.password)
$set(F.bpass -> passEnc(ClearPassword(shaHash)).bytes) ++ $unset(F.password)
).void >>- lila.mon.user.auth.shaBcUpgrade())
case _ => None
@ -55,16 +55,16 @@ final class Authenticator(
def loginCandidateByEmail(email: EmailAddress): Fu[Option[User.LoginCandidate]] =
loginCandidate($doc(F.email -> email))
def setPassword(id: User.ID, pass: String): Funit =
def setPassword(id: User.ID, p: ClearPassword): Funit =
userRepo.coll.update(
$id(id),
$set(F.bpass -> passEnc(pass).bytes) ++ $unset(F.salt, F.password, F.sha512)
$set(F.bpass -> passEnc(p).bytes) ++ $unset(F.salt, F.password, F.sha512)
).void
private def authWithBenefits(auth: AuthData)(p: String): Boolean = {
private def authWithBenefits(auth: AuthData)(p: ClearPassword): Boolean = {
val res = compare(auth, p)
if (res && auth.salt.isDefined && upgradeShaPasswords)
setPassword(id = auth._id, pass = p) >>- lila.mon.user.auth.bcFullMigrate()
setPassword(id = auth._id, p) >>- lila.mon.user.auth.bcFullMigrate()
res
}

View file

@ -3,6 +3,8 @@ package lila.user
import play.api.data._
import play.api.data.Forms._
import User.ClearPassword
final class DataForm(authenticator: Authenticator) {
val note = Form(mapping(
@ -38,7 +40,7 @@ final class DataForm(authenticator: Authenticator) {
def passwd(u: User) = authenticator loginCandidate u map { candidate =>
Form(mapping(
"oldPasswd" -> nonEmptyText.verifying("incorrectPassword", candidate.check),
"oldPasswd" -> nonEmptyText.verifying("incorrectPassword", p => candidate.check(ClearPassword(p))),
"newPasswd1" -> nonEmptyText(minLength = 2),
"newPasswd2" -> nonEmptyText(minLength = 2)
)(Passwd.apply)(Passwd.unapply).verifying("the new passwords don't match", _.samePasswords))

View file

@ -52,19 +52,20 @@ final class PasswordHasher(
hashTimer: (=> Array[Byte]) => Array[Byte] = x => x
) {
import org.mindrot.BCrypt
import User.ClearPassword
private val aes = new Aes(secret)
private def bHash(salt: Array[Byte], p: String) =
hashTimer(BCrypt.hashpwRaw(p.sha512, 'a', logRounds, salt))
private def bHash(salt: Array[Byte], p: ClearPassword) =
hashTimer(BCrypt.hashpwRaw(p.value.sha512, 'a', logRounds, salt))
def hash(p: String): HashedPassword = {
def hash(p: ClearPassword): HashedPassword = {
val salt = new Array[Byte](16)
new SecureRandom().nextBytes(salt)
HashedPassword(salt ++ aes.encrypt(Aes.iv(salt), bHash(salt, p)))
}
def check(bytes: HashedPassword, p: String): Boolean = bytes.parse ?? {
def check(bytes: HashedPassword, p: ClearPassword): Boolean = bytes.parse ?? {
case (salt, encHash) =>
val hash = aes.decrypt(Aes.iv(salt), encHash)
BCrypt.bytesEqualSecure(hash, bHash(salt, p))

View file

@ -111,10 +111,10 @@ object User {
type ID = String
type CredentialCheck = String => Boolean
type CredentialCheck = ClearPassword => Boolean
case class LoginCandidate(user: User, check: CredentialCheck) {
def apply(password: String): Option[User] = {
val res = check(password)
def apply(p: ClearPassword): Option[User] = {
val res = check(p)
lila.mon.user.auth.result(res)()
res option user
}
@ -131,6 +131,10 @@ object User {
case class Emails(current: Option[EmailAddress], previous: Option[EmailAddress])
case class ClearPassword(value: String) extends AnyVal {
override def toString = "ClearPassword(****)"
}
case class PlayTime(total: Int, tv: Int) {
import org.joda.time.Period
def totalPeriod = new Period(total * 1000l)