001: /*************************************************************************
002: * *
003: * EJBCA: The OpenSource Certificate Authority *
004: * *
005: * This software is free software; you can redistribute it and/or *
006: * modify it under the terms of the GNU Lesser General Public *
007: * License as published by the Free Software Foundation; either *
008: * version 2.1 of the License, or any later version. *
009: * *
010: * See terms of license at gnu.org. *
011: * *
012: *************************************************************************/package org.ejbca.util;
013:
014: import java.io.UnsupportedEncodingException;
015: import java.security.InvalidAlgorithmParameterException;
016: import java.security.InvalidKeyException;
017: import java.security.NoSuchAlgorithmException;
018: import java.security.NoSuchProviderException;
019: import java.security.spec.InvalidKeySpecException;
020: import java.util.regex.Pattern;
021:
022: import javax.crypto.BadPaddingException;
023: import javax.crypto.Cipher;
024: import javax.crypto.IllegalBlockSizeException;
025: import javax.crypto.NoSuchPaddingException;
026: import javax.crypto.SecretKeyFactory;
027: import javax.crypto.spec.IvParameterSpec;
028: import javax.crypto.spec.PBEKeySpec;
029: import javax.crypto.spec.SecretKeySpec;
030:
031: import org.apache.commons.lang.StringUtils;
032: import org.apache.log4j.Logger;
033: import org.bouncycastle.crypto.Digest;
034: import org.bouncycastle.crypto.PBEParametersGenerator;
035: import org.bouncycastle.crypto.digests.SHA256Digest;
036: import org.bouncycastle.crypto.generators.PKCS12ParametersGenerator;
037: import org.bouncycastle.crypto.params.KeyParameter;
038: import org.bouncycastle.crypto.params.ParametersWithIV;
039: import org.bouncycastle.util.encoders.Hex;
040:
041: /**
042: * This class implements some utility functions that are useful when handling Strings.
043: *
044: * @version $Id: StringTools.java,v 1.7 2007/10/04 13:23:54 anatom Exp $
045: */
046: public class StringTools {
047: private static Logger log = Logger.getLogger(StringTools.class);
048:
049: // Characters that are not allowed in strings that may be stored in the db
050: private static final char[] stripChars = { '\"', '\n', '\r', '\\',
051: ';', '&', '!', '\0', '%', '`', '<', '>', '?', '$', '~' };
052: // Characters that are not allowed in strings that may be used in db queries
053: private static final char[] stripSqlChars = { '\'', '\"', '\n',
054: '\r', '\\', ';', '&', '|', '!', '\0', '%', '`', '<', '>',
055: '?', '$', '~' };
056: // Characters that are allowed to escape in strings
057: private static final char[] allowedEscapeChars = { ',' };
058:
059: private static final Pattern WS = Pattern.compile("\\s+");
060:
061: /**
062: * Strips all special characters from a string by replacing them with a forward slash, '/'.
063: *
064: * @param str the string whose contents will be stripped.
065: *
066: * @return the stripped version of the input string.
067: */
068: public static String strip(String str) {
069: if (str == null) {
070: return null;
071: }
072: String ret = str;
073: for (int i = 0; i < stripChars.length; i++) {
074: if (ret.indexOf(stripChars[i]) > -1) {
075: // If it is an escape char, we have to process manually
076: if (stripChars[i] == '\\') {
077: // If it is an escaped char, allow it if it is an allowed escapechar
078: int index = ret.indexOf('\\');
079: while (index > -1) {
080: if (!isAllowed(ret.charAt(index + 1))) {
081: ret = StringUtils.overlay(ret, "/", index,
082: index + 1);
083: }
084: index = ret.indexOf('\\', index + 1);
085: }
086: } else {
087: ret = ret.replace(stripChars[i], '/');
088: }
089: }
090: }
091: return ret;
092: } // strip
093:
094: /**
095: * Checks if a string contains characters that would be stripped by 'strip'
096: *
097: * @param str the string whose contents would be stripped.
098: * @return true if some chars in the string would be stripped, false if not.
099: * @see #strip
100: */
101: public static boolean hasSqlStripChars(String str) {
102: if (str == null) {
103: return false;
104: }
105: String ret = str;
106: for (int i = 0; i < stripSqlChars.length; i++) {
107: if (ret.indexOf(stripSqlChars[i]) > -1) {
108: // If it is an escape char, we have to process manually
109: if (stripSqlChars[i] == '\\') {
110: // If it is an escaped char, allow it if it is an allowed escapechar
111: int index = ret.indexOf('\\');
112: while (index > -1) {
113: if (isAllowed(ret.charAt(index + 1))) {
114: return true;
115: }
116: index = ret.indexOf('\\', index + 1);
117: }
118: } else {
119: return true;
120: }
121: }
122: }
123: return false;
124: } // hasSqlStripChars
125:
126: /** Checks if a character is an allowed escape characted according to allowedEscapeChars
127: *
128: * @param ch the char to check
129: * @return true if char is an allowed escape character, false if now
130: */
131: private static boolean isAllowed(char ch) {
132: boolean allowed = false;
133: for (int j = 0; j < allowedEscapeChars.length; j++) {
134: if (ch == allowedEscapeChars[j]) {
135: allowed = true;
136: break;
137: }
138: }
139: return allowed;
140: }
141:
142: /**
143: * Strips all whitespace including space, tabs, newlines etc from the given string.
144: *
145: * @param str the string
146: * @return the string with all whitespace removed
147: * @since 2.1b1
148: */
149: public static String stripWhitespace(String str) {
150: if (str == null) {
151: return null;
152: }
153: return WS.matcher(str).replaceAll("");
154: }
155:
156: /** Converts ip-adress octets, according to ipStringToOctets
157: * to human readable string in form 10.1.1.1 for ipv4 adresses.
158: *
159: * @param octets
160: * @return ip address string, null if input is invalid
161: * @see #ipStringToOctets(String)
162: */
163: public static String ipOctetsToString(byte[] octets) {
164: String ret = null;
165: if (octets.length == 4) {
166: String ip = "";
167: // IPv4 address
168: for (int i = 0; i < 4; i++) {
169: // What is going on there is that we are promoting a (signed) byte to int,
170: // and then doing a bitwise AND operation on it to wipe out everything but
171: // the first 8 bits. Because Java treats the byte as signed, if its unsigned
172: // value is above > 127, the sign bit will be set, and it will appear to java
173: // to be negative. When it gets promoted to int, bits 0 through 7 will be the
174: // same as the byte, and bits 8 through 31 will be set to 1. So the bitwise
175: // AND with 0x000000FF clears out all of those bits.
176: // Note that this could have been written more compactly as; 0xFF & buf[index]
177: int intByte = (0x000000FF & ((int) octets[i]));
178: short t = (short) intByte;
179: if (StringUtils.isNotEmpty(ip)) {
180: ip += ".";
181: }
182: ip += t;
183: }
184: ret = ip;
185: }
186: // TODO: IPv6
187: return ret;
188: }
189:
190: /** Converts an IP-address string to octets of binary ints.
191: * ip is of form a.b.c.d, i.e. at least four octets
192: * @param str string form of ip-address
193: * @return octets, null if input format is invalid
194: */
195: public static byte[] ipStringToOctets(String str) {
196: String[] toks = str.split("[.:]");
197: if (toks.length == 4) {
198: // IPv4 address
199: byte[] ret = new byte[4];
200: for (int i = 0; i < toks.length; i++) {
201: int t = Integer.parseInt(toks[i]);
202: if (t > 255) {
203: log.error("IPv4 address '" + str
204: + "' contains octet > 255.");
205: return null;
206: }
207: ret[i] = (byte) t;
208: }
209: return ret;
210: }
211: if (toks.length == 8) {
212: // IPv6 address
213: byte[] ret = new byte[16];
214: int ind = 0;
215: for (int i = 0; i < toks.length; i++) {
216: int t = Integer.parseInt(toks[i]);
217: if (t > 0xFFFF) {
218: log.error("IPv6 address '" + str
219: + "' contains part > 0xFFFF.");
220: return null;
221: }
222: int b1 = t & 0x00FF;
223: ret[ind++] = (byte) b1;
224: int b2 = t & 0xFF00;
225: ret[ind++] = (byte) b2;
226: }
227: }
228: log.error("Not a IPv4 or IPv6 address.");
229: return null;
230: }
231:
232: /** Takes input and converts to Base64 on the format
233: * "B64:<base64 endoced string>", if the string is not null or empty.
234: *
235: * @param s String to base64 encode
236: * @return Base64 encoded string, or original string if it was null or empty
237: */
238: public static String putBase64String(String s) {
239: if (StringUtils.isEmpty(s)) {
240: return s;
241: }
242: if (s.startsWith("B64:")) {
243: // Only encode once
244: return s;
245: }
246: String n = null;
247: try {
248: // Since we used getBytes(s, "UTF-8") in this method, we must use UTF-8 when doing the reverse in another method
249: n = "B64:"
250: + new String(Base64.encode(s.getBytes("UTF-8"),
251: false));
252: } catch (UnsupportedEncodingException e) {
253: // Do nothing
254: n = s;
255: }
256: return n;
257:
258: }
259:
260: /** Takes input and converts from Base64 if the string begins with B64:, i.e. is on format
261: * "B64:<base64 endoced string>".
262: *
263: * @param s String to Base64 decode
264: * @return Base64 decoded string, or original string if it was not base 64 encoded
265: */
266: public static String getBase64String(String s) {
267: if (StringUtils.isEmpty(s)) {
268: return s;
269: }
270: String s1 = null;
271: if (s.startsWith("B64:")) {
272: s1 = s.substring(4);
273: String n = null;
274: try {
275: // Since we used getBytes(s, "UTF-8") in the method putBase64String, we must use UTF-8 when doing the reverse
276: n = new String(Base64.decode(s1.getBytes("UTF-8")),
277: "UTF-8");
278: } catch (UnsupportedEncodingException e) {
279: n = s;
280: } catch (ArrayIndexOutOfBoundsException e) {
281: // We get this if we try to decode something that is not base 64
282: n = s;
283: }
284: return n;
285: }
286: return s;
287: }
288:
289: /** Makes a string "hard" to read. Does not provide any real security, but at
290: * least lets you hide passwords so that people with no malicious content don't
291: * accidentaly stumble upon information they should not have.
292: *
293: * @param s string to obfuscate
294: * @return an obfuscated string
295: */
296: public static String obfuscate(String s) {
297: StringBuffer buf = new StringBuffer();
298: byte[] b = s.getBytes();
299:
300: synchronized (buf) {
301: buf.append("OBF:");
302: for (int i = 0; i < b.length; i++) {
303: byte b1 = b[i];
304: byte b2 = b[s.length() - (i + 1)];
305: int i1 = b1 + b2 + 127;
306: int i2 = b1 - b2 + 127;
307: int i0 = i1 * 256 + i2;
308: String x = Integer.toString(i0, 36);
309:
310: switch (x.length()) {
311: case 1:
312: buf.append('0');
313: break;
314: case 2:
315: buf.append('0');
316: break;
317: case 3:
318: buf.append('0');
319: break;
320: default:
321: buf.append(x);
322: break;
323: }
324: }
325: return buf.toString();
326: }
327: }
328:
329: /** Retrieves the clear text from a string obfuscated with the obfuscate methods
330: *
331: * @param s obfuscated string, usually (bot not neccesarily) starts with OBF:
332: * @return plain text string
333: */
334: public static String deobfuscate(String s) {
335: if (s.startsWith("OBF:"))
336: s = s.substring(4);
337:
338: byte[] b = new byte[s.length() / 2];
339: int l = 0;
340: for (int i = 0; i < s.length(); i += 4) {
341: String x = s.substring(i, i + 4);
342: int i0 = Integer.parseInt(x, 36);
343: int i1 = (i0 / 256);
344: int i2 = (i0 % 256);
345: b[l++] = (byte) ((i1 + i2 - 254) / 2);
346: }
347:
348: return new String(b, 0, l);
349: }
350:
351: private static final String q = "OBF:1m0r1kmo1ioe1ia01j8z17y41l0q1abo1abm1abg1abe1kyc17ya1j631i5y1ik01kjy1lxf";
352:
353: public static String pbeEncryptStringWithSha256Aes192(String in)
354: throws NoSuchAlgorithmException, NoSuchProviderException,
355: NoSuchPaddingException, InvalidKeyException,
356: InvalidAlgorithmParameterException,
357: IllegalBlockSizeException, BadPaddingException,
358: UnsupportedEncodingException {
359: Digest digest = new SHA256Digest();
360: final char[] p = deobfuscate(q).toCharArray();
361: String saltStr = "1958473059684739584hfurmaqiekcmq";
362: byte[] salt = saltStr.getBytes("UTF-8");
363: int iCount = 100;
364:
365: PKCS12ParametersGenerator pGen = new PKCS12ParametersGenerator(
366: digest);
367: pGen.init(PBEParametersGenerator.PKCS12PasswordToBytes(p),
368: salt, iCount);
369:
370: ParametersWithIV params = (ParametersWithIV) pGen
371: .generateDerivedParameters(192, 128);
372: SecretKeySpec encKey = new SecretKeySpec(((KeyParameter) params
373: .getParameters()).getKey(), "AES");
374: Cipher c;
375: c = Cipher.getInstance("AES/CBC/PKCS7Padding", "BC");
376: c.init(Cipher.ENCRYPT_MODE, encKey, new IvParameterSpec(params
377: .getIV()));
378:
379: byte[] enc = c.doFinal(in.getBytes("UTF-8"));
380:
381: byte[] hex = Hex.encode(enc);
382: return new String(hex);
383: }
384:
385: public static String pbeDecryptStringWithSha256Aes192(String in)
386: throws IllegalBlockSizeException, BadPaddingException,
387: InvalidKeyException, InvalidKeySpecException,
388: NoSuchAlgorithmException, NoSuchProviderException,
389: NoSuchPaddingException, UnsupportedEncodingException {
390: final char[] p = deobfuscate(q).toCharArray();
391: String saltStr = "1958473059684739584hfurmaqiekcmq";
392: byte[] salt = saltStr.getBytes("UTF-8");
393: int iCount = 100;
394:
395: Cipher c = Cipher.getInstance(
396: "PBEWithSHA256And192BitAES-CBC-BC", "BC");
397: PBEKeySpec keySpec = new PBEKeySpec(p, salt, iCount);
398: SecretKeyFactory fact = SecretKeyFactory.getInstance(
399: "PBEWithSHA256And192BitAES-CBC-BC", "BC");
400:
401: c.init(Cipher.DECRYPT_MODE, fact.generateSecret(keySpec));
402:
403: byte[] dec = c.doFinal(Hex.decode(in.getBytes("UTF-8")));
404: return new String(dec);
405: }
406:
407: } // StringTools
|