/* * JBoss, Home of Professional Open Source. * Copyright 2014 Red Hat, Inc., and individual contributors * as indicated by the @author tags. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.wildfly.security.password.impl; import static org.wildfly.common.math.HashMath.multiHashOrdered; import static org.wildfly.security._private.ElytronMessages.log; import java.io.NotSerializableException; import java.io.ObjectInputStream; import java.nio.charset.StandardCharsets; import java.security.InvalidAlgorithmParameterException; import java.security.InvalidKeyException; import java.security.Key; import java.security.NoSuchAlgorithmException; import java.security.spec.AlgorithmParameterSpec; import java.security.spec.InvalidKeySpecException; import java.security.spec.KeySpec; import java.util.Arrays; import javax.crypto.Mac; import javax.crypto.spec.SecretKeySpec; import org.wildfly.security.password.Password; import org.wildfly.security.password.spec.IteratedPasswordAlgorithmSpec; import org.wildfly.security.password.spec.SaltedPasswordAlgorithmSpec; import org.wildfly.security.password.util.PasswordUtil; import org.wildfly.security.password.interfaces.ScramDigestPassword; import org.wildfly.security.password.spec.ClearPasswordSpec; import org.wildfly.security.password.spec.IteratedSaltedPasswordAlgorithmSpec; import org.wildfly.security.password.spec.IteratedSaltedHashPasswordSpec; import org.wildfly.security.password.spec.SaltedHashPasswordSpec; /** * A {@link org.wildfly.security.password.Password} implementation for {@link org.wildfly.security.password.interfaces.ScramDigestPassword}. * * @author <a href="mailto:sguilhen@redhat.com">Stefan Guilhen</a> */ class ScramDigestPasswordImpl extends AbstractPasswordImpl implements ScramDigestPassword { private static final long serialVersionUID = 5831469808883867480L; private static final String HMAC_SHA1_ALGORITHM = "HmacSHA1"; private static final String HMAC_SHA256_ALGORITHM = "HmacSHA256"; private static final String HMAC_SHA384_ALGORITHM = "HmacSHA384"; private static final String HMAC_SHA512_ALGORITHM = "HmacSHA512"; private final String algorithm; private final byte[] digest; private final byte[] salt; private final int iterationCount; ScramDigestPasswordImpl(final String algorithm, final byte[] digest, final byte[] salt, final int iterationCount) { this.algorithm = algorithm; this.digest = digest; this.salt = salt; this.iterationCount = iterationCount; } ScramDigestPasswordImpl(final ScramDigestPassword password) { this(password.getAlgorithm(), password.getDigest().clone(), password.getSalt().clone(), password.getIterationCount()); } ScramDigestPasswordImpl(final String algorithm, final IteratedSaltedHashPasswordSpec spec) { this(algorithm, spec.getHash().clone(), spec.getSalt().clone(), spec.getIterationCount()); } ScramDigestPasswordImpl(final String algorithm, final SaltedHashPasswordSpec spec) { this(algorithm, spec.getHash().clone(), spec.getSalt().clone(), DEFAULT_ITERATION_COUNT); } ScramDigestPasswordImpl(final String algorithm, final ClearPasswordSpec spec) throws InvalidKeySpecException, NoSuchAlgorithmException, InvalidKeyException { this(algorithm, spec.getEncodedPassword(), PasswordUtil.generateRandomSalt(DEFAULT_SALT_SIZE), DEFAULT_ITERATION_COUNT); } ScramDigestPasswordImpl(final String algorithm, final char[] password) throws InvalidKeySpecException, NoSuchAlgorithmException, InvalidKeyException { this(algorithm, password, PasswordUtil.generateRandomSalt(DEFAULT_SALT_SIZE), DEFAULT_ITERATION_COUNT); } ScramDigestPasswordImpl(final String algorithm, final char[] password, final IteratedSaltedPasswordAlgorithmSpec spec) throws InvalidKeySpecException, NoSuchAlgorithmException, InvalidKeyException { this(algorithm, password, spec.getSalt(), spec.getIterationCount()); } ScramDigestPasswordImpl(final String algorithm, final char[] password, final SaltedPasswordAlgorithmSpec spec) throws InvalidKeySpecException, NoSuchAlgorithmException, InvalidKeyException { this(algorithm, password, spec.getSalt(), DEFAULT_ITERATION_COUNT); } ScramDigestPasswordImpl(final String algorithm, final char[] password, final IteratedPasswordAlgorithmSpec spec) throws InvalidKeySpecException, NoSuchAlgorithmException, InvalidKeyException { this(algorithm, password, PasswordUtil.generateRandomSalt(DEFAULT_SALT_SIZE), spec.getIterationCount()); } ScramDigestPasswordImpl(final String algorithm, final char[] password, final byte[] salt, final int iterationCount) throws InvalidKeyException, NoSuchAlgorithmException { this(algorithm, scramDigest(algorithm, getNormalizedPasswordBytes(password), salt, iterationCount), salt, iterationCount); } @Override public String getAlgorithm() { return this.algorithm; } @Override public byte[] getDigest() { try { return this.digest.clone(); } catch (NullPointerException npe) { throw new IllegalStateException(); } } @Override public byte[] getSalt() { try { return this.salt.clone(); } catch (NullPointerException npe) { throw new IllegalStateException(); } } @Override public int getIterationCount() { return this.iterationCount; } @Override <T extends KeySpec> boolean convertibleTo(Class<T> keySpecType) { return keySpecType.isAssignableFrom(IteratedSaltedHashPasswordSpec.class); } @Override Password translate(final AlgorithmParameterSpec parameterSpec) throws InvalidKeyException, InvalidAlgorithmParameterException { if (parameterSpec instanceof IteratedSaltedPasswordAlgorithmSpec) { IteratedSaltedPasswordAlgorithmSpec updateSpec = (IteratedSaltedPasswordAlgorithmSpec) parameterSpec; byte[] updateSalt = updateSpec.getSalt(); if (updateSalt != null && ! Arrays.equals(updateSalt, salt)) { throw new InvalidAlgorithmParameterException(); } int updateIterationCount = updateSpec.getIterationCount(); if (updateIterationCount < this.iterationCount) { throw new InvalidAlgorithmParameterException(); } if (updateIterationCount == this.iterationCount) { return this; } byte[] digest = this.digest.clone(); try { addIterations(digest, getMacInstance(algorithm, digest), this.iterationCount, updateIterationCount); } catch (NoSuchAlgorithmException | InvalidKeyException e) { throw new InvalidKeyException(e); } return new ScramDigestPasswordImpl(algorithm, digest, updateSalt, updateIterationCount); } else if (parameterSpec instanceof IteratedPasswordAlgorithmSpec) { final IteratedPasswordAlgorithmSpec updateSpec = (IteratedPasswordAlgorithmSpec) parameterSpec; int updateIterationCount = updateSpec.getIterationCount(); if (updateIterationCount < this.iterationCount) { throw new InvalidAlgorithmParameterException(); } if (updateIterationCount == this.iterationCount) { return this; } try { addIterations(digest, getMacInstance(algorithm, digest), this.iterationCount, updateIterationCount); } catch (NoSuchAlgorithmException | InvalidKeyException e) { throw new InvalidKeyException(e); } return new ScramDigestPasswordImpl(algorithm, digest, salt, updateIterationCount); } else if (parameterSpec instanceof SaltedPasswordAlgorithmSpec) { SaltedPasswordAlgorithmSpec updateSpec = (SaltedPasswordAlgorithmSpec) parameterSpec; byte[] updateSalt = updateSpec.getSalt(); if (updateSalt != null && ! Arrays.equals(updateSalt, salt)) { throw new InvalidAlgorithmParameterException(); } return this; } throw new InvalidAlgorithmParameterException(); } @Override boolean verify(char[] guess) throws InvalidKeyException { try { byte[] output = scramDigest(this.getAlgorithm(), getNormalizedPasswordBytes(guess), this.getSalt(), this.getIterationCount()); return Arrays.equals(this.digest, output); } catch (NoSuchAlgorithmException nsae) { throw new InvalidKeyException(nsae); } } @Override <S extends KeySpec> S getKeySpec(Class<S> keySpecType) throws InvalidKeySpecException { if (keySpecType.isAssignableFrom(IteratedSaltedHashPasswordSpec.class)) { return keySpecType.cast(new IteratedSaltedHashPasswordSpec(this.getDigest(), this.getSalt(), this.getIterationCount())); } throw new InvalidKeySpecException(); } /** * <p> * This method implements the SCRAM {@code Hi} function as specified by <a href="http://tools.ietf.org/html/rfc5802"> * RFC 5802</a>. The function is defined as follows: * * <pre> * Hi(str, salt, i) * U1 <- HMAC(str, salt + INT(1)) * U2 <- HMAC(str, U1) * ... * Ui-1 <- HMAC(str, Ui-2) * Ui <- HMAC(str, Ui-1) * Hi <- U1 XOR U2 XOR ... XOR Ui * return Hi * </pre> * * where {@code i} is the iteration count, {@code +} is the string concatenation operator, and {@code INT(g)} is a * 4-octet encoding of the integer {@code g}, most significant octet first. * </p> * * @param algorithm the algorithm that should be used to hash the password. * @param password the password to be hashed. * @param salt the salt used to hash the password. * @param iterationCount the iteration count used to hash the password. * * @return a byte[] containing the hashed password. */ static byte[] scramDigest(final String algorithm, final byte[] password, final byte[] salt, final int iterationCount) throws NoSuchAlgorithmException, InvalidKeyException { Mac hmac = getMacInstance(algorithm, password); // compute U1 (see Hi function description in the javadoc). hmac.update(salt); hmac.update("\00\00\00\01".getBytes(StandardCharsets.UTF_8)); byte[] hi = hmac.doFinal(); addIterations(hi, hmac, 1, iterationCount); return hi; } static void addIterations(final byte[] hi, final Mac hmac, final int currentIterationCount, final int newIterationCount) { // compute U2 ... Ui, performing the xor with the previous result as we iterate. byte[] current = hi; for (int i = currentIterationCount; i < newIterationCount; i++) { hmac.update(current); current = hmac.doFinal(); for (int j = 0; j < hi.length; j++) { hi[j] ^= current[j]; } } } /** * <p> * Builds a {@link Mac} instance using the specified algorithm and password. * </p> * * @param algorithm the algorithm that should be used to hash the password. * @param password the password to be hashed. * @return the constructed {@link Mac} instance. */ private static Mac getMacInstance(final String algorithm, final byte[] password) throws NoSuchAlgorithmException, InvalidKeyException { switch (algorithm) { case ALGORITHM_SCRAM_SHA_1: { Mac hmac = Mac.getInstance(HMAC_SHA1_ALGORITHM); Key key = new SecretKeySpec(password, HMAC_SHA1_ALGORITHM); hmac.init(key); return hmac; } case ALGORITHM_SCRAM_SHA_256: { Mac hmac = Mac.getInstance(HMAC_SHA256_ALGORITHM); Key key = new SecretKeySpec(password, HMAC_SHA256_ALGORITHM); hmac.init(key); return hmac; } case ALGORITHM_SCRAM_SHA_384: { Mac hmac = Mac.getInstance(HMAC_SHA384_ALGORITHM); Key key = new SecretKeySpec(password, HMAC_SHA384_ALGORITHM); hmac.init(key); return hmac; } case ALGORITHM_SCRAM_SHA_512: { Mac hmac = Mac.getInstance(HMAC_SHA512_ALGORITHM); Key key = new SecretKeySpec(password, HMAC_SHA512_ALGORITHM); hmac.init(key); return hmac; } default: throw log.noSuchAlgorithmInvalidAlgorithm(algorithm); } } public int hashCode() { return multiHashOrdered(multiHashOrdered(multiHashOrdered(Arrays.hashCode(digest), Arrays.hashCode(salt)), iterationCount), algorithm.hashCode()); } public boolean equals(final Object obj) { if (! (obj instanceof ScramDigestPasswordImpl)) { return false; } ScramDigestPasswordImpl other = (ScramDigestPasswordImpl) obj; return iterationCount == other.iterationCount && algorithm.equals(other.algorithm) && Arrays.equals(digest, other.digest) && Arrays.equals(salt, other.salt); } private void readObject(ObjectInputStream ignored) throws NotSerializableException { throw new NotSerializableException(); } Object writeReplace() { return ScramDigestPassword.createRaw(algorithm, digest, salt, iterationCount); } public ScramDigestPasswordImpl clone() { return this; } }