/*
* 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 static java.lang.Math.max;
import static java.lang.Math.min;
import java.io.NotSerializableException;
import java.io.ObjectInputStream;
import java.nio.ByteBuffer;
import java.security.InvalidKeyException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.KeySpec;
import java.util.Arrays;
import org.wildfly.common.Assert;
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.UnixSHACryptPassword;
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;
/**
* @author <a href="mailto:juraci.javadoc@kroehling.de">Juraci Paixão Kröhling</a>
* @author <a href="mailto:david.lloyd@redhat.com">David M. Lloyd</a>
*/
final class UnixSHACryptPasswordImpl extends AbstractPasswordImpl implements UnixSHACryptPassword {
private static final long serialVersionUID = 1414406780966627792L;
private final String algorithm;
private final byte[] salt;
private final int iterationCount;
private final byte[] hash;
UnixSHACryptPasswordImpl(UnixSHACryptPassword password) {
this(password.getAlgorithm(), truncatedClone(password.getSalt()), password.getIterationCount(), password.getHash().clone());
}
UnixSHACryptPasswordImpl(String algorithm, byte[] clonedSalt, int iterationCount, byte[] hash) {
Assert.checkNotNullParam("algorithm", algorithm);
if (!ALGORITHM_CRYPT_SHA_256.equals(algorithm) && !ALGORITHM_CRYPT_SHA_512.equals(algorithm)) {
throw log.unrecognizedAlgorithm(algorithm);
}
this.salt = clonedSalt;
this.iterationCount = iterationCount;
this.algorithm = algorithm;
this.hash = hash;
}
UnixSHACryptPasswordImpl(final String algorithm, final char[] passwordChars) throws NoSuchAlgorithmException {
this(algorithm, PasswordUtil.generateRandomSalt(SALT_SIZE), DEFAULT_ITERATION_COUNT, passwordChars);
}
UnixSHACryptPasswordImpl(final String algorithm, final IteratedSaltedHashPasswordSpec spec) {
this(algorithm, truncatedClone(spec.getSalt()), min(999_999_999, max(1_000, spec.getIterationCount())), spec.getHash().clone());
}
UnixSHACryptPasswordImpl(final String algorithm, final SaltedHashPasswordSpec spec) {
this(algorithm, truncatedClone(spec.getSalt()), min(999_999_999, max(1_000, DEFAULT_ITERATION_COUNT)), spec.getHash().clone());
}
UnixSHACryptPasswordImpl(final String algorithm, final ClearPasswordSpec spec) throws NoSuchAlgorithmException {
this(algorithm, spec.getEncodedPassword());
}
UnixSHACryptPasswordImpl(final String algorithm, final IteratedSaltedPasswordAlgorithmSpec parameterSpec, final char[] password) throws NoSuchAlgorithmException {
this(algorithm, truncatedClone(parameterSpec.getSalt()), min(999_999_999, max(1_000, parameterSpec.getIterationCount())), password);
}
UnixSHACryptPasswordImpl(final String algorithm, final SaltedPasswordAlgorithmSpec parameterSpec, final char[] password) throws NoSuchAlgorithmException {
this(algorithm, truncatedClone(parameterSpec.getSalt()), DEFAULT_ITERATION_COUNT, password);
}
UnixSHACryptPasswordImpl(final String algorithm, final IteratedPasswordAlgorithmSpec parameterSpec, final char[] password) throws NoSuchAlgorithmException {
this(algorithm, PasswordUtil.generateRandomSalt(SALT_SIZE), min(999_999_999, max(1_000, parameterSpec.getIterationCount())), password);
}
UnixSHACryptPasswordImpl(final String algorithm, final byte[] clonedSalt, final int adjustedIterationCount, final char[] password) throws NoSuchAlgorithmException {
this(algorithm, clonedSalt, adjustedIterationCount, doEncode(algorithm, getNormalizedPasswordBytes(password), clonedSalt, adjustedIterationCount));
}
private static byte[] truncatedClone(final byte[] salt) {
if (salt.length <= 16) {
return salt.clone();
} else {
return Arrays.copyOf(salt, 16);
}
}
@Override
public byte[] getSalt() {
return salt.clone();
}
@Override
public int getIterationCount() {
return iterationCount;
}
@Override
public byte[] getHash() {
return hash.clone();
}
@Override
public String getAlgorithm() {
return algorithm;
}
@Override
<S extends KeySpec> S getKeySpec(Class<S> keySpecType) throws InvalidKeySpecException {
if (keySpecType.isAssignableFrom(IteratedSaltedHashPasswordSpec.class)) {
return keySpecType.cast(new IteratedSaltedHashPasswordSpec(this.getHash(), this.getSalt(), this.getIterationCount()));
} else {
throw log.invalidKeySpecExpectedSpecGotSpec(IteratedSaltedHashPasswordSpec.class.getName(), keySpecType.getName());
}
}
@Override
boolean verify(final char[] guess) throws InvalidKeyException {
try {
byte[] password = getNormalizedPasswordBytes(guess);
byte[] encodedGuess = doEncode(algorithm, password, salt, iterationCount);
return Arrays.equals(getHash(), encodedGuess);
} catch (NoSuchAlgorithmException e) {
throw log.invalidKeyCannotVerifyPassword(e);
}
}
@Override
<T extends KeySpec> boolean convertibleTo(Class<T> keySpecType) {
return keySpecType.isAssignableFrom(IteratedSaltedHashPasswordSpec.class);
}
static byte[] doEncode(final String algorithm, final byte[] password, final byte[] salt, final int iterationCount) throws NoSuchAlgorithmException {
// see ftp://ftp.arlut.utexas.edu/pub/java_hashes/SHA-crypt.txt
// most of the comments from this point and on are copy/paste from the url above, to make it easier
// to correlate the code with the steps.
// implementation note: we use "digestAC" here, because we don't need to duplicate digestA into digestAC,
// as at the time the "digestAC" is "C", then "A" is not needed anymore.
byte[] digestAC = getDigestA(algorithm, password, salt); // at this point, digestAC is "A"
byte[] sequenceP = getSequenceP(algorithm, password);
byte[] sequenceS = getSequenceS(algorithm, digestAC, salt);
for (int i = 0 ; i < iterationCount; i++) {
// 21. repeat a loop according to the number specified in the rounds=<N>
// specification in the salt (or the default value if none is
// present). Each round is numbered, starting with 0 and up to N-1.
//
// The loop uses a digest as input. In the first round it is the
// digest produced in step 12. In the latter steps it is the digest
// produced in step 21.h. The following text uses the notation
// "digest A/C" to describe this behavior.
digestAC = getDigestC(algorithm, digestAC, sequenceP, sequenceS, i);
// implementation note: at this point, digestAC is "C"
}
return digestAC;
}
/**
* Calculates the "digest A", derived from the password and salt.
* @param password the encoded password bytes
* @return the digest A
* @throws NoSuchAlgorithmException
*/
private static byte[] getDigestA(final String algorithm, final byte[] password, final byte[] salt) throws NoSuchAlgorithmException {
byte[] digestBResult = getDigestB(password, salt, algorithm);
int length = password.length;
// 1. start digest A
MessageDigest digestA = getMessageDigest(algorithm);
// 2. the password string is added to digest A
digestA.update(password, 0, length);
// 3. the salt string is added to digest A.
digestA.update(salt, 0, salt.length);
// 9. For each block of 32 or 64 bytes in the password string, add digest B to digest A
int numberOfBlocksPassword = length / getInputSize(algorithm);
for (int i = 0 ; i < numberOfBlocksPassword ; i++ ) {
digestA.update(digestBResult, 0, getInputSize(algorithm));
}
// 10. For the remaining N bytes of the password string add the first N bytes of digest B to digest A
int remainingBytesSizePassword = length % getInputSize(algorithm);
digestA.update(digestBResult, 0, remainingBytesSizePassword);
// 11. For each bit of the binary representation of the length of the
// password string up to and including the highest 1-digit, starting
// from to lowest bit position (numeric value 1):
//
// a) for a 1-digit add digest B to digest A
//
// b) for a 0-digit add the password string
for (int i = length; i > 0 ; i >>= 1) {
if (i % 2 != 0) {
digestA.update(digestBResult, 0, getInputSize(algorithm));
} else {
digestA.update(password, 0, length);
}
}
// 12. finish digest A
return digestA.digest();
}
/**
* Calculates the "sequence S", based on a given "digest A"
*
* @param digestA the digest A
* @return the sequence S
* @throws NoSuchAlgorithmException
*/
private static byte[] getSequenceS(String algorithm, byte[] digestA, byte[] salt) throws NoSuchAlgorithmException {
// 20. produce byte sequence S of the same length as the salt string where
//
// a) for each block of 32 or 64 bytes of length of the salt string
// the entire digest DS is used
//
// b) for the remaining N (up to 31 or 63) bytes use the first N
// bytes of digest DS
byte[] sequenceS = new byte[salt.length];
byte[] digestDSResult = getDigestDS(algorithm, digestA, salt);
ByteBuffer bufferSequenceS = ByteBuffer.wrap(sequenceS);
int numberOfBlocksSalt = salt.length / getInputSize(algorithm);
int remainingBytesSizeSalt = salt.length % getInputSize(algorithm);
for (int i = 0 ; i < numberOfBlocksSalt ; i++ ) {
bufferSequenceS.put(Arrays.copyOfRange(digestDSResult, 0, getInputSize(algorithm)));
}
bufferSequenceS.put(Arrays.copyOfRange(digestDSResult, 0, remainingBytesSizeSalt));
return sequenceS;
}
/**
* Calculates the "digest DS", derived from the salt and on the "digest A"
*
* @param digestA the digest A
* @return the digest DS
* @throws NoSuchAlgorithmException
*/
private static byte[] getDigestDS(String algorithm, byte[] digestA, byte[] salt) throws NoSuchAlgorithmException {
// 17. start digest DS
MessageDigest digestDS = getMessageDigest(algorithm);
// 18. repeat the following 16+A[0] times, where A[0] represents the first
// byte in digest A interpreted as an 8-bit unsigned value
//
// add the salt to digest DS
int repeatTimes = 16 + (digestA[0] & 0xFF); // this binary-and converts the byte into "8-bit unsigned" value
for (int i = 0 ; i < repeatTimes ; i++) {
digestDS.update(salt, 0, salt.length);
}
// 19. finish digest DS
return digestDS.digest();
}
/**
* Returns the "digest B", derived from the password and salt
*
* @param password the encoded password bytes
* @return the digest B
* @throws NoSuchAlgorithmException
*/
private static byte[] getDigestB(final byte[] password, final byte[] salt, final String algorithm) throws NoSuchAlgorithmException {
// 4. start digest B
MessageDigest digestB = getMessageDigest(algorithm);
// 5. add the password to digest B
digestB.update(password, 0, password.length);
// 6. add the salt string to digest B
digestB.update(salt, 0, salt.length);
// 7. add the password again to digest B
digestB.update(password, 0, password.length);
// 8. finish digest B
return digestB.digest();
}
/**
* Calculates the "digest DP", derived from the password
*
* @param password the encoded password bytes
* @return the digest DP
* @throws NoSuchAlgorithmException
*/
private static byte[] getDigestDP(final String algorithm, final byte[] password) throws NoSuchAlgorithmException {
// 13. start digest DP
MessageDigest digestDP = getMessageDigest(algorithm);
// 14. for every byte in the password add the password to digest DP
for (byte ignored : password) {
digestDP.update(password, 0, password.length);
}
// 15. finish digest DP
return digestDP.digest();
}
/**
* Calculates the "sequence P", derived from the password
*
* @param password the encoded password bytes
* @return the sequence P
* @throws NoSuchAlgorithmException
*/
private static byte[] getSequenceP(final String algorithm, final byte[] password) throws NoSuchAlgorithmException {
// 16. produce byte sequence P of the same length as the password where
//
// a) for each block of 32 or 64 bytes of length of the password string
// the entire digest DP is used
//
// b) for the remaining N (up to 31 or 63) bytes use the first N
// bytes of digest DP
byte[] digestDPResult = getDigestDP(algorithm, password);
byte[] sequenceP = new byte[password.length];
ByteBuffer bufferSequenceP = ByteBuffer.wrap(sequenceP);
int numberOfBlocksPassword = password.length / getInputSize(algorithm);
for (int i = 0 ; i < numberOfBlocksPassword ; i++ ) {
bufferSequenceP.put(Arrays.copyOfRange(digestDPResult, 0, getInputSize(algorithm)));
}
int remainingBytesSizePassword = password.length % getInputSize(algorithm);
bufferSequenceP.put(Arrays.copyOfRange(digestDPResult, 0, remainingBytesSizePassword));
return sequenceP;
}
/**
* Calculates the "digest C", derived from the sequenceP, sequenceS, digestAC and the iteration round
*
* @param digestAC the digest A/C
* @param sequenceP the sequence P
* @param sequenceS the sequence S
* @param round the iteration round
* @return the resulting digest C
* @throws NoSuchAlgorithmException
*/
private static byte[] getDigestC(String algorithm, byte[] digestAC, byte[] sequenceP, byte[] sequenceS, int round) throws NoSuchAlgorithmException {
// a) start digest C
MessageDigest digestC = getMessageDigest(algorithm);
// b) for odd round numbers add the byte sequence P to digest C
// c) for even round numbers add digest A/C
if (round % 2 != 0) {
digestC.update(sequenceP, 0, sequenceP.length);
} else {
digestC.update(digestAC, 0, digestAC.length);
}
// d) for all round numbers not divisible by 3 add the byte sequence S
if (round % 3 != 0) {
digestC.update(sequenceS, 0, sequenceS.length);
}
// e) for all round numbers not divisible by 7 add the byte sequence P
if (round % 7 != 0) {
digestC.update(sequenceP, 0, sequenceP.length);
}
// f) for odd round numbers add digest A/C
// g) for even round numbers add the byte sequence P
if (round % 2 != 0) {
digestC.update(digestAC, 0, digestAC.length);
} else {
digestC.update(sequenceP, 0, sequenceP.length);
}
// h) finish digest C.
// from the javadoc: After digest has been called, the MessageDigest object is reset to its initialized state.
return digestC.digest();
}
private static MessageDigest getMessageDigest(String algorithm) throws NoSuchAlgorithmException {
switch (algorithm) {
case ALGORITHM_CRYPT_SHA_256: return MessageDigest.getInstance("SHA-256");
case ALGORITHM_CRYPT_SHA_512: return MessageDigest.getInstance("SHA-512");
default: throw log.noSuchAlgorithmInvalidAlgorithm(algorithm);
}
}
private static int getInputSize(final String algorithm) throws NoSuchAlgorithmException {
switch (algorithm) {
case ALGORITHM_CRYPT_SHA_256: return 32;
case ALGORITHM_CRYPT_SHA_512: return 64;
default: throw log.noSuchAlgorithmInvalidAlgorithm(algorithm);
}
}
public int hashCode() {
return multiHashOrdered(multiHashOrdered(multiHashOrdered(Arrays.hashCode(hash), Arrays.hashCode(salt)), iterationCount), algorithm.hashCode());
}
public boolean equals(final Object obj) {
if (! (obj instanceof UnixSHACryptPasswordImpl)) {
return false;
}
UnixSHACryptPasswordImpl other = (UnixSHACryptPasswordImpl) obj;
return iterationCount == other.iterationCount && algorithm.equals(other.algorithm) && Arrays.equals(hash, other.hash) && Arrays.equals(salt, other.salt);
}
private void readObject(ObjectInputStream ignored) throws NotSerializableException {
throw new NotSerializableException();
}
Object writeReplace() {
return UnixSHACryptPassword.createRaw(algorithm, salt, hash, iterationCount);
}
public UnixSHACryptPasswordImpl clone() {
return this;
}
}