GUACAMOLE-96: Abstract TOTP key into separate class with confirmation semantics.
Project: http://git-wip-us.apache.org/repos/asf/guacamole-client/repo Commit: http://git-wip-us.apache.org/repos/asf/guacamole-client/commit/8e3cbf06 Tree: http://git-wip-us.apache.org/repos/asf/guacamole-client/tree/8e3cbf06 Diff: http://git-wip-us.apache.org/repos/asf/guacamole-client/diff/8e3cbf06 Branch: refs/heads/master Commit: 8e3cbf06274c385afb99340e3b1c153a7946fb08 Parents: 4178a4b Author: Michael Jumper <mjum...@apache.org> Authored: Mon Nov 20 10:56:35 2017 -0800 Committer: Michael Jumper <mjum...@apache.org> Committed: Sun Feb 4 19:45:17 2018 -0800 ---------------------------------------------------------------------- .../apache/guacamole/auth/totp/UserTOTPKey.java | 126 ++++++++++++++++ .../auth/totp/UserVerificationService.java | 147 ++++++++++++++++--- 2 files changed, 256 insertions(+), 17 deletions(-) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/guacamole-client/blob/8e3cbf06/extensions/guacamole-auth-totp/src/main/java/org/apache/guacamole/auth/totp/UserTOTPKey.java ---------------------------------------------------------------------- diff --git a/extensions/guacamole-auth-totp/src/main/java/org/apache/guacamole/auth/totp/UserTOTPKey.java b/extensions/guacamole-auth-totp/src/main/java/org/apache/guacamole/auth/totp/UserTOTPKey.java new file mode 100644 index 0000000..d93d253 --- /dev/null +++ b/extensions/guacamole-auth-totp/src/main/java/org/apache/guacamole/auth/totp/UserTOTPKey.java @@ -0,0 +1,126 @@ +/* + * 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.totp; + +import java.security.SecureRandom; +import java.util.Random; + +/** + * The key used to generate TOTP codes for a particular user. + */ +public class UserTOTPKey { + + /** + * Secure source of random bytes. + */ + private static final Random RANDOM = new SecureRandom(); + + /** + * Whether the associated secret key has been confirmed by the user. A key + * is confirmed once the user has successfully entered a valid TOTP + * derived from that key. + */ + private boolean confirmed; + + /** + * The base32-encoded TOTP key associated with the user. + */ + private byte[] secret; + + /** + * Generates the given number of random bytes. + * + * @param length + * The number of random bytes to generate. + * + * @return + * A new array of exactly the given number of random bytes. + */ + private static byte[] generateBytes(int length) { + byte[] bytes = new byte[length]; + RANDOM.nextBytes(bytes); + return bytes; + } + + /** + * Creates a new, unconfirmed, randomly-generated TOTP key having the given + * length. + * + * @param length + * The length of the key to generate, in bytes. + */ + public UserTOTPKey(int length) { + this(generateBytes(length), false); + } + + /** + * Creates a new UserTOTPKey containing the given key and having the given + * confirmed state. + * + * @param secret + * The raw binary secret key to be used to generate TOTP codes. + * + * @param confirmed + * true if the user associated with the key has confirmed that they can + * successfully generate the corresponding TOTP codes (the user has + * been "enrolled"), false otherwise. + */ + public UserTOTPKey(byte[] secret, boolean confirmed) { + this.confirmed = confirmed; + this.secret = secret; + } + + /** + * Returns the raw binary secret key to be used to generate TOTP codes. + * + * @return + * The raw binary secret key to be used to generate TOTP codes. + */ + public byte[] getSecret() { + return secret; + } + + /** + * Returns whether the user associated with the key has confirmed that they + * can successfully generate the corresponding TOTP codes (the user has + * been "enrolled"). + * + * @return + * true if the user has confirmed that they can successfully generate + * the TOTP codes generated by this key, false otherwise. + */ + public boolean isConfirmed() { + return confirmed; + } + + /** + * Sets whether the user associated with the key has confirmed that they + * can successfully generate the corresponding TOTP codes (the user has + * been "enrolled"). + * + * @param confirmed + * true if the user has confirmed that they can successfully generate + * the TOTP codes generated by this key, false otherwise. + */ + public void setConfirmed(boolean confirmed) { + this.confirmed = confirmed; + } + +} http://git-wip-us.apache.org/repos/asf/guacamole-client/blob/8e3cbf06/extensions/guacamole-auth-totp/src/main/java/org/apache/guacamole/auth/totp/UserVerificationService.java ---------------------------------------------------------------------- diff --git a/extensions/guacamole-auth-totp/src/main/java/org/apache/guacamole/auth/totp/UserVerificationService.java b/extensions/guacamole-auth-totp/src/main/java/org/apache/guacamole/auth/totp/UserVerificationService.java index 823c5ef..d694c5e 100644 --- a/extensions/guacamole-auth-totp/src/main/java/org/apache/guacamole/auth/totp/UserVerificationService.java +++ b/extensions/guacamole-auth-totp/src/main/java/org/apache/guacamole/auth/totp/UserVerificationService.java @@ -22,14 +22,17 @@ package org.apache.guacamole.auth.totp; import com.google.common.io.BaseEncoding; import java.security.InvalidKeyException; import java.util.Collections; +import java.util.HashMap; import java.util.Map; import javax.servlet.http.HttpServletRequest; import org.apache.guacamole.GuacamoleClientException; import org.apache.guacamole.GuacamoleException; +import org.apache.guacamole.GuacamoleUnsupportedException; import org.apache.guacamole.form.Field; import org.apache.guacamole.form.TextField; import org.apache.guacamole.net.auth.AuthenticatedUser; import org.apache.guacamole.net.auth.Credentials; +import org.apache.guacamole.net.auth.User; import org.apache.guacamole.net.auth.UserContext; import org.apache.guacamole.net.auth.credentials.CredentialsInfo; import org.apache.guacamole.net.auth.credentials.GuacamoleInsufficientCredentialsException; @@ -50,7 +53,13 @@ public class UserVerificationService { /** * The name of the user attribute which stores the TOTP key. */ - private static final String TOTP_KEY_ATTRIBUTE_NAME = "guac-totp-key"; + private static final String TOTP_KEY_SECRET_ATTRIBUTE_NAME = "guac-totp-key-secret"; + + /** + * The name of the user attribute defines whether the TOTP key has been + * confirmed by the user, and the user is thus fully enrolled. + */ + private static final String TOTP_KEY_CONFIRMED_ATTRIBUTE_NAME = "guac-totp-key-confirmed"; /** * The name of the HTTP parameter which will contain the TOTP code provided @@ -78,20 +87,115 @@ public class UserVerificationService { private static final BaseEncoding BASE32 = BaseEncoding.base32(); /** - * Retrieves the base32-encoded TOTP key associated with user having the - * given UserContext. If no TOTP key is associated with the user, null is + * Retrieves and decodes the base32-encoded TOTP key associated with user + * having the given UserContext. If no TOTP key is associated with the user, + * a random key is generated and associated with the user. If the extension + * storing the user does not support storage of the TOTP key, null is * returned. * * @param context * The UserContext of the user whose TOTP key should be retrieved. * * @return - * The base32-encoded TOTP key associated with user having the given - * UserContext, or null if no TOTP key is associated with the user. + * The TOTP key associated with the user having the given UserContext, + * or null if the extension storing the user does not support storage + * of the TOTP key. + * + * @throws GuacamoleException + * If a new key is generated, but the extension storing the associated + * user fails while updating the user account. */ - public String getKey(UserContext context){ + private UserTOTPKey getKey(UserContext context) throws GuacamoleException { + + // Retrieve attributes from current user + User self = context.self(); Map<String, String> attributes = context.self().getAttributes(); - return attributes.get(TOTP_KEY_ATTRIBUTE_NAME); + + // If no key is defined, attempt to generate a new key + String secret = attributes.get(TOTP_KEY_SECRET_ATTRIBUTE_NAME); + if (secret == null) { + + // Generate random key for user + UserTOTPKey generated = new UserTOTPKey(TOTPGenerator.Mode.SHA1.getRecommendedKeyLength()); + if (setKey(context, generated)) + return generated; + + // Fail if key cannot be set + return null; + + } + + // Parse retrieved base32 key value + byte[] key; + try { + key = BASE32.decode(secret); + } + + // If key is not valid base32, warn but otherwise pretend the key does + // not exist + catch (IllegalArgumentException e) { + logger.warn("TOTP key of user \"{}\" is not valid base32.", self.getIdentifier()); + logger.debug("TOTP key is not valid base32.", e); + return null; + } + + // Otherwise, parse value from attributes + boolean confirmed = "true".equals(attributes.get(TOTP_KEY_CONFIRMED_ATTRIBUTE_NAME)); + return new UserTOTPKey(key, confirmed); + + } + + /** + * Attempts to store the given TOTP key within the user account of the user + * having the given UserContext. As not all extensions will support storage + * of arbitrary attributes, this operation may fail. + * + * @param context + * The UserContext associated with the user whose TOTP key is to be + * stored. + * + * @param key + * The TOTP key to store. + * + * @return + * true if the TOTP key was successfully stored, false if the extension + * handling storage does not support storage of the key. + * + * @throws GuacamoleException + * If the extension handling storage fails internally while attempting + * to update the user. + */ + private boolean setKey(UserContext context, UserTOTPKey key) + throws GuacamoleException { + + // Get mutable set of attributes + User self = context.self(); + Map<String, String> attributes = new HashMap<String, String>(); + + // Set/overwrite current TOTP key state + attributes.put(TOTP_KEY_SECRET_ATTRIBUTE_NAME, BASE32.encode(key.getSecret())); + attributes.put(TOTP_KEY_CONFIRMED_ATTRIBUTE_NAME, key.isConfirmed() ? "true" : "false"); + self.setAttributes(attributes); + + // Confirm that attributes have actually been set + Map<String, String> setAttributes = self.getAttributes(); + if (!setAttributes.containsKey(TOTP_KEY_SECRET_ATTRIBUTE_NAME) + || !setAttributes.containsKey(TOTP_KEY_CONFIRMED_ATTRIBUTE_NAME)) + return false; + + // Update user object + try { + context.getUserDirectory().update(self); + } + catch (GuacamoleUnsupportedException e) { + logger.debug("Extension storage for user is explicitly read-only. " + + "Cannot update attributes to store TOTP key.", e); + return false; + } + + // TOTP key successfully stored/updated + return true; + } /** @@ -121,8 +225,8 @@ public class UserVerificationService { return; // Ignore users which do not have an associated key - String encodedKey = getKey(context); - if (encodedKey == null) + UserTOTPKey key = getKey(context); + if (key == null) return; // Pull the original HTTP request used to authenticate @@ -133,27 +237,36 @@ public class UserVerificationService { String code = request.getParameter(TOTP_PARAMETER_NAME); // If no TOTP provided, request one - if (code == null) + if (code == null) { + + // FIXME: Handle key.isConfirmed() for initial prompt throw new GuacamoleInsufficientCredentialsException( "LOGIN.INFO_TOTP_REQUIRED", TOTP_CREDENTIALS); + } + try { // Verify provided TOTP against value produced by generator - byte[] key = BASE32.decode(encodedKey); - TOTPGenerator totp = new TOTPGenerator(key, TOTPGenerator.Mode.SHA1, 6); - if (code.equals(totp.generate()) || code.equals(totp.previous())) + TOTPGenerator totp = new TOTPGenerator(key.getSecret(), TOTPGenerator.Mode.SHA1, 6); + if (code.equals(totp.generate()) || code.equals(totp.previous())) { + + // Record key as confirmed, if it hasn't already been so recorded + if (!key.isConfirmed()) { + key.setConfirmed(true); + setKey(context, key); + } + + // User has been verified return; + } + } catch (InvalidKeyException e) { logger.warn("User \"{}\" is associated with an invalid TOTP key.", username); logger.debug("TOTP key is not valid.", e); } - catch (IllegalArgumentException e) { - logger.warn("TOTP key of user \"{}\" is not valid base32.", username); - logger.debug("TOTP key is not valid base32.", e); - } // Provided code is not valid throw new GuacamoleClientException("LOGIN.INFO_TOTP_VERIFICATION_FAILED");