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}