/*
* 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.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.SunUnixMD5CryptPassword;
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;
/**
* Implementation of the Sun variant of the Unix MD5 Crypt password.
*
* @author <a href="mailto:fjuma@redhat.com">Farah Juma</a>
*/
final class SunUnixMD5CryptPasswordImpl extends AbstractPasswordImpl implements SunUnixMD5CryptPassword {
private static final long serialVersionUID = 2894797156094167807L;
static final String MD5 = "MD5";
static final byte[] MAGIC_BYTES = "$md5$".getBytes(StandardCharsets.UTF_8);
static final byte[] MAGIC_BYTES_WITH_ROUNDS = "$md5,rounds=".getBytes(StandardCharsets.UTF_8);
static final byte[] SEPARATOR_BYTES = "$".getBytes(StandardCharsets.UTF_8);
static final int BASIC_ROUND_COUNT = 4096;
private final String algorithm;
private final byte[] hash;
private final byte[] salt;
private final int iterationCount;
// Excerpt from Hamlet III.ii that's used by the Muffet Coin Toss algorithm
// (the excerpt was taken from Project Gutenberg: ftp://metalab.unc.edu/pub/docs/books/gutenberg/etext98/2ws2610.txt)
private static final String HAMLET_EXCERPT = "To be, or not to be,--that is the question:--\n"
+ "Whether 'tis nobler in the mind to suffer\n"
+ "The slings and arrows of outrageous fortune\n"
+ "Or to take arms against a sea of troubles,\n"
+ "And by opposing end them?--To die,--to sleep,--\n"
+ "No more; and by a sleep to say we end\n"
+ "The heartache, and the thousand natural shocks\n"
+ "That flesh is heir to,--'tis a consummation\n"
+ "Devoutly to be wish'd. To die,--to sleep;--\n"
+ "To sleep! perchance to dream:--ay, there's the rub;\n"
+ "For in that sleep of death what dreams may come,\n"
+ "When we have shuffled off this mortal coil,\n"
+ "Must give us pause: there's the respect\n"
+ "That makes calamity of so long life;\n"
+ "For who would bear the whips and scorns of time,\n"
+ "The oppressor's wrong, the proud man's contumely,\n"
+ "The pangs of despis'd love, the law's delay,\n"
+ "The insolence of office, and the spurns\n"
+ "That patient merit of the unworthy takes,\n"
+ "When he himself might his quietus make\n"
+ "With a bare bodkin? who would these fardels bear,\n"
+ "To grunt and sweat under a weary life,\n"
+ "But that the dread of something after death,--\n"
+ "The undiscover'd country, from whose bourn\n"
+ "No traveller returns,--puzzles the will,\n"
+ "And makes us rather bear those ills we have\n"
+ "Than fly to others that we know not of?\n"
+ "Thus conscience does make cowards of us all;\n"
+ "And thus the native hue of resolution\n"
+ "Is sicklied o'er with the pale cast of thought;\n"
+ "And enterprises of great pith and moment,\n"
+ "With this regard, their currents turn awry,\n"
+ "And lose the name of action.--Soft you now!\n"
+ "The fair Ophelia!--Nymph, in thy orisons\n"
+ "Be all my sins remember'd.\n\0"; // trailing null character is needed
SunUnixMD5CryptPasswordImpl(final String algorithm, final byte[] clonedHash, final byte[] clonedSalt, final int iterationCount) {
Assert.checkNotNullParam("algorithm", algorithm);
if (!algorithm.equals(ALGORITHM_SUN_CRYPT_MD5) && !algorithm.equals(ALGORITHM_SUN_CRYPT_MD5_BARE_SALT)) {
throw log.unrecognizedAlgorithm(algorithm);
}
this.algorithm = algorithm;
this.hash = clonedHash;
this.salt = clonedSalt;
this.iterationCount = iterationCount;
}
SunUnixMD5CryptPasswordImpl(SunUnixMD5CryptPassword password) {
this(password.getAlgorithm(), password.getHash().clone(), password.getSalt().clone(), password.getIterationCount());
}
SunUnixMD5CryptPasswordImpl(final String algorithm, final IteratedSaltedHashPasswordSpec spec) {
this(algorithm, spec.getHash().clone(), spec.getSalt().clone(), spec.getIterationCount());
}
SunUnixMD5CryptPasswordImpl(final String algorithm, final SaltedHashPasswordSpec spec) {
this(algorithm, spec.getHash().clone(), spec.getSalt().clone(), DEFAULT_ITERATION_COUNT);
}
SunUnixMD5CryptPasswordImpl(final ClearPasswordSpec spec) throws NoSuchAlgorithmException {
this.algorithm = ALGORITHM_SUN_CRYPT_MD5;
this.salt = PasswordUtil.generateRandomSalt(DEFAULT_SALT_SIZE);
this.iterationCount = DEFAULT_ITERATION_COUNT;
this.hash = sunMD5Crypt(algorithm, getNormalizedPasswordBytes(spec.getEncodedPassword()), salt, iterationCount);
}
SunUnixMD5CryptPasswordImpl(final String algorithm, final char[] password) throws NoSuchAlgorithmException {
this(algorithm, password, PasswordUtil.generateRandomSalt(DEFAULT_SALT_SIZE), DEFAULT_ITERATION_COUNT);
}
SunUnixMD5CryptPasswordImpl(final String algorithm, final char[] password, final IteratedSaltedPasswordAlgorithmSpec spec) throws NoSuchAlgorithmException {
this(algorithm, password, spec.getSalt().clone(), spec.getIterationCount());
}
SunUnixMD5CryptPasswordImpl(final String algorithm, final char[] password, final SaltedPasswordAlgorithmSpec spec) throws NoSuchAlgorithmException {
this(algorithm, password, spec.getSalt().clone(), DEFAULT_ITERATION_COUNT);
}
SunUnixMD5CryptPasswordImpl(final String algorithm, final char[] password, final IteratedPasswordAlgorithmSpec spec) throws NoSuchAlgorithmException {
this(algorithm, password, PasswordUtil.generateRandomSalt(DEFAULT_SALT_SIZE), spec.getIterationCount());
}
private SunUnixMD5CryptPasswordImpl(final String algorithm, final char[] password, final byte[] clonedSalt, final int iterationCount)
throws NoSuchAlgorithmException {
this(algorithm, sunMD5Crypt(algorithm, getNormalizedPasswordBytes(password), clonedSalt, iterationCount), clonedSalt, iterationCount);
}
@Override
public String getAlgorithm() {
return algorithm;
}
@Override
public byte[] getHash() {
return hash.clone();
}
@Override
public byte[] getSalt() {
return salt.clone();
}
@Override
public int getIterationCount() {
return iterationCount;
}
@Override
<S extends KeySpec> S getKeySpec(final Class<S> keySpecType) throws InvalidKeySpecException {
if (keySpecType.isAssignableFrom(IteratedSaltedHashPasswordSpec.class)) {
return keySpecType.cast(new IteratedSaltedHashPasswordSpec(getHash(), getSalt(), getIterationCount()));
}
throw new InvalidKeySpecException();
}
@Override
boolean verify(final char[] guess) throws InvalidKeyException {
byte[] test;
try {
test = sunMD5Crypt(getAlgorithm(), getNormalizedPasswordBytes(guess), getSalt(), getIterationCount());
} catch (NoSuchAlgorithmException e) {
throw log.invalidKeyCannotVerifyPassword(e);
}
return Arrays.equals(getHash(), test);
}
@Override
<T extends KeySpec> boolean convertibleTo(final Class<T> keySpecType) {
return keySpecType.isAssignableFrom(IteratedSaltedHashPasswordSpec.class);
}
/**
* Hashes the given password using the Sun variant of the MD5 Crypt algorithm.
*
* @param algorithm the algorithm to be used. Possible values are available as constants on {link}SunUnixMD5CryptPassword{link}
* @param password the password to be hashed
* @param salt the salt
* @param iterationCount the number of additional iterations to use
* @return a {@code byte[]} containing the hashed password
* @throws NoSuchAlgorithmException if a {@code MessageDigest} object that implements MD5 cannot be retrieved
*/
static byte[] sunMD5Crypt(final String algorithm, final byte[] password, final byte[] salt, final int iterationCount) throws NoSuchAlgorithmException {
// Add the password to the digest first
MessageDigest digest = getMD5MessageDigest();
digest.update(password);
// Now add the magic bytes, followed by the number of rounds (if specified), followed by the salt
if (iterationCount == 0) {
digest.update(MAGIC_BYTES);
} else {
digest.update(MAGIC_BYTES_WITH_ROUNDS);
digest.update(Integer.toString(iterationCount).getBytes(StandardCharsets.UTF_8));
digest.update(SEPARATOR_BYTES);
}
digest.update(salt);
if (algorithm.equals(ALGORITHM_SUN_CRYPT_MD5)) {
// Include the trailing "$" after the salt
digest.update(SEPARATOR_BYTES);
}
byte[] result = digest.digest();
int actualIterationCount = BASIC_ROUND_COUNT + iterationCount;
int a, b, v, x, y;
int[] unsignedResult = new int[16];
for (int round = 0; round < actualIterationCount; round++) {
digest.reset();
// Add the previous digest
digest.update(result, 0, 16);
for(int i = 0; i < 16; i++) {
unsignedResult[i] = result[i] & 0xff;
}
x = 0;
y = 0;
for (int i = 0; i < 8; i++) {
// Build up x (an 8-bit integer)
a = unsignedResult[i];
b = unsignedResult[i+3];
v = unsignedResult[(a >> (b % 5)) & 0x0f] >> ((b >> (a & 0x07)) & 0x01);
x |= (getDigestBit(unsignedResult, v) << i);
// Build up y (an 8-bit integer)
a = unsignedResult[i+8];
b = unsignedResult[(i+11) & 0x0f];
v = unsignedResult[(a >> (b % 5)) & 0x0f] >> ((b >> (a & 0x07)) & 0x01);
y |= (getDigestBit(unsignedResult, v) << i);
}
// Only the top 7 or bottom 7 bits will be used
x = (x >> getDigestBit(unsignedResult, round)) & 0x7f;
y = (y >> getDigestBit(unsignedResult, round + 64)) & 0x7f;
// If the coin toss results in a 1, add a constant phrase to the digest
int muffetCoinToss = getDigestBit(unsignedResult, x) ^ getDigestBit(unsignedResult, y);
if (muffetCoinToss == 1) {
digest.update(HAMLET_EXCERPT.getBytes(StandardCharsets.UTF_8));
}
// Add the ASCII representation of the current round to the digest
digest.update(Integer.toString(round).getBytes(StandardCharsets.US_ASCII));
result = digest.digest();
}
Arrays.fill(unsignedResult, 0);
return result;
}
private static int getDigestBit(int[] unsignedResult, int bitPosition) {
return (unsignedResult[(bitPosition >> 3) & 0x0f] >> (bitPosition & 0x07)) & 0x01;
}
private static MessageDigest getMD5MessageDigest() throws NoSuchAlgorithmException {
return MessageDigest.getInstance(MD5);
}
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 SunUnixMD5CryptPasswordImpl)) {
return false;
}
SunUnixMD5CryptPasswordImpl other = (SunUnixMD5CryptPasswordImpl) 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 SunUnixMD5CryptPassword.createRaw(algorithm, salt, hash, iterationCount);
}
public SunUnixMD5CryptPasswordImpl clone() {
return this;
}
}