blob: 237a4999220cf44a7211fc9b38b668c64a71a872 [file] [log] [blame]
/*
* 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);
}
}
}