001/*
002 * (C) Copyright 2006-2018 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.binary;
020
021import java.io.BufferedInputStream;
022import java.io.BufferedOutputStream;
023import java.io.DataInputStream;
024import java.io.DataOutputStream;
025import java.io.File;
026import java.io.FileInputStream;
027import java.io.FileOutputStream;
028import java.io.FilterOutputStream;
029import java.io.IOException;
030import java.io.InputStream;
031import java.io.OutputStream;
032import java.lang.reflect.Field;
033import java.security.GeneralSecurityException;
034import java.security.Key;
035import java.security.KeyStore;
036import java.security.MessageDigest;
037import java.security.SecureRandom;
038import java.security.spec.AlgorithmParameterSpec;
039import java.util.Arrays;
040import java.util.Map;
041import java.util.Random;
042
043import javax.crypto.BadPaddingException;
044import javax.crypto.Cipher;
045import javax.crypto.CipherInputStream;
046import javax.crypto.SecretKeyFactory;
047import javax.crypto.spec.GCMParameterSpec;
048import javax.crypto.spec.IvParameterSpec;
049import javax.crypto.spec.PBEKeySpec;
050import javax.crypto.spec.SecretKeySpec;
051
052import org.apache.commons.io.IOUtils;
053import org.apache.commons.lang3.StringUtils;
054import org.apache.commons.logging.Log;
055import org.apache.commons.logging.LogFactory;
056import org.nuxeo.ecm.core.api.NuxeoException;
057import org.nuxeo.runtime.api.Framework;
058
059/**
060 * A binary manager that encrypts binaries on the filesystem using AES.
061 * <p>
062 * The configuration holds the keystore information to retrieve the AES key, or the password that is used to generate a
063 * per-file key using PBKDF2. This configuration comes from the {@code <property name="key">...</property>} of the
064 * binary manager configuration.
065 * <p>
066 * The configuration has the form {@code key1=value1,key2=value2,...} where the possible keys are, for keystore use:
067 * <ul>
068 * <li>keyStoreType: the keystore type, for instance JCEKS
069 * <li>keyStoreFile: the path to the keystore, if applicable
070 * <li>keyStorePassword: the keystore password
071 * <li>keyAlias: the alias (name) of the key in the keystore
072 * <li>keyPassword: the key password
073 * </ul>
074 * <p>
075 * And for PBKDF2 use:
076 * <ul>
077 * <li>password: the password
078 * </ul>
079 * <p>
080 * To encrypt a binary, an AES key is needed. This key can be retrieved from a keystore, or generated from a password
081 * using PBKDF2 (in which case each stored file contains a different salt for security reasons). The file format is
082 * described in {@link #storeAndDigest(InputStream, OutputStream)}.
083 * <p>
084 * While the binary is being used by the application, a temporarily-decrypted file is held in a temporary directory. It
085 * is removed as soon as possible.
086 * <p>
087 * Note: if the Java Cryptographic Extension (JCE) is not configured for 256-bit key length, you may get an exception
088 * "java.security.InvalidKeyException: Illegal key size or default parameters". If this is the case, go to
089 * <a href="http://www.oracle.com/technetwork/java/javase/downloads/index.html" >Oracle Java SE Downloads</a> and
090 * download and install the Java Cryptography Extension (JCE) Unlimited Strength Jurisdiction Policy Files for your JDK.
091 *
092 * @since 6.0
093 */
094public class AESBinaryManager extends LocalBinaryManager {
095
096    private static final Log log = LogFactory.getLog(AESBinaryManager.class);
097
098    protected static final byte[] FILE_MAGIC = new byte[] { 'N', 'U', 'X', 'E', 'O', 'C', 'R', 'Y', 'P', 'T' };
099
100    protected static final int FILE_VERSION_1 = 1;
101
102    protected static final int USE_KEYSTORE = 1;
103
104    protected static final int USE_PBKDF2 = 2;
105
106    protected static final String AES = "AES";
107
108    // insecure, see https://find-sec-bugs.github.io/bugs.htm#PADDING_ORACLE
109    protected static final String AES_CBC_PKCS5_PADDING = "AES/CBC/PKCS5Padding";
110
111    protected static final String AES_GCM_NOPADDING = "AES/GCM/NoPadding";
112
113    protected static final String PBKDF2_WITH_HMAC_SHA1 = "PBKDF2WithHmacSHA1";
114
115    protected static final int PBKDF2_ITERATIONS = 10000;
116
117    // AES-256
118    protected static final int PBKDF2_KEY_LENGTH = 256;
119
120    protected static final String PARAM_PASSWORD = "password";
121
122    protected static final String PARAM_KEY_STORE_TYPE = "keyStoreType";
123
124    protected static final String PARAM_KEY_STORE_FILE = "keyStoreFile";
125
126    protected static final String PARAM_KEY_STORE_PASSWORD = "keyStorePassword";
127
128    protected static final String PARAM_KEY_ALIAS = "keyAlias";
129
130    protected static final String PARAM_KEY_PASSWORD = "keyPassword";
131
132    /**
133     * If {@code true}, use the insecure AES/CBC/PKCS5Padding for encryption. The default is {@code false}, to use
134     * AES/GCM/NoPadding.
135     *
136     * @since 10.3
137     */
138    protected static final String PARAM_KEY_USE_INSECURE_CIPHER = "useInsecureCipher";
139
140    // for sanity check during reads
141    private static final int MAX_SALT_LEN = 1024;
142
143    // for sanity check during reads
144    private static final int MAX_IV_LEN = 1024;
145
146    // Random instances are thread-safe
147    protected static final Random RANDOM = new SecureRandom();
148
149    // the digest from the root descriptor
150    protected String digestAlgorithm;
151
152    protected boolean usePBKDF2;
153
154    protected String password;
155
156    protected String keyStoreType;
157
158    protected String keyStoreFile;
159
160    protected String keyStorePassword;
161
162    protected String keyAlias;
163
164    protected String keyPassword;
165
166    protected boolean useInsecureCipher;
167
168    public AESBinaryManager() {
169        setUnlimitedJCEPolicy();
170    }
171
172    /**
173     * By default the JRE may ship with restricted key length. Instead of having administrators download the Java
174     * Cryptography Extension (JCE) Unlimited Strength Jurisdiction Policy Files from
175     * http://www.oracle.com/technetwork/java/javase/downloads/index.html, we attempt to directly unrestrict the JCE
176     * using reflection.
177     * <p>
178     * This is not possible anymore since 8u102 and https://bugs.openjdk.java.net/browse/JDK-8149417
179     */
180    protected static boolean setUnlimitedJCEPolicy() {
181        try {
182            Field field = Class.forName("javax.crypto.JceSecurity").getDeclaredField("isRestricted");
183            field.setAccessible(true);
184            if (Boolean.TRUE.equals(field.get(null))) {
185                log.info("Setting JCE Unlimited Strength");
186                field.set(null, Boolean.FALSE);
187            }
188            return true;
189        } catch (ReflectiveOperationException | SecurityException | IllegalArgumentException e) {
190            log.debug("Cannot check/set JCE Unlimited Strength", e);
191            return false;
192        }
193    }
194
195    @Override
196    public void initialize(String blobProviderId, Map<String, String> properties) throws IOException {
197        super.initialize(blobProviderId, properties);
198        digestAlgorithm = getDigestAlgorithm();
199        String options = properties.get(BinaryManager.PROP_KEY);
200        // TODO parse options from properties directly
201        if (StringUtils.isBlank(options)) {
202            throw new NuxeoException("Missing key for " + getClass().getSimpleName());
203        }
204        initializeOptions(options);
205    }
206
207    protected void initializeOptions(String options) {
208        for (String option : options.split(",")) {
209            String[] split = option.split("=", 2);
210            if (split.length != 2) {
211                throw new NuxeoException("Unrecognized option: " + option);
212            }
213            String value = StringUtils.defaultIfBlank(split[1], null);
214            switch (split[0]) {
215            case PARAM_PASSWORD:
216                password = value;
217                break;
218            case PARAM_KEY_STORE_TYPE:
219                keyStoreType = value;
220                break;
221            case PARAM_KEY_STORE_FILE:
222                keyStoreFile = value;
223                break;
224            case PARAM_KEY_STORE_PASSWORD:
225                keyStorePassword = value;
226                break;
227            case PARAM_KEY_ALIAS:
228                keyAlias = value;
229                break;
230            case PARAM_KEY_PASSWORD:
231                keyPassword = value;
232                break;
233            case PARAM_KEY_USE_INSECURE_CIPHER:
234                useInsecureCipher = Boolean.parseBoolean(value);
235                break;
236            default:
237                throw new NuxeoException("Unrecognized option: " + option);
238            }
239        }
240        usePBKDF2 = password != null;
241        if (usePBKDF2) {
242            if (keyStoreType != null) {
243                throw new NuxeoException("Cannot use " + PARAM_KEY_STORE_TYPE + " with " + PARAM_PASSWORD);
244            }
245            if (keyStoreFile != null) {
246                throw new NuxeoException("Cannot use " + PARAM_KEY_STORE_FILE + " with " + PARAM_PASSWORD);
247            }
248            if (keyStorePassword != null) {
249                throw new NuxeoException("Cannot use " + PARAM_KEY_STORE_PASSWORD + " with " + PARAM_PASSWORD);
250            }
251            if (keyAlias != null) {
252                throw new NuxeoException("Cannot use " + PARAM_KEY_ALIAS + " with " + PARAM_PASSWORD);
253            }
254            if (keyPassword != null) {
255                throw new NuxeoException("Cannot use " + PARAM_KEY_PASSWORD + " with " + PARAM_PASSWORD);
256            }
257        } else {
258            if (keyStoreType == null) {
259                throw new NuxeoException("Missing " + PARAM_KEY_STORE_TYPE);
260            }
261            // keystore file is optional
262            if (keyStoreFile == null && keyStorePassword != null) {
263                throw new NuxeoException("Missing " + PARAM_KEY_STORE_PASSWORD);
264            }
265            if (keyAlias == null) {
266                throw new NuxeoException("Missing " + PARAM_KEY_ALIAS);
267            }
268            if (keyPassword == null) {
269                keyPassword = keyStorePassword;
270            }
271        }
272    }
273
274    /**
275     * Gets the password for PBKDF2.
276     * <p>
277     * The caller must clear it from memory when done with it by calling {@link #clearPassword}.
278     */
279    protected char[] getPassword() {
280        return password.toCharArray();
281    }
282
283    /**
284     * Clears a password from memory.
285     */
286    protected void clearPassword(char[] password) {
287        if (password != null) {
288            Arrays.fill(password, '\0');
289        }
290    }
291
292    /**
293     * Generates an AES key from the password using PBKDF2.
294     *
295     * @param salt the salt
296     */
297    protected Key generateSecretKey(byte[] salt) throws GeneralSecurityException {
298        char[] password = getPassword();
299        SecretKeyFactory factory = SecretKeyFactory.getInstance(PBKDF2_WITH_HMAC_SHA1);
300        PBEKeySpec spec = new PBEKeySpec(password, salt, PBKDF2_ITERATIONS, PBKDF2_KEY_LENGTH);
301        clearPassword(password);
302        Key derived = factory.generateSecret(spec);
303        spec.clearPassword();
304        return new SecretKeySpec(derived.getEncoded(), AES);
305    }
306
307    /**
308     * Gets the AES key from the keystore.
309     */
310    protected Key getSecretKey() throws GeneralSecurityException, IOException {
311        KeyStore keyStore = KeyStore.getInstance(keyStoreType);
312        char[] kspw = keyStorePassword == null ? null : keyStorePassword.toCharArray();
313        if (keyStoreFile != null) {
314            try (InputStream in = new BufferedInputStream(new FileInputStream(keyStoreFile))) {
315                keyStore.load(in, kspw);
316            }
317        } else {
318            // some keystores are not backed by a file
319            keyStore.load(null, kspw);
320        }
321        clearPassword(kspw);
322        char[] kpw = keyPassword == null ? null : keyPassword.toCharArray();
323        Key key = keyStore.getKey(keyAlias, kpw);
324        clearPassword(kpw);
325        return key;
326    }
327
328    @Override
329    protected Binary getBinary(InputStream in) throws IOException {
330        // write to a tmp file that will be used by the returned Binary
331        // TODO if stream source, avoid copy (no-copy optimization)
332        File tmp = File.createTempFile("bin_", ".tmp", tmpDir);
333        Framework.trackFile(tmp, tmp);
334        OutputStream out = new BufferedOutputStream(new FileOutputStream(tmp));
335        IOUtils.copy(in, out);
336        in.close();
337        out.close();
338        // encrypt an digest into final file
339        InputStream nin = new BufferedInputStream(new FileInputStream(tmp));
340        String digest = storeAndDigest(nin); // calls our storeAndDigest
341        // return a binary on our tmp file
342        return new Binary(tmp, digest, blobProviderId);
343    }
344
345    @Override
346    public Binary getBinary(String digest) {
347        File file = getFileForDigest(digest, false);
348        if (file == null) {
349            log.warn("Invalid digest format: " + digest);
350            return null;
351        }
352        if (!file.exists()) {
353            return null;
354        }
355        File tmp;
356        try {
357            tmp = File.createTempFile("bin_", ".tmp", tmpDir);
358            Framework.trackFile(tmp, tmp);
359            try (OutputStream out = new BufferedOutputStream(new FileOutputStream(tmp));
360                    InputStream in = new BufferedInputStream(new FileInputStream(file))) {
361                decrypt(in, out);
362            }
363        } catch (IOException e) {
364            throw new RuntimeException(e);
365        }
366        // return a binary on our tmp file
367        return new Binary(tmp, digest, blobProviderId);
368    }
369
370    @Override
371    protected String storeAndDigest(InputStream in) throws IOException {
372        File tmp = File.createTempFile("create_", ".tmp", tmpDir);
373        /*
374         * First, write the input stream to a temporary file, while computing a digest.
375         */
376        try {
377            String digest;
378            try (OutputStream out = new BufferedOutputStream(new FileOutputStream(tmp))) {
379                digest = storeAndDigest(in, out);
380            } finally {
381                in.close();
382            }
383            /*
384             * Move the tmp file to its destination.
385             */
386            File file = getFileForDigest(digest, true);
387            atomicMove(tmp, file);
388            return digest;
389        } finally {
390            tmp.delete();
391        }
392    }
393
394    /**
395     * Encrypts the given input stream into the given output stream, while also computing the digest of the input
396     * stream.
397     * <p>
398     * File format version 1 (values are in network order):
399     * <ul>
400     * <li>10 bytes: magic number "NUXEOCRYPT"
401     * <li>1 byte: file format version = 1
402     * <li>1 byte: use keystore = 1, use PBKDF2 = 2
403     * <li>if use PBKDF2:
404     * <ul>
405     * <li>4 bytes: salt length = n
406     * <li>n bytes: salt data
407     * </ul>
408     * <li>4 bytes: IV length = p
409     * <li>p bytes: IV data
410     * <li>x bytes: encrypted stream
411     * </ul>
412     *
413     * @param in the input stream containing the data
414     * @param out the output stream into write
415     * @return the digest of the input stream
416     */
417    @Override
418    public String storeAndDigest(InputStream in, OutputStream out) throws IOException {
419        out.write(FILE_MAGIC);
420        DataOutputStream data = new DataOutputStream(out);
421        data.writeByte(FILE_VERSION_1);
422
423        try {
424            // get digest to use
425            MessageDigest messageDigest = MessageDigest.getInstance(digestAlgorithm);
426
427            // secret key
428            Key secret;
429            if (usePBKDF2) {
430                data.writeByte(USE_PBKDF2);
431                // generate a salt
432                byte[] salt = new byte[16];
433                RANDOM.nextBytes(salt);
434                // generate secret key
435                secret = generateSecretKey(salt);
436                // write salt
437                data.writeInt(salt.length);
438                data.write(salt);
439            } else {
440                data.writeByte(USE_KEYSTORE);
441                // find secret key from keystore
442                secret = getSecretKey();
443            }
444
445            // cipher
446            Cipher cipher = getCipher();
447            cipher.init(Cipher.ENCRYPT_MODE, secret);
448
449            // write IV
450            byte[] iv = cipher.getIV();
451            data.writeInt(iv.length);
452            data.write(iv);
453
454            // digest and write the encrypted data
455            CipherAndDigestOutputStream cipherOut = new CipherAndDigestOutputStream(out, cipher, messageDigest);
456            IOUtils.copy(in, cipherOut);
457            cipherOut.close();
458            byte[] digest = cipherOut.getDigest();
459            return toHexString(digest);
460        } catch (GeneralSecurityException e) {
461            throw new NuxeoException(e);
462        }
463
464    }
465
466    /**
467     * Decrypts the given input stream into the given output stream.
468     */
469    protected void decrypt(InputStream in, OutputStream out) throws IOException {
470        byte[] magic = new byte[FILE_MAGIC.length];
471        IOUtils.read(in, magic);
472        if (!Arrays.equals(magic, FILE_MAGIC)) {
473            throw new IOException("Invalid file (bad magic)");
474        }
475        DataInputStream data = new DataInputStream(in);
476        byte magicvers = data.readByte();
477        if (magicvers != FILE_VERSION_1) {
478            throw new IOException("Invalid file (bad version)");
479        }
480
481        byte usepb = data.readByte();
482        if (usepb == USE_PBKDF2) {
483            if (!usePBKDF2) {
484                throw new NuxeoException("File requires PBKDF2 password");
485            }
486        } else if (usepb == USE_KEYSTORE) {
487            if (usePBKDF2) {
488                throw new NuxeoException("File requires keystore");
489            }
490        } else {
491            throw new IOException("Invalid file (bad use)");
492        }
493
494        try {
495            // secret key
496            Key secret;
497            if (usePBKDF2) {
498                // read salt first
499                int saltLen = data.readInt();
500                if (saltLen <= 0 || saltLen > MAX_SALT_LEN) {
501                    throw new NuxeoException("Invalid salt length: " + saltLen);
502                }
503                byte[] salt = new byte[saltLen];
504                data.read(salt, 0, saltLen);
505                secret = generateSecretKey(salt);
506            } else {
507                secret = getSecretKey();
508            }
509
510            // read IV
511            int ivLen = data.readInt();
512            if (ivLen <= 0 || ivLen > MAX_IV_LEN) {
513                throw new NuxeoException("Invalid IV length: " + ivLen);
514            }
515            byte[] iv = new byte[ivLen];
516            data.read(iv, 0, ivLen);
517
518            // cipher
519            Cipher cipher = getCipher();
520            cipher.init(Cipher.DECRYPT_MODE, secret, getParameterSpec(iv));
521
522            // read the encrypted data
523            try (InputStream cipherIn = new CipherInputStream(in, cipher)) {
524                IOUtils.copy(cipherIn, out);
525            } catch (IOException e) {
526                Throwable cause = e.getCause();
527                if (cause != null && cause instanceof BadPaddingException) {
528                    throw new NuxeoException(cause.getMessage(), e);
529                }
530            }
531        } catch (GeneralSecurityException e) {
532            throw new NuxeoException(e);
533        }
534    }
535
536    protected Cipher getCipher() throws GeneralSecurityException {
537        if (useInsecureCipher) {
538            return Cipher.getInstance(AES_CBC_PKCS5_PADDING); // NOSONAR
539        } else {
540            return Cipher.getInstance(AES_GCM_NOPADDING);
541        }
542    }
543
544    protected AlgorithmParameterSpec getParameterSpec(byte[] iv) {
545        if (useInsecureCipher) {
546            return new IvParameterSpec(iv);
547        } else {
548            return new GCMParameterSpec(128, iv);
549        }
550    }
551
552    /**
553     * A {@link javax.crypto.CipherOutputStream CipherOutputStream} that also does a digest of the original stream at
554     * the same time.
555     */
556    public static class CipherAndDigestOutputStream extends FilterOutputStream {
557
558        protected Cipher cipher;
559
560        protected OutputStream out;
561
562        protected MessageDigest messageDigest;
563
564        protected byte[] digest;
565
566        public CipherAndDigestOutputStream(OutputStream out, Cipher cipher, MessageDigest messageDigest) {
567            super(out);
568            this.out = out;
569            this.cipher = cipher;
570            this.messageDigest = messageDigest;
571        }
572
573        public byte[] getDigest() {
574            return digest;
575        }
576
577        @Override
578        public void write(int b) throws IOException {
579            write(new byte[] { (byte) b }, 0, 1);
580        }
581
582        @Override
583        public void write(byte b[], int off, int len) throws IOException {
584            messageDigest.update(b, off, len);
585            byte[] bytes = cipher.update(b, off, len);
586            if (bytes != null) {
587                out.write(bytes);
588                bytes = null; // help GC
589            }
590        }
591
592        @Override
593        public void flush() throws IOException {
594            out.flush();
595        }
596
597        @Override
598        public void close() throws IOException {
599            digest = messageDigest.digest();
600            try {
601                byte[] bytes = cipher.doFinal();
602                out.write(bytes);
603                bytes = null; // help GC
604            } catch (GeneralSecurityException e) {
605                throw new NuxeoException(e);
606            }
607            try {
608                flush();
609            } finally {
610                out.close();
611            }
612        }
613    }
614
615}