001/* 002 * Licensed to the Apache Software Foundation (ASF) under one 003 * or more contributor license agreements. See the NOTICE file 004 * distributed with this work for additional information 005 * regarding copyright ownership. The ASF licenses this file 006 * to you under the Apache License, Version 2.0 (the 007 * "License"); you may not use this file except in compliance 008 * with the License. You may obtain a copy of the License at 009 * 010 * http://www.apache.org/licenses/LICENSE-2.0 011 * 012 * Unless required by applicable law or agreed to in writing, 013 * software distributed under the License is distributed on an 014 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 015 * KIND, either express or implied. See the License for the 016 * specific language governing permissions and limitations 017 * under the License. 018 */ 019 020package org.apache.shiro.crypto.support.hashes.bcrypt; 021 022import org.apache.shiro.crypto.hash.HashRequest; 023import org.apache.shiro.crypto.hash.HashSpi; 024import org.apache.shiro.lang.util.ByteSource; 025import org.apache.shiro.lang.util.SimpleByteSource; 026import org.slf4j.Logger; 027import org.slf4j.LoggerFactory; 028 029import java.security.SecureRandom; 030import java.util.Base64; 031import java.util.Locale; 032import java.util.Map; 033import java.util.NoSuchElementException; 034import java.util.Optional; 035import java.util.Random; 036import java.util.Set; 037 038/** 039 * @since 2.0 040 */ 041public class BCryptProvider implements HashSpi { 042 043 private static final Logger LOG = LoggerFactory.getLogger(BCryptProvider.class); 044 045 @Override 046 public Set<String> getImplementedAlgorithms() { 047 return BCryptHash.getAlgorithmsBcrypt(); 048 } 049 050 @Override 051 public BCryptHash fromString(String format) { 052 return BCryptHash.fromString(format); 053 } 054 055 @Override 056 public HashFactory newHashFactory(Random random) { 057 return new BCryptHashFactory(random); 058 } 059 060 static class BCryptHashFactory implements HashSpi.HashFactory { 061 062 private final SecureRandom random; 063 064 BCryptHashFactory(Random random) { 065 if (!(random instanceof SecureRandom)) { 066 throw new IllegalArgumentException("Only SecureRandom instances are supported at the moment!"); 067 } 068 069 this.random = (SecureRandom) random; 070 } 071 072 @Override 073 public BCryptHash generate(HashRequest hashRequest) { 074 final String algorithmName = hashRequest.getAlgorithmName().orElse(Parameters.DEFAULT_ALGORITHM_NAME); 075 076 final ByteSource salt = getSalt(hashRequest); 077 078 final int cost = getCost(hashRequest); 079 080 return BCryptHash.generate( 081 algorithmName, 082 hashRequest.getSource(), 083 salt, 084 cost 085 ); 086 } 087 088 private int getCost(HashRequest hashRequest) { 089 final Map<String, Object> parameters = hashRequest.getParameters(); 090 final Optional<String> optCostStr = Optional.ofNullable(parameters.get(Parameters.PARAMETER_COST)) 091 .map(obj -> (String) obj); 092 093 if (!optCostStr.isPresent()) { 094 return BCryptHash.DEFAULT_COST; 095 } 096 097 String costStr = optCostStr.orElseThrow(NoSuchElementException::new); 098 try { 099 @SuppressWarnings("checkstyle:MagicNumber") 100 int cost = Integer.parseInt(costStr, 10); 101 BCryptHash.checkValidCost(cost); 102 return cost; 103 } catch (IllegalArgumentException costEx) { 104 String message = String.format( 105 Locale.ENGLISH, 106 "Expected Integer for parameter %s, but %s is not parsable or valid.", 107 Parameters.PARAMETER_COST, costStr 108 ); 109 LOG.warn(message, costEx); 110 111 return BCryptHash.DEFAULT_COST; 112 } 113 } 114 115 private ByteSource getSalt(HashRequest hashRequest) { 116 final Map<String, Object> parameters = hashRequest.getParameters(); 117 final Optional<String> optSaltBase64 = Optional.ofNullable(parameters.get(Parameters.PARAMETER_SALT)) 118 .map(obj -> (String) obj); 119 120 if (!optSaltBase64.isPresent()) { 121 return BCryptHash.createSalt(random); 122 } 123 124 final String saltBase64 = optSaltBase64.orElseThrow(NoSuchElementException::new); 125 final byte[] saltBytes = Base64.getDecoder().decode(saltBase64); 126 127 if (saltBytes.length != BCryptHash.SALT_LENGTH) { 128 return BCryptHash.createSalt(random); 129 } 130 131 return new SimpleByteSource(saltBytes); 132 } 133 } 134 135 public static final class Parameters { 136 137 /** 138 * Set BCryptHash algorithm name to default. 139 */ 140 public static final String DEFAULT_ALGORITHM_NAME = BCryptHash.DEFAULT_ALGORITHM_NAME; 141 142 /** 143 * BCrypt salt param. 144 */ 145 public static final String PARAMETER_SALT = "BCrypt.salt"; 146 147 /** 148 * BCrypt cost param. 149 */ 150 public static final String PARAMETER_COST = "BCrypt.cost"; 151 152 private Parameters() { 153 // utility class 154 } 155 } 156}