Line data Source code
1 : // Copyright (C) 2017 The Android Open Source Project 2 : // 3 : // Licensed under the Apache License, Version 2.0 (the "License"); 4 : // you may not use this file except in compliance with the License. 5 : // You may obtain a copy of the License at 6 : // 7 : // http://www.apache.org/licenses/LICENSE-2.0 8 : // 9 : // Unless required by applicable law or agreed to in writing, software 10 : // distributed under the License is distributed on an "AS IS" BASIS, 11 : // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 : // See the License for the specific language governing permissions and 13 : // limitations under the License. 14 : 15 : package com.google.gerrit.server.account; 16 : 17 : import static com.google.common.base.Preconditions.checkState; 18 : 19 : import com.google.common.base.Splitter; 20 : import com.google.common.io.BaseEncoding; 21 : import com.google.common.primitives.Ints; 22 : import java.nio.charset.StandardCharsets; 23 : import java.security.SecureRandom; 24 : import java.util.List; 25 : import org.bouncycastle.crypto.generators.BCrypt; 26 : import org.bouncycastle.util.Arrays; 27 : 28 : /** 29 : * Holds logic for salted, hashed passwords. It uses BCrypt from BouncyCastle, which truncates 30 : * passwords at 72 bytes. 31 : */ 32 : public class HashedPassword { 33 : private static final String ALGORITHM_PREFIX = "bcrypt:"; 34 : private static final String ALGORITHM_PREFIX_0 = "bcrypt0:"; 35 140 : private static final SecureRandom secureRandom = new SecureRandom(); 36 140 : private static final BaseEncoding codec = BaseEncoding.base64(); 37 : 38 : // bcrypt uses 2^cost rounds. Since we use a generated random password, no need 39 : // for a high cost. 40 : private static final int DEFAULT_COST = 4; 41 : 42 : public static class DecoderException extends Exception { 43 : private static final long serialVersionUID = 1L; 44 : 45 : public DecoderException(String message) { 46 2 : super(message); 47 2 : } 48 : } 49 : 50 : /** 51 : * decodes a hashed password encoded with {@link #encode}. 52 : * 53 : * @throws DecoderException if input is malformed. 54 : */ 55 : public static HashedPassword decode(String encoded) throws DecoderException { 56 39 : if (!encoded.startsWith(ALGORITHM_PREFIX) && !encoded.startsWith(ALGORITHM_PREFIX_0)) { 57 2 : throw new DecoderException("unrecognized algorithm"); 58 : } 59 : 60 39 : List<String> fields = Splitter.on(':').splitToList(encoded); 61 39 : if (fields.size() != 4) { 62 0 : throw new DecoderException("want 4 fields"); 63 : } 64 : 65 39 : Integer cost = Ints.tryParse(fields.get(1)); 66 39 : if (cost == null) { 67 0 : throw new DecoderException("cost parse failed"); 68 : } 69 : 70 39 : if (!(cost >= 4 && cost < 32)) { 71 0 : throw new DecoderException("cost should be 4..31 inclusive, got " + cost); 72 : } 73 : 74 39 : byte[] salt = codec.decode(fields.get(2)); 75 39 : if (salt.length != 16) { 76 0 : throw new DecoderException("salt should be 16 bytes, got " + salt.length); 77 : } 78 39 : return new HashedPassword( 79 39 : codec.decode(fields.get(3)), salt, cost, encoded.startsWith(ALGORITHM_PREFIX_0)); 80 : } 81 : 82 : private static byte[] hashPassword( 83 : String password, byte[] salt, int cost, boolean nullTerminate) { 84 140 : byte[] pwBytes = password.getBytes(StandardCharsets.UTF_8); 85 140 : if (nullTerminate && !password.endsWith("\0")) { 86 140 : pwBytes = Arrays.append(pwBytes, (byte) 0); 87 : } 88 140 : return BCrypt.generate(pwBytes, salt, cost); 89 : } 90 : 91 : public static HashedPassword fromPassword(String password) { 92 140 : byte[] salt = newSalt(); 93 : 94 140 : return new HashedPassword( 95 140 : hashPassword(password, salt, DEFAULT_COST, true), salt, DEFAULT_COST, true); 96 : } 97 : 98 : private static byte[] newSalt() { 99 140 : byte[] bytes = new byte[16]; 100 140 : secureRandom.nextBytes(bytes); 101 140 : return bytes; 102 : } 103 : 104 : private byte[] salt; 105 : private byte[] hashed; 106 : private int cost; 107 : // Raw bcrypt repeats the password, so "ABC" works for "ABCABC" too. To prevent this, add 108 : // the terminating null char to the password. 109 : boolean nullTerminate; 110 : 111 140 : private HashedPassword(byte[] hashed, byte[] salt, int cost, boolean nullTerminate) { 112 140 : this.salt = salt; 113 140 : this.hashed = hashed; 114 140 : this.cost = cost; 115 140 : this.nullTerminate = nullTerminate; 116 : 117 140 : checkState(cost >= 4 && cost < 32); 118 : 119 : // salt must be 128 bit. 120 140 : checkState(salt.length == 16); 121 140 : } 122 : 123 : /** 124 : * Serialize the hashed password and its parameters for persistent storage. 125 : * 126 : * @return one-line string encoding the hash and salt. 127 : */ 128 : public String encode() { 129 140 : return (nullTerminate ? ALGORITHM_PREFIX_0 : ALGORITHM_PREFIX) 130 : + cost 131 : + ":" 132 140 : + codec.encode(salt) 133 : + ":" 134 140 : + codec.encode(hashed); 135 : } 136 : 137 : public boolean checkPassword(String password) { 138 : // Constant-time comparison, because we're paranoid. 139 39 : return Arrays.areEqual(hashPassword(password, salt, cost, nullTerminate), hashed); 140 : } 141 : }