You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@guacamole.apache.org by jm...@apache.org on 2016/12/12 04:43:33 UTC

[1/3] incubator-guacamole-client git commit: GUACAMOLE-136: Remove DuoWeb Java API from codebase. Re-implement cleanly from scratch.

Repository: incubator-guacamole-client
Updated Branches:
  refs/heads/master cf6a2b84a -> 653b7f58a


http://git-wip-us.apache.org/repos/asf/incubator-guacamole-client/blob/9056bb0f/extensions/guacamole-auth-duo/src/main/java/org/apache/guacamole/auth/duo/api/DuoService.java
----------------------------------------------------------------------
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 0000000..11cca13
--- /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;
+
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-guacamole-client/blob/9056bb0f/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/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 0000000..49fb34b
--- /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;
+    }
+
+}


[2/3] incubator-guacamole-client git commit: GUACAMOLE-136: Remove DuoWeb Java API from codebase. Re-implement cleanly from scratch.

Posted by jm...@apache.org.
GUACAMOLE-136: Remove DuoWeb Java API from codebase. Re-implement cleanly from scratch.


Project: http://git-wip-us.apache.org/repos/asf/incubator-guacamole-client/repo
Commit: http://git-wip-us.apache.org/repos/asf/incubator-guacamole-client/commit/9056bb0f
Tree: http://git-wip-us.apache.org/repos/asf/incubator-guacamole-client/tree/9056bb0f
Diff: http://git-wip-us.apache.org/repos/asf/incubator-guacamole-client/diff/9056bb0f

Branch: refs/heads/master
Commit: 9056bb0f4f44192866fe52c66d55d0a501375e3d
Parents: cf6a2b8
Author: Michael Jumper <mj...@apache.org>
Authored: Sat Dec 10 01:06:04 2016 -0800
Committer: Michael Jumper <mj...@apache.org>
Committed: Sat Dec 10 18:53:55 2016 -0800

----------------------------------------------------------------------
 .../java/com/duosecurity/duoweb/Base64.java     | 1500 ------------------
 .../java/com/duosecurity/duoweb/DuoWeb.java     |  138 --
 .../com/duosecurity/duoweb/DuoWebException.java |    8 -
 .../main/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 +++
 .../guacamole/auth/duo/api/SignedDuoCookie.java |  332 ++++
 10 files changed, 789 insertions(+), 1889 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/incubator-guacamole-client/blob/9056bb0f/extensions/guacamole-auth-duo/src/main/java/com/duosecurity/duoweb/Base64.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 8f25477..0000000
