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(-)
----------------------------------------------------------------------