//********************************************************* // // Copyright (c) Microsoft. All rights reserved. // This code is licensed under the Apache License Version 2.0. // THIS CODE IS PROVIDED *AS IS* WITHOUT WARRANTY OF // ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING ANY // IMPLIED WARRANTIES OF FITNESS FOR A PARTICULAR // PURPOSE, MERCHANTABILITY, OR NON-INFRINGEMENT. // //********************************************************* package com.microsoft.uprove; import java.io.EOFException; import java.io.IOException; import java.io.Writer; /** * Provides base64 encoding/decoding. See * <a href="http://www.ietf.org/rfc/rfc2045.txt?number=2045">RFC2045</a> * for details about base64. * * @see <a href="http://www.ietf.org/rfc/rfc2045.txt" target="_top">RFC2045</a> */ final class Base64 { /** * A simple interface to allow the implementation of Base64.encode() to * efficiently support writing to a variety of data sinks. * */ private interface EncodeTarget { /** * Write an array of characters to the data sink. * @param chars an array of characters. * @throws IOException if an error occurs while encoding. */ void append(char[] chars) throws IOException; /** * Write part of an array of characters to the data sink. * @param chars an array of characters. * @param off offset into the array. * @param len number of characters to write. * @throws IOException if an error occurs while encoding. */ void append(char[] chars, int off, int len) throws IOException; } /** * Identifier of the standard base64 alphabet. */ public static final int ALPHA_B64 = 0; /** * Identifier of the base64url (URL and filename safe ) alphabet. */ public static final int ALPHA_B64URL = 1; /** * Default line separator as specified in RFC2045. */ private static final String LINE_SEP = "\r\n"; /** * Default line length as given in RFC2045. */ private static final int LINE_LEN = 76; /** * The base64 translation table. */ private static final char[] B64_ALPHABET = { 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '+', '/' }; private static final int[] B64_MAP = { 62, -1, -1, -1, 63, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, -1, -1, -1, -1, -1, -1, -1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, -1, -1, -1, -1, -1, -1, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51 }; private static final char B64_MAP_LOW_CHAR = '+'; private static final int[] B64URL_MAP = { 62, -1, -1, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, -1, -1, -1, -1, -1, -1, -1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, -1, -1, -1, -1, 63, -1, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51 }; private static final char B64URL_MAP_LOW_CHAR = '-'; /** * The base64url translation table. */ private static final char[] B64URL_ALPHABET = { 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '-', '_' }; // states for the decoder private static final int STATE_START = 0; private static final int STATE_R1 = 1; private static final int STATE_R2 = 2; private static final int STATE_R2OE = 3; private static final int STATE_R3 = 4; private static final int STATE_R3OE = 5; private static final int STATE_R3E = 6; /** * private constructor, prevents instantiation. */ private Base64() { super(); } /** * Base64-encodes the given buffer into the given target. * @param buffer a byte array. * @param target a target where the encoded data will be written. * @param noPad <code>true</code> to skip the padding. * @throws IOException */ private static void encode(final char[] alphabet, final byte[] buffer, final EncodeTarget target, final boolean noPad) throws IOException { final int n = buffer.length; final char[] encoded = new char[4]; int state = 0; int index = 0; for (int i = 0; i < n; i++) { int c = buffer[i] & 0xFF; // Take three bytes of input = 24 bits // Split it into 4 chunks of six bits each // Treat these chunks as indices into the // base64 table above. switch (state) { case 0: encoded[0] = alphabet[(c >> 2) & 0x3F]; index = (c << 4) & 0x30; break; case 1: encoded[1] = alphabet[index | ((c >> 4) & 0x0F)]; index = (c << 2) & 0x3C; break; case 2: encoded[2] = alphabet[index | ((c >> 6) & 0x03)]; encoded[3] = alphabet[c & 0x3F]; target.append(encoded); break; default: throw new AssertionError("Bogus switch value"); } state = (state + 1) % 3; } // Complete the string with zero bits // and pad with "=" characters as necessary switch (state) { case 0: // No padding necessary break; case 1: encoded[1] = alphabet[index]; if (noPad) { target.append(encoded, 0, 2); } else { encoded[2] = '='; encoded[3] = '='; target.append(encoded); } break; case 2: encoded[2] = alphabet[index]; if (noPad) { target.append(encoded, 0, 3); } else { encoded[3] = '='; target.append(encoded); } break; default: throw new AssertionError("Bogus switch value"); } } /** * Returns a base64-encoded string of <code>buffer</code>. * * @param alphabet either <code>ALPHA_B64</code> for the standard base64 * encoding or <code>ALPHA_B64URL</code> for the URL and filename safe * encoding. * @param buffer Buffer to encode. * @param noPad <code>true</code> to omit final padding characters. * @return Base64-encoded string of <code>buffer</code>. */ public static String encode(final int alphabet, final byte[] buffer, final boolean noPad) { if (alphabet != ALPHA_B64 && alphabet != ALPHA_B64URL) { throw new IllegalArgumentException("invalid alphabet"); } if (buffer == null) { throw new IllegalArgumentException("buffer is null"); } // calculate the resulting string length. final int strLen = ((buffer.length + 2) / 3) * 4; final StringBuffer sb = new StringBuffer(strLen); try { encode(alphabet == ALPHA_B64 ? B64_ALPHABET : B64URL_ALPHABET, buffer, new EncodeTarget() { public void append(final char[] chars) { sb.append(chars); } public void append(final char[] chars, final int off, final int len) { sb.append(chars, off, len); } }, noPad); } catch (IOException e) { // the exception will never be thrown from // StringBuffer#append(char[]) AssertionError ae = new AssertionError("Impossible exception"); ae.initCause(e); throw ae; } return sb.toString(); } /** * Returns a base64-encoded string of <code>buffer</code>. * * @param buffer * Buffer to encode. * @return Base64-encoded string of <code>buffer</code>. */ public static String encode(final byte[] buffer) { return encode(ALPHA_B64, buffer, false); } /** * Base64-encodes a buffer, writing the results to a java.io.Writer. * @param buffer the data to encode. * @param w the writer to which the encoded data is to be written. * @throws IOException if there is an error while writing to the Writer. */ public static void encode(final byte[] buffer, final Writer w) throws IOException { if (buffer == null) { throw new IllegalArgumentException("buffer is null"); } if (w == null) { throw new IllegalArgumentException("writer is null"); } encode(B64_ALPHABET, buffer, new EncodeTarget() { public void append(final char[] chars) throws IOException { w.write(chars); } public void append(final char[] chars, final int off, final int len) throws IOException { w.write(chars, off, len); } }, false); } /** * Returns a base64-encoded string of <code>buffer</code>. * * Base64 encode and split results over multiple lines according to the * specifications given in RFC-2045 (76 char max per line) but using * specified lineSep as line delimiter. * * @param buffer * Buffer to encode * @param lineSep * Line separator to use * @return Base64-encoded string of <code>buffer</code>. */ public static String encode(final byte[] buffer, final String lineSep) { return splitToMultLine(encode(buffer), LINE_LEN, lineSep); } /** * Returns a base64-encoded string of <code>buffer</code>. * * Base64 encode and split results over multiple lines according to the * specifications given in RFC-2045 (using CRLF as line separator) but * using with a given (lineLen) line length (instead of default one). * * @param buffer Buffer to encode * @param lineLen Maximum length for a line * @return Base64-encoded string of <code>buffer</code>. */ public static String encode(final byte[] buffer, final int lineLen) { return splitToMultLine(encode(buffer), lineLen, LINE_SEP); } /** * Returns a base64-encoded string of <code>buffer</code>. * * Base64 encode and split results over multiple lines according to the * specifications given in RFC-2045 but using specified lineSep as line * delimiter and using specified lineLen for the length of a line (max char * per line). * * @param buffer * Buffer to encode * @param lineSep * Line separator to use * @param lineLen * Maximum length for a line * @return Base64-encoded string of <code>buffer</code>. */ public static String encode(final byte[] buffer, final String lineSep, final int lineLen) { return splitToMultLine(encode(buffer), lineLen, lineSep); } /** * Returns the 6-bit value corresponding to a character in the base64 * alphabet. <b>Note: </b> the pad character ('=') is considered to have * the value 0. * * @param ch a character in the base64 alphabet. * @return the 6-bit value. * @throws ArrayIndexOutOfBoundsException if <code>ch</code> is not in the * alphabet. */ private static int decodeChar(final int alphabet, final char ch) { final int retVal = alphabet == ALPHA_B64 ? B64_MAP[ch - B64_MAP_LOW_CHAR] : B64URL_MAP[ch - B64URL_MAP_LOW_CHAR]; if (retVal == -1) { throw new ArrayIndexOutOfBoundsException(); } return retVal; } /** * Returns the decoding of the encoded <code>base64String</code>. * Data is read from the source until either the end of the base64 data has * been reached (one or two pad chars are read) or all data from the source * has been read. * * @param alphabet either <code>ALPHA_B64</code> for the standard base64 * encoding or <code>ALPHA_B64URL</code> for the URL and filename safe * encoding. * @param base64String * Base64-encoded string. * @param lenientPad <code>true</code> to allow the padding to be omitted. * @return Decoding of the base64-encoded <code>base64String</code>. * @throws EOFException if the end of <code>base64String</code> is reached * before decoding is complete. * @throws IOException if the buffer contains invalid data. */ public static byte[] decode(final int alphabet, final CharSequence base64String, final boolean lenientPad) throws IOException { if (alphabet != ALPHA_B64 && alphabet != ALPHA_B64URL) { throw new IllegalArgumentException("invalid alphabet"); } final int inLen = base64String.length(); // make an upper-bound calculation for the resulting byte array size. // due to whitespace in the string, this may be larger than needed. final int upperBound = ((inLen + 2) / 4) * 3; byte[] buffer = new byte[upperBound]; int idxIn = 0; int idxOut = 0; char ch = 0; int state = STATE_START; int i = 0; /* note: RFC-2045 suggests that invalid characters may be ignored, but * let's be more strict. */ // hand-built DFA try { parser: while (true) { if (idxIn < inLen) { ch = base64String.charAt(idxIn++); if (Character.isWhitespace(ch)) { // stay in the current state continue; } } else { // handle EOF here outside of the main switch below switch (state) { case STATE_R1: // expect second character case STATE_R2: // expect third character case STATE_R3: // expect fourth character case STATE_R3E: // expect second pad throw new EOFException(); case STATE_R2OE: // expect third character, pad, or end case STATE_R3OE: // expect fourth character, pad, or end if (!lenientPad) { // missing required pad character throw new EOFException(); } // FALL THROUGH default: assert state == STATE_START || state == STATE_R2OE || state == STATE_R3OE; break parser; // we've read everything } } // either eof or we've read a non-whitespace character switch (state) { case STATE_START: // we're at the start i = decodeChar(alphabet, ch) << 18; state = STATE_R1; // we've read the first char break; case STATE_R1: // we've read the first char i |= decodeChar(alphabet, ch) << 12; // push out the first octet buffer[idxOut++] = (byte) (i >> 16); state = ((short) i) == 0 ? STATE_R2OE // we've read two and may be at end : STATE_R2; // we've read the second char break; case STATE_R2: // we've read the second char i |= decodeChar(alphabet, ch) << 6; // push out the second octet buffer[idxOut++] = (byte) (i >> 8); state = ((byte) i) == 0 ? STATE_R3OE // we've read three and may be at end : STATE_R3; // we've read the third char break; case STATE_R2OE: // we've read two and may be at the end if (ch == '=') { state = STATE_R3E; // we've read two and a pad } else { i |= decodeChar(alphabet, ch) << 6; // push out the second octet buffer[idxOut++] = (byte) (i >> 8); state = ((byte) i) == 0 ? STATE_R3OE // we've read three; may be at end : STATE_R3; // we've read the third char } break; case STATE_R3: // we've read the third char i |= decodeChar(alphabet, ch); // push out the third octet buffer[idxOut++] = (byte) i; // get ready to start again state = STATE_START; break; case STATE_R3OE: // we've read three and may be at the end if (ch == '=') { break parser; // we've read three and a pad } i |= decodeChar(alphabet, ch); // push out the third octet buffer[idxOut++] = (byte) i; // get ready to start again state = STATE_START; break; case STATE_R3E: // we've read two and a pad if (ch != '=') { // rethrown with a proper message below throw new ArrayIndexOutOfBoundsException(); } break parser; // we've read two chars and two pads default: throw new AssertionError("impossible state"); } } } catch (final ArrayIndexOutOfBoundsException e) { // decodeChar wasn't pleased throw new IOException("invalid character at position " + (idxIn - 1)); } if (buffer.length == idxOut) { // we allocated exactly what was needed return buffer; } final byte[] retVal = new byte[idxOut]; System.arraycopy(buffer, 0, retVal, 0, idxOut); return retVal; } /** * Returns the decoding of the base64-encoded <code>base64String</code>. * Data is read from the source until either the end of the base64 data has * been reached (one or two pad chars are read) or all data from the source * has been read. * * @param base64String * Base64-encoded string. * @return Decoding of the base64-encoded <code>base64String</code>. * @throws EOFException if the end of <code>base64String</code> is reached * before decoding is complete. * @throws IOException if the buffer contains invalid data. */ public static byte[] decode(final CharSequence base64String) throws IOException { // validate argument if (base64String == null) { throw new IllegalArgumentException("base64String is null"); } return decode(ALPHA_B64, base64String, false); } /** * Split the string in multiples lines. * * Split over multiple lines or lineLen characters each, using * lineSep as the line separator. * * @param s String to split * @param lineLen Line length * @param lineSep Line separator string (usually CRLF, ...) * @return resulting string split over multiple lines */ private static String splitToMultLine(final String s, final int lineLen, final String lineSep) { // validate arguments if (lineLen < 1) { throw new IllegalArgumentException( "line len is negative or equals 0"); } if (lineSep == null) { throw new IllegalArgumentException( "line separator (lineSep) is null"); } final StringBuffer res = new StringBuffer(); final int origSize = s.length(); for (int index = 0; index < origSize; index += lineLen) { int offset = ((index + lineLen) > origSize) ? (index + (origSize % lineLen)) : (index + lineLen); res.append(s.substring(index, offset)); // append end of line (if not at the last line) if (offset < origSize) { res.append(lineSep); } } return res.toString(); } }