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.hash;
021
022import org.apache.shiro.crypto.hash.format.Shiro1CryptFormat;
023import org.apache.shiro.lang.util.ByteSource;
024import org.apache.shiro.lang.util.SimpleByteSource;
025
026import java.util.Arrays;
027import java.util.Base64;
028import java.util.NoSuchElementException;
029import java.util.Optional;
030import java.util.Random;
031import java.util.Set;
032
033import static java.util.Collections.unmodifiableSet;
034import static java.util.stream.Collectors.toSet;
035
036/**
037 * Creates a hash provider for salt (+pepper) and Hash-based KDFs, i.e. where the algorithm name
038 * is a SHA algorithm or similar.
039 *
040 * @since 2.0
041 */
042public class SimpleHashProvider implements HashSpi {
043
044    private static final Set<String> IMPLEMENTED_ALGORITHMS = Arrays.stream(new String[] {
045                    Sha256Hash.ALGORITHM_NAME,
046                    Sha384Hash.ALGORITHM_NAME,
047                    Sha512Hash.ALGORITHM_NAME
048            })
049            .collect(toSet());
050
051    @Override
052    public Set<String> getImplementedAlgorithms() {
053        return unmodifiableSet(IMPLEMENTED_ALGORITHMS);
054    }
055
056    @Override
057    public SimpleHash fromString(String format) {
058        Hash hash = new Shiro1CryptFormat().parse(format);
059
060        if (!(hash instanceof SimpleHash)) {
061            throw new IllegalArgumentException("formatted string was not a simple hash: " + format);
062        }
063
064        return (SimpleHash) hash;
065    }
066
067    @Override
068    public HashFactory newHashFactory(Random random) {
069        return new SimpleHashFactory(random);
070    }
071
072    static class SimpleHashFactory implements HashSpi.HashFactory {
073
074        private final Random random;
075
076        SimpleHashFactory(Random random) {
077            this.random = random;
078        }
079
080        @Override
081        public SimpleHash generate(HashRequest hashRequest) {
082            String algorithmName = hashRequest.getAlgorithmName().orElse(Parameters.DEFAULT_ALGORITHM);
083            ByteSource source = hashRequest.getSource();
084            final int iterations = getIterations(hashRequest);
085
086            final ByteSource publicSalt = getPublicSalt(hashRequest);
087            final /*nullable*/ ByteSource secretSalt = getSecretSalt(hashRequest);
088            final ByteSource salt = combine(secretSalt, publicSalt);
089
090            return createSimpleHash(algorithmName, source, iterations, publicSalt, salt);
091        }
092
093        /**
094         * Returns the public salt that should be used to compute a hash based on the specified request.
095         * <p/>
096         * This implementation functions as follows:
097         * <ol>
098         *   <li>If the request salt is not null and non-empty, this will be used, return it.</li>
099         *   <li>If the request salt is null or empty:
100         *     <ol><li>create a new 16-byte salt.</li></ol>
101         *   </li>
102         * </ol>
103         *
104         * @param request request the request to process
105         * @return the public salt that should be used to compute a hash based on the specified request or
106         * {@code null} if no public salt should be used.
107         */
108        protected ByteSource getPublicSalt(HashRequest request) {
109            Optional<ByteSource> publicSalt = request.getSalt();
110
111            if (publicSalt.isPresent() && !publicSalt.orElseThrow(NoSuchElementException::new).isEmpty()) {
112                //a public salt was explicitly requested to be used - go ahead and use it:
113                return publicSalt.orElseThrow(NoSuchElementException::new);
114            }
115
116            // generate salt if absent from the request.
117            @SuppressWarnings("checkstyle:MagicNumber")
118            byte[] ps = new byte[16];
119            random.nextBytes(ps);
120
121            return new SimpleByteSource(ps);
122        }
123
124        private ByteSource getSecretSalt(HashRequest request) {
125            Optional<Object> secretSalt = Optional.ofNullable(request.getParameters().get(Parameters.PARAMETER_SECRET_SALT));
126
127            return secretSalt
128                    .map(salt -> (String) salt)
129                    .map(salt -> Base64.getDecoder().decode(salt))
130                    .map(SimpleByteSource::new)
131                    .orElse(null);
132        }
133
134        private SimpleHash createSimpleHash(String algorithmName, ByteSource source,
135                                            int iterations, ByteSource publicSalt, ByteSource salt) {
136            Hash computed = new SimpleHash(algorithmName, source, salt, iterations);
137
138            SimpleHash result = new SimpleHash(algorithmName);
139            result.setBytes(computed.getBytes());
140            result.setIterations(iterations);
141            //Only expose the public salt - not the real/combined salt that might have been used:
142            result.setSalt(publicSalt);
143
144            return result;
145        }
146
147        protected int getIterations(HashRequest request) {
148            Object parameterIterations = request.getParameters().getOrDefault(Parameters.PARAMETER_ITERATIONS, 0);
149
150            if (!(parameterIterations instanceof Integer)) {
151                return Parameters.DEFAULT_ITERATIONS;
152            }
153
154            final int iterations = Math.max(0, (Integer) parameterIterations);
155
156            if (iterations < 1) {
157                return Parameters.DEFAULT_ITERATIONS;
158            }
159
160            return iterations;
161        }
162
163        /**
164         * Combines the specified 'private' salt bytes with the specified additional extra bytes to use as the
165         * total salt during hash computation.  {@code privateSaltBytes} will be {@code null} }if no private salt has been
166         * configured.
167         *
168         * @param privateSalt the (possibly {@code null}) 'private' salt to combine with the specified extra bytes
169         * @param publicSalt  the extra bytes to use in addition to the given private salt.
170         * @return a combination of the specified private salt bytes and extra bytes that will be used as the total
171         * salt during hash computation.
172         */
173        protected ByteSource combine(ByteSource privateSalt, ByteSource publicSalt) {
174
175            // optional 'pepper'
176            byte[] privateSaltBytes = privateSalt != null ? privateSalt.getBytes() : null;
177            int privateSaltLength = privateSaltBytes != null ? privateSaltBytes.length : 0;
178
179            // salt must always be present.
180            byte[] publicSaltBytes = publicSalt.getBytes();
181            int extraBytesLength = publicSaltBytes.length;
182
183            int length = privateSaltLength + extraBytesLength;
184
185            if (length <= 0) {
186                return SimpleByteSource.empty();
187            }
188
189            byte[] combined = new byte[length];
190
191            int i = 0;
192            for (int j = 0; j < privateSaltLength; j++) {
193                combined[i++] = privateSaltBytes[j];
194            }
195            for (int j = 0; j < extraBytesLength; j++) {
196                combined[i++] = publicSaltBytes[j];
197            }
198
199            return ByteSource.Util.bytes(combined);
200        }
201    }
202
203    static final class Parameters {
204        public static final String PARAMETER_ITERATIONS = "SimpleHash.iterations";
205
206        /**
207         * A secret part added to the salt. Sometimes also referred to as {@literal "Pepper"}.
208         *
209         * <p>For more information, see <a href="https://en.wikipedia.org/wiki/Pepper_(cryptography)">
210         * Pepper (cryptography) on Wikipedia</a>.</p>
211         */
212        public static final String PARAMETER_SECRET_SALT = "SimpleHash.secretSalt";
213
214        public static final String DEFAULT_ALGORITHM = "SHA-512";
215
216        public static final int DEFAULT_ITERATIONS = 50_000;
217
218
219        private Parameters() {
220            // util class
221        }
222    }
223}