Handle very long passwords

Bcrypt only uses 72 bytes of input, therefore
hashing input first is safer.
This commit is contained in:
Isaac Levy 2017-09-25 16:42:53 -04:00
parent 8e7d86916b
commit e19ac767b4
5 changed files with 37 additions and 29 deletions

View file

@ -4,6 +4,8 @@ import javax.crypto.Cipher
import javax.crypto.spec.{ IvParameterSpec, SecretKeySpec }
import java.util.Base64
import com.roundeights.hasher.Implicits._
/**
* Encryption for bcrypt hashes.
*
@ -34,12 +36,12 @@ private[user] final class DumbAes(secret: String) {
}
final class PasswordHasher(secret: String, logRounds: Int,
hashTimer: (=> Array[Byte]) => Array[Byte] = x => x) {
hashTimer: (=> Array[Byte]) => Array[Byte] = x => x) {
import org.mindrot.BCrypt
private val aes = new DumbAes(secret)
private def bHash(pass: String, salt: Array[Byte]) =
hashTimer(BCrypt.hashpwRaw(pass, 'a', logRounds, salt))
hashTimer(BCrypt.hashpwRaw(pass.sha512, 'a', logRounds, salt))
def hash(pass: String) = {
val salt = BCrypt.gensaltRaw

View file

@ -262,7 +262,7 @@ final class Authenticator(passHasher: PasswordHasher, onShaLogin: => Unit) {
import com.roundeights.hasher.Implicits._
private def salted(p: String, salt: String) = s"$p{$salt}"
def passEnc(id: String, pass: String) = passHasher.hash(salted(pass, id))
def passEnc(id: String, pass: String) = passHasher.hash(salted(id, pass))
case class AuthData(
_id: String,
@ -283,7 +283,7 @@ final class Authenticator(passHasher: PasswordHasher, onShaLogin: => Unit) {
bpass match {
// Deprecated fallback. Log & fail after DB migration.
case None => password ?? { onShaLogin; _ == newP }
case Some(bHash) => passHasher.check(bHash, salted(newP, _id))
case Some(bHash) => passHasher.check(bHash, salted(_id, newP))
}
}

View file

@ -20,6 +20,7 @@ package org.mindrot;
import java.io.UnsupportedEncodingException;
import java.security.SecureRandom;
import java.util.Arrays;
/**
* BCrypt implements OpenBSD-style Blowfish password hashing using
@ -57,7 +58,7 @@ import java.security.SecureRandom;
* String stronger_salt = BCrypt.gensalt(12)<br />
* </code>
* <p>
* The amount of work increases exponentially (2**log_rounds), so
* The amount of work increases exponentially (2**log_rounds), so
* each increment is twice as much work. The default log_rounds is
* 10, and the valid range is 4 to 30.
*
@ -655,16 +656,9 @@ public final class BCrypt {
return ret;
}
public static byte[] hashpwRaw(String password, char minorV, int rounds, byte[] salt) {
byte[] passwordb;
if (minorV >= 'a') password += '\0';
try {
passwordb = password.getBytes("UTF-8");
} catch (UnsupportedEncodingException uee) {
throw new AssertionError("UTF-8 is not supported");
}
return new BCrypt().crypt_raw(passwordb, salt, rounds);
public static byte[] hashpwRaw(byte[] password, char minorV, int rounds, byte[] salt) {
if (minorV >= 'a') password = Arrays.copyOf(password, password.length + 1);
return new BCrypt().crypt_raw(password, salt, rounds);
}
/**
@ -676,7 +670,7 @@ public final class BCrypt {
*/
public static String hashpw(String password, String salt) {
String real_salt;
byte saltb[], hashed[];
byte passwordb[], saltb[], hashed[];
char minor = (char)0;
int rounds, off = 0;
StringBuilder rs = new StringBuilder();
@ -698,9 +692,15 @@ public final class BCrypt {
rounds = Integer.parseInt(salt.substring(off, off + 2));
real_salt = salt.substring(off + 3, off + 25);
try {
passwordb = password.getBytes("UTF-8");
} catch (UnsupportedEncodingException uee) {
throw new AssertionError("UTF-8 is not supported");
}
saltb = decode_base64(real_salt, BCRYPT_SALT_LEN);
hashed = hashpwRaw(password, minor, rounds, saltb);
hashed = hashpwRaw(passwordb, minor, rounds, saltb);
rs.append("$2");
if (minor >= 'a')

View file

@ -31,7 +31,7 @@ class AuthTest extends Specification {
val bCryptUser = AuthData(
_id = "foo",
bpass = Some(Base64.getDecoder.decode(
"nMY6Pi45178YiOMcWncklizO3Z2enZfiFF5RDBkFZFYEOP7rZ2F0"
"qRDaT9KiCL4WlssyZuqezCb/3E0ddU6WX7bTknnNWBu8uv/yqR+F"
))
)
"correct" >> bCryptUser.compare("password")
@ -48,6 +48,22 @@ class AuthTest extends Specification {
bpass = bCryptUser.bpass
).compare("password")
}
"very long password" in {
val longPass = "a" * 100
val user = AuthData("foo", bpass = Some(passEnc("foo", longPass)))
"correct" >> user.compare(longPass)
"wrong fails" >> !user.compare("a" * 99)
}
"handle crazy passwords" in {
val abcUser = AuthData("foo", bpass = Some(passEnc("foo", "abc")))
"test eq" >> abcUser.compare("abc")
"vs null bytes" >> !abcUser.compare("abc\u0000")
"vs unicode" >> !abcUser.compare("abc\uD83D\uDE01")
"vs empty" >> !abcUser.compare("")
}
}
"migrated user" in {

View file

@ -16,7 +16,7 @@ class BCryptTest extends Specification {
val salt = BCrypt.gensaltRaw
"with raw bytes" in {
val rawHash = BCrypt.hashpwRaw(pass, 'a', 6, salt)
val rawHash = BCrypt.hashpwRaw(pass.getBytes("UTF-8"), 'a', 6, salt)
salt.size must_== 16
rawHash.size must_== 23
@ -27,15 +27,5 @@ class BCryptTest extends Specification {
"reject bad" >> !BCrypt.checkpw("", bString)
"uniq salts" >> { salt !== BCrypt.gensaltRaw }
}
"handle crazy passwords" in {
val hashIt = (p: String) => BCrypt.hashpwRaw(p, 'a', 2, salt)
val abcHash = hashIt("abc")
"test eq" >> bcryptEq(abcHash, hashIt("abc"))
"vs null bytes" >> !bcryptEq(abcHash, hashIt("abc\u0000"))
"vs unicode" >> !bcryptEq(abcHash, hashIt("abc\uD83D\uDE01"))
"vs empty" >> !bcryptEq(abcHash, hashIt(""))
}
}
}