diff --git a/extensions/guacamole-auth-duo/src/main/java/com/duosecurity/duoweb/Base64.java b/extensions/guacamole-auth-duo/src/main/java/com/duosecurity/duoweb/Base64.java deleted file mode 100644 index 8f254778b..000000000 --- a/extensions/guacamole-auth-duo/src/main/java/com/duosecurity/duoweb/Base64.java +++ /dev/null @@ -1,1500 +0,0 @@ -package com.duosecurity.duoweb; - -public class Base64 { - - /* ******** P U B L I C F I E L D S ******** */ - - /** No options specified. Value is zero. */ - public final static int NO_OPTIONS = 0; - - /** Specify encoding in first bit. Value is one. */ - public final static int ENCODE = 1; - - /** Specify decoding in first bit. Value is zero. */ - public final static int DECODE = 0; - - /** Specify that data should be gzip-compressed in second bit. Value is two. */ - public final static int GZIP = 2; - - /** - * Specify that gzipped data should not be automatically gunzipped. - */ - public final static int DONT_GUNZIP = 4; - - /** Do break lines when encoding. Value is 8. */ - public final static int DO_BREAK_LINES = 8; - - /** - * Encode using Base64-like encoding that is URL- and Filename-safe as - * described in Section 4 of RFC3548: http://www.faqs.org/rfcs/rfc3548.html. It is important to note that - * data encoded this way is not officially valid Base64, or at the - * very least should not be called Base64 without also specifying that is - * was encoded using the URL- and Filename-safe dialect. - */ - public final static int URL_SAFE = 16; - - /** - * Encode using the special "ordered" dialect of Base64 described here: http://www.faqs.org/qa/rfcc- - * 1940.html. - */ - public final static int ORDERED = 32; - - /* ******** P R I V A T E F I E L D S ******** */ - - /** Maximum line length (76) of Base64 output. */ - private final static int MAX_LINE_LENGTH = 76; - - /** The equals sign (=) as a byte. */ - private final static byte EQUALS_SIGN = (byte) '='; - - /** The new line character (\n) as a byte. */ - private final static byte NEW_LINE = (byte) '\n'; - - /** Preferred encoding. */ - private final static String PREFERRED_ENCODING = "US-ASCII"; - - private final static byte WHITE_SPACE_ENC = -5; // Indicates white space in - // encoding - private final static byte EQUALS_SIGN_ENC = -1; // Indicates equals sign in - // encoding - - /* ******** S T A N D A R D B A S E 6 4 A L P H A B E T ******** */ - - /** The 64 valid Base64 values. */ - /* - * Host platform me be something funny like EBCDIC, so we hardcode these - * values. - */ - private final static byte[] _STANDARD_ALPHABET = { (byte) 'A', (byte) 'B', - (byte) 'C', (byte) 'D', (byte) 'E', (byte) 'F', (byte) 'G', - (byte) 'H', (byte) 'I', (byte) 'J', (byte) 'K', (byte) 'L', - (byte) 'M', (byte) 'N', (byte) 'O', (byte) 'P', (byte) 'Q', - (byte) 'R', (byte) 'S', (byte) 'T', (byte) 'U', (byte) 'V', - (byte) 'W', (byte) 'X', (byte) 'Y', (byte) 'Z', (byte) 'a', - (byte) 'b', (byte) 'c', (byte) 'd', (byte) 'e', (byte) 'f', - (byte) 'g', (byte) 'h', (byte) 'i', (byte) 'j', (byte) 'k', - (byte) 'l', (byte) 'm', (byte) 'n', (byte) 'o', (byte) 'p', - (byte) 'q', (byte) 'r', (byte) 's', (byte) 't', (byte) 'u', - (byte) 'v', (byte) 'w', (byte) 'x', (byte) 'y', (byte) 'z', - (byte) '0', (byte) '1', (byte) '2', (byte) '3', (byte) '4', - (byte) '5', (byte) '6', (byte) '7', (byte) '8', (byte) '9', - (byte) '+', (byte) '/' }; - - /** - * Translates a Base64 value to either its 6-bit reconstruction value or a - * negative number indicating some other meaning. - **/ - private final static byte[] _STANDARD_DECODABET = { -9, -9, -9, -9, -9, -9, - -9, -9, -9, // Decimal 0 - 8 - -5, -5, // Whitespace: Tab and Linefeed - -9, -9, // Decimal 11 - 12 - -5, // Whitespace: Carriage Return - -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 14 - - // 26 - -9, -9, -9, -9, -9, // Decimal 27 - 31 - -5, // Whitespace: Space - -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 33 - 42 - 62, // Plus sign at decimal 43 - -9, -9, -9, // Decimal 44 - 46 - 63, // Slash at decimal 47 - 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, // Numbers zero through nine - -9, -9, -9, // Decimal 58 - 60 - -1, // Equals sign at decimal 61 - -9, -9, -9, // Decimal 62 - 64 - 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, // Letters 'A' through - // 'N' - 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, // Letters 'O' - // through 'Z' - -9, -9, -9, -9, -9, -9, // Decimal 91 - 96 - 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, // Letters 'a' - // through 'm' - 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, // Letters 'n' - // through 'z' - -9, -9, -9, -9, -9 // Decimal 123 - 127 - , -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 128 - - // 139 - -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 140 - - // 152 - -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 153 - - // 165 - -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 166 - - // 178 - -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 179 - - // 191 - -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 192 - - // 204 - -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 205 - - // 217 - -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 218 - - // 230 - -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 231 - - // 243 - -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9 // Decimal 244 - 255 - }; - - /* ******** U R L S A F E B A S E 6 4 A L P H A B E T ******** */ - - /** - * Used in the URL- and Filename-safe dialect described in Section 4 of - * RFC3548: http://www.faqs.org - * /rfcs/rfc3548.html. Notice that the last two bytes become "hyphen" - * and "underscore" instead of "plus" and "slash." - */ - private final static byte[] _URL_SAFE_ALPHABET = { (byte) 'A', (byte) 'B', - (byte) 'C', (byte) 'D', (byte) 'E', (byte) 'F', (byte) 'G', - (byte) 'H', (byte) 'I', (byte) 'J', (byte) 'K', (byte) 'L', - (byte) 'M', (byte) 'N', (byte) 'O', (byte) 'P', (byte) 'Q', - (byte) 'R', (byte) 'S', (byte) 'T', (byte) 'U', (byte) 'V', - (byte) 'W', (byte) 'X', (byte) 'Y', (byte) 'Z', (byte) 'a', - (byte) 'b', (byte) 'c', (byte) 'd', (byte) 'e', (byte) 'f', - (byte) 'g', (byte) 'h', (byte) 'i', (byte) 'j', (byte) 'k', - (byte) 'l', (byte) 'm', (byte) 'n', (byte) 'o', (byte) 'p', - (byte) 'q', (byte) 'r', (byte) 's', (byte) 't', (byte) 'u', - (byte) 'v', (byte) 'w', (byte) 'x', (byte) 'y', (byte) 'z', - (byte) '0', (byte) '1', (byte) '2', (byte) '3', (byte) '4', - (byte) '5', (byte) '6', (byte) '7', (byte) '8', (byte) '9', - (byte) '-', (byte) '_' }; - - /** - * Used in decoding URL- and Filename-safe dialects of Base64. - */ - private final static byte[] _URL_SAFE_DECODABET = { -9, -9, -9, -9, -9, -9, - -9, -9, -9, // Decimal 0 - 8 - -5, -5, // Whitespace: Tab and Linefeed - -9, -9, // Decimal 11 - 12 - -5, // Whitespace: Carriage Return - -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 14 - - // 26 - -9, -9, -9, -9, -9, // Decimal 27 - 31 - -5, // Whitespace: Space - -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 33 - 42 - -9, // Plus sign at decimal 43 - -9, // Decimal 44 - 62, // Minus sign at decimal 45 - -9, // Decimal 46 - -9, // Slash at decimal 47 - 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, // Numbers zero through nine - -9, -9, -9, // Decimal 58 - 60 - -1, // Equals sign at decimal 61 - -9, -9, -9, // Decimal 62 - 64 - 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, // Letters 'A' through - // 'N' - 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, // Letters 'O' - // through 'Z' - -9, -9, -9, -9, // Decimal 91 - 94 - 63, // Underscore at decimal 95 - -9, // Decimal 96 - 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, // Letters 'a' - // through 'm' - 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, // Letters 'n' - // through 'z' - -9, -9, -9, -9, -9 // Decimal 123 - 127 - , -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 128 - - // 139 - -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 140 - - // 152 - -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 153 - - // 165 - -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 166 - - // 178 - -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 179 - - // 191 - -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 192 - - // 204 - -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 205 - - // 217 - -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 218 - - // 230 - -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 231 - - // 243 - -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9 // Decimal 244 - 255 - }; - - /* ******** O R D E R E D B A S E 6 4 A L P H A B E T ******** */ - - /** - * I don't get the point of this technique, but someone requested it, and it - * is described here: http:// - * www.faqs.org/qa/rfcc-1940.html. - */ - private final static byte[] _ORDERED_ALPHABET = { (byte) '-', (byte) '0', - (byte) '1', (byte) '2', (byte) '3', (byte) '4', (byte) '5', - (byte) '6', (byte) '7', (byte) '8', (byte) '9', (byte) 'A', - (byte) 'B', (byte) 'C', (byte) 'D', (byte) 'E', (byte) 'F', - (byte) 'G', (byte) 'H', (byte) 'I', (byte) 'J', (byte) 'K', - (byte) 'L', (byte) 'M', (byte) 'N', (byte) 'O', (byte) 'P', - (byte) 'Q', (byte) 'R', (byte) 'S', (byte) 'T', (byte) 'U', - (byte) 'V', (byte) 'W', (byte) 'X', (byte) 'Y', (byte) 'Z', - (byte) '_', (byte) 'a', (byte) 'b', (byte) 'c', (byte) 'd', - (byte) 'e', (byte) 'f', (byte) 'g', (byte) 'h', (byte) 'i', - (byte) 'j', (byte) 'k', (byte) 'l', (byte) 'm', (byte) 'n', - (byte) 'o', (byte) 'p', (byte) 'q', (byte) 'r', (byte) 's', - (byte) 't', (byte) 'u', (byte) 'v', (byte) 'w', (byte) 'x', - (byte) 'y', (byte) 'z' }; - - /** - * Used in decoding the "ordered" dialect of Base64. - */ - private final static byte[] _ORDERED_DECODABET = { -9, -9, -9, -9, -9, -9, - -9, -9, -9, // Decimal 0 - 8 - -5, -5, // Whitespace: Tab and Linefeed - -9, -9, // Decimal 11 - 12 - -5, // Whitespace: Carriage Return - -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 14 - - // 26 - -9, -9, -9, -9, -9, // Decimal 27 - 31 - -5, // Whitespace: Space - -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 33 - 42 - -9, // Plus sign at decimal 43 - -9, // Decimal 44 - 0, // Minus sign at decimal 45 - -9, // Decimal 46 - -9, // Slash at decimal 47 - 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, // Numbers zero through nine - -9, -9, -9, // Decimal 58 - 60 - -1, // Equals sign at decimal 61 - -9, -9, -9, // Decimal 62 - 64 - 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, // Letters 'A' - // through 'M' - 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, // Letters 'N' - // through 'Z' - -9, -9, -9, -9, // Decimal 91 - 94 - 37, // Underscore at decimal 95 - -9, // Decimal 96 - 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, // Letters 'a' - // through 'm' - 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, // Letters 'n' - // through 'z' - -9, -9, -9, -9, -9 // Decimal 123 - 127 - , -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 128 - // - 139 - -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 140 - - // 152 - -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 153 - - // 165 - -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 166 - - // 178 - -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 179 - - // 191 - -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 192 - - // 204 - -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 205 - - // 217 - -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 218 - - // 230 - -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 231 - - // 243 - -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9 // Decimal 244 - 255 - }; - - /* ******** D E T E R M I N E W H I C H A L H A B E T ******** */ - - /** - * Returns one of the _SOMETHING_ALPHABET byte arrays depending on the - * options specified. It's possible, though silly, to specify ORDERED - * and URLSAFE in which case one of them will be picked, though there - * is no guarantee as to which one will be picked. - */ - private final static byte[] getAlphabet(int options) { - if ((options & URL_SAFE) == URL_SAFE) { - return _URL_SAFE_ALPHABET; - } else if ((options & ORDERED) == ORDERED) { - return _ORDERED_ALPHABET; - } else { - return _STANDARD_ALPHABET; - } - } // end getAlphabet - - /** - * Returns one of the _SOMETHING_DECODABET byte arrays depending on the - * options specified. It's possible, though silly, to specify ORDERED and - * URL_SAFE in which case one of them will be picked, though there is no - * guarantee as to which one will be picked. - */ - private final static byte[] getDecodabet(int options) { - if ((options & URL_SAFE) == URL_SAFE) { - return _URL_SAFE_DECODABET; - } else if ((options & ORDERED) == ORDERED) { - return _ORDERED_DECODABET; - } else { - return _STANDARD_DECODABET; - } - } // end getAlphabet - - /** Defeats instantiation. */ - private Base64() { - } - - /* ******** E N C O D I N G M E T H O D S ******** */ - - /** - * Encodes up to the first three bytes of array threeBytes and - * returns a four-byte array in Base64 notation. The actual number of - * significant bytes in your array is given by numSigBytes. The - * array threeBytes needs only be as big as - * numSigBytes. Code can reuse a byte array by passing a - * four-byte array as b4. - * - * @param b4 - * A reusable byte array to reduce array instantiation - * @param threeBytes - * the array to convert - * @param numSigBytes - * the number of significant bytes in your array - * @return four byte array in Base64 notation. - * @since 1.5.1 - */ - private static byte[] encode3to4(byte[] b4, byte[] threeBytes, - int numSigBytes, int options) { - encode3to4(threeBytes, 0, numSigBytes, b4, 0, options); - return b4; - } // end encode3to4 - - /** - *
- * Encodes up to three bytes of the array source and writes the - * resulting four Base64 bytes to destination. The source and - * destination arrays can be manipulated anywhere along their length by - * specifying srcOffset and destOffset. This method - * does not check to make sure your arrays are large enough to accomodate - * srcOffset + 3 for the source array or - * destOffset + 4 for the destination array. The - * actual number of significant bytes in your array is given by - * numSigBytes. - *
- *- * This is the lowest level of the encoding methods with all possible - * parameters. - *
- * - * @param source - * the array to convert - * @param srcOffset - * the index where conversion begins - * @param numSigBytes - * the number of significant bytes in your array - * @param destination - * the array to hold the conversion - * @param destOffset - * the index where output will be put - * @return the destination array - * @since 1.3 - */ - private static byte[] encode3to4(byte[] source, int srcOffset, - int numSigBytes, byte[] destination, int destOffset, int options) { - - byte[] ALPHABET = getAlphabet(options); - - // 1 2 3 - // 01234567890123456789012345678901 Bit position - // --------000000001111111122222222 Array position from threeBytes - // --------| || || || | Six bit groups to index ALPHABET - // >>18 >>12 >> 6 >> 0 Right shift necessary - // 0x3f 0x3f 0x3f Additional AND - - // Create buffer with zero-padding if there are only one or two - // significant bytes passed in the array. - // We have to shift left 24 in order to flush out the 1's that appear - // when Java treats a value as negative that is cast from a byte to an - // int. - int inBuff = (numSigBytes > 0 ? ((source[srcOffset] << 24) >>> 8) : 0) - | (numSigBytes > 1 ? ((source[srcOffset + 1] << 24) >>> 16) : 0) - | (numSigBytes > 2 ? ((source[srcOffset + 2] << 24) >>> 24) : 0); - - switch (numSigBytes) { - case 3: - destination[destOffset] = ALPHABET[(inBuff >>> 18)]; - destination[destOffset + 1] = ALPHABET[(inBuff >>> 12) & 0x3f]; - destination[destOffset + 2] = ALPHABET[(inBuff >>> 6) & 0x3f]; - destination[destOffset + 3] = ALPHABET[(inBuff) & 0x3f]; - return destination; - - case 2: - destination[destOffset] = ALPHABET[(inBuff >>> 18)]; - destination[destOffset + 1] = ALPHABET[(inBuff >>> 12) & 0x3f]; - destination[destOffset + 2] = ALPHABET[(inBuff >>> 6) & 0x3f]; - destination[destOffset + 3] = EQUALS_SIGN; - return destination; - - case 1: - destination[destOffset] = ALPHABET[(inBuff >>> 18)]; - destination[destOffset + 1] = ALPHABET[(inBuff >>> 12) & 0x3f]; - destination[destOffset + 2] = EQUALS_SIGN; - destination[destOffset + 3] = EQUALS_SIGN; - return destination; - - default: - return destination; - } // end switch - } // end encode3to4 - - /** - * Performs Base64 encoding on theraw
ByteBuffer, writing it
- * to the encoded
ByteBuffer. This is an experimental feature.
- * Currently it does not pass along any options (such as
- * {@link #DO_BREAK_LINES} or {@link #GZIP}.
- *
- * @param raw
- * input buffer
- * @param encoded
- * output buffer
- * @since 2.3
- */
- public static void encode(java.nio.ByteBuffer raw,
- java.nio.ByteBuffer encoded) {
- byte[] raw3 = new byte[3];
- byte[] enc4 = new byte[4];
-
- while (raw.hasRemaining()) {
- int rem = Math.min(3, raw.remaining());
- raw.get(raw3, 0, rem);
- Base64.encode3to4(enc4, raw3, rem, Base64.NO_OPTIONS);
- encoded.put(enc4);
- } // end input remaining
- }
-
- /**
- * Performs Base64 encoding on the raw
ByteBuffer, writing it
- * to the encoded
CharBuffer. This is an experimental feature.
- * Currently it does not pass along any options (such as
- * {@link #DO_BREAK_LINES} or {@link #GZIP}.
- *
- * @param raw
- * input buffer
- * @param encoded
- * output buffer
- * @since 2.3
- */
- public static void encode(java.nio.ByteBuffer raw,
- java.nio.CharBuffer encoded) {
- byte[] raw3 = new byte[3];
- byte[] enc4 = new byte[4];
-
- while (raw.hasRemaining()) {
- int rem = Math.min(3, raw.remaining());
- raw.get(raw3, 0, rem);
- Base64.encode3to4(enc4, raw3, rem, Base64.NO_OPTIONS);
- for (int i = 0; i < 4; i++) {
- encoded.put((char) (enc4[i] & 0xFF));
- }
- } // end input remaining
- }
-
- /**
- * Serializes an object and returns the Base64-encoded version of that
- * serialized object.
- *
- * - * As of v 2.3, if the object cannot be serialized or there is another - * error, the method will throw an java.io.IOException. This is new to - * v2.3! In earlier versions, it just returned a null value, but in - * retrospect that's a pretty poor way to handle it. - *
- * - * The object is not GZip-compressed before being encoded. - * - * @param serializableObject - * The object to encode - * @return The Base64-encoded object - * @throws java.io.IOException - * if there is an error - * @throws NullPointerException - * if serializedObject is null - * @since 1.4 - */ - public static String encodeObject(java.io.Serializable serializableObject) - throws java.io.IOException { - return encodeObject(serializableObject, NO_OPTIONS); - } // end encodeObject - - /** - * Serializes an object and returns the Base64-encoded version of that - * serialized object. - * - *- * As of v 2.3, if the object cannot be serialized or there is another - * error, the method will throw an java.io.IOException. This is new to - * v2.3! In earlier versions, it just returned a null value, but in - * retrospect that's a pretty poor way to handle it. - *
- * - * The object is not GZip-compressed before being encoded. - *- * Example options: - * - *
- * GZIP: gzip-compresses object before encoding it. - * DO_BREAK_LINES: break lines at 76 characters - *- *
- * Example: encodeObject( myObj, Base64.GZIP )
or
- *
- * Example:
- * encodeObject( myObj, Base64.GZIP | Base64.DO_BREAK_LINES )
- *
- * @param serializableObject
- * The object to encode
- * @param options
- * Specified options
- * @return The Base64-encoded object
- * @see Base64#GZIP
- * @see Base64#DO_BREAK_LINES
- * @throws java.io.IOException
- * if there is an error
- * @since 2.0
- */
- public static String encodeObject(java.io.Serializable serializableObject,
- int options) throws java.io.IOException {
-
- if (serializableObject == null) {
- throw new NullPointerException("Cannot serialize a null object.");
- } // end if: null
-
- // Streams
- java.io.ByteArrayOutputStream baos = null;
- java.io.OutputStream b64os = null;
- java.util.zip.GZIPOutputStream gzos = null;
- java.io.ObjectOutputStream oos = null;
-
- try {
- // ObjectOutputStream -> (GZIP) -> Base64 -> ByteArrayOutputStream
- baos = new java.io.ByteArrayOutputStream();
- b64os = new Base64.OutputStream(baos, ENCODE | options);
- if ((options & GZIP) != 0) {
- // Gzip
- gzos = new java.util.zip.GZIPOutputStream(b64os);
- oos = new java.io.ObjectOutputStream(gzos);
- } else {
- // Not gzipped
- oos = new java.io.ObjectOutputStream(b64os);
- }
- oos.writeObject(serializableObject);
- } // end try
- catch (java.io.IOException e) {
- // Catch it and then throw it immediately so that
- // the finally{} block is called for cleanup.
- throw e;
- } // end catch
- finally {
- try {
- oos.close();
- } catch (Exception e) {
- }
- try {
- gzos.close();
- } catch (Exception e) {
- }
- try {
- b64os.close();
- } catch (Exception e) {
- }
- try {
- baos.close();
- } catch (Exception e) {
- }
- } // end finally
-
- // Return value according to relevant encoding.
- try {
- return new String(baos.toByteArray(), PREFERRED_ENCODING);
- } // end try
- catch (java.io.UnsupportedEncodingException uue) {
- // Fall back to some Java default
- return new String(baos.toByteArray());
- } // end catch
-
- } // end encode
-
- /**
- * Encodes a byte array into Base64 notation. Does not GZip-compress data.
- *
- * @param source
- * The data to convert
- * @return The data in Base64-encoded form
- * @throws NullPointerException
- * if source array is null
- * @since 1.4
- */
- public static String encodeBytes(byte[] source) {
- // Since we're not going to have the GZIP encoding turned on,
- // we're not going to have an java.io.IOException thrown, so
- // we should not force the user to have to catch it.
- String encoded = null;
- try {
- encoded = encodeBytes(source, 0, source.length, NO_OPTIONS);
- } catch (java.io.IOException ex) {
- assert false : ex.getMessage();
- } // end catch
- assert encoded != null;
- return encoded;
- } // end encodeBytes
-
- /**
- * Encodes a byte array into Base64 notation.
- *
- * Example options: - * - *
- * GZIP: gzip-compresses object before encoding it. - * DO_BREAK_LINES: break lines at 76 characters - * Note: Technically, this makes your encoding non-compliant. - *- *
- * Example: encodeBytes( myData, Base64.GZIP )
or
- *
- * Example:
- * encodeBytes( myData, Base64.GZIP | Base64.DO_BREAK_LINES )
- *
- *
- *
- * As of v 2.3, if there is an error with the GZIP stream, the method will - * throw an java.io.IOException. This is new to v2.3! In earlier - * versions, it just returned a null value, but in retrospect that's a - * pretty poor way to handle it. - *
- * - * - * @param source - * The data to convert - * @param options - * Specified options - * @return The Base64-encoded data as a String - * @see Base64#GZIP - * @see Base64#DO_BREAK_LINES - * @throws java.io.IOException - * if there is an error - * @throws NullPointerException - * if source array is null - * @since 2.0 - */ - public static String encodeBytes(byte[] source, int options) - throws java.io.IOException { - return encodeBytes(source, 0, source.length, options); - } // end encodeBytes - - /** - * Encodes a byte array into Base64 notation. Does not GZip-compress data. - * - *- * As of v 2.3, if there is an error, the method will throw an - * java.io.IOException. This is new to v2.3! In earlier versions, it - * just returned a null value, but in retrospect that's a pretty poor way to - * handle it. - *
- * - * - * @param source - * The data to convert - * @param off - * Offset in array where conversion should begin - * @param len - * Length of data to convert - * @return The Base64-encoded data as a String - * @throws NullPointerException - * if source array is null - * @throws IllegalArgumentException - * if source array, offset, or length are invalid - * @since 1.4 - */ - public static String encodeBytes(byte[] source, int off, int len) { - // Since we're not going to have the GZIP encoding turned on, - // we're not going to have an java.io.IOException thrown, so - // we should not force the user to have to catch it. - String encoded = null; - try { - encoded = encodeBytes(source, off, len, NO_OPTIONS); - } catch (java.io.IOException ex) { - assert false : ex.getMessage(); - } // end catch - assert encoded != null; - return encoded; - } // end encodeBytes - - /** - * Encodes a byte array into Base64 notation. - *- * Example options: - * - *
- * GZIP: gzip-compresses object before encoding it. - * DO_BREAK_LINES: break lines at 76 characters - * Note: Technically, this makes your encoding non-compliant. - *- *
- * Example: encodeBytes( myData, Base64.GZIP )
or
- *
- * Example:
- * encodeBytes( myData, Base64.GZIP | Base64.DO_BREAK_LINES )
- *
- *
- *
- * As of v 2.3, if there is an error with the GZIP stream, the method will - * throw an java.io.IOException. This is new to v2.3! In earlier - * versions, it just returned a null value, but in retrospect that's a - * pretty poor way to handle it. - *
- * - * - * @param source - * The data to convert - * @param off - * Offset in array where conversion should begin - * @param len - * Length of data to convert - * @param options - * Specified options - * @return The Base64-encoded data as a String - * @see Base64#GZIP - * @see Base64#DO_BREAK_LINES - * @throws java.io.IOException - * if there is an error - * @throws NullPointerException - * if source array is null - * @throws IllegalArgumentException - * if source array, offset, or length are invalid - * @since 2.0 - */ - public static String encodeBytes(byte[] source, int off, int len, - int options) throws java.io.IOException { - byte[] encoded = encodeBytesToBytes(source, off, len, options); - - // Return value according to relevant encoding. - try { - return new String(encoded, PREFERRED_ENCODING); - } // end try - catch (java.io.UnsupportedEncodingException uue) { - return new String(encoded); - } // end catch - - } // end encodeBytes - - /** - * Similar to {@link #encodeBytes(byte[])} but returns a byte array instead - * of instantiating a String. This is more efficient if you're working with - * I/O streams and have large data sets to encode. - * - * - * @param source - * The data to convert - * @return The Base64-encoded data as a byte[] (of ASCII characters) - * @throws NullPointerException - * if source array is null - * @since 2.3.1 - */ - public static byte[] encodeBytesToBytes(byte[] source) { - byte[] encoded = null; - try { - encoded = encodeBytesToBytes(source, 0, source.length, - Base64.NO_OPTIONS); - } catch (java.io.IOException ex) { - assert false : "IOExceptions only come from GZipping, which is turned off: " - + ex.getMessage(); - } - return encoded; - } - - /** - * Similar to {@link #encodeBytes(byte[], int, int, int)} but returns a byte - * array instead of instantiating a String. This is more efficient if you're - * working with I/O streams and have large data sets to encode. - * - * - * @param source - * The data to convert - * @param off - * Offset in array where conversion should begin - * @param len - * Length of data to convert - * @param options - * Specified options - * @return The Base64-encoded data as a String - * @see Base64#GZIP - * @see Base64#DO_BREAK_LINES - * @throws java.io.IOException - * if there is an error - * @throws NullPointerException - * if source array is null - * @throws IllegalArgumentException - * if source array, offset, or length are invalid - * @since 2.3.1 - */ - public static byte[] encodeBytesToBytes(byte[] source, int off, int len, - int options) throws java.io.IOException { - - if (source == null) { - throw new NullPointerException("Cannot serialize a null array."); - } // end if: null - - if (off < 0) { - throw new IllegalArgumentException("Cannot have negative offset: " - + off); - } // end if: off < 0 - - if (len < 0) { - throw new IllegalArgumentException("Cannot have length offset: " - + len); - } // end if: len < 0 - - if (off + len > source.length) { - throw new IllegalArgumentException( - String - .format( - "Cannot have offset of %d and length of %d with array of length %d", - off, len, source.length)); - } // end if: off < 0 - - // Compress? - if ((options & GZIP) != 0) { - java.io.ByteArrayOutputStream baos = null; - java.util.zip.GZIPOutputStream gzos = null; - Base64.OutputStream b64os = null; - - try { - // GZip -> Base64 -> ByteArray - baos = new java.io.ByteArrayOutputStream(); - b64os = new Base64.OutputStream(baos, ENCODE | options); - gzos = new java.util.zip.GZIPOutputStream(b64os); - - gzos.write(source, off, len); - gzos.close(); - } // end try - catch (java.io.IOException e) { - // Catch it and then throw it immediately so that - // the finally{} block is called for cleanup. - throw e; - } // end catch - finally { - try { - gzos.close(); - } catch (Exception e) { - } - try { - b64os.close(); - } catch (Exception e) { - } - try { - baos.close(); - } catch (Exception e) { - } - } // end finally - - return baos.toByteArray(); - } // end if: compress - - // Else, don't compress. Better not to use streams at all then. - else { - boolean breakLines = (options & DO_BREAK_LINES) != 0; - - // int len43 = len * 4 / 3; - // byte[] outBuff = new byte[ ( len43 ) // Main 4:3 - // + ( (len % 3) > 0 ? 4 : 0 ) // Account for padding - // + (breakLines ? ( len43 / MAX_LINE_LENGTH ) : 0) ]; // New lines - // Try to determine more precisely how big the array needs to be. - // If we get it right, we don't have to do an array copy, and - // we save a bunch of memory. - int encLen = (len / 3) * 4 + (len % 3 > 0 ? 4 : 0); // Bytes needed - // for actual - // encoding - if (breakLines) { - encLen += encLen / MAX_LINE_LENGTH; // Plus extra newline - // characters - } - byte[] outBuff = new byte[encLen]; - - int d = 0; - int e = 0; - int len2 = len - 2; - int lineLength = 0; - for (; d < len2; d += 3, e += 4) { - encode3to4(source, d + off, 3, outBuff, e, options); - - lineLength += 4; - if (breakLines && lineLength >= MAX_LINE_LENGTH) { - outBuff[e + 4] = NEW_LINE; - e++; - lineLength = 0; - } // end if: end of line - } // en dfor: each piece of array - - if (d < len) { - encode3to4(source, d + off, len - d, outBuff, e, options); - e += 4; - } // end if: some padding needed - - // Only resize array if we didn't guess it right. - if (e <= outBuff.length - 1) { - // If breaking lines and the last byte falls right at - // the line length (76 bytes per line), there will be - // one extra byte, and the array will need to be resized. - // Not too bad of an estimate on array size, I'd say. - byte[] finalOut = new byte[e]; - System.arraycopy(outBuff, 0, finalOut, 0, e); - // System.err.println("Having to resize array from " + - // outBuff.length + " to " + e ); - return finalOut; - } else { - // System.err.println("No need to resize array."); - return outBuff; - } - - } // end else: don't compress - - } // end encodeBytesToBytes - - /* ******** D E C O D I N G M E T H O D S ******** */ - - /** - * Decodes four bytes from array source and writes the resulting - * bytes (up to three of them) to destination. The source and - * destination arrays can be manipulated anywhere along their length by - * specifying srcOffset and destOffset. This method - * does not check to make sure your arrays are large enough to accomodate - * srcOffset + 4 for the source array or - * destOffset + 3 for the destination array. This - * method returns the actual number of bytes that were converted from the - * Base64 encoding. - *- * This is the lowest level of the decoding methods with all possible - * parameters. - *
- * - * - * @param source - * the array to convert - * @param srcOffset - * the index where conversion begins - * @param destination - * the array to hold the conversion - * @param destOffset - * the index where output will be put - * @param options - * alphabet type is pulled from this (standard, url-safe, - * ordered) - * @return the number of decoded bytes converted - * @throws NullPointerException - * if source or destination arrays are null - * @throws IllegalArgumentException - * if srcOffset or destOffset are invalid or there is not enough - * room in the array. - * @since 1.3 - */ - private static int decode4to3(byte[] source, int srcOffset, - byte[] destination, int destOffset, int options) { - - // Lots of error checking and exception throwing - if (source == null) { - throw new NullPointerException("Source array was null."); - } // end if - if (destination == null) { - throw new NullPointerException("Destination array was null."); - } // end if - if (srcOffset < 0 || srcOffset + 3 >= source.length) { - throw new IllegalArgumentException( - String - .format( - "Source array with length %d cannot have offset of %d and still process four bytes.", - source.length, srcOffset)); - } // end if - if (destOffset < 0 || destOffset + 2 >= destination.length) { - throw new IllegalArgumentException( - String - .format( - "Destination array with length %d cannot have offset of %d and still store three bytes.", - destination.length, destOffset)); - } // end if - - byte[] DECODABET = getDecodabet(options); - - // Example: Dk== - if (source[srcOffset + 2] == EQUALS_SIGN) { - // Two ways to do the same thing. Don't know which way I like best. - // int outBuff = ( ( DECODABET[ source[ srcOffset ] ] << 24 ) >>> 6 - // ) - // | ( ( DECODABET[ source[ srcOffset + 1] ] << 24 ) >>> 12 ); - int outBuff = ((DECODABET[source[srcOffset]] & 0xFF) << 18) - | ((DECODABET[source[srcOffset + 1]] & 0xFF) << 12); - - destination[destOffset] = (byte) (outBuff >>> 16); - return 1; - } - - // Example: DkL= - else if (source[srcOffset + 3] == EQUALS_SIGN) { - // Two ways to do the same thing. Don't know which way I like best. - // int outBuff = ( ( DECODABET[ source[ srcOffset ] ] << 24 ) >>> 6 - // ) - // | ( ( DECODABET[ source[ srcOffset + 1 ] ] << 24 ) >>> 12 ) - // | ( ( DECODABET[ source[ srcOffset + 2 ] ] << 24 ) >>> 18 ); - int outBuff = ((DECODABET[source[srcOffset]] & 0xFF) << 18) - | ((DECODABET[source[srcOffset + 1]] & 0xFF) << 12) - | ((DECODABET[source[srcOffset + 2]] & 0xFF) << 6); - - destination[destOffset] = (byte) (outBuff >>> 16); - destination[destOffset + 1] = (byte) (outBuff >>> 8); - return 2; - } - - // Example: DkLE - else { - // Two ways to do the same thing. Don't know which way I like best. - // int outBuff = ( ( DECODABET[ source[ srcOffset ] ] << 24 ) >>> 6 - // ) - // | ( ( DECODABET[ source[ srcOffset + 1 ] ] << 24 ) >>> 12 ) - // | ( ( DECODABET[ source[ srcOffset + 2 ] ] << 24 ) >>> 18 ) - // | ( ( DECODABET[ source[ srcOffset + 3 ] ] << 24 ) >>> 24 ); - int outBuff = ((DECODABET[source[srcOffset]] & 0xFF) << 18) - | ((DECODABET[source[srcOffset + 1]] & 0xFF) << 12) - | ((DECODABET[source[srcOffset + 2]] & 0xFF) << 6) - | ((DECODABET[source[srcOffset + 3]] & 0xFF)); - - destination[destOffset] = (byte) (outBuff >> 16); - destination[destOffset + 1] = (byte) (outBuff >> 8); - destination[destOffset + 2] = (byte) (outBuff); - - return 3; - } - } // end decodeToBytes - - /** - * Low-level access to decoding ASCII characters in the form of a byte - * array. Ignores GUNZIP option, if it's set. This is not - * generally a recommended method, although it is used internally as part of - * the decoding process. Special case: if len = 0, an empty array is - * returned. Still, if you need more speed and reduced memory footprint (and - * aren't gzipping), consider this method. - * - * @param source - * The Base64 encoded data - * @return decoded data - * @since 2.3.1 - */ - public static byte[] decode(byte[] source) throws java.io.IOException { - byte[] decoded = null; - // try { - decoded = decode(source, 0, source.length, Base64.NO_OPTIONS); - // } catch( java.io.IOException ex ) { - // assert false : - // "IOExceptions only come from GZipping, which is turned off: " + - // ex.getMessage(); - // } - return decoded; - } - - /** - * Low-level access to decoding ASCII characters in the form of a byte - * array. Ignores GUNZIP option, if it's set. This is not - * generally a recommended method, although it is used internally as part of - * the decoding process. Special case: if len = 0, an empty array is - * returned. Still, if you need more speed and reduced memory footprint (and - * aren't gzipping), consider this method. - * - * @param source - * The Base64 encoded data - * @param off - * The offset of where to begin decoding - * @param len - * The length of characters to decode - * @param options - * Can specify options such as alphabet type to use - * @return decoded data - * @throws java.io.IOException - * If bogus characters exist in source data - * @since 1.3 - */ - public static byte[] decode(byte[] source, int off, int len, int options) - throws java.io.IOException { - - // Lots of error checking and exception throwing - if (source == null) { - throw new NullPointerException("Cannot decode null source array."); - } // end if - if (off < 0 || off + len > source.length) { - throw new IllegalArgumentException( - String - .format( - "Source array with length %d cannot have offset of %d and process %d bytes.", - source.length, off, len)); - } // end if - - if (len == 0) { - return new byte[0]; - } else if (len < 4) { - throw new IllegalArgumentException( - "Base64-encoded string must have at least four characters, but length specified was " - + len); - } // end if - - byte[] DECODABET = getDecodabet(options); - - int len34 = len * 3 / 4; // Estimate on array size - byte[] outBuff = new byte[len34]; // Upper limit on size of output - int outBuffPosn = 0; // Keep track of where we're writing - - byte[] b4 = new byte[4]; // Four byte buffer from source, eliminating - // white space - int b4Posn = 0; // Keep track of four byte input buffer - int i = 0; // Source array counter - byte sbiDecode = 0; // Special value from DECODABET - - for (i = off; i < off + len; i++) { // Loop through source - - sbiDecode = DECODABET[source[i] & 0xFF]; - - // White space, Equals sign, or legit Base64 character - // Note the values such as -5 and -9 in the - // DECODABETs at the top of the file. - if (sbiDecode >= WHITE_SPACE_ENC) { - if (sbiDecode >= EQUALS_SIGN_ENC) { - b4[b4Posn++] = source[i]; // Save non-whitespace - if (b4Posn > 3) { // Time to decode? - outBuffPosn += decode4to3(b4, 0, outBuff, outBuffPosn, - options); - b4Posn = 0; - - // If that was the equals sign, break out of 'for' loop - if (source[i] == EQUALS_SIGN) { - break; - } // end if: equals sign - } // end if: quartet built - } // end if: equals sign or better - } // end if: white space, equals sign or better - else { - // There's a bad input character in the Base64 stream. - throw new java.io.IOException( - String - .format( - "Bad Base64 input character decimal %d in array position %d", - ((int) source[i]) & 0xFF, i)); - } // end else: - } // each input character - - byte[] out = new byte[outBuffPosn]; - System.arraycopy(outBuff, 0, out, 0, outBuffPosn); - return out; - } // end decode - - /** - * Decodes data from Base64 notation, automatically detecting - * gzip-compressed data and decompressing it. - * - * @param s - * the string to decode - * @return the decoded data - * @throws java.io.IOException - * If there is a problem - * @since 1.4 - */ - public static byte[] decode(String s) throws java.io.IOException { - return decode(s, NO_OPTIONS); - } - - /** - * Decodes data from Base64 notation, automatically detecting - * gzip-compressed data and decompressing it. - * - * @param s - * the string to decode - * @param options - * encode options such as URL_SAFE - * @return the decoded data - * @throws java.io.IOException - * if there is an error - * @throws NullPointerException - * if s is null - * @since 1.4 - */ - public static byte[] decode(String s, int options) - throws java.io.IOException { - - if (s == null) { - throw new NullPointerException("Input string was null."); - } // end if - - byte[] bytes; - try { - bytes = s.getBytes(PREFERRED_ENCODING); - } // end try - catch (java.io.UnsupportedEncodingException uee) { - bytes = s.getBytes(); - } // end catch - // - - // Decode - bytes = decode(bytes, 0, bytes.length, options); - - // Check to see if it's gzip-compressed - // GZIP Magic Two-Byte Number: 0x8b1f (35615) - boolean dontGunzip = (options & DONT_GUNZIP) != 0; - if ((bytes != null) && (bytes.length >= 4) && (!dontGunzip)) { - - int head = ((int) bytes[0] & 0xff) | ((bytes[1] << 8) & 0xff00); - if (java.util.zip.GZIPInputStream.GZIP_MAGIC == head) { - java.io.ByteArrayInputStream bais = null; - java.util.zip.GZIPInputStream gzis = null; - java.io.ByteArrayOutputStream baos = null; - byte[] buffer = new byte[2048]; - int length = 0; - - try { - baos = new java.io.ByteArrayOutputStream(); - bais = new java.io.ByteArrayInputStream(bytes); - gzis = new java.util.zip.GZIPInputStream(bais); - - while ((length = gzis.read(buffer)) >= 0) { - baos.write(buffer, 0, length); - } // end while: reading input - - // No error? Get new bytes. - bytes = baos.toByteArray(); - - } // end try - catch (java.io.IOException e) { - e.printStackTrace(); - // Just return originally-decoded bytes - } // end catch - finally { - try { - baos.close(); - } catch (Exception e) { - } - try { - gzis.close(); - } catch (Exception e) { - } - try { - bais.close(); - } catch (Exception e) { - } - } // end finally - - } // end if: gzipped - } // end if: bytes.length >= 2 - - return bytes; - } // end decode - - /* ******** I N N E R C L A S S O U T P U T S T R E A M ******** */ - - /** - * A {@link Base64.OutputStream} will write data to another - * java.io.OutputStream, given in the constructor, and - * encode/decode to/from Base64 notation on the fly. - * - * @see Base64 - * @since 1.3 - */ - public static class OutputStream extends java.io.FilterOutputStream { - - private boolean encode; - private int position; - private byte[] buffer; - private int bufferLength; - private int lineLength; - private boolean breakLines; - private byte[] b4; // Scratch used in a few places - private boolean suspendEncoding; - private int options; // Record for later - private byte[] decodabet; // Local copies to avoid extra method calls - - /** - * Constructs a {@link Base64.OutputStream} in ENCODE mode. - * - * @param out - * the java.io.OutputStream to which data will be - * written. - * @since 1.3 - */ - public OutputStream(java.io.OutputStream out) { - this(out, ENCODE); - } // end constructor - - /** - * Constructs a {@link Base64.OutputStream} in either ENCODE or DECODE - * mode. - *- * Valid options: - * - *
- * ENCODE or DECODE: Encode or Decode as data is read. - * DO_BREAK_LINES: don't break lines at 76 characters - * (only meaningful when encoding) - *- *
- * Example: new Base64.OutputStream( out, Base64.ENCODE )
- *
- * @param out
- * the java.io.OutputStream to which data will be
- * written.
- * @param options
- * Specified options.
- * @see Base64#ENCODE
- * @see Base64#DECODE
- * @see Base64#DO_BREAK_LINES
- * @since 1.3
- */
- public OutputStream(java.io.OutputStream out, int options) {
- super(out);
- this.breakLines = (options & DO_BREAK_LINES) != 0;
- this.encode = (options & ENCODE) != 0;
- this.bufferLength = encode ? 3 : 4;
- this.buffer = new byte[bufferLength];
- this.position = 0;
- this.lineLength = 0;
- this.suspendEncoding = false;
- this.b4 = new byte[4];
- this.options = options;
- this.decodabet = getDecodabet(options);
- } // end constructor
-
- /**
- * Writes the byte to the output stream after converting to/from Base64
- * notation. When encoding, bytes are buffered three at a time before
- * the output stream actually gets a write() call. When decoding, bytes
- * are buffered four at a time.
- *
- * @param theByte
- * the byte to write
- * @since 1.3
- */
- @Override
- public void write(int theByte) throws java.io.IOException {
- // Encoding suspended?
- if (suspendEncoding) {
- this.out.write(theByte);
- return;
- } // end if: supsended
-
- // Encode?
- if (encode) {
- buffer[position++] = (byte) theByte;
- if (position >= bufferLength) { // Enough to encode.
-
- this.out
- .write(encode3to4(b4, buffer, bufferLength, options));
-
- lineLength += 4;
- if (breakLines && lineLength >= MAX_LINE_LENGTH) {
- this.out.write(NEW_LINE);
- lineLength = 0;
- } // end if: end of line
-
- position = 0;
- } // end if: enough to output
- } // end if: encoding
-
- // Else, Decoding
- else {
- // Meaningful Base64 character?
- if (decodabet[theByte & 0x7f] > WHITE_SPACE_ENC) {
- buffer[position++] = (byte) theByte;
- if (position >= bufferLength) { // Enough to output.
-
- int len = Base64.decode4to3(buffer, 0, b4, 0, options);
- out.write(b4, 0, len);
- position = 0;
- } // end if: enough to output
- } // end if: meaningful base64 character
- else if (decodabet[theByte & 0x7f] != WHITE_SPACE_ENC) {
- throw new java.io.IOException(
- "Invalid character in Base64 data.");
- } // end else: not white space either
- } // end else: decoding
- } // end write
-
- /**
- * Calls {@link #write(int)} repeatedly until len bytes are
- * written.
- *
- * @param theBytes
- * array from which to read bytes
- * @param off
- * offset for array
- * @param len
- * max number of bytes to read into array
- * @since 1.3
- */
- @Override
- public void write(byte[] theBytes, int off, int len)
- throws java.io.IOException {
- // Encoding suspended?
- if (suspendEncoding) {
- this.out.write(theBytes, off, len);
- return;
- } // end if: supsended
-
- for (int i = 0; i < len; i++) {
- write(theBytes[off + i]);
- } // end for: each byte written
-
- } // end write
-
- /**
- * Method added by PHIL. [Thanks, PHIL. -Rob] This pads the buffer
- * without closing the stream.
- *
- * @throws java.io.IOException
- * if there's an error.
- */
- public void flushBase64() throws java.io.IOException {
- if (position > 0) {
- if (encode) {
- out.write(encode3to4(b4, buffer, position, options));
- position = 0;
- } // end if: encoding
- else {
- throw new java.io.IOException(
- "Base64 input not properly padded.");
- } // end else: decoding
- } // end if: buffer partially full
-
- } // end flush
-
- /**
- * Flushes and closes (I think, in the superclass) the stream.
- *
- * @since 1.3
- */
- @Override
- public void close() throws java.io.IOException {
- // 1. Ensure that pending characters are written
- flushBase64();
-
- // 2. Actually close the stream
- // Base class both flushes and closes.
- super.close();
-
- buffer = null;
- out = null;
- } // end close
-
- /**
- * Suspends encoding of the stream. May be helpful if you need to embed
- * a piece of base64-encoded data in a stream.
- *
- * @throws java.io.IOException
- * if there's an error flushing
- * @since 1.5.1
- */
- public void suspendEncoding() throws java.io.IOException {
- flushBase64();
- this.suspendEncoding = true;
- } // end suspendEncoding
-
- /**
- * Resumes encoding of the stream. May be helpful if you need to embed a
- * piece of base64-encoded data in a stream.
- *
- * @since 1.5.1
- */
- public void resumeEncoding() {
- this.suspendEncoding = false;
- } // end resumeEncoding
-
- } // end inner class OutputStream
-
-} // end class Base64
\ No newline at end of file
diff --git a/extensions/guacamole-auth-duo/src/main/java/com/duosecurity/duoweb/DuoWeb.java b/extensions/guacamole-auth-duo/src/main/java/com/duosecurity/duoweb/DuoWeb.java
deleted file mode 100644
index 223a1102b..000000000
--- a/extensions/guacamole-auth-duo/src/main/java/com/duosecurity/duoweb/DuoWeb.java
+++ /dev/null
@@ -1,138 +0,0 @@
-package com.duosecurity.duoweb;
-
-import java.io.IOException;
-import java.security.InvalidKeyException;
-import java.security.NoSuchAlgorithmException;
-
-public final class DuoWeb {
- private static final String DUO_PREFIX = "TX";
- private static final String APP_PREFIX = "APP";
- private static final String AUTH_PREFIX = "AUTH";
-
- private static final int DUO_EXPIRE = 300;
- private static final int APP_EXPIRE = 3600;
-
- private static final int IKEY_LEN = 20;
- private static final int SKEY_LEN = 40;
- private static final int AKEY_LEN = 40;
-
- public static final String ERR_USER = "ERR|The username passed to sign_request() is invalid.";
- public static final String ERR_IKEY = "ERR|The Duo integration key passed to sign_request() is invalid.";
- public static final String ERR_SKEY = "ERR|The Duo secret key passed to sign_request() is invalid.";
- public static final String ERR_AKEY = "ERR|The application secret key passed to sign_request() must be at least " + AKEY_LEN + " characters.";
- public static final String ERR_UNKNOWN = "ERR|An unknown error has occurred.";
-
- public static String signRequest(final String ikey, final String skey, final String akey, final String username) {
- return signRequest(ikey, skey, akey, username, System.currentTimeMillis() / 1000);
- }
-
- public static String signRequest(final String ikey, final String skey, final String akey, final String username, final long time) {
- final String duo_sig;
- final String app_sig;
-
- if (username.equals("")) {
- return ERR_USER;
- }
- if (username.indexOf('|') != -1) {
- return ERR_USER;
- }
- if (ikey.equals("") || ikey.length() != IKEY_LEN) {
- return ERR_IKEY;
- }
- if (skey.equals("") || skey.length() != SKEY_LEN) {
- return ERR_SKEY;
- }
- if (akey.equals("") || akey.length() < AKEY_LEN) {
- return ERR_AKEY;
- }
-
- try {
- duo_sig = signVals(skey, username, ikey, DUO_PREFIX, DUO_EXPIRE, time);
- app_sig = signVals(akey, username, ikey, APP_PREFIX, APP_EXPIRE, time);
- } catch (Exception e) {
- return ERR_UNKNOWN;
- }
-
- return duo_sig + ":" + app_sig;
- }
-
- public static String verifyResponse(final String ikey, final String skey, final String akey, final String sig_response)
- throws DuoWebException, NoSuchAlgorithmException, InvalidKeyException, IOException {
- return verifyResponse(ikey, skey, akey, sig_response, System.currentTimeMillis() / 1000);
- }
-
- public static String verifyResponse(final String ikey, final String skey, final String akey, final String sig_response, final long time)
- throws DuoWebException, NoSuchAlgorithmException, InvalidKeyException, IOException {
- String auth_user = null;
- String app_user = null;
-
- final String[] sigs = sig_response.split(":");
- final String auth_sig = sigs[0];
- final String app_sig = sigs[1];
-
- auth_user = parseVals(skey, auth_sig, AUTH_PREFIX, ikey, time);
- app_user = parseVals(akey, app_sig, APP_PREFIX, ikey, time);
-
- if (!auth_user.equals(app_user)) {
- throw new DuoWebException("Authentication failed.");
- }
-
- return auth_user;
- }
-
- private static String signVals(final String key, final String username, final String ikey, final String prefix, final int expire, final long time)
- throws InvalidKeyException, NoSuchAlgorithmException {
- final long expire_ts = time + expire;
- final String exp = Long.toString(expire_ts);
-
- final String val = username + "|" + ikey + "|" + exp;
- final String cookie = prefix + "|" + Base64.encodeBytes(val.getBytes());
- final String sig = Util.hmacSign(key, cookie);
-
- return cookie + "|" + sig;
- }
-
- private static String parseVals(final String key, final String val, final String prefix, final String ikey, final long time)
- throws InvalidKeyException, NoSuchAlgorithmException, IOException, DuoWebException {
-
- final String[] parts = val.split("\\|");
- if (parts.length != 3) {
- throw new DuoWebException("Invalid response");
- }
-
- final String u_prefix = parts[0];
- final String u_b64 = parts[1];
- final String u_sig = parts[2];
-
- final String sig = Util.hmacSign(key, u_prefix + "|" + u_b64);
- if (!Util.hmacSign(key, sig).equals(Util.hmacSign(key, u_sig))) {
- throw new DuoWebException("Invalid response");
- }
-
- if (!u_prefix.equals(prefix)) {
- throw new DuoWebException("Invalid response");
- }
-
- final byte[] decoded = Base64.decode(u_b64);
- final String cookie = new String(decoded);
-
- final String[] cookie_parts = cookie.split("\\|");
- if (cookie_parts.length != 3) {
- throw new DuoWebException("Invalid response");
- }
- final String username = cookie_parts[0];
- final String u_ikey = cookie_parts[1];
- final String expire = cookie_parts[2];
-
- if (!u_ikey.equals(ikey)) {
- throw new DuoWebException("Invalid response");
- }
-
- final long expire_ts = Long.parseLong(expire);
- if (time >= expire_ts) {
- throw new DuoWebException("Transaction has expired. Please check that the system time is correct.");
- }
-
- return username;
- }
-}
diff --git a/extensions/guacamole-auth-duo/src/main/java/com/duosecurity/duoweb/DuoWebException.java b/extensions/guacamole-auth-duo/src/main/java/com/duosecurity/duoweb/DuoWebException.java
deleted file mode 100644
index f721df71d..000000000
--- a/extensions/guacamole-auth-duo/src/main/java/com/duosecurity/duoweb/DuoWebException.java
+++ /dev/null
@@ -1,8 +0,0 @@
-package com.duosecurity.duoweb;
-
-public class DuoWebException extends Exception {
-
- public DuoWebException(String message) {
- super(message);
- }
-}
diff --git a/extensions/guacamole-auth-duo/src/main/java/com/duosecurity/duoweb/Util.java b/extensions/guacamole-auth-duo/src/main/java/com/duosecurity/duoweb/Util.java
deleted file mode 100644
index 55d771296..000000000
--- a/extensions/guacamole-auth-duo/src/main/java/com/duosecurity/duoweb/Util.java
+++ /dev/null
@@ -1,26 +0,0 @@
-package com.duosecurity.duoweb;
-
-import java.security.InvalidKeyException;
-import java.security.NoSuchAlgorithmException;
-
-import javax.crypto.Mac;
-import javax.crypto.spec.SecretKeySpec;
-
-public class Util {
- public static String hmacSign(String skey, String data)
- throws NoSuchAlgorithmException, InvalidKeyException {
- SecretKeySpec key = new SecretKeySpec(skey.getBytes(), "HmacSHA1");
- Mac mac = Mac.getInstance("HmacSHA1");
- mac.init(key);
- byte[] raw = mac.doFinal(data.getBytes());
- return bytesToHex(raw);
- }
-
- public static String bytesToHex(byte[] b) {
- String result = "";
- for (int i = 0; i < b.length; i++) {
- result += Integer.toString((b[i] & 0xff) + 0x100, 16).substring(1);
- }
- return result;
- }
-}
diff --git a/extensions/guacamole-auth-duo/src/main/java/org/apache/guacamole/auth/duo/DuoAuthenticationProviderModule.java b/extensions/guacamole-auth-duo/src/main/java/org/apache/guacamole/auth/duo/DuoAuthenticationProviderModule.java
index 2dfc4eb77..705e37dec 100644
--- a/extensions/guacamole-auth-duo/src/main/java/org/apache/guacamole/auth/duo/DuoAuthenticationProviderModule.java
+++ b/extensions/guacamole-auth-duo/src/main/java/org/apache/guacamole/auth/duo/DuoAuthenticationProviderModule.java
@@ -21,6 +21,7 @@ package org.apache.guacamole.auth.duo;
import com.google.inject.AbstractModule;
import org.apache.guacamole.GuacamoleException;
+import org.apache.guacamole.auth.duo.api.DuoService;
import org.apache.guacamole.auth.duo.conf.ConfigurationService;
import org.apache.guacamole.environment.Environment;
import org.apache.guacamole.environment.LocalEnvironment;
@@ -73,7 +74,7 @@ public class DuoAuthenticationProviderModule extends AbstractModule {
// Bind Duo-specific services
bind(ConfigurationService.class);
- bind(DuoWebService.class);
+ bind(DuoService.class);
bind(UserVerificationService.class);
}
diff --git a/extensions/guacamole-auth-duo/src/main/java/org/apache/guacamole/auth/duo/DuoWebService.java b/extensions/guacamole-auth-duo/src/main/java/org/apache/guacamole/auth/duo/DuoWebService.java
deleted file mode 100644
index 3cdfdded2..000000000
--- a/extensions/guacamole-auth-duo/src/main/java/org/apache/guacamole/auth/duo/DuoWebService.java
+++ /dev/null
@@ -1,212 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements. See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership. The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied. See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-
-package org.apache.guacamole.auth.duo;
-
-import com.duosecurity.duoweb.DuoWeb;
-import com.duosecurity.duoweb.DuoWebException;
-import com.google.inject.Inject;
-import java.io.IOException;
-import java.security.InvalidKeyException;
-import java.security.NoSuchAlgorithmException;
-import java.util.regex.Matcher;
-import java.util.regex.Pattern;
-import org.apache.guacamole.GuacamoleClientException;
-import org.apache.guacamole.GuacamoleException;
-import org.apache.guacamole.GuacamoleServerException;
-import org.apache.guacamole.auth.duo.conf.ConfigurationService;
-import org.apache.guacamole.net.auth.AuthenticatedUser;
-
-/**
- * Service which wraps the DuoWeb Java API, providing predictable behavior and
- * error handling.
- */
-public class DuoWebService {
-
- /**
- * A regular expression which matches a valid signature part of a Duo
- * signed response. A signature part may not contain pipe symbols (which
- * act as delimiters between parts) nor colons (which act as delimiters
- * between signatures).
- */
- private final String SIGNATURE_PART = "[^:|]*";
-
- /**
- * A regular expression which matches a valid signature within a Duo
- * signed response. Each signature is made up of three distinct parts,
- * separated by pipe symbols.
- */
- private final String SIGNATURE = SIGNATURE_PART + "\\|" + SIGNATURE_PART + "\\|" + SIGNATURE_PART;
-
- /**
- * A regular expression which matches a valid Duo signed response. Each
- * response is made up of two signatures, separated by a colon.
- */
- private final String RESPONSE = SIGNATURE + ":" + SIGNATURE;
-
- /**
- * A Pattern which matches valid Duo signed responses. Strings which will
- * be passed to DuoWeb.verifyResponse() MUST be matched against this
- * Pattern. Strings which do not match this Pattern may cause
- * DuoWeb.verifyResponse() to throw unchecked exceptions.
- */
- private final Pattern RESPONSE_PATTERN = Pattern.compile(RESPONSE);
-
- /**
- * Service for retrieving Duo configuration information.
- */
- @Inject
- private ConfigurationService confService;
-
- /**
- * Creates and signs a new request to verify the identity of the given
- * user. This request may ultimately be sent to Duo, resulting in a signed
- * response from Duo if that verification succeeds.
- *
- * @param authenticatedUser
- * The user whose identity should be verified.
- *
- * @return
- * A signed user verification request which can be sent to Duo.
- *
- * @throws GuacamoleException
- * If required Duo-specific configuration options are missing or
- * invalid, or if an error occurs within the DuoWeb API which prevents
- * generation of the signed request.
- */
- public String createSignedRequest(AuthenticatedUser authenticatedUser)
- throws GuacamoleException {
-
- // Retrieve username from externally-authenticated user
- String username = authenticatedUser.getIdentifier();
-
- // Retrieve Duo-specific keys from configuration
- String ikey = confService.getIntegrationKey();
- String skey = confService.getSecretKey();
- String akey = confService.getApplicationKey();
-
- // Create signed request for the provided user
- String signedRequest = DuoWeb.signRequest(ikey, skey, akey, username);
-
- if (DuoWeb.ERR_AKEY.equals(signedRequest))
- throw new GuacamoleServerException("The Duo application key "
- + "must is not valid. Duo application keys must be at "
- + "least 40 characters long.");
-
- if (DuoWeb.ERR_IKEY.equals(signedRequest))
- throw new GuacamoleServerException("The provided Duo integration "
- + "key is not valid. Integration keys must be exactly 20 "
- + "characters long.");
-
- if (DuoWeb.ERR_SKEY.equals(signedRequest))
- throw new GuacamoleServerException("The provided Duo secret key "
- + "is not valid. Secret keys must be exactly 40 "
- + "characters long.");
-
- if (DuoWeb.ERR_USER.equals(signedRequest))
- throw new GuacamoleServerException("The provided username is "
- + "not valid. Duo usernames may not be blank, nor may "
- + "they contain pipe symbols (\"|\").");
-
- if (DuoWeb.ERR_UNKNOWN.equals(signedRequest))
- throw new GuacamoleServerException("An unknown error within the "
- + "DuoWeb API prevented the signed request from being "
- + "generated.");
-
- // Return signed request if no error is indicated
- return signedRequest;
-
- }
-
- /**
- * Returns whether the given signed response is a valid response from Duo
- * which verifies the identity of the given user. If the given response is
- * invalid or does not verify the identity of the given user (including if
- * it is a valid response which verifies the identity of a DIFFERENT user),
- * false is returned.
- *
- * @param authenticatedUser
- * The user that the given signed response should verify.
- *
- * @param signedResponse
- * The signed response received from Duo in response to a signed
- * request.
- *
- * @return
- * true if the signed response is a valid response from Duo AND verifies
- * the identity of the given user, false otherwise.
- *
- * @throws GuacamoleException
- * If required Duo-specific configuration options are missing or
- * invalid, or if an error occurs within the DuoWeb API which prevents
- * validation of the signed response.
- */
- public boolean isValidSignedResponse(AuthenticatedUser authenticatedUser,
- String signedResponse) throws GuacamoleException {
-
- // Verify signature response format will not cause
- // DuoWeb.verifyResponse() to fail with unchecked exceptions
- Matcher responseMatcher = RESPONSE_PATTERN.matcher(signedResponse);
- if (!responseMatcher.matches())
- throw new GuacamoleClientException("Invalid Duo response format.");
-
- // Retrieve username from externally-authenticated user
- String username = authenticatedUser.getIdentifier();
-
- // Retrieve Duo-specific keys from configuration
- String ikey = confService.getIntegrationKey();
- String skey = confService.getSecretKey();
- String akey = confService.getApplicationKey();
-
- // Verify validity of signed response
- String verifiedUsername;
- try {
- verifiedUsername = DuoWeb.verifyResponse(ikey, skey, akey,
- signedResponse);
- }
-
- // Rethrow any errors as appropriate GuacamoleExceptions
- catch (IOException e) {
- throw new GuacamoleClientException("Decoding of Duo response "
- + "failed: Invalid base64 content.", e);
- }
- catch (NumberFormatException e) {
- throw new GuacamoleClientException("Decoding of Duo response "
- + "failed: Invalid expiry timestamp.", e);
- }
- catch (InvalidKeyException e) {
- throw new GuacamoleServerException("Unable to produce HMAC "
- + "signature: " + e.getMessage(), e);
- }
- catch (NoSuchAlgorithmException e) {
- throw new GuacamoleServerException("Environment is missing "
- + "support for producing HMAC-SHA1 signatures.", e);
- }
- catch (DuoWebException e) {
- throw new GuacamoleClientException("Duo response verification "
- + "failed: " + e.getMessage(), e);
- }
-
- // Signed response is valid iff the associated username matches the
- // user's username
- return username.equals(verifiedUsername);
-
- }
-
-}
diff --git a/extensions/guacamole-auth-duo/src/main/java/org/apache/guacamole/auth/duo/UserVerificationService.java b/extensions/guacamole-auth-duo/src/main/java/org/apache/guacamole/auth/duo/UserVerificationService.java
index 3209be2ab..777c96bd2 100644
--- a/extensions/guacamole-auth-duo/src/main/java/org/apache/guacamole/auth/duo/UserVerificationService.java
+++ b/extensions/guacamole-auth-duo/src/main/java/org/apache/guacamole/auth/duo/UserVerificationService.java
@@ -24,6 +24,7 @@ import java.util.Collections;
import javax.servlet.http.HttpServletRequest;
import org.apache.guacamole.GuacamoleClientException;
import org.apache.guacamole.GuacamoleException;
+import org.apache.guacamole.auth.duo.api.DuoService;
import org.apache.guacamole.auth.duo.conf.ConfigurationService;
import org.apache.guacamole.auth.duo.form.DuoSignedResponseField;
import org.apache.guacamole.form.Field;
@@ -44,10 +45,10 @@ public class UserVerificationService {
private ConfigurationService confService;
/**
- * Service for verifying users with the DuoWeb API.
+ * Service for verifying users against Duo.
*/
@Inject
- private DuoWebService duoWebService;
+ private DuoService duoService;
/**
* Verifies the identity of the given user via the Duo multi-factor
@@ -86,7 +87,7 @@ public class UserVerificationService {
// Duo API endpoint
Field signedResponseField = new DuoSignedResponseField(
confService.getAPIHostname(),
- duoWebService.createSignedRequest(authenticatedUser));
+ duoService.createSignedRequest(authenticatedUser));
// Create an overall description of the additional credentials
// required to verify identity
@@ -100,7 +101,7 @@ public class UserVerificationService {
}
// If signed response does not verify this user's identity, abort auth
- if (!duoWebService.isValidSignedResponse(authenticatedUser, signedResponse))
+ if (!duoService.isValidSignedResponse(authenticatedUser, signedResponse))
throw new GuacamoleClientException("LOGIN.INFO_DUO_VALIDATION_CODE_INCORRECT");
}
diff --git a/extensions/guacamole-auth-duo/src/main/java/org/apache/guacamole/auth/duo/api/DuoCookie.java b/extensions/guacamole-auth-duo/src/main/java/org/apache/guacamole/auth/duo/api/DuoCookie.java
new file mode 100644
index 000000000..1de9a672b
--- /dev/null
+++ b/extensions/guacamole-auth-duo/src/main/java/org/apache/guacamole/auth/duo/api/DuoCookie.java
@@ -0,0 +1,245 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.guacamole.auth.duo.api;
+
+import java.io.UnsupportedEncodingException;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import javax.xml.bind.DatatypeConverter;
+import org.apache.guacamole.GuacamoleClientException;
+import org.apache.guacamole.GuacamoleException;
+
+/**
+ * Data which describes the identity of the user being verified by Duo.
+ */
+public class DuoCookie {
+
+ /**
+ * Pattern which matches valid cookies. Each cookie is made up of three
+ * sections, separated from each other by pipe symbols ("|").
+ */
+ private static final Pattern COOKIE_FORMAT = Pattern.compile("([^|]+)\\|([^|]+)\\|([0-9]+)");
+
+ /**
+ * The index of the capturing group within COOKIE_FORMAT which contains the
+ * username.
+ */
+ private static final int USERNAME_GROUP = 1;
+
+ /**
+ * The index of the capturing group within COOKIE_FORMAT which contains the
+ * integration key.
+ */
+ private static final int INTEGRATION_KEY_GROUP = 2;
+
+ /**
+ * The index of the capturing group within COOKIE_FORMAT which contains the
+ * expiration timestamp.
+ */
+ private static final int EXPIRATION_TIMESTAMP_GROUP = 3;
+
+ /**
+ * The username of the user being verified.
+ */
+ private final String username;
+
+ /**
+ * The integration key provided by Duo and specific to this deployment of
+ * Guacamole.
+ */
+ private final String integrationKey;
+
+ /**
+ * The time that this cookie expires, in seconds since midnight of
+ * 1970-01-01 (UTC).
+ */
+ private final long expires;
+
+ /**
+ * Creates a new DuoCookie which describes the identity of a user being
+ * verified.
+ *
+ * @param username
+ * The username of the user being verified.
+ *
+ * @param integrationKey
+ * The integration key provided by Duo and specific to this deployment
+ * of Guacamole.
+ *
+ * @param expires
+ * The time that this cookie expires, in seconds since midnight of
+ * 1970-01-01 (UTC).
+ */
+ public DuoCookie(String username, String integrationKey, long expires) {
+ this.username = username;
+ this.integrationKey = integrationKey;
+ this.expires = expires;
+ }
+
+ /**
+ * Returns the username of the user being verified.
+ *
+ * @return
+ * The username of the user being verified.
+ */
+ public String getUsername() {
+ return username;
+ }
+
+ /**
+ * Returns the integration key provided by Duo and specific to this
+ * deployment of Guacamole.
+ *
+ * @return
+ * The integration key provided by Duo and specific to this deployment
+ * of Guacamole.
+ */
+ public String getIntegrationKey() {
+ return integrationKey;
+ }
+
+ /**
+ * Returns the time that this cookie expires. The expiration time is
+ * represented in seconds since midnight of 1970-01-01 (UTC).
+ *
+ * @return
+ * The time that this cookie expires, in seconds since midnight of
+ * 1970-01-01 (UTC).
+ */
+ public long getExpirationTimestamp(){
+ return expires;
+ }
+
+ /**
+ * Returns the current time as the number of seconds elapsed since
+ * midnight of 1970-01-01 (UTC).
+ *
+ * @return
+ * The current time as the number of seconds elapsed since midnight of
+ * 1970-01-01 (UTC).
+ */
+ public static long currentTimestamp() {
+ return System.currentTimeMillis() / 1000;
+ }
+
+ /**
+ * Returns whether this cookie has expired (the current time has met or
+ * exceeded the expiration timestamp).
+ *
+ * @return
+ * true if this cookie has expired, false otherwise.
+ */
+ public boolean isExpired() {
+ return currentTimestamp() >= expires;
+ }
+
+ /**
+ * Parses a base64-encoded Duo cookie, producing a new DuoCookie object
+ * containing the data therein. If the given string is not a valid Duo
+ * cookie, an exception is thrown. Note that the cookie may be expired, and
+ * must be checked for expiration prior to actual use.
+ *
+ * @param str
+ * The base64-encoded Duo cookie to parse.
+ *
+ * @return
+ * A new DuoCookie object containing the same data as the given
+ * base64-encoded Duo cookie string.
+ *
+ * @throws GuacamoleException
+ * If the given string is not a valid base64-encoded Duo cookie.
+ */
+ public static DuoCookie parseDuoCookie(String str) throws GuacamoleException {
+
+ // Attempt to decode data as base64
+ String data;
+ try {
+ data = new String(DatatypeConverter.parseBase64Binary(str), "UTF-8");
+ }
+
+ // Bail if invalid base64 is provided
+ catch (IllegalArgumentException e) {
+ throw new GuacamoleClientException("Username is not correctly "
+ + "encoded as base64.", e);
+ }
+
+ // Throw hard errors if standard pieces of Java are missing
+ catch (UnsupportedEncodingException e) {
+ throw new UnsupportedOperationException("Unexpected lack of "
+ + "UTF-8 support.", e);
+ }
+
+ // Verify format of provided data
+ Matcher matcher = COOKIE_FORMAT.matcher(data);
+ if (!matcher.matches())
+ throw new GuacamoleClientException("Format of base64-encoded "
+ + "username is invalid.");
+
+ // Get username and key (simple strings)
+ String username = matcher.group(USERNAME_GROUP);
+ String key = matcher.group(INTEGRATION_KEY_GROUP);
+
+ // Parse expiration time
+ long expires;
+ try {
+ expires = Long.parseLong(matcher.group(EXPIRATION_TIMESTAMP_GROUP));
+ }
+
+ // Bail if expiration timestamp is not a valid long
+ catch (NumberFormatException e) {
+ throw new GuacamoleClientException("Expiration timestamp is "
+ + "not valid.", e);
+ }
+
+ // Return parsed cookie
+ return new DuoCookie(username, key, expires);
+
+ }
+
+ /**
+ * Returns the base64-encoded string representation of this DuoCookie. The
+ * format used is identical to that required by the Duo service: the
+ * username, integration key, and expiration timestamp separated by pipe
+ * symbols ("|") and encoded with base64.
+ *
+ * @return
+ * The base64-encoded string representation of this DuoCookie.
+ */
+ @Override
+ public String toString() {
+
+ try {
+
+ // Separate each cookie field with pipe symbols
+ String data = username + "|" + integrationKey + "|" + expires;
+
+ // Encode resulting cookie string with base64
+ return DatatypeConverter.printBase64Binary(data.getBytes("UTF-8"));
+
+ }
+
+ // Throw hard errors if standard pieces of Java are missing
+ catch (UnsupportedEncodingException e) {
+ throw new UnsupportedOperationException("Unexpected lack of UTF-8 support.", e);
+ }
+
+ }
+
+}
diff --git a/extensions/guacamole-auth-duo/src/main/java/org/apache/guacamole/auth/duo/api/DuoService.java b/extensions/guacamole-auth-duo/src/main/java/org/apache/guacamole/auth/duo/api/DuoService.java
new file mode 100644
index 000000000..11cca13c5
--- /dev/null
+++ b/extensions/guacamole-auth-duo/src/main/java/org/apache/guacamole/auth/duo/api/DuoService.java
@@ -0,0 +1,205 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.guacamole.auth.duo.api;
+
+import com.google.inject.Inject;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import org.apache.guacamole.GuacamoleException;
+import org.apache.guacamole.auth.duo.conf.ConfigurationService;
+import org.apache.guacamole.net.auth.AuthenticatedUser;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Service which produces signed requests and parses/verifies signed responses
+ * as required by Duo's API.
+ */
+public class DuoService {
+
+ /**
+ * Logger for this class.
+ */
+ private static final Logger logger = LoggerFactory.getLogger(DuoService.class);
+
+ /**
+ * Pattern which matches valid Duo responses. Each response is made up of
+ * two sections, separated from each other by a colon, where each section
+ * is a signed Duo cookie.
+ */
+ private static final Pattern RESPONSE_FORMAT = Pattern.compile("([^:]+):([^:]+)");
+
+ /**
+ * The index of the capturing group within RESPONSE_FORMAT which
+ * contains the DUO_RESPONSE cookie signed by the secret key.
+ */
+ private static final int DUO_COOKIE_GROUP = 1;
+
+ /**
+ * The index of the capturing group within RESPONSE_FORMAT which
+ * contains the APPLICATION cookie signed by the application key.
+ */
+ private static final int APP_COOKIE_GROUP = 2;
+
+ /**
+ * The amount of time that each generated cookie remains valid, in seconds.
+ */
+ private static final int COOKIE_EXPIRATION_TIME = 300;
+
+ /**
+ * Service for retrieving Duo configuration information.
+ */
+ @Inject
+ private ConfigurationService confService;
+
+ /**
+ * Creates and signs a new request to verify the identity of the given
+ * user. This request may ultimately be sent to Duo, resulting in a signed
+ * response from Duo if that verification succeeds.
+ *
+ * @param authenticatedUser
+ * The user whose identity should be verified.
+ *
+ * @return
+ * A signed user verification request which can be sent to Duo.
+ *
+ * @throws GuacamoleException
+ * If required Duo-specific configuration options are missing or
+ * invalid, or if an error prevents generation of the signature.
+ */
+ public String createSignedRequest(AuthenticatedUser authenticatedUser)
+ throws GuacamoleException {
+
+ // Generate a cookie associating the username with the integration key
+ DuoCookie cookie = new DuoCookie(authenticatedUser.getIdentifier(),
+ confService.getIntegrationKey(),
+ DuoCookie.currentTimestamp() + COOKIE_EXPIRATION_TIME);
+
+ // Sign cookie with secret key
+ SignedDuoCookie duoCookie = new SignedDuoCookie(cookie,
+ SignedDuoCookie.Type.DUO_REQUEST,
+ confService.getSecretKey());
+
+ // Sign cookie with application key
+ SignedDuoCookie appCookie = new SignedDuoCookie(cookie,
+ SignedDuoCookie.Type.APPLICATION,
+ confService.getApplicationKey());
+
+ // Return signed request containing both signed cookies, separated by
+ // a colon (as required by Duo)
+ return duoCookie + ":" + appCookie;
+
+ }
+
+ /**
+ * Returns whether the given signed response is a valid response from Duo
+ * which verifies the identity of the given user. If the given response is
+ * invalid or does not verify the identity of the given user (including if
+ * it is a valid response which verifies the identity of a DIFFERENT user),
+ * false is returned.
+ *
+ * @param authenticatedUser
+ * The user that the given signed response should verify.
+ *
+ * @param signedResponse
+ * The signed response received from Duo in response to a signed
+ * request.
+ *
+ * @return
+ * true if the signed response is a valid response from Duo AND verifies
+ * the identity of the given user, false otherwise.
+ *
+ * @throws GuacamoleException
+ * If required Duo-specific configuration options are missing or
+ * invalid, or if an error occurs prevents validation of the signature.
+ */
+ public boolean isValidSignedResponse(AuthenticatedUser authenticatedUser,
+ String signedResponse) throws GuacamoleException {
+
+ SignedDuoCookie duoCookie;
+ SignedDuoCookie appCookie;
+
+ // Retrieve username from externally-authenticated user
+ String username = authenticatedUser.getIdentifier();
+
+ // Retrieve Duo-specific keys from configuration
+ String applicationKey = confService.getApplicationKey();
+ String integrationKey = confService.getIntegrationKey();
+ String secretKey = confService.getSecretKey();
+
+ try {
+
+ // Verify format of response
+ Matcher matcher = RESPONSE_FORMAT.matcher(signedResponse);
+ if (!matcher.matches()) {
+ logger.debug("Duo response is not in correct format.");
+ return false;
+ }
+
+ // Parse signed cookie defining the user verified by Duo
+ duoCookie = SignedDuoCookie.parseSignedDuoCookie(secretKey,
+ matcher.group(DUO_COOKIE_GROUP));
+
+ // Parse signed cookie defining the user this application
+ // originally requested
+ appCookie = SignedDuoCookie.parseSignedDuoCookie(applicationKey,
+ matcher.group(APP_COOKIE_GROUP));
+
+ }
+
+ // Simply return false if signature fails to verify
+ catch (GuacamoleException e) {
+ logger.debug("Duo signature verification failed.", e);
+ return false;
+ }
+
+ // Verify neither cookie is expired
+ if (duoCookie.isExpired() || appCookie.isExpired()) {
+ logger.debug("Duo response contained expired cookie(s).");
+ return false;
+ }
+
+ // Verify the cookies in the response have the correct types
+ if (duoCookie.getType() != SignedDuoCookie.Type.DUO_RESPONSE
+ || appCookie.getType() != SignedDuoCookie.Type.APPLICATION) {
+ logger.debug("Duo response did not contain correct cookie type(s).");
+ return false;
+ }
+
+ // Verify integration key matches both cookies
+ if (!duoCookie.getIntegrationKey().equals(integrationKey)
+ || !appCookie.getIntegrationKey().equals(integrationKey)) {
+ logger.debug("Integration key of Duo response is incorrect.");
+ return false;
+ }
+
+ // Verify both cookies are for the current user
+ if (!duoCookie.getUsername().equals(username)
+ || !appCookie.getUsername().equals(username)) {
+ logger.debug("Username of Duo response is incorrect.");
+ return false;
+ }
+
+ // All verifications tests pass
+ return true;
+
+ }
+
+}
diff --git a/extensions/guacamole-auth-duo/src/main/java/org/apache/guacamole/auth/duo/api/SignedDuoCookie.java b/extensions/guacamole-auth-duo/src/main/java/org/apache/guacamole/auth/duo/api/SignedDuoCookie.java
new file mode 100644
index 000000000..49fb34b01
--- /dev/null
+++ b/extensions/guacamole-auth-duo/src/main/java/org/apache/guacamole/auth/duo/api/SignedDuoCookie.java
@@ -0,0 +1,332 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.guacamole.auth.duo.api;
+
+import java.io.UnsupportedEncodingException;
+import java.security.InvalidKeyException;
+import java.security.NoSuchAlgorithmException;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import javax.crypto.Mac;
+import javax.crypto.spec.SecretKeySpec;
+import javax.xml.bind.DatatypeConverter;
+import org.apache.guacamole.GuacamoleClientException;
+import org.apache.guacamole.GuacamoleException;
+import org.apache.guacamole.GuacamoleServerException;
+
+/**
+ * A DuoCookie which is cryptographically signed with a provided key using
+ * HMAC-SHA1.
+ */
+public class SignedDuoCookie extends DuoCookie {
+
+ /**
+ * Pattern which matches valid signed cookies. Like unsigned cookies, each
+ * signed cookie is made up of three sections, separated from each other by
+ * pipe symbols ("|").
+ */
+ private static final Pattern SIGNED_COOKIE_FORMAT = Pattern.compile("([^|]+)\\|([^|]+)\\|([0-9a-f]+)");
+
+ /**
+ * The index of the capturing group within SIGNED_COOKIE_FORMAT which
+ * contains the cookie type prefix.
+ */
+ private static final int PREFIX_GROUP = 1;
+
+ /**
+ * The index of the capturing group within SIGNED_COOKIE_FORMAT which
+ * contains the cookie's base64-encoded data.
+ */
+ private static final int DATA_GROUP = 2;
+
+ /**
+ * The index of the capturing group within SIGNED_COOKIE_FORMAT which
+ * contains the signature.
+ */
+ private static final int SIGNATURE_GROUP = 3;
+
+ /**
+ * The signature algorithm that should be used to sign the cookie, as
+ * defined by:
+ * http://docs.oracle.com/javase/7/docs/technotes/guides/security/StandardNames.html#Mac
+ */
+ private static final String SIGNATURE_ALGORITHM = "HmacSHA1";
+
+ /**
+ * The type of a signed Duo cookie. Each signed Duo cookie has an
+ * associated type which determines the prefix included in the string
+ * representation of that cookie. As that type is included in the data
+ * that is signed, different types will result in different signatures,
+ * even if the data portion of the cookie is otherwise identical.
+ */
+ public enum Type {
+
+ /**
+ * A Duo cookie which has been signed with the secret key for inclusion
+ * in a Duo request.
+ */
+ DUO_REQUEST("TX"),
+
+ /**
+ * A Duo cookie which has been signed with the secret key by Duo and
+ * was included in a Duo response.
+ */
+ DUO_RESPONSE("AUTH"),
+
+ /**
+ * A Duo cookie which has been signed with the application key for
+ * inclusion in a Duo request. Such cookies are also included in Duo
+ * responses, for verification by the application.
+ */
+ APPLICATION("APP");
+
+ /**
+ * The prefix associated with the Duo cookie type. This prefix will
+ * be included in the string representation of the cookie.
+ */
+ private final String prefix;
+
+ /**
+ * Creates a new Duo cookie type associated with the given string
+ * prefix. This prefix will be included in the string representation of
+ * the cookie.
+ *
+ * @param prefix
+ * The prefix to associated with the Duo cookie type.
+ */
+ Type(String prefix) {
+ this.prefix = prefix;
+ }
+
+ /**
+ * Returns the prefix associated with the Duo cookie type.
+ *
+ * @return
+ * The prefix to associated with this Duo cookie type.
+ */
+ public String getPrefix() {
+ return prefix;
+ }
+
+ /**
+ * Returns the cookie type associated with the given prefix. If no such
+ * cookie type exists, null is returned.
+ *
+ * @param prefix
+ * The prefix of the cookie type to search for.
+ *
+ * @return
+ * The cookie type associated with the given prefix, or null if no
+ * such cookie type exists.
+ */
+ public static Type fromPrefix(String prefix) {
+
+ // Search through all defined cookie types for the given prefix
+ for (Type type : Type.values()) {
+ if (type.getPrefix().equals(prefix))
+ return type;
+ }
+
+ // No such cookie type exists
+ return null;
+
+ }
+
+ }
+
+ /**
+ * The type of this Duo cookie.
+ */
+ private final Type type;
+
+ /**
+ * The signature produced when the cookie was signed with HMAC-SHA1. The
+ * signature covers the prefix of the type and the cookie's base64-encoded
+ * data, separated by a pipe symbol.
+ */
+ private final String signature;
+
+ /**
+ * Creates a new SignedDuoCookie which describes the identity of a user
+ * being verified and is cryptographically signed with HMAC-SHA1 by a given
+ * key.
+ *
+ * @param cookie
+ * The cookie defining the identity being verified.
+ *
+ * @param type
+ * The type of the cookie being created.
+ *
+ * @param key
+ * The key to use to generate the cryptographic signature. This key
+ * will not be stored within the cookie.
+ *
+ * @throws GuacamoleException
+ * If the given signing key is invalid.
+ */
+ public SignedDuoCookie(DuoCookie cookie, Type type, String key)
+ throws GuacamoleException {
+
+ // Init underlying cookie
+ super(cookie.getUsername(), cookie.getIntegrationKey(),
+ cookie.getExpirationTimestamp());
+
+ // Store cookie type and signature
+ this.type = type;
+ this.signature = sign(key, type.getPrefix() + "|" + cookie.toString());
+
+ }
+
+ /**
+ * Signs the given arbitrary string data with the given key using the
+ * algorithm defined by SIGNATURE_ALGORITHM. Both the data and the key will
+ * be interpreted as UTF-8 bytes.
+ *
+ * @param key
+ * The key which should be used to sign the given data.
+ *
+ * @param data
+ * The data being signed.
+ *
+ * @return
+ * The signature produced by signing the given data with the given key,
+ * encoded as lowercase hexadecimal.
+ *
+ * @throws GuacamoleException
+ * If the given signing key is invalid.
+ */
+ private static String sign(String key, String data) throws GuacamoleException {
+
+ try {
+
+ // Attempt to sign UTF-8 bytes of provided data
+ Mac mac = Mac.getInstance(SIGNATURE_ALGORITHM);
+ mac.init(new SecretKeySpec(key.getBytes("UTF-8"), SIGNATURE_ALGORITHM));
+
+ // Return signature as hex
+ return DatatypeConverter.printHexBinary(mac.doFinal(data.getBytes("UTF-8"))).toLowerCase();
+
+ }
+
+ // Re-throw any errors which prevent signature
+ catch (InvalidKeyException e){
+ throw new GuacamoleServerException("Signing key is invalid.", e);
+ }
+
+ // Throw hard errors if standard pieces of Java are missing
+ catch (UnsupportedEncodingException e) {
+ throw new UnsupportedOperationException("Unexpected lack of UTF-8 support.", e);
+ }
+ catch (NoSuchAlgorithmException e) {
+ throw new UnsupportedOperationException("Unexpected lack of support "
+ + "for required signature algorithm "
+ + "\"" + SIGNATURE_ALGORITHM + "\".", e);
+ }
+
+ }
+
+ /**
+ * Returns the type of this Duo cookie. The Duo cookie type is dictated
+ * by the context of the cookie's use, and is included with the cookie's
+ * underlying data when generating the signature.
+ *
+ * @return
+ * The type of this Duo cookie.
+ */
+ public Type getType() {
+ return type;
+ }
+
+ /**
+ * Returns the signature produced when the cookie was signed with HMAC-SHA1.
+ * The signature covers the prefix of the cookie's type and the cookie's
+ * base64-encoded data, separated by a pipe symbol.
+ *
+ * @return
+ * The signature produced when the cookie was signed with HMAC-SHA1.
+ */
+ public String getSignature() {
+ return signature;
+ }
+
+ /**
+ * Parses a signed Duo cookie string, such as that produced by the
+ * toString() function or received from the Duo service, producing a new
+ * SignedDuoCookie object containing the associated cookie data and
+ * signature. If the given string is not a valid Duo cookie, or if the
+ * signature is incorrect, an exception is thrown. Note that the cookie may
+ * be expired, and must be checked for expiration prior to actual use.
+ *
+ * @param key
+ * The key that was used to sign the Duo cookie.
+ *
+ * @param str
+ * The Duo cookie string to parse.
+ *
+ * @return
+ * A new SignedDuoCookie object containing the same data and signature
+ * as the given Duo cookie string.
+ *
+ * @throws GuacamoleException
+ * If the given string is not a valid Duo cookie string, or if the
+ * signature of the cookie is invalid.
+ */
+ public static SignedDuoCookie parseSignedDuoCookie(String key, String str)
+ throws GuacamoleException {
+
+ // Verify format of provided data
+ Matcher matcher = SIGNED_COOKIE_FORMAT.matcher(str);
+ if (!matcher.matches())
+ throw new GuacamoleClientException("Format of signed Duo cookie "
+ + "is invalid.");
+
+ // Parse type from prefix
+ Type type = Type.fromPrefix(matcher.group(PREFIX_GROUP));
+ if (type == null)
+ throw new GuacamoleClientException("Invalid Duo cookie prefix.");
+
+ // Parse cookie from base64-encoded data
+ DuoCookie cookie = DuoCookie.parseDuoCookie(matcher.group(DATA_GROUP));
+
+ // Verify signature of cookie
+ SignedDuoCookie signedCookie = new SignedDuoCookie(cookie, type, key);
+ if (!signedCookie.getSignature().equals(matcher.group(SIGNATURE_GROUP)))
+ throw new GuacamoleClientException("Duo cookie has incorrect signature.");
+
+ // Cookie has valid signature and has parsed successfully
+ return signedCookie;
+
+ }
+
+ /**
+ * Returns the string representation of this SignedDuoCookie. The format
+ * used is identical to that required by the Duo service: the type prefix,
+ * base64-encoded cookie data, and HMAC-SHA1 signature separated by pipe
+ * symbols ("|").
+ *
+ * @return
+ * The string representation of this SignedDuoCookie.
+ */
+ @Override
+ public String toString() {
+ return type.getPrefix() + "|" + super.toString() + "|" + signature;
+ }
+
+}