--- 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 <em>not</em> 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: <a
-	 * href="http://www.faqs.org/rfcs/rfc3548.html"
-	 * >http://www.faqs.org/rfcs/rfc3548.html</a>. It is important to note that
-	 * data encoded this way is <em>not</em> 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: <a
-	 * href="http://www.faqs.org/qa/rfcc-1940.html">http://www.faqs.org/qa/rfcc-
-	 * 1940.html</a>.
-	 */
-	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: <a
-	 * href="http://www.faqs.org/rfcs/rfc3548.html">http://www.faqs.org
-	 * /rfcs/rfc3548.html</a>. 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: <a
-	 * href="http://www.faqs.org/qa/rfcc-1940.html">http://
-	 * www.faqs.org/qa/rfcc-1940.html</a>.
-	 */
-	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
-	 * <b>and</b> 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 <var>threeBytes</var> and
-	 * returns a four-byte array in Base64 notation. The actual number of
-	 * significant bytes in your array is given by <var>numSigBytes</var>. The
-	 * array <var>threeBytes</var> needs only be as big as
-	 * <var>numSigBytes</var>. Code can reuse a byte array by passing a
-	 * four-byte array as <var>b4</var>.
-	 * 
-	 * @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
-
-	/**
-	 * <p>
-	 * Encodes up to three bytes of the array <var>source</var> and writes the
-	 * resulting four Base64 bytes to <var>destination</var>. The source and
-	 * destination arrays can be manipulated anywhere along their length by
-	 * specifying <var>srcOffset</var> and <var>destOffset</var>. This method
-	 * does not check to make sure your arrays are large enough to accomodate
-	 * <var>srcOffset</var> + 3 for the <var>source</var> array or
-	 * <var>destOffset</var> + 4 for the <var>destination</var> array. The
-	 * actual number of significant bytes in your array is given by
-	 * <var>numSigBytes</var>.
-	 * </p>
-	 * <p>
-	 * This is the lowest level of the encoding methods with all possible
-	 * parameters.
-	 * </p>
-	 * 
-	 * @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 <var>destination</var> 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 <code>raw</code> ByteBuffer, writing it
-	 * to the <code>encoded</code> 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 <code>raw</code> ByteBuffer, writing it
-	 * to the <code>encoded</code> 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.
-	 * 
-	 * <p>
-	 * As of v 2.3, if the object cannot be serialized or there is another
-	 * error, the method will throw an java.io.IOException. <b>This is new to
-	 * v2.3!</b> In earlier versions, it just returned a null value, but in
-	 * retrospect that's a pretty poor way to handle it.
-	 * </p>
-	 * 
-	 * 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.
-	 * 
-	 * <p>
-	 * As of v 2.3, if the object cannot be serialized or there is another
-	 * error, the method will throw an java.io.IOException. <b>This is new to
-	 * v2.3!</b> In earlier versions, it just returned a null value, but in
-	 * retrospect that's a pretty poor way to handle it.
-	 * </p>
-	 * 
-	 * The object is not GZip-compressed before being encoded.
-	 * <p>
-	 * Example options:
-	 * 
-	 * <pre>
-	 *   GZIP: gzip-compresses object before encoding it.
-	 *   DO_BREAK_LINES: break lines at 76 characters
-	 * </pre>
-	 * <p>
-	 * Example: <code>encodeObject( myObj, Base64.GZIP )</code> or
-	 * <p>
-	 * Example:
-	 * <code>encodeObject( myObj, Base64.GZIP | Base64.DO_BREAK_LINES )</code>
-	 * 
-	 * @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.
-	 * <p>
-	 * Example options:
-	 * 
-	 * <pre>
-	 *   GZIP: gzip-compresses object before encoding it.
-	 *   DO_BREAK_LINES: break lines at 76 characters
-	 *     <i>Note: Technically, this makes your encoding non-compliant.</i>
-	 * </pre>
-	 * <p>
-	 * Example: <code>encodeBytes( myData, Base64.GZIP )</code> or
-	 * <p>
-	 * Example:
-	 * <code>encodeBytes( myData, Base64.GZIP | Base64.DO_BREAK_LINES )</code>
-	 * 
-	 * 
-	 * <p>
-	 * As of v 2.3, if there is an error with the GZIP stream, the method will
-	 * throw an java.io.IOException. <b>This is new to v2.3!</b> In earlier
-	 * versions, it just returned a null value, but in retrospect that's a
-	 * pretty poor way to handle it.
-	 * </p>
-	 * 
-	 * 
-	 * @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.
-	 * 
-	 * <p>
-	 * As of v 2.3, if there is an error, the method will throw an
-	 * java.io.IOException. <b>This is new to v2.3!</b> In earlier versions, it
-	 * just returned a null value, but in retrospect that's a pretty poor way to
-	 * handle it.
-	 * </p>
-	 * 
-	 * 
-	 * @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.
-	 * <p>
-	 * Example options:
-	 * 
-	 * <pre>
-	 *   GZIP: gzip-compresses object before encoding it.
-	 *   DO_BREAK_LINES: break lines at 76 characters
-	 *     <i>Note: Technically, this makes your encoding non-compliant.</i>
-	 * </pre>
-	 * <p>
-	 * Example: <code>encodeBytes( myData, Base64.GZIP )</code> or
-	 * <p>
-	 * Example:
-	 * <code>encodeBytes( myData, Base64.GZIP | Base64.DO_BREAK_LINES )</code>
-	 * 
-	 * 
-	 * <p>
-	 * As of v 2.3, if there is an error with the GZIP stream, the method will
-	 * throw an java.io.IOException. <b>This is new to v2.3!</b> In earlier
-	 * versions, it just returned a null value, but in retrospect that's a
-	 * pretty poor way to handle it.
-	 * </p>
-	 * 
-	 * 
-	 * @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 <var>source</var> and writes the resulting
-	 * bytes (up to three of them) to <var>destination</var>. The source and
-	 * destination arrays can be manipulated anywhere along their length by
-	 * specifying <var>srcOffset</var> and <var>destOffset</var>. This method
-	 * does not check to make sure your arrays are large enough to accomodate
-	 * <var>srcOffset</var> + 4 for the <var>source</var> array or
-	 * <var>destOffset</var> + 3 for the <var>destination</var> array. This
-	 * method returns the actual number of bytes that were converted from the
-	 * Base64 encoding.
-	 * <p>
-	 * This is the lowest level of the decoding methods with all possible
-	 * parameters.
-	 * </p>
-	 * 
-	 * 
-	 * @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. <strong>Ignores GUNZIP option, if it's set.</strong> 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. <strong>Ignores GUNZIP option, if it's set.</strong> 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 <tt>s</tt> 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
-		// </change>
-
-		// 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
-	 * <tt>java.io.OutputStream</tt>, 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 <tt>java.io.OutputStream</tt> 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.
-		 * <p>
-		 * Valid options:
-		 * 
-		 * <pre>
-		 *   ENCODE or DECODE: Encode or Decode as data is read.
-		 *   DO_BREAK_LINES: don't break lines at 76 characters
-		 *     (only meaningful when encoding)</i>
-		 * </pre>
-		 * <p>
-		 * Example: <code>new Base64.OutputStream( out, Base64.ENCODE )</code>
-		 * 
-		 * @param out
-		 *            the <tt>java.io.OutputStream</tt> 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 <var>len</var> 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

http://git-wip-us.apache.org/repos/asf/incubator-guacamole-client/blob/9056bb0f/extensions/guacamole-auth-duo/src/main/java/com/duosecurity/duoweb/DuoWeb.java
----------------------------------------------------------------------
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 223a110..0000000
--- 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;
-	}
-}

http://git-wip-us.apache.org/repos/asf/incubator-guacamole-client/blob/9056bb0f/extensions/guacamole-auth-duo/src/main/java/com/duosecurity/duoweb/DuoWebException.java
----------------------------------------------------------------------
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 f721df7..0000000
--- 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);
-  }
-}

http://git-wip-us.apache.org/repos/asf/incubator-guacamole-client/blob/9056bb0f/extensions/guacamole-auth-duo/src/main/java/com/duosecurity/duoweb/Util.java
----------------------------------------------------------------------
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 55d7712..0000000
--- 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;
-	}
-}

http://git-wip-us.apache.org/repos/asf/incubator-guacamole-client/blob/9056bb0f/extensions/guacamole-auth-duo/src/main/java/org/apache/guacamole/auth/duo/DuoAuthenticationProviderModule.java
----------------------------------------------------------------------
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 2dfc4eb..705e37d 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);
 
     }

http://git-wip-us.apache.org/repos/asf/incubator-guacamole-client/blob/9056bb0f/extensions/guacamole-auth-duo/src/main/java/org/apache/guacamole/auth/duo/DuoWebService.java
----------------------------------------------------------------------
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 3cdfdde..0000000
--- 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);
-
-    }
-
-}

http://git-wip-us.apache.org/repos/asf/incubator-guacamole-client/blob/9056bb0f/extensions/guacamole-auth-duo/src/main/java/org/apache/guacamole/auth/duo/UserVerificationService.java
----------------------------------------------------------------------
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 3209be2..777c96b 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");
 
     }

http://git-wip-us.apache.org/repos/asf/incubator-guacamole-client/blob/9056bb0f/extensions/guacamole-auth-duo/src/main/java/org/apache/guacamole/auth/duo/api/DuoCookie.java
----------------------------------------------------------------------
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 0000000..1de9a67
--- /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);
+        }
+
+    }
+
+}


[3/3] incubator-guacamole-client git commit: GUACAMOLE-136: Merge replacement of sketchy DuoWeb java code.

Posted by jm...@apache.org.
GUACAMOLE-136: Merge replacement of sketchy DuoWeb java code.


Project: http://git-wip-us.apache.org/repos/asf/incubator-guacamole-client/repo
Commit: http://git-wip-us.apache.org/repos/asf/incubator-guacamole-client/commit/653b7f58
Tree: http://git-wip-us.apache.org/repos/asf/incubator-guacamole-client/tree/653b7f58
Diff: http://git-wip-us.apache.org/repos/asf/incubator-guacamole-client/diff/653b7f58

Branch: refs/heads/master
Commit: 653b7f58afb93bea4fc5f0a5f89d037d4aab8c21
Parents: cf6a2b8 9056bb0
Author: James Muehlner <ja...@guac-dev.org>
Authored: Sun Dec 11 20:41:11 2016 -0800
Committer: James Muehlner <ja...@guac-dev.org>
Committed: Sun Dec 11 20:41:55 2016 -0800

----------------------------------------------------------------------
 .../java/com/duosecurity/duoweb/Base64.java     | 1500 ------------------
 .../java/com/duosecurity/duoweb/DuoWeb.java     |  138 --
 .../com/duosecurity/duoweb/DuoWebException.java |    8 -
 .../main/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 +++
 .../guacamole/auth/duo/api/SignedDuoCookie.java |  332 ++++
 10 files changed, 789 insertions(+), 1889 deletions(-)
----------------------------------------------------------------------