blob: 49fb34b017d408c234da24cd0167462ba2c064d5 [file] [log] [blame]
/*
* 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;
}
}