| /* |
| * 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.solr.encryption; |
| |
| import org.apache.solr.common.SolrException; |
| import org.apache.solr.core.DirectoryFactory; |
| import org.apache.solr.core.SolrCore; |
| import org.apache.solr.encryption.EncryptionTransactionLog.EncryptionDirectorySupplier; |
| import org.apache.solr.encryption.crypto.DecryptingChannelInputStream; |
| import org.apache.solr.encryption.crypto.EncryptingOutputStream; |
| import org.apache.solr.update.TransactionLog; |
| import org.apache.solr.update.TransactionLog.ChannelFastInputStream; |
| import org.apache.solr.update.UpdateHandler; |
| import org.apache.solr.update.UpdateLog; |
| import org.slf4j.Logger; |
| import org.slf4j.LoggerFactory; |
| |
| import java.io.FileOutputStream; |
| import java.io.IOException; |
| import java.io.OutputStream; |
| import java.lang.invoke.MethodHandles; |
| import java.nio.ByteBuffer; |
| import java.nio.channels.FileChannel; |
| import java.nio.file.CopyOption; |
| import java.nio.file.Files; |
| import java.nio.file.OpenOption; |
| import java.nio.file.Path; |
| import java.nio.file.StandardCopyOption; |
| import java.nio.file.StandardOpenOption; |
| import java.util.ArrayList; |
| import java.util.Collection; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.Objects; |
| |
| import static org.apache.solr.encryption.EncryptionTransactionLog.ENCRYPTION_KEY_HEADER_LENGTH; |
| import static org.apache.solr.encryption.EncryptionTransactionLog.readEncryptionHeader; |
| import static org.apache.solr.encryption.EncryptionTransactionLog.writeEncryptionHeader; |
| import static org.apache.solr.encryption.EncryptionUtil.getActiveKeyRefFromCommit; |
| import static org.apache.solr.encryption.EncryptionUtil.getKeyIdFromCommit; |
| |
| /** |
| * {@link UpdateLog} that creates {@link EncryptionUpdateLog} to encrypts logs if the |
| * core index is marked for encryption. |
| * <p> |
| * The encryption is triggered when the {@link EncryptionRequestHandler} is called. It |
| * commits with some metadata that mark the index for encryption. It also calls |
| * {@link #encryptLogs} to (re)encrypt old log files with the active encryption key id |
| * (nearly as fast as a file copy). New log files created with {@link #newTransactionLog} |
| * will be encrypted using the active key stored in the commit metadata. |
| */ |
| public class EncryptionUpdateLog extends UpdateLog { |
| |
| private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); |
| |
| protected static final int REENCRYPTION_BUFFER_SIZE = 1024; |
| |
| protected final DirectorySupplier directorySupplier = new DirectorySupplier(); |
| |
| protected boolean shouldEncryptOldLogs = true; |
| |
| @Override |
| public void init(UpdateHandler updateHandler, SolrCore core) { |
| directorySupplier.init(core); |
| super.init(updateHandler, core); |
| try { |
| encryptLogs(); |
| } catch (IOException e) { |
| log.error("Exception while encrypting old transaction logs", e); |
| } |
| } |
| |
| @Override |
| public TransactionLog newTransactionLog(Path tlogFile, Collection<String> globalStrings, boolean openExisting) { |
| return new EncryptionTransactionLog(tlogFile, globalStrings, openExisting, directorySupplier); |
| } |
| |
| /** |
| * (Re)encrypts all existing transaction logs if an encryption key is defined in the {@link SolrCore} |
| * index commit metadata. |
| * |
| * @return {@code true} if all logs were successfully encrypted; {@code false} if some logs were not |
| * encrypted because they are still in use, in this case the encryption will be retried at the next |
| * commit when {@link #addOldLog} is called. |
| */ |
| public synchronized boolean encryptLogs() throws IOException { |
| Map<String, String> latestCommitData; |
| String activeKeyRef; |
| EncryptionDirectory directory = directorySupplier.get(); |
| try { |
| directory.forceReadCommitUserData(); |
| latestCommitData = directory.getLatestCommitData().data; |
| activeKeyRef = getActiveKeyRefFromCommit(latestCommitData); |
| for (TransactionLog log : logs) { |
| EncryptionTransactionLog encLog = (EncryptionTransactionLog) log; |
| if (encLog.refCount() <= 1) { |
| // The log is only owned by this update log. We can encrypt it. |
| encryptLog(encLog, activeKeyRef, directory); |
| } |
| } |
| } finally { |
| directorySupplier.release(directory); |
| } |
| String keyId = activeKeyRef == null ? null : getKeyIdFromCommit(activeKeyRef, latestCommitData); |
| boolean allLogsEncrypted = areAllLogsEncryptedWithKeyId(keyId, latestCommitData); |
| shouldEncryptOldLogs = !allLogsEncrypted; |
| return allLogsEncrypted; |
| } |
| |
| @Override |
| protected synchronized void addOldLog(TransactionLog oldLog, boolean removeOld) { |
| super.addOldLog(oldLog, removeOld); |
| if (shouldEncryptOldLogs) { |
| try { |
| encryptLogs(); |
| } catch (IOException e) { |
| log.error("Exception while encrypting old transaction logs", e); |
| } |
| } |
| } |
| |
| /** |
| * Returns whether all logs are encrypted with the provided key id. |
| */ |
| public synchronized boolean areAllLogsEncryptedWithKeyId(String keyId, Map<String, String> commitUserData) |
| throws IOException { |
| List<TransactionLog> allLogs = getAllLogs(); |
| if (!allLogs.isEmpty()) { |
| ByteBuffer readBuffer = ByteBuffer.allocate(4); |
| for (TransactionLog log : allLogs) { |
| try (FileChannel logChannel = FileChannel.open(((EncryptionTransactionLog) log).path(), StandardOpenOption.READ)) { |
| String logKeyRef = readEncryptionHeader(logChannel, readBuffer); |
| String logKeyId = logKeyRef == null ? null : getKeyIdFromCommit(logKeyRef, commitUserData); |
| if (!Objects.equals(logKeyId, keyId)) { |
| return false; |
| } |
| } |
| } |
| } |
| return true; |
| } |
| |
| private List<TransactionLog> getAllLogs() { |
| List<TransactionLog> allLogs = new ArrayList<>(logs.size() + 3); |
| if (tlog != null) { |
| allLogs.add(tlog); |
| } |
| if (prevTlog != null) { |
| allLogs.add(prevTlog); |
| } |
| if (bufferTlog != null) { |
| allLogs.add(bufferTlog); |
| } |
| allLogs.addAll(logs); |
| return allLogs; |
| } |
| |
| /** |
| * Encrypts a transaction log if it is not encrypted with the latest encryption key. |
| * The transaction log is toggled in readonly mode to ensure it is not written anymore. |
| */ |
| protected void encryptLog(EncryptionTransactionLog log, String activeKeyRef, EncryptionDirectory directory) |
| throws IOException { |
| assert log.refCount() <= 1; |
| if (Files.size(log.path()) > 0) { |
| try (FileChannel inputChannel = FileChannel.open(log.path(), StandardOpenOption.READ)) { |
| String inputKeyRef = readEncryptionHeader(inputChannel, ByteBuffer.allocate(4)); |
| if (!Objects.equals(inputKeyRef, activeKeyRef)) { |
| Path newLogPath = log.path().resolveSibling(log.path().getFileName() + ".enc"); |
| try (OutputStream outputStream = Files.newOutputStream(newLogPath, StandardOpenOption.CREATE)) { |
| reencrypt(inputChannel, inputKeyRef, outputStream, activeKeyRef, directory); |
| } |
| Path backupLog = log.path().resolveSibling(log.path().getFileName() + ".bak"); |
| Files.move(log.path(), backupLog, StandardCopyOption.REPLACE_EXISTING); |
| Files.move(newLogPath, log.path(), StandardCopyOption.REPLACE_EXISTING); |
| Files.delete(backupLog); |
| // Toggle to readonly mode to make sure we don't write anymore to the transaction |
| // log output stream. Reading the transaction log is ok because it will open new |
| // input streams. |
| log.readOnly(); |
| } |
| } |
| } |
| } |
| |
| protected void reencrypt(FileChannel inputChannel, |
| String inputKeyRef, |
| OutputStream outputStream, |
| String activeKeyRef, |
| EncryptionDirectory directory) |
| throws IOException { |
| ChannelFastInputStream cfis = inputKeyRef == null ? |
| new ChannelFastInputStream(inputChannel, 0) |
| : new DecryptingChannelInputStream(inputChannel, |
| ENCRYPTION_KEY_HEADER_LENGTH, |
| 0, |
| directory.getKeySecret(inputKeyRef), |
| directory.getEncrypterFactory()); |
| if (activeKeyRef != null) { |
| // Get the key secret first. If it fails, we do not write anything. |
| byte[] keySecret = directory.getKeySecret(activeKeyRef); |
| writeEncryptionHeader(activeKeyRef, outputStream); |
| outputStream = new EncryptingOutputStream(outputStream, |
| keySecret, |
| directory.getEncrypterFactory()); |
| } |
| byte[] buffer = new byte[REENCRYPTION_BUFFER_SIZE]; |
| int nRead; |
| while ((nRead = cfis.read(buffer, 0, buffer.length)) >= 0) { |
| outputStream.write(buffer, 0, nRead); |
| } |
| outputStream.flush(); |
| } |
| |
| protected static class DirectorySupplier implements EncryptionDirectorySupplier { |
| |
| private SolrCore core; |
| |
| void init(SolrCore core) { |
| this.core = core; |
| } |
| |
| @Override |
| public EncryptionDirectory get() { |
| try { |
| return (EncryptionDirectory) EncryptionDirectoryFactory.getFactory(core) |
| .get(core.getIndexDir(), |
| DirectoryFactory.DirContext.DEFAULT, |
| core.getSolrConfig().indexConfig.lockType); |
| } catch (IOException e) { |
| throw new SolrException(SolrException.ErrorCode.SERVER_ERROR, "Cannot encrypt tlogs", e); |
| } |
| } |
| |
| @Override |
| public void release(EncryptionDirectory directory) throws IOException { |
| core.getDirectoryFactory().release(directory); |
| } |
| } |
| } |