GUACAMOLE-96: Abstract TOTP key into separate class with confirmation semantics.

This commit is contained in:
Michael Jumper
2017-11-20 10:56:35 -08:00
parent 4178a4b8b3
commit 8e3cbf0627
2 changed files with 256 additions and 17 deletions

View File

@@ -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;
}
}

View File

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