All Downloads are FREE. Search and download functionalities are using the official Maven repository.

de.schlichtherle.crypto.io.CipherReadOnlyFile Maven / Gradle / Ivy

/*
 * Copyright (C) 2006-2010 Schlichtherle IT Services
 *
 * 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.schlichtherle.crypto.io;

import de.schlichtherle.crypto.*;
import de.schlichtherle.io.rof.*;

import java.io.*;

import org.bouncycastle.crypto.*;

/**
 * A read only file for transparent random read access to an encrypted file.
 * 

* The client must call {@link #init(SeekableBlockCipher, long, long)} * before it can actually read anything! *

* Note that this class implements its own virtual file pointer. * Thus, if you would like to access the underlying {@code ReadOnlyFile} * again after you have finished working with an instance of this class, * you should synchronize their file pointers using the pattern as described * in the base class {@link FilterReadOnlyFile}. * * @author Christian Schlichtherle * @version $Id: CipherReadOnlyFile.java,v 1.4 2010/08/20 13:09:45 christian_schlichtherle Exp $ * @since TrueZIP 6.0 */ // // Tactical notes: // // In order to provide optimum performance, this class implements a read ahead // strategy with lazy decryption. // So the encrypted data of the file is read ahead into an internal window // buffer in order to minimize file access. // Upon request by the application only, this buffer is then decrypted block by // block into the buffer provided by the application. // // For similar reasons, this class is NOT a subclass of // BufferedReadOnlyFile though their algorithm and code is pretty similar. // In fact, this class uses an important performance optimization: // Whenever possible, encrypted data in the window buffer is directly // decrypted into the user provided buffer. // If BufferedReadOnlyFile would be used as the base class instead, we would // have to provide another buffer to copy the data into before we could // actually decrypt it, which is redundant. // public abstract class CipherReadOnlyFile extends FilterReadOnlyFile { /** * The maximum buffer length of the window to the encrypted file. * This value has been adjusted to provide optimum performance at minimal * size on a Windows XP computer - results may vary. * Note that the actual size of the window is a multiple of the * cipher's block size and may be smaller than the maximum window size. */ private static final int MAX_WINDOW_LEN = 1024; /** Returns the smaller parameter. */ private static final long min(long a, long b) { return a < b ? a : b; } /** Returns the greater parameter. */ /*private static final long max(long a, long b) { return a < b ? b : a; }*/ /** Start offset of the encrypted data. */ private long start; /** The length of the encrypted data. */ private long length; /** * The virtual file pointer in the encrypted data. * This is relative to the start. */ private long fp; /** * The current offset in the encrypted file where the buffer window starts. * This is always a multiple of the block size. */ private long windowOff; /** * The buffer window to the encrypted file. * Note that this buffer contains encrypted data only. * The actual size of the window is a multiple of the cipher's block size * and may be slightly smaller than {@link #MAX_WINDOW_LEN}. */ private byte[] window; /** The seekable block cipher which allows random access. */ private SeekableBlockCipher cipher; /** * The current offset in the encrypted file where the data starts that * has been decrypted to the block. * This is always a multiple of the block size. */ private long blockOff; /** * The block buffer to use for decryption of partial blocks. * Note that this buffer contains decrypted data only. */ private byte[] block; /** Whether this read only file has been closed or not. */ private boolean closed; /** * Creates a read only file for transparent random read access to an * encrypted file. * The client must call {@link #init(SeekableBlockCipher, long, long)} * before it can actually read anything! * * @param rof A read-only file. * This may be {@code null}, but must be properly init before * the call to {@code init()}. */ public CipherReadOnlyFile(ReadOnlyFile rof) { super(rof); } /** * Initializes this cipher read only file - must be called before first * read access! * * @param start The start offset of the encrypted data in this file. * @param length The length of the encrypted data in this file. * * @throws IOException If this read only file has already been closed. * This exception is not recoverable. * @throws IllegalStateException If this object has already been * initialized. * This exception is not recoverable. * @throws NullPointerException If {@link #rof} is {@code null} * or {@code cipher} is {@code null}. * This exception is recoverable. */ public void init( final SeekableBlockCipher cipher, final long start, final long length) throws IOException { // Check state. if (closed) throw new IOException("file has been closed"); if (this.cipher != null) throw new IllegalStateException("file is already initialized"); // Check state (recoverable). if (rof == null) throw new NullPointerException("rof"); // Check parameters (fail fast). if (cipher == null) throw new NullPointerException("cipher"); if (start < 0 || length < 0) throw new IllegalArgumentException(); this.cipher = cipher; this.start = start; this.length = length; blockOff = length; final int blockLen = cipher.getBlockSize(); block = new byte[blockLen]; windowOff = Long.MIN_VALUE; // invalidate window window = new byte[(MAX_WINDOW_LEN / blockLen) * blockLen]; // round down to multiple of block size assert fp == 0; assert block.length > 0; assert window.length > 0; assert window.length % block.length == 0; } /** * Returns the authentication code of the encrypted data in this cipher * read only file using the given Message Authentication Code (MAC) object. * It is safe to call this method multiple times to detect if the file * has been tampered with meanwhile. * * @param mac A properly initialized MAC object. * * @throws IOException On any I/O related issue. */ protected byte[] computeMac(final Mac mac) throws IOException { final int windowLen = window.length; final byte[] buf = new byte[mac.getMacSize()]; final long safedFp = getFilePointer(); try { for (fp = 0; fp < length; fp += windowLen) { positionWindow(); final long remaining = length - windowOff; mac.update(window, 0, (int) min(windowLen, remaining)); } final int bufLen = mac.doFinal(buf, 0); assert bufLen == buf.length; } finally { fp = safedFp; } return buf; } public long length() throws IOException { ensureInit(); return length; } public long getFilePointer() throws IOException { ensureInit(); return fp; } public void seek(final long fp) throws IOException { ensureInit(); if (fp < 0) throw new IOException("file pointer must not be negative"); if (fp > length) throw new IOException("file pointer (" + fp + ") is larger than file length (" + length + ")"); this.fp = fp; } public int read() throws IOException { // Check state. ensureInit(); if (fp >= length) return -1; // Position block and return its decrypted data. positionBlock(); return block[(int) (fp++ % block.length)] & 0xff; } public int read(final byte[] buf, final int off, final int len) throws IOException { if (len == 0) return 0; // be fault-tolerant and compatible to RandomAccessFile // Check state. ensureInit(); if (fp >= length) return -1; // Check parameters. if (buf == null) throw new NullPointerException("buf"); final int offPlusLen = off + len; if ((off | len | offPlusLen | buf.length - offPlusLen) < 0) throw new IndexOutOfBoundsException(); // Setup. final int blockLen = block.length; int read = 0; // amount of decrypted data copied to buf { // Partial read of decrypted data block at the start. final int o = (int) (fp % blockLen); if (o != 0) { // The file pointer is not on a block boundary. positionBlock(); read = (int) min(len, blockLen - o); read = (int) min(read, length - fp); System.arraycopy(block, o, buf, off, read); fp += read; } } { // Full read of decrypted data blocks in the middle. long blockCounter = fp / blockLen; while (read + blockLen < len && fp + blockLen <= length) { // The file pointer is starting and ending on block boundaries. positionWindow(); cipher.setBlockCounter(blockCounter++); cipher.processBlock(window, (int) (fp - windowOff), buf, off + read); read += blockLen; fp += blockLen; } } // Partial read of decrypted data block at the end. if (read < len && fp < length) { // The file pointer is not on a block boundary. positionBlock(); final int n = (int) min(len - read, length - fp); System.arraycopy(block, 0, buf, off + read, n); read += n; fp += n; } // Assert that at least one byte has been read if len isn't zero. // Note that EOF has been tested before. assert read > 0; return read; } /** * Ensures that this file is open and has been initialized. * * @throws IOException If the preconditions do not hold. */ private final void ensureInit() throws IOException { if (cipher == null) throw new IOException("file is closed or not initialized"); } /** * Closes this read only file and releases any resources associated with it. * This method invalidates the state of this object, causing any subsequent * calls to a public method to fail with an {@link IOException}. * * @throws IOException If an I/O error occurs. */ public void close() throws IOException { if (closed) return; // Order is important here! closed = true; cipher = null; rof.close(); } /** * Ensures that the block with the decrypted data for partial reading is * positioned so that it contains the current virtual file pointer * in the encrypted file. * * @throws IOException On any I/O related issue. * The block is not moved in this case. */ private void positionBlock() throws IOException { // Check block position. final long fp = this.fp; final int blockLen = block.length; if (blockOff <= fp) { final long nextBlockOff = blockOff + blockLen; if (fp < nextBlockOff) return; } // Move block. positionWindow(); final long blockCounter = fp / blockLen; blockOff = blockCounter * blockLen; // Decrypt block from window. cipher.setBlockCounter(blockCounter); cipher.processBlock(window, (int) (blockOff - windowOff), block, 0); } /** * Ensures that the window is positioned so that the block containing * the current virtual file pointer in the encrypted file is entirely * contained in it. * * @throws IOException On any I/O related issue. * The window is invalidated in this case. */ private void positionWindow() throws IOException { // Check window position. final long fp = this.fp; final int windowLen = window.length; final long nextWindowOff = windowOff + windowLen; if (windowOff <= fp && fp < nextWindowOff) return; try { // Move window in the encrypted file. final int blockLen = block.length; windowOff = fp / blockLen * blockLen; // round down to multiple of block size if (windowOff != nextWindowOff) rof.seek(windowOff + start); // Fill window until end of file or buffer. // This should normally complete in one loop cycle, but we do not // depend on this as it would be a violation of ReadOnlyFile's // contract. int n = 0; do { int read = rof.read(window, n, windowLen - n); if (read < 0) break; n += read; } while (n < windowLen); } catch (IOException ioe) { windowOff = -windowLen - 1; // force seek() at next positionWindow() throw ioe; } } }





© 2015 - 2025 Weber Informatics LLC | Privacy Policy