/* * Copyright 2015 Jo Rabin * * 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 com.intellij.credentialStore.kdbx; import org.jetbrains.annotations.NotNull; import java.io.ByteArrayInputStream; import java.io.EOFException; import java.io.IOException; import java.io.InputStream; import java.security.MessageDigest; import java.util.Arrays; /** * Takes an underlying input stream formatted as Hashed Blocks * and provides the content of the blocks as an input stream * <p> * <p>A Hashed block consists of: * <p> * <ol> * <li>A 4 byte block sequence number, increments from 0 * <li>A 32 byte MD5 hash of the content * <li>A 4 byte length field * <li>Content * </ol> * <p> * <p>The stream of blocks is terminated with a 0 length 0 hash block. * <p> * <p>Originally developed for KeePass. A KeePass hash block * stream is little endian, i.e. the sequence * number and length fields are low order byte first. * * @author Jo */ public class HashedBlockInputStream extends InputStream { private static final int HASH_SIZE = 32; private static final byte[] ZERO_HASH = new byte[HASH_SIZE]; private long expectedSequenceNumber = 0; private boolean done = false; private InputStream inputStream; private ByteArrayInputStream blockInputStream = new ByteArrayInputStream(new byte[0]); private final MessageDigest md = KdbxHeaderKt.sha256MessageDigest(); public HashedBlockInputStream(InputStream inputStream) { this.inputStream = inputStream; } @Override public int read(@NotNull byte[] b, int offset, int length) throws IOException { return get(b, offset, length); } @Override public int read() throws IOException { byte[] buffer = new byte[1]; if (get(buffer, 0, 1) != 1) { throw new IOException("Could not read int"); } return buffer[0] & 0xFF; } @Override public void close() throws IOException { inputStream.close(); } /** * Gets bytes from the internal buffer and replenishes the buffer as necessary * * @param b a byte array to fill * @param offset the offset to start from * @param length the number of bytes to return * @return the number of bytes actually returned, , -1 if end of file * @throws IOException */ protected int get(byte[] b, int offset, int length) throws IOException { if (done) { return -1; } int totalBytesRead = 0; int bytesRead; while ((bytesRead = blockInputStream.read(b, offset, length)) < length && !done) { if (bytesRead == -1) { load(); } else { offset += bytesRead; length -= bytesRead; totalBytesRead += bytesRead; } } return bytesRead > 0 ? totalBytesRead + bytesRead : totalBytesRead; } /** * Reload the internal buffer from the underlying input stream */ private void load() throws IOException { // read the sequence number of the block long sequenceNumber = readUInt(); if (sequenceNumber != expectedSequenceNumber) { throw new IllegalStateException("Expected sequence number " + expectedSequenceNumber + " got " + sequenceNumber); } expectedSequenceNumber++; // get the block hash byte[] hash = new byte[HASH_SIZE]; readFully(hash); // get the length long readLength = readUInt(); if (readLength < 0) { throw new IllegalStateException("Got negative length for block"); } // length 0 means end of file if (readLength == 0) { if (!Arrays.equals(hash, ZERO_HASH)) { throw new IllegalStateException("Block hash was not zero on final block"); } done = true; return; } // get the new buffer byte[] readBuffer = new byte[(int)readLength]; readFully(readBuffer); md.update(readBuffer); if (!Arrays.equals(md.digest(), hash)) { throw new IllegalStateException("MD5 check failed while reading HashBlock"); } blockInputStream = new ByteArrayInputStream(readBuffer); } /** * Read an unsigned 4 byte int decoding from the endian format * * @return a long holding the value read */ private long readUInt() throws IOException { byte[] buf = new byte[4]; readFully(buf); return buf[3] << 24 | (buf[2] & 0xFF) << 16 | (buf[1] & 0xFF) << 8 | (buf[0] & 0xFF); } /** * Fill the buffer passed * * @param buffer the buffer to fill */ private void readFully(byte[] buffer) throws IOException { int bytesToRead = buffer.length; int bytesSoFar = 0; while (bytesSoFar < buffer.length) { int bytesRead = inputStream.read(buffer, bytesSoFar, bytesToRead); if (bytesRead <= 0) { throw new EOFException(); } bytesSoFar += bytesRead; bytesToRead -= bytesRead; } } }