LCOV - code coverage report
Current view: top level - server/account - HashedPassword.java (source / functions) Hit Total Coverage
Test: _coverage_report.dat Lines: 37 41 90.2 %
Date: 2022-11-19 15:00:39 Functions: 9 9 100.0 %

          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             : }

Generated by: LCOV version 1.16+git.20220603.dfeb750