435 lines
14 KiB
Java
Executable File

/*
*
* Autopsy Forensic Browser
*
* Copyright 2019 Basis Technology Corp.
*
* 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 org.sleuthkit.autopsy.recentactivity;
import java.io.DataInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.util.Iterator;
import java.util.NoSuchElementException;
import java.util.logging.Level;
import org.sleuthkit.autopsy.coreutils.Logger;
import org.sleuthkit.autopsy.recentactivity.BinaryCookieReader.Cookie;
/**
* The binary cookie reader encapsulates all the knowledge of how to read the
* mac .binarycookie files into one class.
*
* The binarycookie file has a header which describes how many pages of cookies
* and where they are located. Each cookie page has a header and a list of
* cookies.
*
*/
final class BinaryCookieReader implements Iterable<Cookie> {
private static final int MAGIC_SIZE = 4;
private static final int SIZEOF_INT_BYTES = 4;
private static final int PAGE_HEADER_VALUE = 256;
private static final String COOKIE_MAGIC = "cook"; //NON-NLS
private static final int MAC_EPOC_FIX = 978307200;
private final int[] pageSizeArray;
private final File cookieFile;
private static final Logger LOG = Logger.getLogger(BinaryCookieReader.class.getName());
/**
* The binary cookie reader encapsulates all the knowledge of how to read
* the mac .binarycookie files into one class.
*
*/
private BinaryCookieReader(File cookieFile, int[] pageSizeArray) {
this.cookieFile = cookieFile;
this.pageSizeArray = pageSizeArray;
}
/**
* initalizeReader opens the given file, reads the header and checks that
* the file is a binarycookie file. This function does not keep the file
* open.
*
* @param cookieFile binarycookie file
* @return An instance of the reader
* @throws FileNotFoundException
* @throws IOException
*/
public static BinaryCookieReader initalizeReader(File cookieFile) throws FileNotFoundException, IOException {
BinaryCookieReader reader = null;
try (DataInputStream dataStream = new DataInputStream(new FileInputStream(cookieFile))) {
byte[] magic = new byte[MAGIC_SIZE];
if (dataStream.read(magic) != MAGIC_SIZE) {
throw new IOException("Failed to read header, invalid file size (" + cookieFile.getName() + ")"); //NON-NLS
}
if (!(new String(magic)).equals(COOKIE_MAGIC)) {
throw new IOException(cookieFile.getName() + " is not a cookie file"); //NON-NLS
}
int[] sizeArray = null;
int pageCount = dataStream.readInt();
if (pageCount != 0) {
sizeArray = new int[pageCount];
for (int cnt = 0; cnt < pageCount; cnt++) {
sizeArray[cnt] = dataStream.readInt();
}
LOG.log(Level.INFO, "No cookies found in {0}", cookieFile.getName()); //NON-NLS
}
reader = new BinaryCookieReader(cookieFile, sizeArray);
}
return reader;
}
/**
* Creates and returns a instance of CookiePageIterator.
*
* @return CookiePageIterator
*/
@Override
public Iterator<Cookie> iterator() {
return new CookiePageIterator();
}
/**
* The cookiePageIterator iterates the binarycookie file by page.
*/
private class CookiePageIterator implements Iterator<Cookie> {
int pageIndex = 0;
CookiePage currentPage = null;
Iterator<Cookie> currentIterator = null;
DataInputStream dataStream = null;
/**
* The cookiePageIterator iterates the binarycookie file by page.
*/
CookiePageIterator() {
if (pageSizeArray == null || pageSizeArray.length == 0) {
return;
}
try {
dataStream = new DataInputStream(new FileInputStream(cookieFile));
// skip to the first page
dataStream.skipBytes((2 * SIZEOF_INT_BYTES) + (pageSizeArray.length * SIZEOF_INT_BYTES));
} catch (IOException ex) {
String errorMessage = String.format("An error occurred creating an input stream for %s", cookieFile.getName());
LOG.log(Level.WARNING, errorMessage, ex); //NON-NLS
closeStream(); // Just incase the error was from skip
}
}
/**
* Returns true if there are more cookies in the binarycookie file.
*
* @return True if there are more cookies
*/
@Override
public boolean hasNext() {
if (dataStream == null) {
return false;
}
if (currentIterator == null || !currentIterator.hasNext()) {
try {
if (pageIndex < pageSizeArray.length) {
byte[] nextPage = new byte[pageSizeArray[pageIndex]];
dataStream.read(nextPage);
currentPage = new CookiePage(nextPage);
currentIterator = currentPage.iterator();
} else {
closeStream();
return false;
}
pageIndex++;
} catch (IOException ex) {
closeStream();
String errorMessage = String.format("A read error occured for file %s (pageIndex = %d)", cookieFile.getName(), pageIndex);
LOG.log(Level.WARNING, errorMessage, ex); //NON-NLS
return false;
}
}
return currentIterator.hasNext();
}
/**
* Get the next cookie from the current CookieIterator.
*
* @return The next cookie
*/
@Override
public Cookie next() {
// Just in case someone uses next without hasNext, this check will
// make sure there are more elements and that we iterate properly
// through the pages.
if (!hasNext()) {
throw new NoSuchElementException();
}
return currentIterator.next();
}
/**
* Close the DataInputStream
*/
private void closeStream() {
if (dataStream != null) {
try {
dataStream.close();
dataStream = null;
} catch (IOException ex) {
String errorMessage = String.format("An error occurred trying to close stream for file %s", cookieFile.getName());
LOG.log(Level.WARNING, errorMessage, ex); //NON-NLS
}
}
}
}
/**
* Wrapper class for an instance of a CookiePage in the binarycookie file.
*/
private class CookiePage implements Iterable<Cookie> {
int[] cookieOffSets;
ByteBuffer pageBuffer;
/**
* Setup the CookiePage object. Calidates that the page bytes are in the
* correct format by checking for the header value of 0x0100.
*
* @param page byte array representing a cookie page
* @throws IOException
*/
CookiePage(byte[] page) throws IOException {
if (page == null || page.length == 0) {
throw new IllegalArgumentException("Invalid value for page passed to CookiePage constructor"); //NON-NLS
}
pageBuffer = ByteBuffer.wrap(page);
if (pageBuffer.getInt() != PAGE_HEADER_VALUE) {
pageBuffer = null;
throw new IOException("Invalid file format, bad page head value found"); //NON-NLS
}
pageBuffer.order(ByteOrder.LITTLE_ENDIAN);
int count = pageBuffer.getInt();
cookieOffSets = new int[count];
for (int cnt = 0; cnt < count; cnt++) {
cookieOffSets[cnt] = pageBuffer.getInt();
}
pageBuffer.getInt(); // All 0, not needed
}
/**
* Returns an instance of a CookieIterator.
*
* @return CookieIterator
*/
@Override
public Iterator<Cookie> iterator() {
return new CookieIterator();
}
/**
* Implements Iterator to iterate over the cookies in the page.
*/
private class CookieIterator implements Iterator<Cookie> {
int index = 0;
/**
* Checks to see if there are more cookies.
*
* @return True if there are more cookies, false if there are not
*/
@Override
public boolean hasNext() {
if (pageBuffer == null) {
return false;
}
return index < cookieOffSets.length;
}
/**
* Gets the next cookie from the page.
*
* @return Next cookie
*/
@Override
public Cookie next() {
if (!hasNext()) {
throw new NoSuchElementException();
}
int offset = cookieOffSets[index];
int size = pageBuffer.getInt(offset);
byte[] cookieBytes = new byte[size];
pageBuffer.get(cookieBytes, 0, size);
index++;
return new Cookie(cookieBytes);
}
}
}
/**
* Represents an instance of a cookie from the binarycookie file.
*/
public class Cookie {
private final static int COOKIE_HEAD_SKIP = 16;
private final double expirationDate;
private final double creationDate;
private final String name;
private final String url;
private final String path;
private final String value;
/**
* Creates a cookie object from the given array of bytes.
*
* @param cookieBytes Byte array for the cookie
*/
protected Cookie(byte[] cookieBytes) {
if (cookieBytes == null || cookieBytes.length == 0) {
throw new IllegalArgumentException("Invalid value for cookieBytes passed to Cookie constructor"); //NON-NLS
}
ByteBuffer byteBuffer = ByteBuffer.wrap(cookieBytes);
byteBuffer.order(ByteOrder.LITTLE_ENDIAN);
// Skip past the four int values that we are not interested in
byteBuffer.position(byteBuffer.position() + COOKIE_HEAD_SKIP);
int urlOffset = byteBuffer.getInt();
int nameOffset = byteBuffer.getInt();
int pathOffset = byteBuffer.getInt();
int valueOffset = byteBuffer.getInt();
byteBuffer.getLong(); // 8 bytes of not needed
expirationDate = byteBuffer.getDouble();
creationDate = byteBuffer.getDouble();
url = decodeString(cookieBytes, urlOffset);
name = decodeString(cookieBytes, nameOffset);
path = decodeString(cookieBytes, pathOffset);
value = decodeString(cookieBytes, valueOffset);
}
/**
* Returns the expiration date of the cookie represented by this cookie
* object.
*
* @return Cookie expiration date in milliseconds with java epoch
*/
public final Long getExpirationDate() {
return ((long) expirationDate) + MAC_EPOC_FIX;
}
/**
* Returns the creation date of the cookie represented by this cookie
* object.
*
* @return Cookie creation date in milliseconds with java epoch
*/
public final Long getCreationDate() {
return ((long) creationDate) + MAC_EPOC_FIX;
}
/**
* Returns the url of the cookie represented by this cookie object.
*
* @return the cookie URL
*/
public final String getURL() {
return url;
}
/**
* Returns the name of the cookie represented by this cookie object.
*
* @return The cookie name
*/
public final String getName() {
return name;
}
/**
* Returns the path of the cookie represented by this cookie object.
*
* @return The cookie path
*/
public final String getPath() {
return path;
}
/**
* Returns the value of the cookie represented by this cookie object.
*
* @return The cookie value
*/
public final String getValue() {
return value;
}
/**
* Creates an ascii string from the bytes in byteArray starting at
* offset ending at the first null terminator found.
*
* @param byteArray Array of bytes
* @param offset starting offset in the array
* @return String with bytes converted to ascii
*/
private String decodeString(byte[] byteArray, int offset) {
byte[] stringBytes = new byte[byteArray.length - offset];
for (int index = 0; index < stringBytes.length; index++) {
byte nibble = byteArray[offset + index];
if (nibble != '\0') { //NON-NLS
stringBytes[index] = nibble;
} else {
break;
}
}
return new String(stringBytes);
}
}
}