| // 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 static com.google.common.base.Preconditions.checkState; | |
| import com.google.common.base.Splitter; | |
| import com.google.common.io.BaseEncoding; | |
| import com.google.common.primitives.Ints; | |
| import java.nio.charset.StandardCharsets; | |
| import java.security.SecureRandom; | |
| import java.util.List; | |
| 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; | |
| public static class DecoderException extends Exception { | |
| private static final long serialVersionUID = 1L; | |
| public DecoderException(String message) { | |
| super(message); | |
| } | |
| } | |
| /** | |
| * 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"); | |
| } | |
| List<String> fields = Splitter.on(':').splitToList(encoded); | |
| if (fields.size() != 4) { | |
| throw new DecoderException("want 4 fields"); | |
| } | |
| Integer cost = Ints.tryParse(fields.get(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.get(2)); | |
| if (salt.length != 16) { | |
| throw new DecoderException("salt should be 16 bytes, got " + salt.length); | |
| } | |
| return new HashedPassword(codec.decode(fields.get(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; | |
| checkState(cost >= 4 && cost < 32); | |
| // salt must be 128 bit. | |
| 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); | |
| } | |
| } |