From 9056bb0f4f44192866fe52c66d55d0a501375e3d Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Sat, 10 Dec 2016 01:06:04 -0800 Subject: [PATCH] GUACAMOLE-136: Remove DuoWeb Java API from codebase. Re-implement cleanly from scratch. --- .../java/com/duosecurity/duoweb/Base64.java | 1500 ----------------- .../java/com/duosecurity/duoweb/DuoWeb.java | 138 -- .../duosecurity/duoweb/DuoWebException.java | 8 - .../java/com/duosecurity/duoweb/Util.java | 26 - .../duo/DuoAuthenticationProviderModule.java | 3 +- .../guacamole/auth/duo/DuoWebService.java | 212 --- .../auth/duo/UserVerificationService.java | 9 +- .../guacamole/auth/duo/api/DuoCookie.java | 245 +++ .../guacamole/auth/duo/api/DuoService.java | 205 +++ .../auth/duo/api/SignedDuoCookie.java | 332 ++++ 10 files changed, 789 insertions(+), 1889 deletions(-) delete mode 100644 extensions/guacamole-auth-duo/src/main/java/com/duosecurity/duoweb/Base64.java delete mode 100644 extensions/guacamole-auth-duo/src/main/java/com/duosecurity/duoweb/DuoWeb.java delete mode 100644 extensions/guacamole-auth-duo/src/main/java/com/duosecurity/duoweb/DuoWebException.java delete mode 100644 extensions/guacamole-auth-duo/src/main/java/com/duosecurity/duoweb/Util.java delete mode 100644 extensions/guacamole-auth-duo/src/main/java/org/apache/guacamole/auth/duo/DuoWebService.java create mode 100644 extensions/guacamole-auth-duo/src/main/java/org/apache/guacamole/auth/duo/api/DuoCookie.java create mode 100644 extensions/guacamole-auth-duo/src/main/java/org/apache/guacamole/auth/duo/api/DuoService.java create mode 100644 extensions/guacamole-auth-duo/src/main/java/org/apache/guacamole/auth/duo/api/SignedDuoCookie.java 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 the raw 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; + } + +}