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");

Reply via email to