package com.hwlcn.ldap.util; import com.hwlcn.core.annotation.ThreadSafety; import java.io.IOException; import java.text.ParseException; import static com.hwlcn.ldap.util.UtilityMessages.*; import static com.hwlcn.ldap.util.Validator.*; /** * This class provides methods for encoding and decoding data in base64 as * defined in <A HREF="http://www.ietf.org/rfc/rfc4648.txt">RFC 4648</A>. It * provides a relatively compact way of representing binary data using only * printable characters. It uses a six-bit encoding mechanism in which every * three bytes of raw data is converted to four bytes of base64-encoded data, * which means that it only requires about a 33% increase in size (as compared * with a hexadecimal representation, which requires a 100% increase in size). * <BR><BR> * Base64 encoding is used in LDIF processing as per * <A HREF="http://www.ietf.org/rfc/rfc2849.txt">RFC 2849</A> to represent data * that contains special characters or might otherwise be ambiguous. It is also * used in a number of other areas (e.g., for the ASCII representation of * certificates) where it is desirable to deal with a string containing only * printable characters but the raw data may contain other characters outside of * that range. * <BR><BR> * This class also provides support for the URL-safe variant (called base64url) * as described in RFC 4648 section 5. This is nearly the same as base64, * except that the '+' and '/' characters are replaced with '-' and '_', * respectively. The padding may be omitted if the context makes the data size * clear, but if padding is to be used then the URL-encoded "%3d" will be used * instead of "=". * <BR><BR> * <H2>Example</H2> * The following examples demonstrate the process for base64-encoding raw data, * and for decoding a string containing base64-encoded data back to the raw * data used to create it: * <PRE> * // Base64-encode some raw data: * String base64String = Base64.encode(rawDataBytes); * System.out.println("Base64 encoded representation of the raw data is " + * base64String); * * // Decode a base64 string back to raw data: * try * { * byte[] decodedRawDataBytes = Base64.decode(base64String); * } * catch (ParseException pe) * { * System.err.println("The string did not contain valid base64-encoded " + * "data: " + pe.getMessage()); * } * </PRE> */ @ThreadSafety(level=ThreadSafetyLevel.COMPLETELY_THREADSAFE) public final class Base64 { private static final char[] BASE64_ALPHABET = ("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz" + "0123456789+/").toCharArray(); private static final char[] BASE64URL_ALPHABET = ("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz" + "0123456789-_").toCharArray(); private Base64() { } public static String encode(final String data) { ensureNotNull(data); return encode(StaticUtils.getBytes(data)); } public static String encode(final byte[] data) { ensureNotNull(data); final StringBuilder buffer = new StringBuilder(4*data.length/3+1); encode(BASE64_ALPHABET, data, 0, data.length, buffer, "="); return buffer.toString(); } public static void encode(final String data, final StringBuilder buffer) { ensureNotNull(data); encode(StaticUtils.getBytes(data), buffer); } public static void encode(final String data, final ByteStringBuffer buffer) { ensureNotNull(data); encode(StaticUtils.getBytes(data), buffer); } public static void encode(final byte[] data, final StringBuilder buffer) { encode(BASE64_ALPHABET, data, 0, data.length, buffer, "="); } public static void encode(final byte[] data, final int off, final int length, final StringBuilder buffer) { encode(BASE64_ALPHABET, data, off, length, buffer, "="); } public static void encode(final byte[] data, final ByteStringBuffer buffer) { encode(BASE64_ALPHABET, data, 0, data.length, buffer, "="); } public static void encode(final byte[] data, final int off, final int length, final ByteStringBuffer buffer) { encode(BASE64_ALPHABET, data, off, length, buffer, "="); } public static String urlEncode(final String data, final boolean pad) { return urlEncode(StaticUtils.getBytes(data), pad); } public static void urlEncode(final String data, final StringBuilder buffer, final boolean pad) { final byte[] dataBytes = StaticUtils.getBytes(data); encode(BASE64_ALPHABET, dataBytes, 0, dataBytes.length, buffer, (pad ? "%3d" : null)); } public static void urlEncode(final String data, final ByteStringBuffer buffer, final boolean pad) { final byte[] dataBytes = StaticUtils.getBytes(data); encode(BASE64_ALPHABET, dataBytes, 0, dataBytes.length, buffer, (pad ? "%3d" : null)); } public static String urlEncode(final byte[] data, final boolean pad) { final StringBuilder buffer = new StringBuilder(4*data.length/3+6); encode(BASE64URL_ALPHABET, data, 0, data.length, buffer, (pad ? "%3d" : null)); return buffer.toString(); } public static void urlEncode(final byte[] data, final int off, final int length, final StringBuilder buffer, final boolean pad) { encode(BASE64URL_ALPHABET, data, off, length, buffer, (pad ? "%3d" : null)); } public static void urlEncode(final byte[] data, final int off, final int length, final ByteStringBuffer buffer, final boolean pad) { encode(BASE64URL_ALPHABET, data, off, length, buffer, (pad ? "%3d" : null)); } private static void encode(final char[] alphabet, final byte[] data, final int off, final int length, final Appendable buffer, final String padStr) { ensureNotNull(data); ensureTrue(data.length >= off); ensureTrue(data.length >= (off+length)); if (length == 0) { return; } try { int pos = off; for (int i=0; i < (length / 3); i++) { final int intValue = ((data[pos++] & 0xFF) << 16) | ((data[pos++] & 0xFF) << 8) | (data[pos++] & 0xFF); buffer.append(alphabet[(intValue >> 18) & 0x3F]); buffer.append(alphabet[(intValue >> 12) & 0x3F]); buffer.append(alphabet[(intValue >> 6) & 0x3F]); buffer.append(alphabet[intValue & 0x3F]); } switch ((off+length) - pos) { case 1: int intValue = (data[pos] & 0xFF) << 16; buffer.append(alphabet[(intValue >> 18) & 0x3F]); buffer.append(alphabet[(intValue >> 12) & 0x3F]); if (padStr != null) { buffer.append(padStr); buffer.append(padStr); } return; case 2: intValue = ((data[pos++] & 0xFF) << 16) | ((data[pos] & 0xFF) << 8); buffer.append(alphabet[(intValue >> 18) & 0x3F]); buffer.append(alphabet[(intValue >> 12) & 0x3F]); buffer.append(alphabet[(intValue >> 6) & 0x3F]); if (padStr != null) { buffer.append(padStr); } return; } } catch (final IOException ioe) { Debug.debugException(ioe); // This should never happen. throw new RuntimeException(ioe.getMessage(), ioe); } } public static byte[] decode(final String data) throws ParseException { ensureNotNull(data); final int length = data.length(); if (length == 0) { return new byte[0]; } if ((length % 4) != 0) { throw new ParseException(ERR_BASE64_DECODE_INVALID_LENGTH.get(), length); } int numBytes = 3 * (length / 4); if (data.charAt(length-2) == '=') { numBytes -= 2; } else if (data.charAt(length-1) == '=') { numBytes--; } final byte[] b = new byte[numBytes]; int stringPos = 0; int arrayPos = 0; while (stringPos < length) { int intValue = 0x00; for (int i=0; i < 4; i++) { intValue <<= 6; switch (data.charAt(stringPos++)) { case 'A': intValue |= 0x00; break; case 'B': intValue |= 0x01; break; case 'C': intValue |= 0x02; break; case 'D': intValue |= 0x03; break; case 'E': intValue |= 0x04; break; case 'F': intValue |= 0x05; break; case 'G': intValue |= 0x06; break; case 'H': intValue |= 0x07; break; case 'I': intValue |= 0x08; break; case 'J': intValue |= 0x09; break; case 'K': intValue |= 0x0A; break; case 'L': intValue |= 0x0B; break; case 'M': intValue |= 0x0C; break; case 'N': intValue |= 0x0D; break; case 'O': intValue |= 0x0E; break; case 'P': intValue |= 0x0F; break; case 'Q': intValue |= 0x10; break; case 'R': intValue |= 0x11; break; case 'S': intValue |= 0x12; break; case 'T': intValue |= 0x13; break; case 'U': intValue |= 0x14; break; case 'V': intValue |= 0x15; break; case 'W': intValue |= 0x16; break; case 'X': intValue |= 0x17; break; case 'Y': intValue |= 0x18; break; case 'Z': intValue |= 0x19; break; case 'a': intValue |= 0x1A; break; case 'b': intValue |= 0x1B; break; case 'c': intValue |= 0x1C; break; case 'd': intValue |= 0x1D; break; case 'e': intValue |= 0x1E; break; case 'f': intValue |= 0x1F; break; case 'g': intValue |= 0x20; break; case 'h': intValue |= 0x21; break; case 'i': intValue |= 0x22; break; case 'j': intValue |= 0x23; break; case 'k': intValue |= 0x24; break; case 'l': intValue |= 0x25; break; case 'm': intValue |= 0x26; break; case 'n': intValue |= 0x27; break; case 'o': intValue |= 0x28; break; case 'p': intValue |= 0x29; break; case 'q': intValue |= 0x2A; break; case 'r': intValue |= 0x2B; break; case 's': intValue |= 0x2C; break; case 't': intValue |= 0x2D; break; case 'u': intValue |= 0x2E; break; case 'v': intValue |= 0x2F; break; case 'w': intValue |= 0x30; break; case 'x': intValue |= 0x31; break; case 'y': intValue |= 0x32; break; case 'z': intValue |= 0x33; break; case '0': intValue |= 0x34; break; case '1': intValue |= 0x35; break; case '2': intValue |= 0x36; break; case '3': intValue |= 0x37; break; case '4': intValue |= 0x38; break; case '5': intValue |= 0x39; break; case '6': intValue |= 0x3A; break; case '7': intValue |= 0x3B; break; case '8': intValue |= 0x3C; break; case '9': intValue |= 0x3D; break; case '+': intValue |= 0x3E; break; case '/': intValue |= 0x3F; break; case '=': switch (length - stringPos) { case 0: intValue >>= 8; b[arrayPos++] = (byte) ((intValue >> 8) & 0xFF); b[arrayPos] = (byte) (intValue & 0xFF); return b; case 1: intValue >>= 10; b[arrayPos] = (byte) (intValue & 0xFF); return b; default: throw new ParseException(ERR_BASE64_DECODE_UNEXPECTED_EQUAL.get( (stringPos-1)), (stringPos-1)); } default: throw new ParseException(ERR_BASE64_DECODE_UNEXPECTED_CHAR.get( data.charAt(stringPos-1)), (stringPos-1)); } } b[arrayPos++] = (byte) ((intValue >> 16) & 0xFF); b[arrayPos++] = (byte) ((intValue >> 8) & 0xFF); b[arrayPos++] = (byte) (intValue & 0xFF); } return b; } public static String decodeToString(final String data) throws ParseException { ensureNotNull(data); final byte[] decodedBytes = decode(data); return StaticUtils.toUTF8String(decodedBytes); } public static byte[] urlDecode(final String data) throws ParseException { ensureNotNull(data); final int length = data.length(); if (length == 0) { return new byte[0]; } int stringPos = 0; final ByteStringBuffer buffer = new ByteStringBuffer(length); decodeLoop: while (stringPos < length) { int intValue = 0x00; for (int i=0; i < 4; i++) { final char c; if (stringPos >= length) { c = '='; stringPos++; } else { c = data.charAt(stringPos++); } intValue <<= 6; switch (c) { case 'A': intValue |= 0x00; break; case 'B': intValue |= 0x01; break; case 'C': intValue |= 0x02; break; case 'D': intValue |= 0x03; break; case 'E': intValue |= 0x04; break; case 'F': intValue |= 0x05; break; case 'G': intValue |= 0x06; break; case 'H': intValue |= 0x07; break; case 'I': intValue |= 0x08; break; case 'J': intValue |= 0x09; break; case 'K': intValue |= 0x0A; break; case 'L': intValue |= 0x0B; break; case 'M': intValue |= 0x0C; break; case 'N': intValue |= 0x0D; break; case 'O': intValue |= 0x0E; break; case 'P': intValue |= 0x0F; break; case 'Q': intValue |= 0x10; break; case 'R': intValue |= 0x11; break; case 'S': intValue |= 0x12; break; case 'T': intValue |= 0x13; break; case 'U': intValue |= 0x14; break; case 'V': intValue |= 0x15; break; case 'W': intValue |= 0x16; break; case 'X': intValue |= 0x17; break; case 'Y': intValue |= 0x18; break; case 'Z': intValue |= 0x19; break; case 'a': intValue |= 0x1A; break; case 'b': intValue |= 0x1B; break; case 'c': intValue |= 0x1C; break; case 'd': intValue |= 0x1D; break; case 'e': intValue |= 0x1E; break; case 'f': intValue |= 0x1F; break; case 'g': intValue |= 0x20; break; case 'h': intValue |= 0x21; break; case 'i': intValue |= 0x22; break; case 'j': intValue |= 0x23; break; case 'k': intValue |= 0x24; break; case 'l': intValue |= 0x25; break; case 'm': intValue |= 0x26; break; case 'n': intValue |= 0x27; break; case 'o': intValue |= 0x28; break; case 'p': intValue |= 0x29; break; case 'q': intValue |= 0x2A; break; case 'r': intValue |= 0x2B; break; case 's': intValue |= 0x2C; break; case 't': intValue |= 0x2D; break; case 'u': intValue |= 0x2E; break; case 'v': intValue |= 0x2F; break; case 'w': intValue |= 0x30; break; case 'x': intValue |= 0x31; break; case 'y': intValue |= 0x32; break; case 'z': intValue |= 0x33; break; case '0': intValue |= 0x34; break; case '1': intValue |= 0x35; break; case '2': intValue |= 0x36; break; case '3': intValue |= 0x37; break; case '4': intValue |= 0x38; break; case '5': intValue |= 0x39; break; case '6': intValue |= 0x3A; break; case '7': intValue |= 0x3B; break; case '8': intValue |= 0x3C; break; case '9': intValue |= 0x3D; break; case '-': intValue |= 0x3E; break; case '_': intValue |= 0x3F; break; case '=': case '%': switch ((stringPos-1) % 4) { case 2: intValue >>= 10; buffer.append((byte) (intValue & 0xFF)); break decodeLoop; case 3: intValue >>= 8; buffer.append((byte) ((intValue >> 8) & 0xFF)); buffer.append((byte) (intValue & 0xFF)); break decodeLoop; } throw new ParseException(ERR_BASE64_URLDECODE_INVALID_LENGTH.get(), (stringPos-1)); default: throw new ParseException( ERR_BASE64_DECODE_UNEXPECTED_CHAR.get( data.charAt(stringPos-1)), (stringPos-1)); } } buffer.append((byte) ((intValue >> 16) & 0xFF)); buffer.append((byte) ((intValue >> 8) & 0xFF)); buffer.append((byte) (intValue & 0xFF)); } return buffer.toByteArray(); } public static String urlDecodeToString(final String data) throws ParseException { ensureNotNull(data); final byte[] decodedBytes = urlDecode(data); return StaticUtils.toUTF8String(decodedBytes); } }