/* * $Id$ * * Copyright 2006, The jCoderZ.org Project. All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are * met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above * copyright notice, this list of conditions and the following * disclaimer in the documentation and/or other materials * provided with the distribution. * * Neither the name of the jCoderZ.org Project nor the names of * its contributors may be used to endorse or promote products * derived from this software without specific prior written * permission. * * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS "AS IS" AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS AND CONTRIBUTORS * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR * BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR * OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ package org.jcoderz.commons.util; import java.util.Arrays; import org.jcoderz.commons.ArgumentMalformedException; /** * This class provides encode/decode for RFC 2045 Base64 as * defined by RFC 2045, N. Freed and N. Borenstein. * RFC 2045: Multipurpose Internet Mail Extensions (MIME) * Part One: Format of Internet Message Bodies. Reference * 1996 Available at: http://www.ietf.org/rfc/rfc2045.txt * This class is used by XML Schema binary format validation * * This implementation does not encode/decode streaming * data. You need the data that you will encode/decode * already on a byte array. * * @author Michael Griffel * * TODO: remove deep copy of decoded Base64 data in case of padding chars. */ public final class Base64Util { private static final String ENCODED_PARAMETER = "encoded"; private static final int LOWER_SIX_BITS = 0x3f; private static final int BASELENGTH = 255; private static final int BITS_PER_BASE64_CHAR = 6; private static final int FOURBYTE = 4; private static final int BYTES_PER_BASE64_CHUNK = 3; private static final int TWENTYFOURBITGROUP = 3 * Constants.BITS_PER_BYTE; private static final char PAD = '='; private static final char[] LOOKUP_BASE64_ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/" .toCharArray(); private static final byte[] BASE64_ALPHABET = new byte[BASELENGTH]; static { Arrays.fill(BASE64_ALPHABET, (byte) -1); for (int i = 0; i < LOOKUP_BASE64_ALPHABET.length; i++) { BASE64_ALPHABET[LOOKUP_BASE64_ALPHABET[i]] = (byte) i; } } private Base64Util () { // no instances allowed - only static methods } /** * Encodes hex octets into Base64. * * @param binaryData Array containing binary data. * @return Encoded Base64 array */ public static char[] encodeToChars (byte[] binaryData) { final char[] result; if (binaryData == null) { result = null; } else if (binaryData.length == 0) { result = new char[0]; } else { final int dataBits = binaryData.length * Constants.BITS_PER_BYTE; final int remainingBits = dataBits % TWENTYFOURBITGROUP; final int numberTriplets = dataBits / TWENTYFOURBITGROUP; final int numberQuartet = remainingBits != 0 ? numberTriplets + 1 : numberTriplets; final char [] encodedData = new char[numberQuartet * FOURBYTE]; int encodedIndex = 0; int dataIndex = 0; for (int i = 0; i < numberTriplets; i++) { // b1 b2 b3 // +---------+---------+---------+ // |765432 10|7654 3210|76 543210| = x // +--------16---------8---------+ // | | | | | // ^^^^^^ ^^^^^^^ ^^^^^^^ ^^^^^^ // d1 d2 d3 d4 final int x = (binaryData[dataIndex++] & Constants.BYTE_MASK) << (2 * Constants.BITS_PER_BYTE) // b1 | (binaryData[dataIndex++] & Constants.BYTE_MASK) << Constants.BITS_PER_BYTE // b2 | (binaryData[dataIndex++] & Constants.BYTE_MASK); // b3 encodedData[encodedIndex++] = LOOKUP_BASE64_ALPHABET // d1 [(x >>> (3 * BITS_PER_BASE64_CHAR)) & LOWER_SIX_BITS]; encodedData[encodedIndex++] = LOOKUP_BASE64_ALPHABET // d2 [(x >>> (2 * BITS_PER_BASE64_CHAR)) & LOWER_SIX_BITS]; encodedData[encodedIndex++] = LOOKUP_BASE64_ALPHABET // d3 [(x >>> BITS_PER_BASE64_CHAR) & LOWER_SIX_BITS]; encodedData[encodedIndex++] = LOOKUP_BASE64_ALPHABET // d4 [x & LOWER_SIX_BITS]; } // two bytes left if (remainingBits == 2 * Constants.BITS_PER_BYTE) { // b2 b3 // +---------+---------+ // |765432 10|7654 3210| = x // +---------8---------+ // | | | | pad | // ^^^^^^ ^^^^^^^ ^^^^^^^ ^^^^^^ // d1 d2 d3 d4 final int x = (binaryData[dataIndex++] & Constants.BYTE_MASK) << Constants.BITS_PER_BYTE // b2 | (binaryData[dataIndex++] & Constants.BYTE_MASK); // b3 encodedData[encodedIndex++] = LOOKUP_BASE64_ALPHABET // d1 [x >>> 10 & LOWER_SIX_BITS]; encodedData[encodedIndex++] = LOOKUP_BASE64_ALPHABET // d2 [x >>> 4 & LOWER_SIX_BITS]; encodedData[encodedIndex++] = LOOKUP_BASE64_ALPHABET // d3 [x << 2 & LOWER_SIX_BITS]; encodedData[encodedIndex++] = PAD; // d4 } // one byte left else if (remainingBits == Constants.BITS_PER_BYTE) { // b3 // +---------+ // |765432 10| = x // +---------+ // | | | pad | pad | // ^^^^^^ ^^^^^^ ^^^^^^ ^^^^^^ // d1 d2 d3 d4 final int x = (binaryData[dataIndex++] & Constants.BYTE_MASK); // b3 encodedData[encodedIndex++] = LOOKUP_BASE64_ALPHABET // d1 [(x >>> 2) & LOWER_SIX_BITS]; encodedData[encodedIndex++] = LOOKUP_BASE64_ALPHABET // d2 [(x << 4) & LOWER_SIX_BITS]; encodedData[encodedIndex++] = PAD; // d3 encodedData[encodedIndex++] = PAD; // d4 } result = encodedData; } return result; } /** * Encodes hex octets into Base64. * * @param binaryData Array containing binary data. * @return Encoded Base64 string. */ public static String encode (byte[] binaryData) { return new String(encodeToChars(binaryData)); } /** * Encodes hex octets into Base64. * The encoded characters are written to the given string * buffer <tt>sb</tt>. * * @param sb the string buffer that is used to write the * Base64 characters to. * @param binaryData Array containing binary data. */ public static void appendEncoded (StringBuffer sb, byte[] binaryData) { sb.append(encodeToChars(binaryData)); } /** * Decodes Base64 data into octets. * * @param encoded Base64 encoded string. * @return an array containing decoded data. * @throws ArgumentMalformedException if the given string is not * Base64 encoded. */ public static byte[] decode (String encoded) throws ArgumentMalformedException { Assert.notNull(encoded, ENCODED_PARAMETER); final byte[] result; if (encoded.length() % FOURBYTE != 0) { throw new ArgumentMalformedException(ENCODED_PARAMETER, encoded, "Base64 length must be a multiple of " + FOURBYTE); } final char[] base64Data = encoded.toCharArray(); final int numberQuadruple = base64Data.length / FOURBYTE; if (numberQuadruple == 0) { throw new ArgumentMalformedException(ENCODED_PARAMETER, encoded, "Base64 length " + base64Data.length + " must be at least " + FOURBYTE + " bytes"); } byte b1 = 0, b2 = 0, b3 = 0, b4 = 0; int encodedIndex = 0; int dataIndex = 0; final byte[] decodedData = new byte[(numberQuadruple) * BYTES_PER_BASE64_CHUNK]; final int pureBase64Chunks = numberQuadruple - 1; for (int i = 0; i < pureBase64Chunks; i++) { b1 = base64AlphabetLookup(base64Data[dataIndex++]); b2 = base64AlphabetLookup(base64Data[dataIndex++]); b3 = base64AlphabetLookup(base64Data[dataIndex++]); b4 = base64AlphabetLookup(base64Data[dataIndex++]); // b1 b2 b3 b4 // +---------+---------+---------+--------+ // |00 543210|0054 3210|005432 10|00543210| // +---------+---------+---------+--------+ // |^^^^^^ ^^|^^^^ ^^^^|^^ ^^^^^^| // d1 d2 d3 decodedData[encodedIndex++] = (byte) (b1 << 2 | b2 >> 4); // d1 decodedData[encodedIndex++] = (byte) (b2 << 4 | b3 >> 2); // d2 decodedData[encodedIndex++] = (byte) (b3 << 6 | b4); // d3 } // read last chunk b1 = base64AlphabetLookup(base64Data[dataIndex++]); b2 = base64AlphabetLookup(base64Data[dataIndex++]); final char beforeLastChar = base64Data[dataIndex++]; final char lastChar = base64Data[dataIndex++]; if (isData((beforeLastChar)) && isData((lastChar))) //No PAD e.g 3cQl { // b1 b2 b3 b4 // +---------+---------+---------+--------+ // |00 543210|0054 3210|005432 10|00543210| // +---------+---------+---------+--------+ // |^^^^^^ ^^|^^^^ ^^^^|^^ ^^^^^^| // d1 d2 d3 b3 = BASE64_ALPHABET[beforeLastChar]; b4 = BASE64_ALPHABET[lastChar]; decodedData[encodedIndex++] = (byte) (b1 << 2 | b2 >> 4); // d1 decodedData[encodedIndex++] = (byte) (b2 << 4 | b3 >> 2); // d2 decodedData[encodedIndex++] = (byte) (b3 << 6 | b4); // d3 result = decodedData; } else { final int decodedDataLength = encodedIndex; // Check if they are PAD character(s) if (isPad(beforeLastChar) && isPad(lastChar)) { // Two PAD e.g. 3c[Pad][Pad] assertLastFourBitsZero(encoded, b2); final byte[] tmp = new byte[decodedDataLength + 1]; System.arraycopy(decodedData, 0, tmp, 0, decodedDataLength); tmp[encodedIndex] = (byte) (b1 << 2 | b2 >> 4); result = tmp; } else if (isData(beforeLastChar) && isPad(lastChar)) { // One PAD e.g. 3cQ[Pad] b3 = BASE64_ALPHABET[beforeLastChar]; assertLastTwoBitsZero(encoded, b3); final byte[] tmp = new byte[decodedDataLength + 2]; System.arraycopy(decodedData, 0, tmp, 0, decodedDataLength); tmp[encodedIndex++] = (byte) (b1 << 2 | b2 >> 4); tmp[encodedIndex] = (byte) (b2 << 4 | b3 >> 2); result = tmp; } else { // an error like "3c[Pad]r", "3cdX", "3cXd", "3cXX" // where X is non data throw new ArgumentMalformedException(ENCODED_PARAMETER, encoded, "At least one of the last 2 characters '" + new StringBuffer().append(beforeLastChar).append(lastChar) + "' are not a valid Base64 [padding] character"); } } return result; } private static void assertLastFourBitsZero (String encoded, byte b) { if ((b & 0xf) != 0) // last 4 bits should be zero { throw new ArgumentMalformedException(ENCODED_PARAMETER, encoded, "Last 4 bits should be zero of the last " + "non-padding character '" + Integer.toHexString(b) + "'"); } } private static void assertLastTwoBitsZero (String encoded, byte b) { if ((b & 0x3) != 0) // last 2 bits should be zero { throw new ArgumentMalformedException(ENCODED_PARAMETER, encoded, "Last 2 bits should be zero of the last " + "non-padding character '" + Integer.toHexString(b) + "'"); } } private static byte base64AlphabetLookup (char octect) { if (!isData(octect)) { throw new ArgumentMalformedException("octect", Character.toString(octect), "Illegal Base64 character '" + octect + "'"); } return BASE64_ALPHABET[octect]; } private static boolean isPad (char octect) { return (octect == PAD); } private static boolean isData (char octect) { return (BASE64_ALPHABET[octect] != -1); } }