|  | // 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); | 
|  | } | 
|  | } |