/* * 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.cassandra.security; import java.io.IOException; import java.nio.ByteBuffer; import java.nio.channels.ReadableByteChannel; import java.nio.channels.WritableByteChannel; import javax.crypto.BadPaddingException; import javax.crypto.Cipher; import javax.crypto.IllegalBlockSizeException; import javax.crypto.ShortBufferException; import com.google.common.base.Preconditions; import io.netty.util.concurrent.FastThreadLocal; import org.apache.cassandra.db.commitlog.EncryptedSegment; import org.apache.cassandra.io.compress.ICompressor; import org.apache.cassandra.io.util.ChannelProxy; import org.apache.cassandra.io.util.FileDataInput; import org.apache.cassandra.utils.ByteBufferUtil; /** * Encryption and decryption functions specific to the commit log. * See comments in {@link EncryptedSegment} for details on the binary format. * The normal, and expected, invocation pattern is to compress then encrypt the data on the encryption pass, * then decrypt and uncompress the data on the decrypt pass. */ public class EncryptionUtils { public static final int COMPRESSED_BLOCK_HEADER_SIZE = 4; public static final int ENCRYPTED_BLOCK_HEADER_SIZE = 8; private static final FastThreadLocal<ByteBuffer> reusableBuffers = new FastThreadLocal<ByteBuffer>() { protected ByteBuffer initialValue() { return ByteBuffer.allocate(ENCRYPTED_BLOCK_HEADER_SIZE); } }; /** * Compress the raw data, as well as manage sizing of the {@code outputBuffer}; if the buffer is not big enough, * deallocate current, and allocate a large enough buffer. * Write the two header lengths (plain text length, compressed length) to the beginning of the buffer as we want those * values encapsulated in the encrypted block, as well. * * @return the byte buffer that was actaully written to; it may be the {@code outputBuffer} if it had enough capacity, * or it may be a new, larger instance. Callers should capture the return buffer (if calling multiple times). */ public static ByteBuffer compress(ByteBuffer inputBuffer, ByteBuffer outputBuffer, boolean allowBufferResize, ICompressor compressor) throws IOException { int inputLength = inputBuffer.remaining(); final int compressedLength = compressor.initialCompressedBufferLength(inputLength); outputBuffer = ByteBufferUtil.ensureCapacity(outputBuffer, compressedLength + COMPRESSED_BLOCK_HEADER_SIZE, allowBufferResize); outputBuffer.putInt(inputLength); compressor.compress(inputBuffer, outputBuffer); outputBuffer.flip(); return outputBuffer; } /** * Encrypt the input data, and writes out to the same input buffer; if the buffer is not big enough, * deallocate current, and allocate a large enough buffer. * Writes the cipher text and headers out to the channel, as well. * * Note: channel is a parameter as we cannot write header info to the output buffer as we assume the input and output * buffers can be the same buffer (and writing the headers to a shared buffer will corrupt any input data). Hence, * we write out the headers directly to the channel, and then the cipher text (once encrypted). */ public static ByteBuffer encryptAndWrite(ByteBuffer inputBuffer, WritableByteChannel channel, boolean allowBufferResize, Cipher cipher) throws IOException { final int plainTextLength = inputBuffer.remaining(); final int encryptLength = cipher.getOutputSize(plainTextLength); ByteBuffer outputBuffer = inputBuffer.duplicate(); outputBuffer = ByteBufferUtil.ensureCapacity(outputBuffer, encryptLength, allowBufferResize); // it's unfortunate that we need to allocate a small buffer here just for the headers, but if we reuse the input buffer // for the output, then we would overwrite the first n bytes of the real data with the header data. ByteBuffer intBuf = ByteBuffer.allocate(ENCRYPTED_BLOCK_HEADER_SIZE); intBuf.putInt(0, encryptLength); intBuf.putInt(4, plainTextLength); channel.write(intBuf); try { cipher.doFinal(inputBuffer, outputBuffer); } catch (ShortBufferException | IllegalBlockSizeException | BadPaddingException e) { throw new IOException("failed to encrypt commit log block", e); } outputBuffer.position(0).limit(encryptLength); channel.write(outputBuffer); outputBuffer.position(0).limit(encryptLength); return outputBuffer; } @SuppressWarnings("resource") public static ByteBuffer encrypt(ByteBuffer inputBuffer, ByteBuffer outputBuffer, boolean allowBufferResize, Cipher cipher) throws IOException { Preconditions.checkNotNull(outputBuffer, "output buffer may not be null"); return encryptAndWrite(inputBuffer, new ChannelAdapter(outputBuffer), allowBufferResize, cipher); } /** * Decrypt the input data, as well as manage sizing of the {@code outputBuffer}; if the buffer is not big enough, * deallocate current, and allocate a large enough buffer. * * @return the byte buffer that was actaully written to; it may be the {@code outputBuffer} if it had enough capacity, * or it may be a new, larger instance. Callers should capture the return buffer (if calling multiple times). */ public static ByteBuffer decrypt(ReadableByteChannel channel, ByteBuffer outputBuffer, boolean allowBufferResize, Cipher cipher) throws IOException { ByteBuffer metadataBuffer = reusableBuffers.get(); if (metadataBuffer.capacity() < ENCRYPTED_BLOCK_HEADER_SIZE) { metadataBuffer = ByteBufferUtil.ensureCapacity(metadataBuffer, ENCRYPTED_BLOCK_HEADER_SIZE, true); reusableBuffers.set(metadataBuffer); } metadataBuffer.position(0).limit(ENCRYPTED_BLOCK_HEADER_SIZE); channel.read(metadataBuffer); if (metadataBuffer.remaining() < ENCRYPTED_BLOCK_HEADER_SIZE) throw new IllegalStateException("could not read encrypted blocked metadata header"); int encryptedLength = metadataBuffer.getInt(); // this is the length of the compressed data int plainTextLength = metadataBuffer.getInt(); outputBuffer = ByteBufferUtil.ensureCapacity(outputBuffer, Math.max(plainTextLength, encryptedLength), allowBufferResize); outputBuffer.position(0).limit(encryptedLength); channel.read(outputBuffer); ByteBuffer dupe = outputBuffer.duplicate(); dupe.clear(); try { cipher.doFinal(outputBuffer, dupe); } catch (ShortBufferException | IllegalBlockSizeException | BadPaddingException e) { throw new IOException("failed to decrypt commit log block", e); } dupe.position(0).limit(plainTextLength); return dupe; } // path used when decrypting commit log files @SuppressWarnings("resource") public static ByteBuffer decrypt(FileDataInput fileDataInput, ByteBuffer outputBuffer, boolean allowBufferResize, Cipher cipher) throws IOException { return decrypt(new DataInputReadChannel(fileDataInput), outputBuffer, allowBufferResize, cipher); } /** * Uncompress the input data, as well as manage sizing of the {@code outputBuffer}; if the buffer is not big enough, * deallocate current, and allocate a large enough buffer. * * @return the byte buffer that was actaully written to; it may be the {@code outputBuffer} if it had enough capacity, * or it may be a new, larger instance. Callers should capture the return buffer (if calling multiple times). */ public static ByteBuffer uncompress(ByteBuffer inputBuffer, ByteBuffer outputBuffer, boolean allowBufferResize, ICompressor compressor) throws IOException { int outputLength = inputBuffer.getInt(); outputBuffer = ByteBufferUtil.ensureCapacity(outputBuffer, outputLength, allowBufferResize); compressor.uncompress(inputBuffer, outputBuffer); outputBuffer.position(0).limit(outputLength); return outputBuffer; } public static int uncompress(byte[] input, int inputOffset, int inputLength, byte[] output, int outputOffset, ICompressor compressor) throws IOException { int outputLength = readInt(input, inputOffset); inputOffset += 4; inputLength -= 4; if (output.length - outputOffset < outputLength) { String msg = String.format("buffer to uncompress into is not large enough; buf size = %d, buf offset = %d, target size = %s", output.length, outputOffset, outputLength); throw new IllegalStateException(msg); } return compressor.uncompress(input, inputOffset, inputLength, output, outputOffset); } private static int readInt(byte[] input, int inputOffset) { return (input[inputOffset + 3] & 0xFF) | ((input[inputOffset + 2] & 0xFF) << 8) | ((input[inputOffset + 1] & 0xFF) << 16) | ((input[inputOffset] & 0xFF) << 24); } /** * A simple {@link java.nio.channels.Channel} adapter for ByteBuffers. */ private static final class ChannelAdapter implements WritableByteChannel { private final ByteBuffer buffer; private ChannelAdapter(ByteBuffer buffer) { this.buffer = buffer; } public int write(ByteBuffer src) { int count = src.remaining(); buffer.put(src); return count; } public boolean isOpen() { return true; } public void close() { // nop } } private static class DataInputReadChannel implements ReadableByteChannel { private final FileDataInput fileDataInput; private DataInputReadChannel(FileDataInput dataInput) { this.fileDataInput = dataInput; } public int read(ByteBuffer dst) throws IOException { int readLength = dst.remaining(); // we should only be performing encrypt/decrypt operations with on-heap buffers, so calling BB.array() should be legit here fileDataInput.readFully(dst.array(), dst.position(), readLength); return readLength; } public boolean isOpen() { try { return fileDataInput.isEOF(); } catch (IOException e) { return true; } } public void close() { // nop } } public static class ChannelProxyReadChannel implements ReadableByteChannel { private final ChannelProxy channelProxy; private volatile long currentPosition; public ChannelProxyReadChannel(ChannelProxy channelProxy, long currentPosition) { this.channelProxy = channelProxy; this.currentPosition = currentPosition; } public int read(ByteBuffer dst) throws IOException { int bytesRead = channelProxy.read(dst, currentPosition); dst.flip(); currentPosition += bytesRead; return bytesRead; } public long getCurrentPosition() { return currentPosition; } public boolean isOpen() { return channelProxy.isCleanedUp(); } public void close() { // nop } public void setPosition(long sourcePosition) { this.currentPosition = sourcePosition; } } }