blob: c1765759f51331a7b47db83f378b4fe9a79f1a93 [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.qpid.server.security.encryption;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.attribute.AclEntry;
import java.nio.file.attribute.AclEntryPermission;
import java.nio.file.attribute.AclEntryType;
import java.nio.file.attribute.AclFileAttributeView;
import java.nio.file.attribute.FileAttribute;
import java.nio.file.attribute.PosixFileAttributeView;
import java.nio.file.attribute.PosixFilePermission;
import java.nio.file.attribute.PosixFilePermissions;
import java.nio.file.attribute.UserPrincipal;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.EnumSet;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.ListIterator;
import java.util.Map;
import java.util.Set;
import javax.crypto.Cipher;
import javax.crypto.KeyGenerator;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.apache.qpid.server.configuration.IllegalConfigurationException;
import org.apache.qpid.server.model.ConfiguredObject;
import org.apache.qpid.server.model.SystemConfig;
import org.apache.qpid.server.plugin.ConditionallyAvailable;
import org.apache.qpid.server.plugin.ConfigurationSecretEncrypterFactory;
import org.apache.qpid.server.plugin.PluggableService;
@PluggableService
public class AESKeyFileEncrypterFactory implements ConfigurationSecretEncrypterFactory, ConditionallyAvailable
{
private static final Logger LOGGER = LoggerFactory.getLogger(AESKeyFileEncrypterFactory.class);
static final String ENCRYPTER_KEY_FILE = "encrypter.key.file";
private static final int AES_KEY_SIZE_BITS = 256;
private static final int AES_KEY_SIZE_BYTES = AES_KEY_SIZE_BITS / 8;
private static final String AES_ALGORITHM = "AES";
public static final String TYPE = "AESKeyFile";
static final String DEFAULT_KEYS_SUBDIR_NAME = ".keys";
private static final boolean IS_AVAILABLE;
private static final String ILLEGAL_ARG_EXCEPTION = "Unable to determine a mechanism to protect access to the key file on this filesystem";
static
{
boolean isAvailable;
try
{
final int allowedKeyLength = Cipher.getMaxAllowedKeyLength(AES_ALGORITHM);
isAvailable = allowedKeyLength >=AES_KEY_SIZE_BITS;
if(!isAvailable)
{
LOGGER.warn("The {} configuration encryption encryption mechanism is not available. "
+ "Maximum available AES key length is {} but {} is required. "
+ "Ensure the full strength JCE policy has been installed into your JVM.", TYPE, allowedKeyLength, AES_KEY_SIZE_BITS);
}
}
catch (NoSuchAlgorithmException e)
{
isAvailable = false;
LOGGER.error("The " + TYPE + " configuration encryption encryption mechanism is not available. "
+ "The " + AES_ALGORITHM + " algorithm is not available within the JVM (despite it being a requirement).");
}
IS_AVAILABLE = isAvailable;
}
@Override
public ConfigurationSecretEncrypter createEncrypter(final ConfiguredObject<?> object)
{
String fileLocation;
if(object.getContextKeys(false).contains(ENCRYPTER_KEY_FILE))
{
fileLocation = object.getContextValue(String.class, ENCRYPTER_KEY_FILE);
}
else
{
fileLocation = object.getContextValue(String.class, SystemConfig.QPID_WORK_DIR)
+ File.separator + DEFAULT_KEYS_SUBDIR_NAME + File.separator
+ object.getCategoryClass().getSimpleName() + "_"
+ object.getName() + ".key";
Map<String, String> context = object.getContext();
Map<String, String> modifiedContext = new LinkedHashMap<>(context);
modifiedContext.put(ENCRYPTER_KEY_FILE, fileLocation);
object.setAttributes(Collections.<String, Object>singletonMap(ConfiguredObject.CONTEXT, modifiedContext));
}
File file = new File(fileLocation);
if(!file.exists())
{
LOGGER.warn("Configuration encryption is enabled, but no configuration secret was found. A new configuration secret will be created at '{}'.", fileLocation);
createAndPopulateKeyFile(file);
}
if(!file.isFile())
{
throw new IllegalArgumentException("File '"+fileLocation+"' is not a regular file.");
}
try
{
checkFilePermissions(fileLocation, file);
if(Files.size(file.toPath()) != AES_KEY_SIZE_BYTES)
{
throw new IllegalArgumentException("Key file '" + fileLocation + "' contains an incorrect about of data");
}
try(FileInputStream inputStream = new FileInputStream(file))
{
byte[] key = new byte[AES_KEY_SIZE_BYTES];
int pos = 0;
int read;
while(pos < key.length && -1 != ( read = inputStream.read(key, pos, key.length - pos)))
{
pos += read;
}
if(pos != key.length)
{
throw new IllegalConfigurationException("Key file '" + fileLocation + "' contained an incorrect about of data");
}
SecretKeySpec keySpec = new SecretKeySpec(key, AES_ALGORITHM);
return new AESKeyFileEncrypter(keySpec);
}
}
catch (IOException e)
{
throw new IllegalConfigurationException("Unable to get file permissions: " + e.getMessage(), e);
}
}
private void checkFilePermissions(String fileLocation, File file) throws IOException
{
if(isPosixFileSystem(file))
{
Set<PosixFilePermission> permissions = Files.getPosixFilePermissions(file.toPath());
if (permissions.contains(PosixFilePermission.GROUP_READ)
|| permissions.contains(PosixFilePermission.OTHERS_READ)
|| permissions.contains(PosixFilePermission.GROUP_WRITE)
|| permissions.contains(PosixFilePermission.OTHERS_WRITE)) {
throw new IllegalArgumentException("Key file '"
+ fileLocation
+ "' has incorrect permissions. Only the owner "
+ "should be able to read or write this file.");
}
}
else if(isAclFileSystem(file))
{
AclFileAttributeView attributeView = Files.getFileAttributeView(file.toPath(), AclFileAttributeView.class);
ArrayList<AclEntry> acls = new ArrayList<>(attributeView.getAcl());
ListIterator<AclEntry> iter = acls.listIterator();
UserPrincipal owner = Files.getOwner(file.toPath());
while(iter.hasNext())
{
AclEntry acl = iter.next();
if(acl.type() == AclEntryType.ALLOW)
{
Set<AclEntryPermission> originalPermissions = acl.permissions();
Set<AclEntryPermission> updatedPermissions = EnumSet.copyOf(originalPermissions);
if (updatedPermissions.removeAll(EnumSet.of(AclEntryPermission.APPEND_DATA,
AclEntryPermission.EXECUTE,
AclEntryPermission.WRITE_ACL,
AclEntryPermission.WRITE_DATA,
AclEntryPermission.WRITE_OWNER))) {
throw new IllegalArgumentException("Key file '"
+ fileLocation
+ "' has incorrect permissions. The file should not be modifiable by any user.");
}
if (!owner.equals(acl.principal()) && updatedPermissions.removeAll(EnumSet.of(AclEntryPermission.READ_DATA))) {
throw new IllegalArgumentException("Key file '"
+ fileLocation
+ "' has incorrect permissions. Only the owner should be able to read from the file.");
}
}
}
}
else
{
throw new IllegalArgumentException(ILLEGAL_ARG_EXCEPTION);
}
}
private boolean isPosixFileSystem(File file) throws IOException
{
return Files.getFileAttributeView(file.toPath(), PosixFileAttributeView.class) != null;
}
private boolean isAclFileSystem(File file) throws IOException
{
return Files.getFileAttributeView(file.toPath(), AclFileAttributeView.class) != null;
}
private void createAndPopulateKeyFile(final File file)
{
try
{
createEmptyKeyFile(file);
KeyGenerator keyGenerator = KeyGenerator.getInstance(AES_ALGORITHM);
keyGenerator.init(AES_KEY_SIZE_BITS);
SecretKey key = keyGenerator.generateKey();
try(FileOutputStream os = new FileOutputStream(file))
{
os.write(key.getEncoded());
}
makeKeyFileReadOnly(file);
}
catch (NoSuchAlgorithmException | IOException e)
{
throw new IllegalArgumentException("Cannot create key file: " + e.getMessage(), e);
}
}
private void makeKeyFileReadOnly(File file) throws IOException
{
if(isPosixFileSystem(file))
{
Files.setPosixFilePermissions(file.toPath(), EnumSet.of(PosixFilePermission.OWNER_READ));
}
else if(isAclFileSystem(file))
{
AclFileAttributeView attributeView = Files.getFileAttributeView(file.toPath(), AclFileAttributeView.class);
ArrayList<AclEntry> acls = new ArrayList<>(attributeView.getAcl());
ListIterator<AclEntry> iter = acls.listIterator();
file.setReadOnly();
while(iter.hasNext())
{
AclEntry acl = iter.next();
Set<AclEntryPermission> originalPermissions = acl.permissions();
Set<AclEntryPermission> updatedPermissions = EnumSet.copyOf(originalPermissions);
if(updatedPermissions.removeAll(EnumSet.of(AclEntryPermission.APPEND_DATA,
AclEntryPermission.DELETE,
AclEntryPermission.EXECUTE,
AclEntryPermission.WRITE_ACL,
AclEntryPermission.WRITE_DATA,
AclEntryPermission.WRITE_ATTRIBUTES,
AclEntryPermission.WRITE_NAMED_ATTRS,
AclEntryPermission.WRITE_OWNER)))
{
AclEntry.Builder builder = AclEntry.newBuilder(acl);
builder.setPermissions(updatedPermissions);
iter.set(builder.build());
}
}
attributeView.setAcl(acls);
}
else
{
throw new IllegalArgumentException(ILLEGAL_ARG_EXCEPTION);
}
}
private void createEmptyKeyFile(File file) throws IOException
{
final Path parentFilePath = file.getAbsoluteFile().getParentFile().toPath();
if(isPosixFileSystem(file)) {
Set<PosixFilePermission> ownerOnly = EnumSet.of(PosixFilePermission.OWNER_READ,
PosixFilePermission.OWNER_WRITE,
PosixFilePermission.OWNER_EXECUTE);
Files.createDirectories(parentFilePath, PosixFilePermissions.asFileAttribute(ownerOnly));
Files.createFile(file.toPath(), PosixFilePermissions.asFileAttribute(
EnumSet.of(PosixFilePermission.OWNER_READ, PosixFilePermission.OWNER_WRITE)));
}
else if(isAclFileSystem(file))
{
Files.createDirectories(parentFilePath);
final UserPrincipal owner = Files.getOwner(parentFilePath);
AclFileAttributeView attributeView = Files.getFileAttributeView(parentFilePath, AclFileAttributeView.class);
List<AclEntry> acls = new ArrayList<>(attributeView.getAcl());
ListIterator<AclEntry> iter = acls.listIterator();
boolean found = false;
while(iter.hasNext())
{
AclEntry acl = iter.next();
if(!owner.equals(acl.principal()))
{
iter.remove();
}
else if(acl.type() == AclEntryType.ALLOW)
{
found = true;
AclEntry.Builder builder = AclEntry.newBuilder(acl);
Set<AclEntryPermission> permissions = acl.permissions().isEmpty() ? new HashSet<AclEntryPermission>() : EnumSet.copyOf(acl.permissions());
permissions.addAll(Arrays.asList(AclEntryPermission.ADD_FILE, AclEntryPermission.ADD_SUBDIRECTORY, AclEntryPermission.LIST_DIRECTORY));
builder.setPermissions(permissions);
iter.set(builder.build());
}
}
if(!found)
{
AclEntry.Builder builder = AclEntry.newBuilder();
builder.setPermissions(AclEntryPermission.ADD_FILE, AclEntryPermission.ADD_SUBDIRECTORY, AclEntryPermission.LIST_DIRECTORY);
builder.setType(AclEntryType.ALLOW);
builder.setPrincipal(owner);
acls.add(builder.build());
}
attributeView.setAcl(acls);
Files.createFile(file.toPath(), new FileAttribute<List<AclEntry>>()
{
@Override
public String name()
{
return "acl:acl";
}
@Override
public List<AclEntry> value() {
AclEntry.Builder builder = AclEntry.newBuilder();
builder.setType(AclEntryType.ALLOW);
builder.setPermissions(EnumSet.allOf(AclEntryPermission.class));
builder.setPrincipal(owner);
return Collections.singletonList(builder.build());
}
});
}
else
{
throw new IllegalArgumentException(ILLEGAL_ARG_EXCEPTION);
}
}
@Override
public String getType()
{
return TYPE;
}
@Override
public boolean isAvailable()
{
return IS_AVAILABLE;
}
}