/* * Copyright 2017 - 2020 Acosix GmbH * * Licensed 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 de.acosix.alfresco.simplecontentstores.repo.store.facade; import java.io.BufferedInputStream; import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; import java.nio.ByteBuffer; import java.security.Key; import java.security.KeyStore; import java.security.KeyStoreException; import java.security.NoSuchAlgorithmException; import java.security.NoSuchProviderException; import java.security.UnrecoverableKeyException; import java.security.cert.CertificateException; import javax.crypto.KeyGenerator; import javax.crypto.SecretKey; import javax.crypto.spec.SecretKeySpec; import org.alfresco.error.AlfrescoRuntimeException; import org.alfresco.repo.content.ContentContext; import org.alfresco.repo.domain.contentdata.ContentDataDAO; import org.alfresco.repo.domain.contentdata.ContentUrlEntity; import org.alfresco.repo.domain.contentdata.ContentUrlKeyEntity; import org.alfresco.repo.domain.contentdata.EncryptedKey; import org.alfresco.service.cmr.repository.ContentData; import org.alfresco.service.cmr.repository.ContentIOException; import org.alfresco.service.cmr.repository.ContentReader; import org.alfresco.service.cmr.repository.ContentWriter; import org.alfresco.util.EqualsHelper; import org.alfresco.util.Pair; import org.alfresco.util.PropertyCheck; import org.apache.commons.codec.DecoderException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.BeansException; import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContextAware; import org.springframework.core.io.Resource; import org.springframework.util.ResourceUtils; /** * @author Axel Faust */ public class EncryptingContentStore extends CommonFacadingContentStore implements ApplicationContextAware { private static final Logger LOGGER = LoggerFactory.getLogger(EncryptingContentStore.class); private static final String DEFAULT_KEY_ALGORITHM = "AES"; private static final int DEFAULT_KEY_SIZE = 128; // assume at least 4096 bit key to properly initialise buffers for symmetric key encryption private static final int DEFAULT_MASTER_KEY_SIZE = 4096; protected ApplicationContext applicationContext; protected ContentDataDAO contentDataDAO; protected String keyStorePath; protected String keyStoreType = KeyStore.getDefaultType(); protected String keyStoreProvider; protected String keyStorePassword; protected String masterKeyAlias; protected String masterKeyPassword; protected String keyAlgorithm = DEFAULT_KEY_ALGORITHM; protected String keyAlgorithmProvider; protected int keySize = DEFAULT_KEY_SIZE; protected String masterKeyStoreId; protected int masterKeySize = DEFAULT_MASTER_KEY_SIZE; protected transient Key masterPublicKey; protected transient Key masterPrivateKey; /** * * {@inheritDoc} */ @Override public void afterPropertiesSet() { super.afterPropertiesSet(); PropertyCheck.mandatory(this, "contentDataDAO", this.contentDataDAO); PropertyCheck.mandatory(this, "keyStorePath", this.keyStorePath); PropertyCheck.mandatory(this, "keyStoreType", this.keyStoreType); PropertyCheck.mandatory(this, "masterKeyAlias", this.masterKeyAlias); PropertyCheck.mandatory(this, "masterKeyStoreId", this.masterKeyStoreId); PropertyCheck.mandatory(this, "keyAlgorithm", this.keyAlgorithm); this.loadMasterKey(); } /** * {@inheritDoc} */ @Override public void setApplicationContext(final ApplicationContext applicationContext) throws BeansException { this.applicationContext = applicationContext; } /** * @param contentDataDAO * the contentDataDAO to set */ public void setContentDataDAO(final ContentDataDAO contentDataDAO) { this.contentDataDAO = contentDataDAO; } /** * @param keyStorePath * the keyStorePath to set */ public void setKeyStorePath(final String keyStorePath) { this.keyStorePath = keyStorePath; } /** * @param keyStoreType * the keyStoreType to set */ public void setKeyStoreType(final String keyStoreType) { this.keyStoreType = keyStoreType; } /** * @param keyStoreProvider * the keyStoreProvider to set */ public void setKeyStoreProvider(final String keyStoreProvider) { this.keyStoreProvider = keyStoreProvider; } /** * @param keyStorePassword * the keyStorePassword to set */ public void setKeyStorePassword(final String keyStorePassword) { this.keyStorePassword = keyStorePassword; } /** * @param masterKeyAlias * the masterKeyAlias to set */ public void setMasterKeyAlias(final String masterKeyAlias) { this.masterKeyAlias = masterKeyAlias; } /** * @param masterKeyPassword * the masterKeyPassword to set */ public void setMasterKeyPassword(final String masterKeyPassword) { this.masterKeyPassword = masterKeyPassword; } /** * @param masterKey * the masterKey to set */ public void setMasterKey(final Key masterKey) { this.masterPrivateKey = masterKey; } /** * @param keyAlgorithm * the keyAlgorithm to set */ public void setKeyAlgorithm(final String keyAlgorithm) { this.keyAlgorithm = keyAlgorithm; } /** * @param keyAlgorithmProvider * the keyAlgorithmProvider to set */ public void setKeyAlgorithmProvider(final String keyAlgorithmProvider) { this.keyAlgorithmProvider = keyAlgorithmProvider; } /** * @param masterKeyStoreId * the masterKeyStoreId to set */ public void setMasterKeyStoreId(final String masterKeyStoreId) { this.masterKeyStoreId = masterKeyStoreId; } /** * @param keySize * the keySize to set */ public void setKeySize(final int keySize) { if (keySize <= 0) { throw new IllegalArgumentException("keySize must be a positive integer"); } this.keySize = keySize; } /** * {@inheritDoc} */ @Override public ContentReader getReader(final String contentUrl) { ContentReader reader; final ContentReader backingReader = super.getReader(contentUrl); if (backingReader != null && backingReader.exists()) { final String effectiveContentUrl = backingReader.getContentUrl(); final ContentUrlEntity urlEntity = this.contentDataDAO.getContentUrl(effectiveContentUrl); if (urlEntity == null) { throw new ContentIOException("Missing content URL entity for " + effectiveContentUrl); } final ContentUrlKeyEntity urlKeyEntity = urlEntity.getContentUrlKey(); if (urlKeyEntity != null) { try { final Key key; final EncryptedKey encryptedKey = urlKeyEntity.getEncryptedKey(); if (!EqualsHelper.nullSafeEquals(this.masterKeyStoreId, encryptedKey.getMasterKeystoreId()) || !EqualsHelper.nullSafeEquals(this.masterKeyAlias, encryptedKey.getMasterKeyAlias())) { throw new ContentIOException( "Content encryption key was encrypted with a master key from a different master key store / with a different key alias"); } final ByteBuffer ekBuffer = encryptedKey.getByteBuffer(); try (final ByteBufferByteChannel ekChannel = new ByteBufferByteChannel(ekBuffer)) { try (final DecryptingReadableByteChannel dkChannel = new DecryptingReadableByteChannel(ekChannel, this.masterPrivateKey)) { // due to overhead the key should always have fewer bytes than the encrypted key final ByteBuffer dkBuffer = ByteBuffer.allocate(ekBuffer.capacity()); dkChannel.read(dkBuffer); dkBuffer.flip(); final byte[] keyBytes = new byte[dkBuffer.remaining()]; dkBuffer.get(keyBytes); key = new SecretKeySpec(keyBytes, encryptedKey.getAlgorithm()); } } reader = new DecryptingContentReaderFacade(backingReader, key, urlKeyEntity.getUnencryptedFileSize()); } catch (final DecoderException | IOException e) { LOGGER.error("Error loading symmetric content encryption key", e); throw new ContentIOException("Error loading symmetric content encryption key", e); } } else { reader = backingReader; } } else { reader = backingReader; } return reader; } /** * {@inheritDoc} */ @Override public ContentWriter getWriter(final ContentContext context) { final ContentReader existingContentReader; final String contentUrl = context.getContentUrl(); if (contentUrl != null && this.isContentUrlSupported(contentUrl) && this.exists(contentUrl)) { final ContentReader reader = this.getReader(contentUrl); if (reader != null && reader.exists()) { existingContentReader = reader; } else { existingContentReader = null; } } else { existingContentReader = null; } final ContentWriter backingWriter = super.getWriter(context); final Key key = this.createNewKey(); final EncryptingContentWriterFacade facadeWriter = new EncryptingContentWriterFacade(backingWriter, context, key, existingContentReader); facadeWriter.addListener(() -> { final byte[] keyBytes = key.getEncoded(); final ByteBuffer dkBuffer = ByteBuffer.wrap(keyBytes); EncryptedKey eKey; try { // allocate twice as many bytes for buffer then we actually expect // min expectation: master key size in bits / 8 // key-dependant expectation: key byte count + buffer => rounded up to next power of 2 final int masterKeyMinBlockSize = EncryptingContentStore.this.masterKeySize / 8; final int keyBlockOverhead = 42; // (RSA with default SHA-1) int expectedKeyBlockSize = dkBuffer.capacity() + keyBlockOverhead; if (Integer.highestOneBit(expectedKeyBlockSize) != Integer.lowestOneBit(expectedKeyBlockSize)) { // round up to next power of two expectedKeyBlockSize = Integer.highestOneBit(expectedKeyBlockSize) << 1; } final ByteBuffer ekBuffer = ByteBuffer.allocateDirect(Math.max(masterKeyMinBlockSize, expectedKeyBlockSize) * 2); try (final ByteBufferByteChannel ekChannel = new ByteBufferByteChannel(ekBuffer)) { try (final EncryptingWritableByteChannel dkChannel = new EncryptingWritableByteChannel(ekChannel, EncryptingContentStore.this.masterPublicKey)) { dkChannel.write(dkBuffer); } } ekBuffer.flip(); eKey = new EncryptedKey(EncryptingContentStore.this.masterKeyStoreId, EncryptingContentStore.this.masterKeyAlias, key.getAlgorithm(), ekBuffer); } catch (final IOException e) { LOGGER.error("Error storing symmetric content encryption key", e); throw new ContentIOException("Error storing symmetric content encryption key", e); } // can't create content URL entity directly so create via ContentData final Pair<Long, ContentData> contentDataPair = EncryptingContentStore.this.contentDataDAO .createContentData(facadeWriter.getContentData()); final ContentUrlKeyEntity contentUrlKeyEntity = new ContentUrlKeyEntity(); contentUrlKeyEntity.setUnencryptedFileSize(Long.valueOf(facadeWriter.getSize())); contentUrlKeyEntity.setEncryptedKey(eKey); EncryptingContentStore.this.contentDataDAO.updateContentUrlKey(contentDataPair.getSecond().getContentUrl(), contentUrlKeyEntity); }); return facadeWriter; } protected void loadMasterKey() { try { InputStream keyStoreInput; final Resource resource = this.applicationContext.getResource(this.keyStorePath); if (resource.exists()) { keyStoreInput = new BufferedInputStream(resource.getInputStream()); } else { final File keyStoreFile = ResourceUtils.getFile(this.keyStorePath); if (keyStoreFile.exists()) { keyStoreInput = new BufferedInputStream(new FileInputStream(keyStoreFile)); } else { keyStoreInput = null; } } if (keyStoreInput == null) { throw new IllegalStateException("keystore file " + this.keyStorePath + " does not exist / cannot be found"); } try { final KeyStore keyStore = this.keyStoreProvider != null ? KeyStore.getInstance(this.keyStoreType, this.keyStoreProvider) : KeyStore.getInstance(this.keyStoreType); keyStore.load(keyStoreInput, this.keyStorePassword != null ? this.keyStorePassword.toCharArray() : null); this.masterPublicKey = keyStore.getCertificate(this.masterKeyAlias).getPublicKey(); this.masterPrivateKey = keyStore.getKey(this.masterKeyAlias, this.masterKeyPassword != null ? this.masterKeyPassword.toCharArray() : null); } finally { try { keyStoreInput.close(); } catch (final IOException ignore) { // NO-OP } } } catch (final NoSuchAlgorithmException | NoSuchProviderException | KeyStoreException | UnrecoverableKeyException | IOException | CertificateException e) { LOGGER.error("Error loading master key from {}", this.keyStorePath, e); throw new AlfrescoRuntimeException("Error loading master key", e); } } protected Key createNewKey() { try { final KeyGenerator keygen = this.keyAlgorithmProvider != null ? KeyGenerator.getInstance(this.keyAlgorithm, this.keyAlgorithmProvider) : KeyGenerator.getInstance(this.keyAlgorithm); keygen.init(this.keySize); final SecretKey key = keygen.generateKey(); return key; } catch (final NoSuchAlgorithmException | NoSuchProviderException e) { LOGGER.error("Error generating encryption key", e); throw new ContentIOException("Error generating encryption key", e); } } }