/* * * 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; } }