// Copyright (C) 2017 The Android Open Source Project // // 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 com.google.gerrit.server.account; import com.google.common.base.Preconditions; import com.google.common.io.BaseEncoding; import com.google.common.primitives.Ints; import java.nio.charset.StandardCharsets; import java.security.SecureRandom; import org.apache.commons.codec.DecoderException; import org.bouncycastle.crypto.generators.BCrypt; import org.bouncycastle.util.Arrays; /** * Holds logic for salted, hashed passwords. It uses BCrypt from BouncyCastle, which truncates * passwords at 72 bytes. */ public class HashedPassword { private static final String ALGORITHM_PREFIX = "bcrypt:"; private static final SecureRandom secureRandom = new SecureRandom(); private static final BaseEncoding codec = BaseEncoding.base64(); // bcrypt uses 2^cost rounds. Since we use a generated random password, no need // for a high cost. private static final int DEFAULT_COST = 4; /** * decodes a hashed password encoded with {@link #encode}. * * @throws DecoderException if input is malformed. */ public static HashedPassword decode(String encoded) throws DecoderException { if (!encoded.startsWith(ALGORITHM_PREFIX)) { throw new DecoderException("unrecognized algorithm"); } String[] fields = encoded.split(":"); if (fields.length != 4) { throw new DecoderException("want 4 fields"); } Integer cost = Ints.tryParse(fields[1]); if (cost == null) { throw new DecoderException("cost parse failed"); } if (!(cost >= 4 && cost < 32)) { throw new DecoderException("cost should be 4..31 inclusive, got " + cost); } byte[] salt = codec.decode(fields[2]); if (salt.length != 16) { throw new DecoderException("salt should be 16 bytes, got " + salt.length); } return new HashedPassword(codec.decode(fields[3]), salt, cost); } private static byte[] hashPassword(String password, byte[] salt, int cost) { byte[] pwBytes = password.getBytes(StandardCharsets.UTF_8); return BCrypt.generate(pwBytes, salt, cost); } public static HashedPassword fromPassword(String password) { byte[] salt = newSalt(); return new HashedPassword(hashPassword(password, salt, DEFAULT_COST), salt, DEFAULT_COST); } private static byte[] newSalt() { byte[] bytes = new byte[16]; secureRandom.nextBytes(bytes); return bytes; } private byte[] salt; private byte[] hashed; private int cost; private HashedPassword(byte[] hashed, byte[] salt, int cost) { this.salt = salt; this.hashed = hashed; this.cost = cost; Preconditions.checkState(cost >= 4 && cost < 32); // salt must be 128 bit. Preconditions.checkState(salt.length == 16); } /** * Serialize the hashed password and its parameters for persistent storage. * * @return one-line string encoding the hash and salt. */ public String encode() { return ALGORITHM_PREFIX + cost + ":" + codec.encode(salt) + ":" + codec.encode(hashed); } public boolean checkPassword(String password) { // Constant-time comparison, because we're paranoid. return Arrays.areEqual(hashPassword(password, salt, cost), hashed); } }