001/* 002 * (C) Copyright 2019 Nuxeo (http://nuxeo.com/) and others. 003 * 004 * Licensed under the Apache License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * http://www.apache.org/licenses/LICENSE-2.0 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 * 016 * Contributors: 017 * Florent Guillaume 018 */ 019package org.nuxeo.ecm.core.blob; 020 021import static org.apache.commons.lang3.StringUtils.defaultIfBlank; 022import static org.apache.commons.lang3.StringUtils.isBlank; 023 024import java.io.BufferedInputStream; 025import java.io.IOException; 026import java.io.InputStream; 027import java.nio.file.Files; 028import java.nio.file.Paths; 029import java.security.GeneralSecurityException; 030import java.security.Key; 031import java.security.KeyStore; 032import java.security.spec.AlgorithmParameterSpec; 033import java.util.Arrays; 034import java.util.Map; 035 036import javax.crypto.Cipher; 037import javax.crypto.SecretKeyFactory; 038import javax.crypto.spec.GCMParameterSpec; 039import javax.crypto.spec.IvParameterSpec; 040import javax.crypto.spec.PBEKeySpec; 041import javax.crypto.spec.SecretKeySpec; 042 043import org.apache.logging.log4j.LogManager; 044import org.apache.logging.log4j.Logger; 045import org.nuxeo.ecm.core.api.NuxeoException; 046import org.nuxeo.runtime.api.Framework; 047 048/** 049 * Configuration for the AES-encrypted storage of files. 050 * 051 * @since 11.1 052 */ 053public class AESBlobStoreConfiguration extends PropertyBasedConfiguration { 054 055 private static final Logger log = LogManager.getLogger(AESBlobStoreConfiguration.class); 056 057 protected static final String AES = "AES"; 058 059 protected static final String PBKDF2_WITH_HMAC_SHA1 = "PBKDF2WithHmacSHA1"; 060 061 protected static final int PBKDF2_ITERATIONS = 10000; 062 063 // AES-256 064 protected static final int PBKDF2_KEY_LENGTH = 256; 065 066 // insecure, see https://find-sec-bugs.github.io/bugs.htm#PADDING_ORACLE 067 protected static final String AES_CBC_PKCS5_PADDING = "AES/CBC/PKCS5Padding"; 068 069 protected static final String AES_GCM_NOPADDING = "AES/GCM/NoPadding"; 070 071 public static final String PROP_COMPAT_KEY = "key"; 072 073 public static final String PROP_PASSWORD = "password"; 074 075 public static final String PROP_KEY_STORE_TYPE = "keyStoreType"; 076 077 public static final String PROP_KEY_STORE_FILE = "keyStoreFile"; 078 079 public static final String PROP_KEY_STORE_PASSWORD = "keyStorePassword"; 080 081 public static final String PROP_KEY_ALIAS = "keyAlias"; 082 083 public static final String PROP_KEY_PASSWORD = "keyPassword"; 084 085 /** 086 * If {@code true}, use the insecure AES/CBC/PKCS5Padding for encryption. The default is {@code false}, to use 087 * AES/GCM/NoPadding. 088 */ 089 public static final String PROP_KEY_USE_INSECURE_CIPHER = "useInsecureCipher"; 090 091 public final boolean usePBKDF2; 092 093 public final String password; 094 095 public final String keyStoreType; 096 097 public final String keyStoreFile; 098 099 public final String keyStorePassword; 100 101 public final String keyAlias; 102 103 public final String keyPassword; 104 105 public final boolean useInsecureCipher; 106 107 public AESBlobStoreConfiguration(Map<String, String> properties) throws IOException { 108 super(null, properties); 109 parseCompat(); 110 password = getProperty(PROP_PASSWORD); 111 keyStoreType = getProperty(PROP_KEY_STORE_TYPE); 112 keyStoreFile = getProperty(PROP_KEY_STORE_FILE); 113 keyStorePassword = getProperty(PROP_KEY_STORE_PASSWORD); 114 keyAlias = getProperty(PROP_KEY_ALIAS); 115 String keyPassword = getProperty(PROP_KEY_PASSWORD); // NOSONAR 116 useInsecureCipher = Boolean.parseBoolean(getProperty(PROP_KEY_USE_INSECURE_CIPHER)); 117 118 usePBKDF2 = password != null; 119 if (usePBKDF2) { 120 if (keyStoreType != null) { 121 throw new NuxeoException("Cannot use " + PROP_KEY_STORE_TYPE + " with " + PROP_PASSWORD); 122 } 123 if (keyStoreFile != null) { 124 throw new NuxeoException("Cannot use " + PROP_KEY_STORE_FILE + " with " + PROP_PASSWORD); 125 } 126 if (keyStorePassword != null) { 127 throw new NuxeoException("Cannot use " + PROP_KEY_STORE_PASSWORD + " with " + PROP_PASSWORD); 128 } 129 if (keyAlias != null) { 130 throw new NuxeoException("Cannot use " + PROP_KEY_ALIAS + " with " + PROP_PASSWORD); 131 } 132 if (keyPassword != null) { 133 throw new NuxeoException("Cannot use " + PROP_KEY_PASSWORD + " with " + PROP_PASSWORD); 134 } 135 } else { 136 if (keyStoreType == null) { 137 throw new NuxeoException("Missing " + PROP_KEY_STORE_TYPE); 138 } 139 // keystore file is optional 140 if (keyStoreFile == null && keyStorePassword != null) { 141 throw new NuxeoException("Missing " + PROP_KEY_STORE_PASSWORD); 142 } 143 if (keyAlias == null) { 144 throw new NuxeoException("Missing " + PROP_KEY_ALIAS); 145 } 146 if (keyPassword == null) { 147 keyPassword = keyStorePassword; 148 } 149 } 150 this.keyPassword = keyPassword; 151 } 152 153 protected void parseCompat() { 154 String compatKey = getProperty(PROP_COMPAT_KEY); 155 if (isBlank(compatKey)) { 156 return; 157 } 158 for (String option : compatKey.split(",")) { 159 String[] split = option.split("=", 2); 160 if (split.length != 2) { 161 log.error("Unrecognized option '" + option + "' in compatibility property '" + PROP_COMPAT_KEY + "'"); 162 continue; 163 } 164 String prop = split[0]; 165 String value = defaultIfBlank(split[1], null); 166 if (!Arrays.asList(PROP_PASSWORD, // 167 PROP_KEY_STORE_TYPE, // 168 PROP_KEY_STORE_FILE, // 169 PROP_KEY_STORE_PASSWORD, // 170 PROP_KEY_ALIAS, // 171 PROP_KEY_PASSWORD, // 172 PROP_KEY_USE_INSECURE_CIPHER).contains(prop)) { 173 log.error("Unrecognized property '" + prop + "' in compatibility property '" + PROP_COMPAT_KEY + "'"); 174 continue; 175 } 176 if (properties.containsKey(prop)) { 177 log.error("Ignoring property " + option + " in compatibility property '" + PROP_COMPAT_KEY 178 + "' because it is already present as a standard property"); 179 continue; 180 } 181 properties.put(prop, value); 182 } 183 } 184 185 /** 186 * Generates an AES key from the password using PBKDF2. 187 * 188 * @param salt the salt 189 */ 190 protected Key generateSecretKey(byte[] salt) throws GeneralSecurityException { 191 char[] pw = password.toCharArray(); 192 SecretKeyFactory factory = SecretKeyFactory.getInstance(PBKDF2_WITH_HMAC_SHA1); 193 PBEKeySpec spec = new PBEKeySpec(pw, salt, PBKDF2_ITERATIONS, PBKDF2_KEY_LENGTH); 194 Key derived = factory.generateSecret(spec); 195 spec.clearPassword(); 196 return new SecretKeySpec(derived.getEncoded(), AES); 197 } 198 199 /** 200 * Gets the AES key from the keystore. 201 */ 202 protected Key getSecretKey() throws GeneralSecurityException, IOException { 203 KeyStore keyStore = KeyStore.getInstance(keyStoreType); 204 char[] kspw = keyStorePassword == null ? null : keyStorePassword.toCharArray(); 205 String keyStoreFile = this.keyStoreFile; // NOSONAR 206 if (Framework.isTestModeSet() && keyStoreFile != null) { 207 keyStoreFile = Framework.expandVars(keyStoreFile); 208 } 209 if (keyStoreFile == null) { 210 // some keystores are not backed by a file 211 keyStore.load(null, kspw); 212 } else { 213 try (InputStream in = new BufferedInputStream(Files.newInputStream(Paths.get(keyStoreFile)))) { 214 keyStore.load(in, kspw); 215 } 216 } 217 char[] kpw = keyPassword == null ? null : keyPassword.toCharArray(); 218 return keyStore.getKey(keyAlias, kpw); 219 } 220 221 protected Cipher getCipher() throws GeneralSecurityException { 222 if (useInsecureCipher) { 223 return Cipher.getInstance(AES_CBC_PKCS5_PADDING); // NOSONAR 224 } else { 225 return Cipher.getInstance(AES_GCM_NOPADDING); 226 } 227 } 228 229 protected AlgorithmParameterSpec getParameterSpec(byte[] iv) { 230 if (useInsecureCipher) { 231 return new IvParameterSpec(iv); 232 } else { 233 return new GCMParameterSpec(128, iv); 234 } 235 } 236 237}