/*************************************************************************
* *
* EJBCA: The OpenSource Certificate Authority *
* *
* This software is free software; you can redistribute it and/or *
* modify it under the terms of the GNU Lesser General Public *
* License as published by the Free Software Foundation; either *
* version 2.1 of the License, or any later version. *
* *
* See terms of license at gnu.org. *
* *
*************************************************************************/
package org.ejbca.util;
import java.io.UnsupportedEncodingException;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.security.NoSuchProviderException;
import java.security.spec.InvalidKeySpecException;
import java.text.DecimalFormat;
import java.util.Collection;
import java.util.LinkedList;
import java.util.Locale;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.PBEKeySpec;
import javax.crypto.spec.SecretKeySpec;
import org.apache.commons.lang.CharUtils;
import org.apache.commons.lang.StringUtils;
import org.apache.log4j.Logger;
import org.bouncycastle.crypto.Digest;
import org.bouncycastle.crypto.PBEParametersGenerator;
import org.bouncycastle.crypto.digests.SHA256Digest;
import org.bouncycastle.crypto.generators.PKCS12ParametersGenerator;
import org.bouncycastle.crypto.params.KeyParameter;
import org.bouncycastle.crypto.params.ParametersWithIV;
import org.bouncycastle.util.encoders.Hex;
/**
* This class implements some utility functions that are useful when handling Strings.
*
* @version $Id: StringTools.java 11623 2011-03-28 15:59:01Z jeklund $
*/
public final class StringTools {
private static final Logger log = Logger.getLogger(StringTools.class);
private StringTools() {} // Not for instantiation
// Characters that are not allowed in strings that may be stored in the db.
private static final char[] stripChars = {
'\n', '\r', ';', '!', '\0', '%', '`', '?', '$', '~'
};
// Characters that are not allowed in strings that may be used in db queries
private static final char[] stripSqlChars = {
'\'', '\"', '\n', '\r', '\\', ';', '&', '|', '!', '\0', '%', '`', '<', '>', '?',
'$', '~'
};
// Characters that are allowed to escape in strings.
// RFC 2253, section 2.4 lists ',' '"' '\' '+' '<' '>' ';' as valid escaped chars.
// Also allow '=' to be escaped.
private static final char[] allowedEscapeChars = {
',', '\"', '\\', '+','<', '>', ';', '=', '#'
};
private static final Pattern WS = Pattern.compile("\\s+");
public static final int KEY_SEQUENCE_FORMAT_NUMERIC = 1;
public static final int KEY_SEQUENCE_FORMAT_ALPHANUMERIC = 2;
public static final int KEY_SEQUENCE_FORMAT_COUNTRY_CODE_PLUS_NUMERIC = 4;
public static final int KEY_SEQUENCE_FORMAT_COUNTRY_CODE_PLUS_ALPHANUMERIC = 8;
/**
* Strips all special characters from a string by replacing them with a forward slash, '/'.
* This method is used for various Strings, like SubjectDNs and usernames.
*
* @param str the string whose contents will be stripped.
*
* @return the stripped version of the input string.
*/
public static String strip(final String str) {
if (str == null) {
return null;
}
final StringBuilder buf = new StringBuilder(str);
for (int i = 0; i< stripChars.length; i++) {
int index = 0;
int end = buf.length();
while (index < end) {
if (buf.charAt(index) == stripChars[i]) {
// Found an illegal character. Replace it with a '/'.
buf.setCharAt(index, '/');
} else if (buf.charAt(index) == '\\') {
// Found an escape character.
if (index + 1 == end) {
// If this is the last character we should remove it.
buf.setCharAt(index, '/');
} else if (!isAllowed(buf.charAt(index+1))) {
// We did not allow this character to be escaped. Replace both the \ and the character with a single '/'.
buf.setCharAt(index, '/');
buf.deleteCharAt(index+1);
end--;
} else {
index++;
}
}
index++;
}
}
return buf.toString();
}
/**
* Checks if a string contains characters that would be potentially dangerous to use in an SQL query.
*
* @param str the string whose contents would be stripped.
* @return true if some chars in the string would be stripped, false if not.
* @see #strip
*/
public static boolean hasSqlStripChars(final String str) {
if (str == null) {
return false;
}
for (int i = 0; i< stripSqlChars.length; i++) {
int index = 0;
final int end = str.length();
while (index < end) {
if (str.charAt(index) == stripSqlChars[i] && stripSqlChars[i] != '\\') {
// Found an illegal character.
return true;
} else if (str.charAt(index) == '\\') {
// Found an escape character.
if (index + 1 == end) {
// If this is the last character.
return true;
} else if (!isAllowed(str.charAt(index+1))) {
// We did not allow this character to be escaped.
return true;
}
index++; // Skip one extra..
}
index++;
}
}
return false;
}
/** Checks if a character is an allowed escape character according to allowedEscapeChars
*
* @param ch the char to check
* @return true if char is an allowed escape character, false if now
*/
private static boolean isAllowed(final char ch) {
boolean allowed = false;
for (int j = 0; j < allowedEscapeChars.length; j++) {
if (ch == allowedEscapeChars[j]) {
allowed = true;
break;
}
}
return allowed;
}
/**
* Strips all whitespace including space, tabs, newlines etc from the given string.
*
* @param str the string
* @return the string with all whitespace removed
* @since 2.1b1
*/
public static String stripWhitespace(final String str) {
if (str == null) {
return null;
}
return WS.matcher(str).replaceAll("");
}
/** Converts ip-adress octets, according to ipStringToOctets
* to human readable string in form 10.1.1.1 for ipv4 adresses.
*
* @param octets
* @return ip address string, null if input is invalid
* @see #ipStringToOctets(String)
*/
public static String ipOctetsToString(final byte[] octets) {
String ret = null;
if (octets.length == 4){
String ip = "";
// IPv4 address
for (int i = 0; i < 4; i++) {
// What is going on there is that we are promoting a (signed) byte to int,
// and then doing a bitwise AND operation on it to wipe out everything but
// the first 8 bits. Because Java treats the byte as signed, if its unsigned
// value is above > 127, the sign bit will be set, and it will appear to java
// to be negative. When it gets promoted to int, bits 0 through 7 will be the
// same as the byte, and bits 8 through 31 will be set to 1. So the bitwise
// AND with 0x000000FF clears out all of those bits.
// Note that this could have been written more compactly as; 0xFF & buf[index]
final int intByte = (0x000000FF & ((int)octets[i]));
final short t = (short)intByte; // NOPMD, we need short
if (StringUtils.isNotEmpty(ip)) {
ip += ".";
}
ip += t;
}
ret = ip;
}
// TODO: IPv6
return ret;
}
/** Converts an IP-address string to octets of binary ints.
* ipv4 is of form a.b.c.d, i.e. at least four octets for example 192.168.5.54
* ipv6 is of form a:b:c:d:e:f:g:h, for example 2001:0db8:85a3:0000:0000:8a2e:0370:7334
*
* Result is tested with openssl, that it's subjectAltName displays as intended.
*
* @param str string form of ip-address
* @return octets, empty array if input format is invalid, never null
*/
public static byte[] ipStringToOctets(final String str) {
final String[] toks = str.split("[.:]");
if (toks.length == 4) {
// IPv4 address such as 192.168.5.45
final byte[] ret = new byte[4];
for (int i = 0;i<toks.length;i++) {
final int t = Integer.parseInt(toks[i]);
if (t>255) {
log.error("IPv4 address '"+str+"' contains octet > 255.");
return null;
}
ret[i] = (byte)t;
}
return ret;
}
if (toks.length == 8) {
// IPv6 address such as 2001:0db8:85a3:0000:0000:8a2e:0370:7334
final byte[] ret = new byte[16];
int ind = 0;
for (int i = 0;i<toks.length;i++) {
final int t = Integer.parseInt(toks[i], 16);
if (t>0xFFFF) {
log.error("IPv6 address '"+str+"' contains part > 0xFFFF.");
return null;
}
final int t1 = t >> 8;
final int b1 = t1 & 0x00FF;
//int b1 = t & 0x00FF;
ret[ind++] = (byte)b1;
//int b2 = t & 0xFF00;
final int b2 = t & 0x00FF;
ret[ind++] = (byte)b2;
}
return ret;
}
log.error("Not a IPv4 or IPv6 address.");
return new byte[0];
}
/** Takes input and converts to Base64 on the format
* "B64:<base64 endoced string>", if the string is not null or empty.
*
* @param s String to base64 encode
* @return Base64 encoded string, or original string if it was null or empty
*/
public static String putBase64String(final String s) {
if (StringUtils.isEmpty(s)) {
return s;
}
if (s.startsWith("B64:")) {
// Only encode once
return s;
}
String n = null;
try {
// Since we used getBytes(s, "UTF-8") in this method, we must use UTF-8 when doing the reverse in another method
n="B64:"+new String(Base64.encode(s.getBytes("UTF-8"), false));
} catch (UnsupportedEncodingException e) {
// Do nothing
n=s;
}
return n;
}
/** Takes input and converts from Base64 if the string begins with B64:, i.e. is on format
* "B64:<base64 encoded string>".
*
* @param s String to Base64 decode
* @return Base64 decoded string, or original string if it was not base 64 encoded
*/
public static String getBase64String(final String s) {
if (StringUtils.isEmpty(s)) {
return s;
}
String s1 = null;
if (s.startsWith("B64:")) {
s1 = new String(s.substring(4));
String n = null;
try {
// Since we used getBytes(s, "UTF-8") in the method putBase64String, we must use UTF-8 when doing the reverse
n = new String(Base64.decode(s1.getBytes("UTF-8")), "UTF-8");
} catch (UnsupportedEncodingException e) {
n = s;
} catch (ArrayIndexOutOfBoundsException e) {
// We get this if we try to decode something that is not base 64
n = s;
}
return n;
}
return s;
}
/** Makes a string "hard" to read. Does not provide any real security, but at
* least lets you hide passwords so that people with no malicious content don't
* accidentaly stumble upon information they should not have.
*
* @param s string to obfuscate
* @return an obfuscated string
*/
public static String obfuscate(final String s) {
final StringBuilder buf = new StringBuilder("OBF:");
final byte[] b = s.getBytes();
for (int i=0; i<b.length; i++) {
final byte b1 = b[i];
final byte b2 = b[s.length()-(i+1)];
final int i1= b1+b2+127;
final int i2= b1-b2+127;
final int i0=i1*256+i2;
final String x = Integer.toString(i0,36);
switch (x.length()) {
case 1:
case 2:
case 3:buf.append('0'); break;
default:buf.append(x); break;
}
}
return buf.toString();
}
/** Retrieves the clear text from a string obfuscated with the obfuscate methods
*
* @param s obfuscated string, usually (bot not neccesarily) starts with OBF:
* @return plain text string
*/
public static String deobfuscate(final String in)
{
String s = in;
if (s.startsWith("OBF:")) {
s=s.substring(4);
}
byte[] b=new byte[s.length()/2];
int l=0;
for (int i=0;i<s.length();i+=4)
{
final String x = s.substring(i,i+4);
final int i0 = Integer.parseInt(x,36);
final int i1=(i0/256);
final int i2=(i0%256);
b[l++]=(byte)((i1+i2-254)/2);
}
return new String(b,0,l);
}
private static byte[] getSalt() throws UnsupportedEncodingException {
final String saltStr = "1958473059684739584hfurmaqiekcmq";
return saltStr.getBytes("UTF-8");
}
private static final char[] p = deobfuscate("OBF:1m0r1kmo1ioe1ia01j8z17y41l0q1abo1abm1abg1abe1kyc17ya1j631i5y1ik01kjy1lxf").toCharArray();
private static final int iCount = 100;
public static String pbeEncryptStringWithSha256Aes192(final String in) throws NoSuchAlgorithmException, NoSuchProviderException, NoSuchPaddingException, InvalidKeyException, InvalidAlgorithmParameterException, IllegalBlockSizeException, BadPaddingException, UnsupportedEncodingException {
if (CryptoProviderTools.isUsingExportableCryptography()) {
log.warn("Obfuscation not possible due to weak crypto policy.");
return in;
}
final Digest digest = new SHA256Digest();
final PKCS12ParametersGenerator pGen = new PKCS12ParametersGenerator(digest);
pGen.init(
PBEParametersGenerator.PKCS12PasswordToBytes(p),
getSalt(),
iCount);
final ParametersWithIV params = (ParametersWithIV)pGen.generateDerivedParameters(192, 128);
final SecretKeySpec encKey = new SecretKeySpec(((KeyParameter)params.getParameters()).getKey(), "AES");
final Cipher c;
c = Cipher.getInstance("AES/CBC/PKCS7Padding", "BC");
c.init(Cipher.ENCRYPT_MODE, encKey, new IvParameterSpec(params.getIV()));
final byte[] enc = c.doFinal(in.getBytes("UTF-8"));
final byte[] hex = Hex.encode(enc);
return new String(hex);
}
public static String pbeDecryptStringWithSha256Aes192(final String in) throws IllegalBlockSizeException, BadPaddingException, InvalidKeyException, InvalidKeySpecException, NoSuchAlgorithmException, NoSuchProviderException, NoSuchPaddingException, UnsupportedEncodingException {
if (CryptoProviderTools.isUsingExportableCryptography()) {
log.warn("De-obfuscation not possible due to weak crypto policy.");
return in;
}
final String algorithm = "PBEWithSHA256And192BitAES-CBC-BC";
final Cipher c = Cipher.getInstance(algorithm, "BC");
final PBEKeySpec keySpec = new PBEKeySpec(p, getSalt(), iCount);
final SecretKeyFactory fact = SecretKeyFactory.getInstance(algorithm, "BC");
c.init(Cipher.DECRYPT_MODE, fact.generateSecret(keySpec));
final byte[] dec = c.doFinal(Hex.decode(in.getBytes("UTF-8")));
return new String(dec);
}
public static String passwordDecryption(final String in, final String sDebug) {
try {
final String tmp = pbeDecryptStringWithSha256Aes192(in);
log.debug("Using encrypted "+sDebug);
return tmp;
} catch( Throwable t ) {
log.debug("Using cleartext "+sDebug);
return in;
}
}
public static String incrementKeySequence(final int keySequenceFormat, final String oldSequence) {
if (log.isTraceEnabled()) {
log.trace(">incrementKeySequence: " + keySequenceFormat + ", " + oldSequence);
}
// If the sequence does not contain any number in it at all, we can only return the same
String ret = null;
// If the sequence starts with a country code we will increment the remaining characters leaving
// the first two untouched. Per character 10 [0-9] or 36 [0-9A-Z] different values
// can be coded
if (keySequenceFormat == KEY_SEQUENCE_FORMAT_NUMERIC) {
ret = incrementNumeric(oldSequence);
} else if (keySequenceFormat == KEY_SEQUENCE_FORMAT_ALPHANUMERIC) {
ret = incrementAlphaNumeric(oldSequence);
} else if (keySequenceFormat == KEY_SEQUENCE_FORMAT_COUNTRY_CODE_PLUS_NUMERIC) {
final String countryCode = oldSequence.substring(0, Math.min(2, oldSequence.length()));
if (log.isDebugEnabled()) {
log.debug("countryCode: " + countryCode);
}
final String inc = incrementNumeric(oldSequence.substring(2));
// Cut off the country code
if (oldSequence.length() > 2 && inc != null) {
ret = countryCode + inc;
}
} else if (keySequenceFormat == KEY_SEQUENCE_FORMAT_COUNTRY_CODE_PLUS_ALPHANUMERIC) {
final String countryCode = oldSequence.substring(0, Math.min(2, oldSequence.length()));
log.debug("countryCode: " + countryCode);
final String inc = incrementAlphaNumeric(oldSequence.substring(2));
// Cut off the country code
if (oldSequence.length() > 2 && inc != null) {
ret = countryCode + inc;
}
}
// unknown, fall back to old implementation
if (ret == null) {
ret = oldSequence;
// A sequence can be 00001, or SE001 for example
// Here we will strip any sequence number at the end of the key label and add the new sequence there
// We will only count decimal (0-9) to ensure that we will not accidentally update the first to
// characters to the provided country code
final StringBuilder buf = new StringBuilder();
for (int i = oldSequence.length()-1; i >= 0; i--) {
final char c = oldSequence.charAt(i);
if (CharUtils.isAsciiNumeric(c)) {
buf.insert(0, c);
} else {
break; // at first non numeric character we break
}
}
final int restlen = oldSequence.length() - buf.length();
final String rest = oldSequence.substring(0, restlen);
final String intStr = buf.toString();
if (StringUtils.isNotEmpty(intStr)) {
Integer seq = Integer.valueOf(intStr);
seq = seq + 1;
// We want this to be the same number of numbers as we converted and incremented
final DecimalFormat df = new DecimalFormat("0000000000".substring(0,intStr.length()));
final String fseq = df.format(seq);
ret = rest + fseq;
if (log.isTraceEnabled()) {
log.trace("<incrementKeySequence: "+ ret);
}
} else {
log.info("incrementKeySequence - Sequence does not contain any nummeric part: "+ret);
}
}
return ret;
}
private static String incrementNumeric(final String s) {
// check if input is valid, if not return null
if (!s.matches("[0-9]{1,5}")) {
return null;
}
final int len = s.length();
// Parse to int and increment by 1
int incrSeq = Integer.parseInt(s, 10) + 1;
// Reset if the maximum value is exceeded
if (incrSeq == Math.pow(10, len)) {
incrSeq = 0;
}
// Make a nice String again
String newSeq = "00000" + Integer.toString(incrSeq, 10);
newSeq = newSeq.substring(newSeq.length()-len);
return newSeq.toUpperCase(Locale.ENGLISH);
}
private static String incrementAlphaNumeric(final String s) {
// check if input is valid, if not return null
if (!s.matches("[0-9A-Z]{1,5}")) {
return null;
}
final int len = s.length();
// Parse to int and increment by 1
int incrSeq = Integer.parseInt(s, 36) + 1;
// Reset if the maximum value is exceeded
if (incrSeq == Math.pow(36, len)) {
incrSeq = 0;
}
// Make a nice String again
String newSeq = "00000" + Integer.toString(incrSeq, 36);
newSeq = newSeq.substring(newSeq.length()-len);
return newSeq.toUpperCase(Locale.ENGLISH);
}
/**
* Splits a string with semicolon separated and optionally double-quoted
* strings into a collection of strings.
* <p>
* Strings that contains semicolon has to be quoted.
* Unbalanced quotes (the end quote is missing) is handled as if there
* was a quote at the end of the string.
* <pre>
* Examples:
* splitURIs("a;b;c") => [a, b, c]
* splitURIs("a;\"b;c\";d") => [a, b;c, d]
* splitURIs("a;\"b;c;d") => [a, b;c;d]
* </pre>
* <p>
* See org.ejbca.core.model.ca.certextensions.TestCertificateExtensionManager#test03TestSplitURIs()
* for more examples.
* @param dispPoints The semicolon separated string and which optionally
* uses double-quotes
* @return A collection of strings
*/
public static Collection<String> splitURIs(String dPoints) {
String dispPoints = dPoints.trim();
final LinkedList<String> result = new LinkedList<String>();
for(int i = 0; i < dispPoints.length(); i++) {
int nextQ = dispPoints.indexOf('"', i);
if(nextQ == i) {
nextQ = dispPoints.indexOf('"', i+1);
if(nextQ == -1) {
nextQ = dispPoints.length(); // unbalanced so eat(the rest)
}
// eat(to quote)
result.add(dispPoints.substring(i+1, nextQ).trim());
i = nextQ;
} else {
final int nextSep = dispPoints.indexOf(';', i);
if(nextSep != i) {
if(nextSep != -1) { // eat(to sep)
result.add(dispPoints.substring(i, nextSep).trim());
i = nextSep;
} else if (i < dispPoints.length()) { // eat(the rest)
result.add(dispPoints.substring(i).trim());
break;
}
} // Else skip
}
}
return result;
}
/**
* Parses the given string according to a specific format based on the certificate-data stored in the LogEntryData table in the database.
*
* @param certdata the string containing the certificate details
* @return a String array with two elements, the first is the certificate serialnumber and the second one is the certificate issuerDN
*/
public static String[] parseCertData(final String certdata){
if(certdata == null) {
return null;
}
final String dnStrings = "(unstructuredName|dnQualifier|postalAddress|name|emailAddress|UID|OU|NIF|CIF|ST|businessCategory|streetAddress|CN|postalCode|O|pseudonym|DC|surname|C|initials|serialNumber|L|givenName|telephoneNumber|title|DC)";
final String formats[] = {"(^[0-9A-Fa-f]+), ?((" + dnStrings + "=[^,]+,)*(" + dnStrings + "=[^,]+)*)",
"(^[0-9A-Fa-f]+) : DN : \"([^\"]*)\"( ?: SubjectDN : \"[^\"]*\")?"
};
String ret[] = null;
for(int i=0; i<formats.length; i++){
final Pattern p = Pattern.compile(formats[i]);
final Matcher m = p.matcher(certdata);
if(m.find()){
ret = new String[2];
ret[0] = m.group(1);
ret[1] = m.group(2);
break;
}
}
return ret;
}
} // StringTools