mirror of
				https://github.com/gyurix1968/guacamole-client.git
				synced 2025-10-27 15:13:07 +00:00 
			
		
		
		
	GUACAMOLE-136: Remove DuoWeb Java API from codebase. Re-implement cleanly from scratch.
This commit is contained in:
		
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							| @@ -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; | ||||
| 	} | ||||
| } | ||||
| @@ -1,8 +0,0 @@ | ||||
| package com.duosecurity.duoweb; | ||||
|  | ||||
| public class DuoWebException extends Exception { | ||||
|  | ||||
|   public DuoWebException(String message) { | ||||
|     super(message); | ||||
|   } | ||||
| } | ||||
| @@ -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; | ||||
| 	} | ||||
| } | ||||
| @@ -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); | ||||
|  | ||||
|     } | ||||
|   | ||||
| @@ -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); | ||||
|  | ||||
|     } | ||||
|  | ||||
| } | ||||
| @@ -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"); | ||||
|  | ||||
|     } | ||||
|   | ||||
| @@ -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); | ||||
|         } | ||||
|  | ||||
|     } | ||||
|  | ||||
| } | ||||
| @@ -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; | ||||
|  | ||||
|     } | ||||
|  | ||||
| } | ||||
| @@ -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; | ||||
|     } | ||||
|  | ||||
| } | ||||
		Reference in New Issue
	
	Block a user