001: /*
002: * JBoss, Home of Professional Open Source.
003: * Copyright 2006, Red Hat Middleware LLC, and individual contributors
004: * as indicated by the @author tags. See the copyright.txt file in the
005: * distribution for a full listing of individual contributors.
006: *
007: * This is free software; you can redistribute it and/or modify it
008: * under the terms of the GNU Lesser General Public License as
009: * published by the Free Software Foundation; either version 2.1 of
010: * the License, or (at your option) any later version.
011: *
012: * This software is distributed in the hope that it will be useful,
013: * but WITHOUT ANY WARRANTY; without even the implied warranty of
014: * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
015: * Lesser General Public License for more details.
016: *
017: * You should have received a copy of the GNU Lesser General Public
018: * License along with this software; if not, write to the Free
019: * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
020: * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
021: */
022: package org.jboss.security;
023:
024: import java.io.Serializable;
025: import java.io.UnsupportedEncodingException;
026: import java.lang.reflect.Constructor;
027: import java.lang.reflect.Method;
028: import java.math.BigInteger;
029: import java.security.GeneralSecurityException;
030: import java.security.KeyException;
031: import java.security.MessageDigest;
032: import java.security.NoSuchAlgorithmException;
033: import java.security.Provider;
034: import java.security.Security;
035: import java.security.SecureRandom;
036: import java.util.ArrayList;
037: import java.util.Random;
038: import java.util.StringTokenizer;
039:
040: import org.jboss.crypto.JBossSXProvider;
041: import org.jboss.crypto.digest.DigestCallback;
042: import org.jboss.logging.Logger;
043:
044: /** Various security related utilities like MessageDigest
045: factories, SecureRandom access, password hashing.
046:
047: This product includes software developed by Tom Wu and Eugene
048: Jhong for the SRP Distribution (http://srp.stanford.edu/srp/).
049:
050: @author Scott.Stark@jboss.org
051: @version $Revision: 59905 $
052: */
053: public class Util {
054: private static Logger log = Logger.getLogger(Util.class);
055: private static final int HASH_LEN = 20;
056: public static final String BASE64_ENCODING = "BASE64";
057: public static final String BASE16_ENCODING = "HEX";
058: public static final String RFC2617_ENCODING = "RFC2617";
059: /**
060: The ASCII printable characters the MD5 digest maps to for RFC2617
061: */
062: private static char[] MD5_HEX = "0123456789abcdef".toCharArray();
063:
064: private static SecureRandom psuedoRng;
065: private static MessageDigest sha1Digest;
066: private static boolean initialized;
067:
068: public static void init() throws NoSuchAlgorithmException {
069: if (initialized)
070: return;
071: init(null);
072: }
073:
074: public static void init(byte[] prngSeed)
075: throws NoSuchAlgorithmException {
076: // Get an instance of the SHA-1 digest
077: sha1Digest = MessageDigest.getInstance("SHA");
078: // Get a cryptographically strong pseudo-random generator
079: psuedoRng = SecureRandom.getInstance("SHA1PRNG");
080: if (prngSeed != null)
081: psuedoRng.setSeed(prngSeed);
082: // Install the JBossSX security provider
083: Provider provider = new JBossSXProvider();
084: Security.addProvider(provider);
085: initialized = true;
086: }
087:
088: public static MessageDigest newDigest() {
089: MessageDigest md = null;
090: try {
091: md = (MessageDigest) sha1Digest.clone();
092: } catch (CloneNotSupportedException e) {
093: }
094: return md;
095: }
096:
097: public static MessageDigest copy(MessageDigest md) {
098: MessageDigest copy = null;
099: try {
100: copy = (MessageDigest) md.clone();
101: } catch (CloneNotSupportedException e) {
102: }
103: return copy;
104: }
105:
106: public static Random getPRNG() {
107: return psuedoRng;
108: }
109:
110: /** Returns the next pseudorandom, uniformly distributed double value
111: between 0.0 and 1.0 from this random number generator's sequence.
112: */
113: public static double nextDouble() {
114: return psuedoRng.nextDouble();
115: }
116:
117: /** Returns the next pseudorandom, uniformly distributed long value from
118: this random number generator's sequence. The general contract of
119: nextLong is that one long value is pseudorandomly generated and
120: returned. All 264 possible long values are produced with
121: (approximately) equal probability.
122: */
123: public static long nextLong() {
124: return psuedoRng.nextLong();
125: }
126:
127: /** Generates random bytes and places them into a user-supplied byte
128: array. The number of random bytes produced is equal to the length
129: of the byte array.
130: */
131: public static void nextBytes(byte[] bytes) {
132: psuedoRng.nextBytes(bytes);
133: }
134:
135: /** Returns the given number of seed bytes, computed using the seed
136: generation algorithm that this class uses to seed itself. This call
137: may be used to seed other random number generators.
138: */
139: public static byte[] generateSeed(int numBytes) {
140: return psuedoRng.generateSeed(numBytes);
141: }
142:
143: /** Cacluate the SRP RFC2945 password hash = H(salt | H(username | ':' | password))
144: where H = SHA secure hash. The username is converted to a byte[] using the
145: UTF-8 encoding.
146: */
147: public static byte[] calculatePasswordHash(String username,
148: char[] password, byte[] salt) {
149: // Calculate x = H(s | H(U | ':' | password))
150: MessageDigest xd = newDigest();
151: // Try to convert the username to a byte[] using UTF-8
152: byte[] user = null;
153: byte[] colon = {};
154: try {
155: user = username.getBytes("UTF-8");
156: colon = ":".getBytes("UTF-8");
157: } catch (UnsupportedEncodingException e) {
158: log.error(
159: "Failed to convert username to byte[] using UTF-8",
160: e);
161: // Use the default platform encoding
162: user = username.getBytes();
163: colon = ":".getBytes();
164: }
165: byte[] passBytes = new byte[2 * password.length];
166: int passBytesLength = 0;
167: for (int p = 0; p < password.length; p++) {
168: int c = (password[p] & 0x00FFFF);
169: // The low byte of the char
170: byte b0 = (byte) (c & 0x0000FF);
171: // The high byte of the char
172: byte b1 = (byte) ((c & 0x00FF00) >> 8);
173: passBytes[passBytesLength++] = b0;
174: // Only encode the high byte if c is a multi-byte char
175: if (c > 255)
176: passBytes[passBytesLength++] = b1;
177: }
178:
179: // Build the hash
180: xd.update(user);
181: xd.update(colon);
182: xd.update(passBytes, 0, passBytesLength);
183: byte[] h = xd.digest();
184: xd.reset();
185: xd.update(salt);
186: xd.update(h);
187: byte[] xb = xd.digest();
188: return xb;
189: }
190:
191: /** Calculate x = H(s | H(U | ':' | password)) verifier
192: v = g^x % N
193: described in RFC2945.
194: */
195: public static byte[] calculateVerifier(String username,
196: char[] password, byte[] salt, byte[] Nb, byte[] gb) {
197: BigInteger g = new BigInteger(1, gb);
198: BigInteger N = new BigInteger(1, Nb);
199: return calculateVerifier(username, password, salt, N, g);
200: }
201:
202: /** Calculate x = H(s | H(U | ':' | password)) verifier
203: v = g^x % N
204: described in RFC2945.
205: */
206: public static byte[] calculateVerifier(String username,
207: char[] password, byte[] salt, BigInteger N, BigInteger g) {
208: byte[] xb = calculatePasswordHash(username, password, salt);
209: BigInteger x = new BigInteger(1, xb);
210: BigInteger v = g.modPow(x, N);
211: return v.toByteArray();
212: }
213:
214: /** Perform an interleaved even-odd hash on the byte string
215: */
216: public static byte[] sessionKeyHash(byte[] number) {
217: int i, offset;
218:
219: for (offset = 0; offset < number.length && number[offset] == 0; ++offset)
220: ;
221:
222: byte[] key = new byte[2 * HASH_LEN];
223: byte[] hout;
224:
225: int klen = (number.length - offset) / 2;
226: byte[] hbuf = new byte[klen];
227:
228: for (i = 0; i < klen; ++i) {
229: hbuf[i] = number[number.length - 2 * i - 1];
230: }
231: hout = newDigest().digest(hbuf);
232: for (i = 0; i < HASH_LEN; ++i)
233: key[2 * i] = hout[i];
234:
235: for (i = 0; i < klen; ++i) {
236: hbuf[i] = number[number.length - 2 * i - 2];
237: }
238: hout = newDigest().digest(hbuf);
239: for (i = 0; i < HASH_LEN; ++i)
240: key[2 * i + 1] = hout[i];
241:
242: return key;
243: }
244:
245: /** Treat the input as the MSB representation of a number,
246: and lop off leading zero elements. For efficiency, the
247: input is simply returned if no leading zeroes are found.
248: */
249: public static byte[] trim(byte[] in) {
250: if (in.length == 0 || in[0] != 0)
251: return in;
252:
253: int len = in.length;
254: int i = 1;
255: while (in[i] == 0 && i < len)
256: ++i;
257: byte[] ret = new byte[len - i];
258: System.arraycopy(in, i, ret, 0, len - i);
259: return ret;
260: }
261:
262: public static byte[] xor(byte[] b1, byte[] b2, int length) {
263: byte[] result = new byte[length];
264: for (int i = 0; i < length; ++i)
265: result[i] = (byte) (b1[i] ^ b2[i]);
266: return result;
267: }
268:
269: /**
270: 3.1.3 Representation of digest values
271:
272: An optional header allows the server to specify the algorithm used to create
273: the checksum or digest. By default the MD5 algorithm is used and that is the
274: only algorithm described in this document.
275:
276: For the purposes of this document, an MD5 digest of 128 bits is represented
277: as 32 ASCII printable characters. The bits in the 128 bit digest are
278: converted from most significant to least significant bit, four bits at a time
279: to their ASCII presentation as follows. Each four bits is represented by its
280: familiar hexadecimal notation from the characters 0123456789abcdef. That is,
281: binary 0000 getInfos represented by the character '0', 0001, by '1', and so
282: on up to the representation of 1111 as 'f'.
283:
284: @param data - the raw MD5 hash data
285: @return the encoded MD5 representation
286: */
287: public static String encodeRFC2617(byte[] data) {
288: char[] hash = new char[32];
289: for (int i = 0; i < 16; i++) {
290: int j = (data[i] >> 4) & 0xf;
291: hash[i * 2] = MD5_HEX[j];
292: j = data[i] & 0xf;
293: hash[i * 2 + 1] = MD5_HEX[j];
294: }
295: return new String(hash);
296: }
297:
298: /**
299: * Hex encoding of hashes, as used by Catalina. Each byte is converted to
300: * the corresponding two hex characters.
301: */
302: public static String encodeBase16(byte[] bytes) {
303: StringBuffer sb = new StringBuffer(bytes.length * 2);
304: for (int i = 0; i < bytes.length; i++) {
305: byte b = bytes[i];
306: // top 4 bits
307: char c = (char) ((b >> 4) & 0xf);
308: if (c > 9)
309: c = (char) ((c - 10) + 'a');
310: else
311: c = (char) (c + '0');
312: sb.append(c);
313: // bottom 4 bits
314: c = (char) (b & 0xf);
315: if (c > 9)
316: c = (char) ((c - 10) + 'a');
317: else
318: c = (char) (c + '0');
319: sb.append(c);
320: }
321: return sb.toString();
322: }
323:
324: /**
325: * BASE64 encoder implementation.
326: * Provides encoding methods, using the BASE64 encoding rules, as defined
327: * in the MIME specification, <a href="http://ietf.org/rfc/rfc1521.txt">rfc1521</a>.
328: */
329: public static String encodeBase64(byte[] bytes) {
330: String base64 = null;
331: try {
332: base64 = Base64Encoder.encode(bytes);
333: } catch (Exception e) {
334: }
335: return base64;
336: }
337:
338: /**
339: * Calculate a password hash using a MessageDigest.
340: *
341: * @param hashAlgorithm - the MessageDigest algorithm name
342: * @param hashEncoding - either base64 or hex to specify the type of
343: encoding the MessageDigest as a string.
344: * @param hashCharset - the charset used to create the byte[] passed to the
345: * MessageDigestfrom the password String. If null the platform default is
346: * used.
347: * @param username - ignored in default version
348: * @param password - the password string to be hashed
349: * @return the hashed string if successful, null if there is a digest exception
350: */
351: public static String createPasswordHash(String hashAlgorithm,
352: String hashEncoding, String hashCharset, String username,
353: String password) {
354: return createPasswordHash(hashAlgorithm, hashEncoding,
355: hashCharset, username, password, null);
356: }
357:
358: /**
359: * Calculate a password hash using a MessageDigest.
360: *
361: * @param hashAlgorithm - the MessageDigest algorithm name
362: * @param hashEncoding - either base64 or hex to specify the type of
363: encoding the MessageDigest as a string.
364: * @param hashCharset - the charset used to create the byte[] passed to the
365: * MessageDigestfrom the password String. If null the platform default is
366: * used.
367: * @param username - ignored in default version
368: * @param password - the password string to be hashed
369: * @param callback - the callback used to allow customization of the hash
370: * to occur. The preDigest method is called before the password is added
371: * and the postDigest method is called after the password has been added.
372: * @return the hashed string if successful, null if there is a digest exception
373: */
374: public static String createPasswordHash(String hashAlgorithm,
375: String hashEncoding, String hashCharset, String username,
376: String password, DigestCallback callback) {
377: byte[] passBytes;
378: String passwordHash = null;
379:
380: // convert password to byte data
381: try {
382: if (hashCharset == null)
383: passBytes = password.getBytes();
384: else
385: passBytes = password.getBytes(hashCharset);
386: } catch (UnsupportedEncodingException uee) {
387: log.error("charset " + hashCharset
388: + " not found. Using platform default.", uee);
389: passBytes = password.getBytes();
390: }
391:
392: // calculate the hash and apply the encoding.
393: try {
394: MessageDigest md = MessageDigest.getInstance(hashAlgorithm);
395: if (callback != null)
396: callback.preDigest(md);
397: md.update(passBytes);
398: if (callback != null)
399: callback.postDigest(md);
400: byte[] hash = md.digest();
401: if (hashEncoding.equalsIgnoreCase(BASE64_ENCODING)) {
402: passwordHash = Util.encodeBase64(hash);
403: } else if (hashEncoding.equalsIgnoreCase(BASE16_ENCODING)) {
404: passwordHash = Util.encodeBase16(hash);
405: } else if (hashEncoding.equalsIgnoreCase(RFC2617_ENCODING)) {
406: passwordHash = Util.encodeRFC2617(hash);
407: } else {
408: log.error("Unsupported hash encoding format "
409: + hashEncoding);
410: }
411: } catch (Exception e) {
412: log.error("Password hash calculation failed ", e);
413: }
414: return passwordHash;
415: }
416:
417: // These functions assume that the byte array has MSB at 0, LSB at end.
418: // Reverse the byte array (not the String) if this is not the case.
419: // All base64 strings are in natural order, least significant digit last.
420: public static String tob64(byte[] buffer) {
421: return Base64Utils.tob64(buffer);
422: }
423:
424: public static byte[] fromb64(String str)
425: throws NumberFormatException {
426: return Base64Utils.fromb64(str);
427: }
428:
429: /** From Appendix E of the JCE ref guide, the xaximum key size
430: * allowed by the "Strong" jurisdiction policy files allows a maximum Blowfish
431: * cipher size of 128 bits.
432: * @return true if a Blowfish key can be initialized with 256 bit
433: * size, false otherwise.
434: */
435: public static boolean hasUnlimitedCrypto() {
436: boolean hasUnlimitedCrypto = false;
437: try {
438: ClassLoader loader = Thread.currentThread()
439: .getContextClassLoader();
440: Class keyGenClass = loader
441: .loadClass("javax.crypto.KeyGenerator");
442: Class[] sig = { String.class };
443: Object[] args = { "Blowfish" };
444: Method kgenInstance = keyGenClass.getDeclaredMethod(
445: "getInstance", sig);
446: Object kgen = kgenInstance.invoke(null, args);
447:
448: Class[] sig2 = { int.class };
449: Object[] args2 = { new Integer(256) };
450: Method init = keyGenClass.getDeclaredMethod("init", sig2);
451: init.invoke(kgen, args2);
452: hasUnlimitedCrypto = true;
453: } catch (Throwable e) {
454: log.debug("hasUnlimitedCrypto error", e);
455: }
456: return hasUnlimitedCrypto;
457: }
458:
459: /** Use reflection to create a javax.crypto.spec.SecretKeySpec to avoid
460: an explicit reference to SecretKeySpec so that the JCE is not needed
461: unless the SRP parameters indicate that encryption is needed.
462: @return a javax.cyrpto.SecretKey
463: */
464: public static Object createSecretKey(String cipherAlgorithm,
465: Object key) throws KeyException {
466: Class[] signature = { key.getClass(), String.class };
467: Object[] args = { key, cipherAlgorithm };
468: Object secretKey = null;
469: try {
470: ClassLoader loader = Thread.currentThread()
471: .getContextClassLoader();
472: Class secretKeySpecClass = loader
473: .loadClass("javax.crypto.spec.SecretKeySpec");
474: Constructor ctor = secretKeySpecClass
475: .getDeclaredConstructor(signature);
476: secretKey = ctor.newInstance(args);
477: } catch (Exception e) {
478: throw new KeyException(
479: "Failed to create SecretKeySpec from session key, msg="
480: + e.getMessage());
481: } catch (Throwable e) {
482: throw new KeyException(
483: "Unexpected exception during SecretKeySpec creation, msg="
484: + e.getMessage());
485: }
486: return secretKey;
487: }
488:
489: /**
490: * @param cipherAlgorithm
491: * @return A javax.crypto.Cipher
492: * @throws GeneralSecurityException
493: */
494: public static Object createCipher(String cipherAlgorithm)
495: throws GeneralSecurityException {
496: javax.crypto.Cipher cipher = javax.crypto.Cipher
497: .getInstance(cipherAlgorithm);
498: return cipher;
499: }
500:
501: public static Object createSealedObject(String cipherAlgorithm,
502: Object key, byte[] cipherIV, Serializable data)
503: throws GeneralSecurityException {
504: Object sealedObject = null;
505: try {
506: javax.crypto.Cipher cipher = javax.crypto.Cipher
507: .getInstance(cipherAlgorithm);
508: javax.crypto.SecretKey skey = (javax.crypto.SecretKey) key;
509: if (cipherIV != null) {
510: javax.crypto.spec.IvParameterSpec iv = new javax.crypto.spec.IvParameterSpec(
511: cipherIV);
512: cipher.init(javax.crypto.Cipher.ENCRYPT_MODE, skey, iv);
513: } else {
514: cipher.init(javax.crypto.Cipher.ENCRYPT_MODE, skey);
515: }
516: sealedObject = new javax.crypto.SealedObject(data, cipher);
517: } catch (GeneralSecurityException e) {
518: throw e;
519: } catch (Throwable e) {
520: throw new GeneralSecurityException(
521: "Failed to create SealedObject, msg="
522: + e.getMessage());
523: }
524: return sealedObject;
525: }
526:
527: public static Object accessSealedObject(String cipherAlgorithm,
528: Object key, byte[] cipherIV, Object obj)
529: throws GeneralSecurityException {
530: Object data = null;
531: try {
532: javax.crypto.Cipher cipher = javax.crypto.Cipher
533: .getInstance(cipherAlgorithm);
534: javax.crypto.SecretKey skey = (javax.crypto.SecretKey) key;
535: if (cipherIV != null) {
536: javax.crypto.spec.IvParameterSpec iv = new javax.crypto.spec.IvParameterSpec(
537: cipherIV);
538: cipher.init(javax.crypto.Cipher.DECRYPT_MODE, skey, iv);
539: } else {
540: cipher.init(javax.crypto.Cipher.DECRYPT_MODE, skey);
541: }
542: javax.crypto.SealedObject sealedObj = (javax.crypto.SealedObject) obj;
543: data = sealedObj.getObject(cipher);
544: } catch (GeneralSecurityException e) {
545: throw e;
546: } catch (Throwable e) {
547: throw new GeneralSecurityException(
548: "Failed to access SealedObject, msg="
549: + e.getMessage());
550: }
551: return data;
552: }
553:
554: /**
555: * Execute a password load command to obtain the char[] contents of a
556: * password.
557: * @param passwordCmd - A command to execute to obtain the plaintext
558: * password. The format is one of:
559: * '{EXT}...' where the '...' is the exact command
560: * line that will be passed to the Runtime.exec(String) method to execute a
561: * platform command. The first line of the command output is used as the
562: * password.
563: * '{CLASS}classname[:ctorargs]' where the '[:ctorargs]' is an optional
564: * string delimited by the ':' from the classname that will be passed to the
565: * classname ctor. The ctorargs itself is a comma delimited list of strings.
566: * The password is obtained from classname by invoking a
567: * 'char[] toCharArray()' method if found, otherwise, the 'String toString()'
568: * method is used.
569: * @throws Exception
570: */
571: public static char[] loadPassword(String passwordCmd)
572: throws Exception {
573: char[] password = null;
574: String passwordCmdType = null;
575:
576: // Look for a {...} prefix indicating a password command
577: if (passwordCmd.charAt(0) == '{') {
578: StringTokenizer tokenizer = new StringTokenizer(
579: passwordCmd, "{}");
580: passwordCmdType = tokenizer.nextToken();
581: passwordCmd = tokenizer.nextToken();
582: } else {
583: // Its just the password string
584: password = passwordCmd.toCharArray();
585: }
586:
587: if (password == null) {
588: // Load the password
589: if (passwordCmdType.equals("EXT"))
590: password = execPasswordCmd(passwordCmd);
591: else if (passwordCmdType.equals("CLASS"))
592: password = invokePasswordClass(passwordCmd);
593: else
594: throw new IllegalArgumentException(
595: "Unknown passwordCmdType: " + passwordCmdType);
596: }
597: return password;
598: }
599:
600: /**
601: * Execute a Runtime command to load a password.
602: * @param passwordCmd
603: * @return
604: * @throws Exception
605: */
606: private static char[] execPasswordCmd(String passwordCmd)
607: throws Exception {
608: log.debug("Executing command: " + passwordCmd);
609: String password = SecurityActions.execCmd(passwordCmd);
610: return password.toCharArray();
611: }
612:
613: private static char[] invokePasswordClass(String passwordCmd)
614: throws Exception {
615: char[] password = null;
616:
617: // Check for a ctor argument delimited by ':'
618: String classname = passwordCmd;
619: String ctorArgs = null;
620: int colon = passwordCmd.indexOf(':');
621: if (colon > 0) {
622: classname = passwordCmd.substring(0, colon);
623: ctorArgs = passwordCmd.substring(colon + 1);
624: }
625: log.debug("Loading class: " + classname + ", ctorArgs="
626: + ctorArgs);
627: ClassLoader loader = SecurityActions.getContextClassLoader();
628: Class c = loader.loadClass(classname);
629: Object instance = null;
630: // Check for a ctor(String,...) if ctorArg is not null
631: if (ctorArgs != null) {
632: Object[] args = ctorArgs.split(",");
633: Class[] sig = new Class[args.length];
634: ArrayList<Class> sigl = new ArrayList<Class>();
635: for (int n = 0; n < args.length; n++)
636: sigl.add(String.class);
637: sigl.toArray(sig);
638: Constructor ctor = c.getConstructor(sig);
639: instance = ctor.newInstance(args);
640: } else {
641: // Use the default ctor
642: instance = c.newInstance();
643: }
644:
645: // Look for a toCharArray() method
646: try {
647: log.debug("Checking for toCharArray");
648: Class[] sig = {};
649: Method toCharArray = c.getMethod("toCharArray", sig);
650: Object[] args = {};
651: log.debug("Invoking toCharArray");
652: password = (char[]) toCharArray.invoke(instance, args);
653: } catch (NoSuchMethodException e) {
654: log.debug("No toCharArray found, invoking toString");
655: String tmp = instance.toString();
656: if (tmp != null)
657: password = tmp.toCharArray();
658: }
659: return password;
660: }
661:
662: }
|