Java tutorial
/************************************************************************* * * * 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 12606 2011-09-16 09:40:43Z anatom $ */ 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) { return putBase64String(s, false); } /** * 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 * @param dontEncodeAsciiPrintable if the String is made up of pure ASCII printable characters, we will not B64 encode it * @return Base64 encoded string, or original string if it was null or empty */ public static String putBase64String(final String s, boolean dontEncodeAsciiPrintable) { if (StringUtils.isEmpty(s)) { return s; } if (s.startsWith("B64:")) { // Only encode once return s; } if (dontEncodeAsciiPrintable && StringUtils.isAsciiPrintable(s)) { 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