/*
* 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.auth.realm.ldap;
import static org.wildfly.security._private.ElytronMessages.log;
import static org.wildfly.security.password.interfaces.SimpleDigestPassword.*;
import static org.wildfly.security.password.interfaces.SaltedSimpleDigestPassword.*;
import static org.wildfly.security.password.interfaces.BSDUnixDESCryptPassword.ALGORITHM_BSD_CRYPT_DES;
import static org.wildfly.security.password.interfaces.UnixDESCryptPassword.ALGORITHM_CRYPT_DES;
import static org.wildfly.security.password.interfaces.ClearPassword.ALGORITHM_CLEAR;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.security.spec.InvalidKeySpecException;
import java.util.Arrays;
import org.wildfly.common.Assert;
import org.wildfly.security.password.Password;
import org.wildfly.security.password.interfaces.BSDUnixDESCryptPassword;
import org.wildfly.security.password.interfaces.ClearPassword;
import org.wildfly.security.password.interfaces.SaltedSimpleDigestPassword;
import org.wildfly.security.password.interfaces.SimpleDigestPassword;
import org.wildfly.security.password.interfaces.UnixDESCryptPassword;
import org.wildfly.security.util.Alphabet.Base64Alphabet;
import org.wildfly.security.util.ByteIterator;
import org.wildfly.security.util.ByteStringBuilder;
import org.wildfly.security.util.CodePointIterator;
import org.wildfly.security.util._private.Arrays2;
/**
* A password utility for LDAP formatted passwords.
*
* @author <a href="mailto:darran.lofthouse@jboss.com">Darran Lofthouse</a>
*/
class UserPasswordPasswordUtil {
private UserPasswordPasswordUtil() {
}
public static Password parseUserPassword(byte[] userPassword) throws InvalidKeySpecException {
Assert.checkNotNullParam("userPassword", userPassword);
if (userPassword.length == 0) throw log.emptyParameter("userPassword");
if (prefixEqual(0, new byte[] { '{', 'S', 'H', 'A' }, userPassword)) {
if (prefixEqual(4, new byte[] { '}' }, userPassword)) {
return createSimpleDigestPassword(ALGORITHM_SIMPLE_DIGEST_SHA_1, 5, userPassword);
}
if (prefixEqual(4, new byte[] { '2', '5', '6', '}' }, userPassword)) {
return createSimpleDigestPassword(ALGORITHM_SIMPLE_DIGEST_SHA_256, 8, userPassword);
}
if (prefixEqual(4, new byte[] { '3', '8', '4', '}' }, userPassword)) {
return createSimpleDigestPassword(ALGORITHM_SIMPLE_DIGEST_SHA_384, 8, userPassword);
}
if (prefixEqual(4, new byte[] { '5', '1', '2', '}' }, userPassword)) {
return createSimpleDigestPassword(ALGORITHM_SIMPLE_DIGEST_SHA_512, 8, userPassword);
}
}
if (prefixEqual(0, new byte[] { '{', 'S', 'S', 'H', 'A' }, userPassword)) {
if (prefixEqual(5, new byte[] { '}' }, userPassword)) {
return createSaltedSimpleDigestPassword(ALGORITHM_PASSWORD_SALT_DIGEST_SHA_1, 6, userPassword);
}
if (prefixEqual(5, new byte[] { '2', '5', '6', '}' }, userPassword)) {
return createSaltedSimpleDigestPassword(ALGORITHM_PASSWORD_SALT_DIGEST_SHA_256, 9, userPassword);
}
if (prefixEqual(5, new byte[] { '3', '8', '4', '}' }, userPassword)) {
return createSaltedSimpleDigestPassword(ALGORITHM_PASSWORD_SALT_DIGEST_SHA_384, 9, userPassword);
}
if (prefixEqual(5, new byte[] { '5', '1', '2', '}' }, userPassword)) {
return createSaltedSimpleDigestPassword(ALGORITHM_PASSWORD_SALT_DIGEST_SHA_512, 9, userPassword);
}
}
if (prefixEqual(0, new byte[] { '{', 'C', 'R', 'Y', 'P', 'T', '}' }, userPassword)) {
if(userPassword[7] == '_') {
return createBsdCryptBasedPassword(userPassword);
} else {
return createCryptBasedPassword(userPassword);
}
}
if (prefixEqual(0, new byte[] { '{', 'M', 'D', '5', '}' }, userPassword)) {
return createSimpleDigestPassword(ALGORITHM_SIMPLE_DIGEST_MD5, 5, userPassword);
}
if (prefixEqual(0, new byte[] { '{', 'S', 'M', 'D', '5', '}' }, userPassword)) {
return createSaltedSimpleDigestPassword(ALGORITHM_PASSWORD_SALT_DIGEST_MD5, 6, userPassword);
}
if (prefixEqual(0, new byte[] { '{', 'C', 'L', 'E', 'A', 'R', '}' }, userPassword)) {
return createClearPassword(7, userPassword);
}
if(userPassword[0] == '{' && Arrays2.indexOf(userPassword, '}') > 0) {
throw log.unknownLdapPasswordScheme();
}
return createClearPassword(0, userPassword);
}
/* fast conversion of char to upper letter (for ASCII only) */
private static byte upper(byte character) {
return (byte) (character >= 'a' && character <= 'z' ? character - 'a' + 'A' : character);
}
private static boolean prefixEqual(int skip, byte[] pattern, byte[] array) {
if (skip + pattern.length > array.length) return false;
for (int i = 0; i < pattern.length; i++) {
if (upper(array[i+skip]) != pattern[i]) return false;
}
return true;
}
private static Password createClearPassword(int skip, byte[] userPassword) {
if (skip != 0) userPassword = Arrays.copyOfRange(userPassword, skip, userPassword.length);
return ClearPassword.createRaw(ALGORITHM_CLEAR, new String(userPassword, StandardCharsets.UTF_8).toCharArray());
}
private static Password createSimpleDigestPassword(String algorithm, int prefixSize, byte[] userPassword)
throws InvalidKeySpecException {
int length = userPassword.length - prefixSize;
byte[] digest = CodePointIterator.ofUtf8Bytes(userPassword, prefixSize, length).base64Decode().drain();
return SimpleDigestPassword.createRaw(algorithm, digest);
}
private static Password createSaltedSimpleDigestPassword(String algorithm, int prefixSize, byte[] userPassword)
throws InvalidKeySpecException {
int length = userPassword.length - prefixSize;
byte[] decoded = CodePointIterator.ofUtf8Bytes(userPassword, prefixSize, length).base64Decode().drain();
int digestLength = expectedDigestLengthBytes(algorithm);
int saltLength = decoded.length - digestLength;
if (saltLength < 1) {
throw log.insufficientDataToFormDigestAndSalt();
}
byte[] digest = new byte[digestLength];
byte[] salt = new byte[saltLength];
System.arraycopy(decoded, 0, digest, 0, digestLength);
System.arraycopy(decoded, digestLength, salt, 0, saltLength);
return SaltedSimpleDigestPassword.createRaw(algorithm, digest, salt);
}
private static Password createCryptBasedPassword(byte[] userPassword) throws InvalidKeySpecException {
if (userPassword.length != 20) {
throw log.insufficientDataToFormDigestAndSalt();
}
final int lo = Base64Alphabet.MOD_CRYPT.decode(userPassword[7] & 0xff);
final int hi = Base64Alphabet.MOD_CRYPT.decode(userPassword[8] & 0xff);
if (lo == -1 || hi == -1) {
throw log.invalidSalt((char) lo, (char) hi);
}
short salt = (short) (lo | hi << 6);
byte[] hash = CodePointIterator.ofUtf8Bytes(userPassword, 9, 11).base64Decode(Base64Alphabet.MOD_CRYPT, false).drain();
return UnixDESCryptPassword.createRaw(ALGORITHM_CRYPT_DES, salt, hash);
}
private static Password createBsdCryptBasedPassword(byte[] userPassword) throws InvalidKeySpecException {
if (userPassword.length != 27) {
throw log.insufficientDataToFormDigestAndSalt();
}
int b0 = Base64Alphabet.MOD_CRYPT.decode(userPassword[8] & 0xff);
int b1 = Base64Alphabet.MOD_CRYPT.decode(userPassword[9] & 0xff);
int b2 = Base64Alphabet.MOD_CRYPT.decode(userPassword[10] & 0xff);
int b3 = Base64Alphabet.MOD_CRYPT.decode(userPassword[11] & 0xff);
if (b0 == -1 || b1 == -1 || b2 == -1 || b3 == -1) {
throw log.invalidRounds((char) b0, (char) b1, (char) b2, (char) b3);
}
int iterationCount = b0 | b1 << 6 | b2 << 12 | b3 << 18;
b0 = Base64Alphabet.MOD_CRYPT.decode(userPassword[12] & 0xff);
b1 = Base64Alphabet.MOD_CRYPT.decode(userPassword[13] & 0xff);
b2 = Base64Alphabet.MOD_CRYPT.decode(userPassword[14] & 0xff);
b3 = Base64Alphabet.MOD_CRYPT.decode(userPassword[15] & 0xff);
if (b0 == -1 || b1 == -1 || b2 == -1 || b3 == -1) {
throw log.invalidSalt((char) b0, (char) b1, (char) b2, (char) b3);
}
int salt = b0 | b1 << 6 | b2 << 12 | b3 << 18;
byte[] hash = CodePointIterator.ofUtf8Bytes(userPassword, 16, 11).base64Decode(Base64Alphabet.MOD_CRYPT, false).drain();
return BSDUnixDESCryptPassword.createRaw(ALGORITHM_BSD_CRYPT_DES, hash, salt, iterationCount);
}
private static int expectedDigestLengthBytes(final String algorithm) {
switch (algorithm) {
case ALGORITHM_PASSWORD_SALT_DIGEST_MD5:
return 16;
case ALGORITHM_PASSWORD_SALT_DIGEST_SHA_1:
return 20;
case ALGORITHM_PASSWORD_SALT_DIGEST_SHA_256:
return 32;
case ALGORITHM_PASSWORD_SALT_DIGEST_SHA_384:
return 48;
case ALGORITHM_PASSWORD_SALT_DIGEST_SHA_512:
return 64;
default:
throw log.unrecognizedAlgorithm(algorithm);
}
}
public static byte[] composeUserPassword(Password password) throws IOException {
String algorithm = password.getAlgorithm();
ByteArrayOutputStream out = new ByteArrayOutputStream();
if (ALGORITHM_SIMPLE_DIGEST_MD5.equals(algorithm)) {
out.write(new byte[] { '{', 'm', 'd', '5', '}' });
out.write(ByteIterator.ofBytes(((SimpleDigestPassword)password).getDigest()).base64Encode().asUtf8().drain());
} else if (ALGORITHM_SIMPLE_DIGEST_SHA_1.equals(algorithm)) {
out.write(new byte[]{'{','s','h','a','}'});
out.write(ByteIterator.ofBytes(((SimpleDigestPassword)password).getDigest()).base64Encode().asUtf8().drain());
} else if (ALGORITHM_SIMPLE_DIGEST_SHA_256.equals(algorithm)) {
out.write(new byte[]{'{','s','h','a','2','5','6','}'});
out.write(ByteIterator.ofBytes(((SimpleDigestPassword)password).getDigest()).base64Encode().asUtf8().drain());
} else if (ALGORITHM_SIMPLE_DIGEST_SHA_384.equals(algorithm)) {
out.write(new byte[]{'{','s','h','a','3','8','4','}'});
out.write(ByteIterator.ofBytes(((SimpleDigestPassword)password).getDigest()).base64Encode().asUtf8().drain());
} else if (ALGORITHM_SIMPLE_DIGEST_SHA_512.equals(algorithm)) {
out.write(new byte[]{'{','s','h','a','5','1','2','}'});
out.write(ByteIterator.ofBytes(((SimpleDigestPassword)password).getDigest()).base64Encode().asUtf8().drain());
} else if (ALGORITHM_PASSWORD_SALT_DIGEST_MD5.equals(algorithm)) {
out.write(new byte[]{'{','s','m','d','5','}'});
out.write(composeDigestSalt((SaltedSimpleDigestPassword) password));
} else if (ALGORITHM_PASSWORD_SALT_DIGEST_SHA_1.equals(algorithm)) {
out.write(new byte[]{'{','s','s','h','a','}'});
out.write(composeDigestSalt((SaltedSimpleDigestPassword) password));
} else if (ALGORITHM_PASSWORD_SALT_DIGEST_SHA_256.equals(algorithm)) {
out.write(new byte[]{'{','s','s','h','a','2','5','6','}'});
out.write(composeDigestSalt((SaltedSimpleDigestPassword) password));
} else if (ALGORITHM_PASSWORD_SALT_DIGEST_SHA_384.equals(algorithm)) {
out.write(new byte[]{'{','s','s','h','a','3','8','4','}'});
out.write(composeDigestSalt((SaltedSimpleDigestPassword) password));
} else if (ALGORITHM_PASSWORD_SALT_DIGEST_SHA_512.equals(algorithm)) {
out.write(new byte[]{'{','s','s','h','a','5','1','2','}'});
out.write(composeDigestSalt((SaltedSimpleDigestPassword) password));
} else if (ALGORITHM_BSD_CRYPT_DES.equals(algorithm)) {
out.write(new byte[] { '{', 'c', 'r', 'y', 'p', 't', '}', '_' });
composeBsdCryptBasedPassword(out, (BSDUnixDESCryptPassword) password);
} else if (ALGORITHM_CRYPT_DES.equals(algorithm)) {
out.write(new byte[]{'{','c','r','y','p','t','}'});
composeCryptBasedPassword(out, (UnixDESCryptPassword) password);
} else if (ALGORITHM_CLEAR.equals(algorithm)) {
return CodePointIterator.ofChars(((ClearPassword)password).getPassword()).asUtf8().drain();
} else {
return null;
}
return out.toByteArray();
}
private static byte[] composeDigestSalt(SaltedSimpleDigestPassword password) {
return ByteIterator.ofBytes(new ByteStringBuilder()
.append(password.getDigest())
.append(password.getSalt())
.toArray()
).base64Encode().asUtf8().drain();
}
private static void composeCryptBasedPassword(ByteArrayOutputStream out, UnixDESCryptPassword password) throws IOException {
out.write(Base64Alphabet.MOD_CRYPT.encode(password.getSalt() & 0x3f));
out.write(Base64Alphabet.MOD_CRYPT.encode(password.getSalt() >> 6 & 0x3f));
out.write(ByteIterator.ofBytes(password.getHash()).base64Encode(Base64Alphabet.MOD_CRYPT, false).asUtf8().drain());
}
private static void composeBsdCryptBasedPassword(ByteArrayOutputStream out, BSDUnixDESCryptPassword password) throws IOException {
out.write(Base64Alphabet.MOD_CRYPT.encode(password.getIterationCount() & 0x3f));
out.write(Base64Alphabet.MOD_CRYPT.encode(password.getIterationCount() >> 6 & 0x3f));
out.write(Base64Alphabet.MOD_CRYPT.encode(password.getIterationCount() >> 12 & 0x3f));
out.write(Base64Alphabet.MOD_CRYPT.encode(password.getIterationCount() >> 18 & 0x3f));
out.write(Base64Alphabet.MOD_CRYPT.encode(password.getSalt() & 0x3f));
out.write(Base64Alphabet.MOD_CRYPT.encode(password.getSalt() >> 6 & 0x3f));
out.write(Base64Alphabet.MOD_CRYPT.encode(password.getSalt() >> 12 & 0x3f));
out.write(Base64Alphabet.MOD_CRYPT.encode(password.getSalt() >> 18 & 0x3f));
out.write(ByteIterator.ofBytes(password.getHash()).base64Encode(Base64Alphabet.MOD_CRYPT, false).asUtf8().drain());
}
public static boolean isAlgorithmSupported(String algorithm) {
switch (algorithm) {
case ALGORITHM_SIMPLE_DIGEST_MD5:
case ALGORITHM_SIMPLE_DIGEST_SHA_1:
case ALGORITHM_SIMPLE_DIGEST_SHA_256:
case ALGORITHM_SIMPLE_DIGEST_SHA_384:
case ALGORITHM_SIMPLE_DIGEST_SHA_512:
case ALGORITHM_PASSWORD_SALT_DIGEST_MD5:
case ALGORITHM_PASSWORD_SALT_DIGEST_SHA_1:
case ALGORITHM_PASSWORD_SALT_DIGEST_SHA_256:
case ALGORITHM_PASSWORD_SALT_DIGEST_SHA_384:
case ALGORITHM_PASSWORD_SALT_DIGEST_SHA_512:
case ALGORITHM_BSD_CRYPT_DES:
case ALGORITHM_CRYPT_DES:
case ALGORITHM_CLEAR:
return true;
default:
return false;
}
}
}