You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@guacamole.apache.org by vn...@apache.org on 2018/02/05 18:04:22 UTC
[04/21] guacamole-client git commit: GUACAMOLE-96: Abstract TOTP key
into separate class with confirmation semantics.
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 <mj...@apache.org>
Authored: Mon Nov 20 10:56:35 2017 -0800
Committer: Michael Jumper <mj...@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");