/*
* JBoss, Home of Professional Open Source.
* Copyright 2016 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 java.io.NotSerializableException;
import java.io.ObjectInputStream;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.KeySpec;
import java.util.Arrays;
import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.SecretKey;
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.PBEKeySpec;
import javax.crypto.spec.PBEParameterSpec;
import org.wildfly.common.Assert;
import org.wildfly.security.password.interfaces.MaskedPassword;
import org.wildfly.security.password.spec.ClearPasswordSpec;
import org.wildfly.security.password.spec.IteratedPasswordAlgorithmSpec;
import org.wildfly.security.password.spec.IteratedSaltedPasswordAlgorithmSpec;
import org.wildfly.security.password.spec.MaskedPasswordAlgorithmSpec;
import org.wildfly.security.password.spec.MaskedPasswordSpec;
import org.wildfly.security.password.spec.SaltedPasswordAlgorithmSpec;
import org.wildfly.security.password.util.PasswordUtil;
import org.wildfly.security.util.ByteIterator;
import org.wildfly.security.util.CodePointIterator;
final class MaskedPasswordImpl extends AbstractPasswordImpl implements MaskedPassword {
private static final long serialVersionUID = - 4107081797004604247L;
@SuppressWarnings("SpellCheckingInspection")
private static final char[] DEFAULT_PBE_KEY = "somearbitrarycrazystringthatdoesnotmatter".toCharArray();
// Required size for many schemes according to RFC 2898
private static final int DEFAULT_SALT_SIZE = 8;
// Recommended minimum by RFC 2898
private static final int DEFAULT_ITERATION_COUNT = 1000;
private final String algorithm;
private final char[] initialKeyMaterial;
private final int iterationCount;
private final byte[] salt;
private final byte[] maskedPasswordBytes;
private MaskedPasswordImpl(final String algorithm, final char[] initialKeyMaterial, final int iterationCount, final byte[] salt, final byte[] maskedPasswordBytes, final boolean validated) throws InvalidKeySpecException {
Assert.checkMinimumParameter("iterationCount", 1, iterationCount);
// perform an unmask to validate parameters
if (! validated) unmask(algorithm, initialKeyMaterial, iterationCount, salt, maskedPasswordBytes);
this.algorithm = algorithm;
this.initialKeyMaterial = initialKeyMaterial;
this.iterationCount = iterationCount;
this.salt = salt;
this.maskedPasswordBytes = maskedPasswordBytes;
}
private MaskedPasswordImpl(final String algorithm, final char[] initialKeyMaterial, final int iterationCount, final byte[] salt, final char[] chars) throws InvalidKeySpecException {
this(algorithm, initialKeyMaterial, iterationCount, salt, mask(algorithm, initialKeyMaterial, iterationCount, salt, chars), true);
}
MaskedPasswordImpl(final String algorithm, final MaskedPasswordSpec passwordSpec) throws InvalidKeySpecException {
this(algorithm, passwordSpec.getInitialKeyMaterial().clone(), passwordSpec.getIterationCount(), passwordSpec.getSalt().clone(), passwordSpec.getMaskedPasswordBytes().clone(), false);
}
MaskedPasswordImpl(final String algorithm, final char[] clearPassword) throws InvalidKeySpecException {
this(algorithm, DEFAULT_PBE_KEY, DEFAULT_ITERATION_COUNT, PasswordUtil.generateRandomSalt(DEFAULT_SALT_SIZE), clearPassword);
}
MaskedPasswordImpl(final String algorithm, final char[] clearPassword, final MaskedPasswordAlgorithmSpec parameterSpec) throws InvalidKeySpecException {
this(algorithm, parameterSpec.getInitialKeyMaterial().clone(), parameterSpec.getIterationCount(), parameterSpec.getSalt().clone(), clearPassword);
}
MaskedPasswordImpl(final String algorithm, final char[] clearPassword, final IteratedSaltedPasswordAlgorithmSpec parameterSpec) throws InvalidKeySpecException {
this(algorithm, DEFAULT_PBE_KEY, parameterSpec.getIterationCount(), parameterSpec.getSalt().clone(), clearPassword);
}
MaskedPasswordImpl(final String algorithm, final char[] clearPassword, final SaltedPasswordAlgorithmSpec parameterSpec) throws InvalidKeySpecException {
this(algorithm, DEFAULT_PBE_KEY, DEFAULT_ITERATION_COUNT, parameterSpec.getSalt().clone(), clearPassword);
}
MaskedPasswordImpl(final String algorithm, final char[] clearPassword, final IteratedPasswordAlgorithmSpec parameterSpec) throws InvalidKeySpecException {
this(algorithm, DEFAULT_PBE_KEY, parameterSpec.getIterationCount(), PasswordUtil.generateRandomSalt(DEFAULT_SALT_SIZE), clearPassword);
}
MaskedPasswordImpl(final String algorithm, final ClearPasswordSpec keySpec) throws InvalidKeySpecException {
this(algorithm, keySpec.getEncodedPassword());
}
MaskedPasswordImpl(final MaskedPassword password) throws InvalidKeySpecException {
this(password.getAlgorithm(), password.getInitialKeyMaterial().clone(), password.getIterationCount(), password.getSalt().clone(), password.getMaskedPasswordBytes().clone(), false);
}
public String getAlgorithm() {
return algorithm;
}
public char[] getInitialKeyMaterial() {
return initialKeyMaterial.clone();
}
public int getIterationCount() {
return iterationCount;
}
public byte[] getSalt() {
return salt.clone();
}
public byte[] getMaskedPasswordBytes() {
return maskedPasswordBytes.clone();
}
<S extends KeySpec> S getKeySpec(final Class<S> keySpecType) throws InvalidKeySpecException {
if (keySpecType.isAssignableFrom(MaskedPasswordSpec.class)) {
return keySpecType.cast(new MaskedPasswordSpec(initialKeyMaterial.clone(), iterationCount, salt.clone(), maskedPasswordBytes.clone()));
} else if (keySpecType.isAssignableFrom(ClearPasswordSpec.class)) {
return keySpecType.cast(new ClearPasswordSpec(unmask(algorithm, initialKeyMaterial, iterationCount, salt, maskedPasswordBytes)));
} else {
throw new InvalidKeySpecException();
}
}
boolean verify(final char[] guess) throws InvalidKeyException {
try {
return Arrays.equals(guess, unmask(algorithm, initialKeyMaterial, iterationCount, salt, maskedPasswordBytes));
} catch (InvalidKeySpecException e) {
throw new InvalidKeyException(e);
}
}
<T extends KeySpec> boolean convertibleTo(final Class<T> keySpecType) {
return keySpecType.isAssignableFrom(MaskedPasswordSpec.class) || keySpecType.isAssignableFrom(ClearPasswordSpec.class);
}
public MaskedPasswordImpl clone() {
return this;
}
private static byte[] mask(final String algorithm, final char[] initialKeyMaterial, final int iterationCount, final byte[] salt, final char[] chars) throws InvalidKeySpecException {
final Cipher cipher = getCipher(algorithm, initialKeyMaterial, iterationCount, salt, Cipher.ENCRYPT_MODE);
try {
return cipher.doFinal(CodePointIterator.ofChars(chars).asUtf8().drain());
} catch (IllegalBlockSizeException | BadPaddingException e) {
throw new InvalidKeySpecException(e);
}
}
private static char[] unmask(final String algorithm, final char[] initialKeyMaterial, final int iterationCount, final byte[] salt, final byte[] bytes) throws InvalidKeySpecException {
final Cipher cipher = getCipher(algorithm, initialKeyMaterial, iterationCount, salt, Cipher.DECRYPT_MODE);
try {
return ByteIterator.ofBytes(cipher.doFinal(bytes)).asUtf8String().drainToString().toCharArray();
} catch (IllegalBlockSizeException | BadPaddingException e) {
throw new InvalidKeySpecException(e);
}
}
private static Cipher getCipher(final String algorithm, final char[] initialKeyMaterial, final int iterationCount, final byte[] salt, final int mode) throws InvalidKeySpecException {
try {
// Create the factories first to fail fast
final String pbeName = MaskedPassword.getPBEName(algorithm);
Assert.assertNotNull(pbeName);
final SecretKeyFactory factory = SecretKeyFactory.getInstance(pbeName);
final Cipher cipher = Cipher.getInstance(pbeName);
// Create the PBE secret key
final PBEParameterSpec cipherSpec = new PBEParameterSpec(salt, iterationCount);
final PBEKeySpec keySpec = new PBEKeySpec(initialKeyMaterial);
final SecretKey cipherKey = factory.generateSecret(keySpec);
cipher.init(mode, cipherKey, cipherSpec);
return cipher;
} catch (NoSuchAlgorithmException | NoSuchPaddingException | InvalidAlgorithmParameterException | InvalidKeyException e) {
throw new InvalidKeySpecException(e);
}
}
public int hashCode() {
return multiHashOrdered(multiHashOrdered(multiHashOrdered(multiHashOrdered(Arrays.hashCode(initialKeyMaterial), Arrays.hashCode(salt)), Arrays.hashCode(maskedPasswordBytes)), iterationCount), algorithm.hashCode());
}
public boolean equals(final Object obj) {
if (! (obj instanceof MaskedPasswordImpl)) {
return false;
}
MaskedPasswordImpl other = (MaskedPasswordImpl) obj;
return iterationCount == other.iterationCount && Arrays.equals(initialKeyMaterial, other.initialKeyMaterial) && Arrays.equals(salt, other.salt) && Arrays.equals(maskedPasswordBytes, other.maskedPasswordBytes) && algorithm.equals(other.algorithm);
}
Object writeReplace() {
return MaskedPassword.createRaw(algorithm, initialKeyMaterial.clone(), iterationCount, salt.clone(), maskedPasswordBytes.clone());
}
private void readObject(ObjectInputStream ignored) throws NotSerializableException {
throw new NotSerializableException();
}
}