ch.inftec.ju.security.JuSecurityUtils Maven / Gradle / Ivy
package ch.inftec.ju.security;
import java.io.IOException;
import java.io.Reader;
import java.io.StringReader;
import java.net.URL;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Date;
import java.util.Enumeration;
import java.util.List;
import java.util.Properties;
import java.util.regex.Pattern;
import org.apache.commons.lang3.StringUtils;
import org.jasypt.util.text.BasicTextEncryptor;
import org.jasypt.util.text.StrongTextEncryptor;
import org.jasypt.util.text.TextEncryptor;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import au.com.bytecode.opencsv.CSVReader;
import ch.inftec.ju.util.AssertUtil;
import ch.inftec.ju.util.IOUtil;
import ch.inftec.ju.util.JuException;
import ch.inftec.ju.util.JuRuntimeException;
import ch.inftec.ju.util.JuStringUtils;
import ch.inftec.ju.util.JuUrl;
import ch.inftec.ju.util.RegexUtil;
/**
* Utility class providing security related functionality, like encryption and decryption algorithms.
* @author [email protected]
*
*/
public final class JuSecurityUtils {
/**
* RegexUtil consisting of the Regex pattern to find encrypted properties.
*
* Group 1 (index 0) contains the encrypted value.
*/
private static final RegexUtil ENCRYPTION_REGEX = new RegexUtil("ENC\\((.*)\\)");
/**
* Gets a new EncryptorBuilder to configure and build encryptor instances.
* @return EncryptorBuilder
*/
public static EncryptorBuilder buildEncryptor() {
return new EncryptorBuilder();
}
/**
* Gets a new EncryptionBuilder to perform encryption, e.g. of property files.
* @return EncryptionBuilder
*/
public static EncryptionBuilder performEncryption() {
return new EncryptionBuilder();
}
/**
* Decrypts the specified value (in case it is encrypted and has the form
* ENC(value). Otherwise, the value is just returned.
* @param value Value to be decrypted
* @param encryptor TextEncryptor to decrypt the value if necessary
* @return Decrypted value or same value
*/
public static String decryptTaggedValueIfNecessary(String value, JuTextEncryptor encryptor) {
if (encryptor != null && isEncryptedByTag(value)) {
String encryptedMessage = ENCRYPTION_REGEX.getMatches(value)[0].getGroups()[0];
return encryptor.decrypt(encryptedMessage);
} else {
return value;
}
}
/**
* Gets whether the specified value is encrypted by an ENC(value) tag.
* @param value Value
* @return True if value is encrypted using ENC(value), false otherwise
*/
public static boolean isEncryptedByTag(String value) {
return ENCRYPTION_REGEX.matches(value);
}
private static JuTextEncryptor asJuTextEncryptor(final TextEncryptor encryptor) {
return new JuTextEncryptor() {
@Override
public String decrypt(String encryptedMessage) {
return encryptor.decrypt(encryptedMessage);
}
@Override
public String encrypt(String message) {
return encryptor.encrypt(message);
}
};
}
/**
* Helper class to build encryptors.
* @author [email protected]
*
*/
public static final class EncryptorBuilder {
private boolean strongEncryption = false;
private String password;
/**
* Creates a strong (and more CPU intensive) encryptor.
*
* May need the Java Cryptography Extension (JCE) Unlimited Strength Jurisdiction Policy Files.
* @return This builder
*/
public EncryptorBuilder strong() {
return this.strong(true);
}
/**
* Sets whether to use strong (and more CPU intensive) encryption.
*
* May need the Java Cryptography Extension (JCE) Unlimited Strength Jurisdiction Policy Files.
* @param strong If true, strong encryption will be used, if false, basic encryption will be used
* @return This builder
*/
public EncryptorBuilder strong(boolean strong) {
this.strongEncryption = strong;
return this;
}
/**
* Sets the password for the encryptor.
* @param password Password
* @return This builder
*/
public EncryptorBuilder password(String password) {
this.password = password;
return this;
}
/**
* Sets the password for the encryptor from a resource (e.g. a file).
*
* Encoding of the resource is supposed to be UTF-8
* @param keyFile Path to resource containing the password
* @return This builder
*/
public EncryptorBuilder passwordByUrl(URL keyFile) {
try {
String password = new IOUtil().loadTextFromUrl(keyFile);
AssertUtil.assertNotEmpty("Encryption password must not be empty", password);
// We'll trim the password to avoid problems with new lines and the like
return this.password(password.trim());
} catch (Exception ex) {
throw new JuRuntimeException("Couldn't load password from resource %s", ex, keyFile);
}
}
/**
* Creates a text encryptor / decryptor.
*
* Encrypted text will be encoded using Base64 encoding {@link http://en.wikipedia.org/wiki/Base64}. This
* means that encrypted Strings will contain upper and lower case letters along with the characters
* '+', '/' and '='.
* @return JuTextEncryptor instance
*/
public JuTextEncryptor createTextEncryptor() {
AssertUtil.assertFalse("Password must be set", StringUtils.isEmpty(this.password));
if (this.strongEncryption) {
StrongTextEncryptor encryptor = new StrongTextEncryptor();
encryptor.setPassword(this.password);
return asJuTextEncryptor(encryptor);
} else {
BasicTextEncryptor encryptor = new BasicTextEncryptor();
encryptor.setPassword(this.password);
return asJuTextEncryptor(encryptor);
}
}
}
public static final class EncryptionBuilder {
private Logger logger = LoggerFactory.getLogger(EncryptionBuilder.class);
public static char DEFAULT_CSV_DELIMITER = ';';
private URL sourceUrl;
private String unencryptedString;
private JuTextEncryptor encryptor;
// List of encrypted token, in order they appear in the String to be encrypted
private List encryptionTokens;
private boolean outputDebugMessages = false;
private static final class EncryptionToken {
private String originalToken;
private String unencryptedValue;
private EncryptionToken(String originalToken, String unencryptedValue) {
this.originalToken = originalToken;
this.unencryptedValue = unencryptedValue;
}
}
private RegexUtil doEncRegex = new RegexUtil(RegexUtil.WHITESPACE + "*doENC\\((.+)\\)" + RegexUtil.WHITESPACE + "*");
/**
* Activates debug messages. Note that this will output the password and unencrypted
* values.
* @return Output Debug messages
*/
public EncryptionBuilder outputDebugMessages() {
this.outputDebugMessages = true;
return this;
}
/**
* Encrypts the specified property file.
*
* Values to be encrypted must take the form doENC(unencryptedValue)
*
* Encoding must be UTF-8.
* @param propertyFile URL to property file
* @return This builder
* @throws JuException if the property file cannot be read
*/
public EncryptionBuilder propertyFile(URL propertyFile) throws JuException {
AssertUtil.assertNull("Source has already been defined", this.encryptionTokens);
this.sourceUrl = propertyFile;
this.unencryptedString = new IOUtil().loadTextFromUrl(propertyFile);
// Load tokens
this.encryptionTokens = new ArrayList<>();
try (StringReader propReader = new StringReader(this.unencryptedString)) {
Properties props = new Properties();
props.load(propReader);
for (Enumeration> names = props.propertyNames(); names.hasMoreElements(); ) {
String val = props.getProperty(names.nextElement().toString());
this.processUnencryptedValue(val, val);
}
} catch (IOException ex) {
throw new JuException("Couldn't load properties", ex);
}
return this;
}
private void processUnencryptedValue(String unescapedValue, String escapedValue) {
if (this.doEncRegex.matches(unescapedValue)) {
String unencryptedValue = doEncRegex.getMatches(unescapedValue)[0].getGroups()[0];
this.encryptionTokens.add(new EncryptionToken(escapedValue, unencryptedValue));
}
}
public EncryptionBuilder csvFile(URL csvFile) throws JuException {
AssertUtil.assertNull("Source has already been defined", this.encryptionTokens);
this.sourceUrl = csvFile;
this.unencryptedString = new IOUtil().loadTextFromUrl(csvFile);
// Load tokens
this.encryptionTokens = new ArrayList<>();
try (Reader urlReader = new IOUtil().createReader(csvFile);
CSVReader csvReader = new CSVReader(urlReader, DEFAULT_CSV_DELIMITER)) {
List allEntries = csvReader.readAll();
for (String[] line : allEntries) {
for (String cell : line) {
// The CSVReader will return the actual String, but within the source CSV,
// characters might have been escaped
String escapedCell = cell.replaceAll("\"", "\"\"");
this.processUnencryptedValue(cell, escapedCell);
}
}
} catch (IOException ex) {
throw new JuException("Error reading CSV File", ex);
}
return this;
}
/**
* Set the text encryptor used to perform the encryption
* @param encryptor Encryptor instance
* @return This builder
*/
public EncryptionBuilder encryptor(JuTextEncryptor encryptor) {
this.encryptor = encryptor;
return this;
}
private String getEncryptedToken(String encryptedVal) {
return String.format("ENC(%s)", encryptedVal);
}
/**
* Encrypts the source to a String.
* @return String resource
* @throws JuException If the encryption fails
*/
public String encryptToString() throws JuException {
AssertUtil.assertNotNull("No resource to encrypt specified", this.encryptionTokens);
AssertUtil.assertNotNull("No encryptor to encrypt specified", this.encryptor);
String encryptedString = this.unencryptedString;
for (EncryptionToken token : this.encryptionTokens) {
String encryptedValue = this.encryptor.encrypt(token.unencryptedValue);
String newEncryptedString = encryptedString.replaceFirst(
Pattern.quote(token.originalToken),
this.getEncryptedToken(encryptedValue));
if (newEncryptedString.equals(encryptedString)) {
throw new JuException("Couldn't replace token: %s", token.unencryptedValue);
} else {
encryptedString = newEncryptedString;
}
if (this.outputDebugMessages) {
logger.info("Encrypted token {}. Encrypted Value: {}"
, token.originalToken
, encryptedValue);
}
}
return encryptedString;
}
/**
* Encrypts the source to the source file, overriding the doENC tokens with
* the ENC encrypted tokens.
* @throws JuException If source file cannot be written to
*/
public void encryptToSourceFile() throws JuException {
this.encryptToSourceFile(null);
}
/**
* Encrypts the source to the source file, overriding the doENC tokens with
* the ENC encrypted tokens.
*
* Prior to overwriting the source file, a backup with the original contents is created and
* and a Path to the backup file is returned
* @throws JuException If files cannot be written to
*/
public Path encryptToSourceFileWithBackup() throws JuException {
Path srcFile = JuUrl.toPath(this.sourceUrl);
Path backup = srcFile.getParent().resolve(srcFile.getFileName().toString() + "_backup_"
+ JuStringUtils.TIMESTAMP_FORMAT_SECONDS.format(new Date()));
this.encryptToSourceFile(backup);
return backup;
}
private void encryptToSourceFile(Path backupFile) throws JuException {
Path sourceFile = JuUrl.toPath(this.sourceUrl);
if (backupFile != null) {
IOUtil.copyFile(sourceFile, backupFile, false);
}
String encryptedString = this.encryptToString();
new IOUtil().writeTextToFile(encryptedString, sourceFile, true);
}
}
}