/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you 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.apache.syncope.core.spring.security;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.nio.charset.StandardCharsets;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.util.Map;
import java.util.Properties;
import java.util.concurrent.ConcurrentHashMap;
import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.spec.SecretKeySpec;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.ArrayUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.syncope.common.lib.types.CipherAlgorithm;
import org.jasypt.commons.CommonUtils;
import org.jasypt.digest.StandardStringDigester;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.security.crypto.bcrypt.BCrypt;
import org.springframework.security.crypto.codec.Base64;
public final class Encryptor {
private static final Logger LOG = LoggerFactory.getLogger(Encryptor.class);
private static final Map<String, Encryptor> INSTANCES = new ConcurrentHashMap<>();
private static final String DEFAULT_SECRET_KEY = "1abcdefghilmnopqrstuvz2!";
/**
* Default value for salted {@link StandardStringDigester#setIterations(int)}.
*/
private static final int DEFAULT_SALT_ITERATIONS = 1;
/**
* Default value for {@link StandardStringDigester#setSaltSizeBytes(int)}.
*/
private static final int DEFAULT_SALT_SIZE_BYTES = 8;
/**
* Default value for {@link StandardStringDigester#setInvertPositionOfPlainSaltInEncryptionResults(boolean)}.
*/
private static final boolean DEFAULT_IPOPSIER = true;
/**
* Default value for salted {@link StandardStringDigester#setInvertPositionOfSaltInMessageBeforeDigesting(boolean)}.
*/
private static final boolean DEFAULT_IPOSIMBD = true;
/**
* Default value for salted {@link StandardStringDigester#setUseLenientSaltSizeCheck(boolean)}.
*/
private static final boolean DEFAULT_ULSSC = true;
private static String SECRET_KEY;
private static Integer SALT_ITERATIONS;
private static Integer SALT_SIZE_BYTES;
private static Boolean IPOPSIER;
private static Boolean IPOSIMBD;
private static Boolean ULSSC;
static {
InputStream propStream = null;
try {
propStream = Encryptor.class.getResourceAsStream("/security.properties");
Properties props = new Properties();
props.load(propStream);
SECRET_KEY = props.getProperty("secretKey");
SALT_ITERATIONS = Integer.valueOf(props.getProperty("digester.saltIterations"));
SALT_SIZE_BYTES = Integer.valueOf(props.getProperty("digester.saltSizeBytes"));
IPOPSIER = Boolean.valueOf(props.getProperty("digester.invertPositionOfPlainSaltInEncryptionResults"));
IPOSIMBD = Boolean.valueOf(props.getProperty("digester.invertPositionOfSaltInMessageBeforeDigesting"));
ULSSC = Boolean.valueOf(props.getProperty("digester.useLenientSaltSizeCheck"));
} catch (Exception e) {
LOG.error("Could not read security parameters", e);
} finally {
IOUtils.closeQuietly(propStream);
}
if (SECRET_KEY == null) {
SECRET_KEY = DEFAULT_SECRET_KEY;
LOG.debug("secretKey not found, reverting to default");
}
if (SALT_ITERATIONS == null) {
SALT_ITERATIONS = DEFAULT_SALT_ITERATIONS;
LOG.debug("digester.saltIterations not found, reverting to default");
}
if (SALT_SIZE_BYTES == null) {
SALT_SIZE_BYTES = DEFAULT_SALT_SIZE_BYTES;
LOG.debug("digester.saltSizeBytes not found, reverting to default");
}
if (IPOPSIER == null) {
IPOPSIER = DEFAULT_IPOPSIER;
LOG.debug("digester.invertPositionOfPlainSaltInEncryptionResults not found, reverting to default");
}
if (IPOSIMBD == null) {
IPOSIMBD = DEFAULT_IPOSIMBD;
LOG.debug("digester.invertPositionOfSaltInMessageBeforeDigesting not found, reverting to default");
}
if (ULSSC == null) {
ULSSC = DEFAULT_ULSSC;
LOG.debug("digester.useLenientSaltSizeCheck not found, reverting to default");
}
}
public static Encryptor getInstance() {
return getInstance(SECRET_KEY);
}
public static Encryptor getInstance(final String secretKey) {
String actualKey = StringUtils.isBlank(secretKey) ? DEFAULT_SECRET_KEY : secretKey;
Encryptor instance = INSTANCES.get(actualKey);
if (instance == null) {
instance = new Encryptor(actualKey);
INSTANCES.put(actualKey, instance);
}
return instance;
}
private SecretKeySpec keySpec;
private Encryptor(final String secretKey) {
String actualKey = secretKey;
if (actualKey.length() < 16) {
StringBuilder actualKeyPadding = new StringBuilder(actualKey);
for (int i = 0; i < 16 - actualKey.length(); i++) {
actualKeyPadding.append('0');
}
actualKey = actualKeyPadding.toString();
LOG.debug("actualKey too short, adding some random characters");
}
try {
keySpec = new SecretKeySpec(ArrayUtils.subarray(
actualKey.getBytes(StandardCharsets.UTF_8), 0, 16),
CipherAlgorithm.AES.getAlgorithm());
} catch (Exception e) {
LOG.error("Error during key specification", e);
}
}
public String encode(final String value, final CipherAlgorithm cipherAlgorithm)
throws UnsupportedEncodingException, NoSuchAlgorithmException, NoSuchPaddingException, InvalidKeyException,
IllegalBlockSizeException, BadPaddingException {
String encodedValue = null;
if (value != null) {
if (cipherAlgorithm == null || cipherAlgorithm == CipherAlgorithm.AES) {
final byte[] cleartext = value.getBytes(StandardCharsets.UTF_8);
final Cipher cipher = Cipher.getInstance(CipherAlgorithm.AES.getAlgorithm());
cipher.init(Cipher.ENCRYPT_MODE, keySpec);
encodedValue = new String(Base64.encode(cipher.doFinal(cleartext)));
} else if (cipherAlgorithm == CipherAlgorithm.BCRYPT) {
encodedValue = BCrypt.hashpw(value, BCrypt.gensalt());
} else {
encodedValue = getDigester(cipherAlgorithm).digest(value);
}
}
return encodedValue;
}
public boolean verify(final String value, final CipherAlgorithm cipherAlgorithm, final String encodedValue) {
boolean res = false;
try {
if (value != null) {
if (cipherAlgorithm == null || cipherAlgorithm == CipherAlgorithm.AES) {
res = encode(value, cipherAlgorithm).equals(encodedValue);
} else if (cipherAlgorithm == CipherAlgorithm.BCRYPT) {
res = BCrypt.checkpw(value, encodedValue);
} else {
res = getDigester(cipherAlgorithm).matches(value, encodedValue);
}
}
} catch (Exception e) {
LOG.error("Could not verify encoded value", e);
}
return res;
}
public String decode(final String encodedValue, final CipherAlgorithm cipherAlgorithm)
throws UnsupportedEncodingException, NoSuchAlgorithmException, NoSuchPaddingException, InvalidKeyException,
IllegalBlockSizeException, BadPaddingException {
String value = null;
if (encodedValue != null && cipherAlgorithm == CipherAlgorithm.AES) {
final byte[] encoded = encodedValue.getBytes(StandardCharsets.UTF_8);
final Cipher cipher = Cipher.getInstance(CipherAlgorithm.AES.getAlgorithm());
cipher.init(Cipher.DECRYPT_MODE, keySpec);
value = new String(cipher.doFinal(Base64.decode(encoded)), StandardCharsets.UTF_8);
}
return value;
}
private StandardStringDigester getDigester(final CipherAlgorithm cipherAlgorithm) {
StandardStringDigester digester = new StandardStringDigester();
if (cipherAlgorithm.getAlgorithm().startsWith("S-")) {
// Salted ...
digester.setAlgorithm(cipherAlgorithm.getAlgorithm().replaceFirst("S\\-", ""));
digester.setIterations(SALT_ITERATIONS);
digester.setSaltSizeBytes(SALT_SIZE_BYTES);
digester.setInvertPositionOfPlainSaltInEncryptionResults(IPOPSIER);
digester.setInvertPositionOfSaltInMessageBeforeDigesting(IPOSIMBD);
digester.setUseLenientSaltSizeCheck(ULSSC);
} else {
// Not salted ...
digester.setAlgorithm(cipherAlgorithm.getAlgorithm());
digester.setIterations(1);
digester.setSaltSizeBytes(0);
}
digester.setStringOutputType(CommonUtils.STRING_OUTPUT_TYPE_HEXADECIMAL);
return digester;
}
}