/*
* Autopsy Forensic Browser
*
* Copyright 2014-2015 Basis Technology Corp.
* Contact: carrier <at> sleuthkit <dot> org
*
* 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.modules.filetypeid;
import java.io.Serializable;
import java.io.UnsupportedEncodingException;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import java.util.logging.Level;
import javax.swing.JOptionPane;
import javax.xml.bind.DatatypeConverter;
import org.sleuthkit.autopsy.coreutils.Logger;
import org.sleuthkit.datamodel.AbstractFile;
import org.sleuthkit.datamodel.TskCoreException;
/**
* Represents a file type characterized by file signatures.
* <p>
* Thread-safe (immutable).
*/
class FileType implements Serializable {
private static final long serialVersionUID = 1L;
private final String mimeType;
private final List<Signature> signatures;
private final boolean createInterestingFileHit;
private final String interestingFilesSetName;
/**
* Creates a representation of a file type characterized by file signatures.
*
* @param mimeType The mime type to associate with this file type.
* @param signatures The signatures that characterize this file type.
*
* @throws IllegalArgumentException If an empty list of signatures is given.
*/
FileType(String mimeType, List<Signature> signatures) throws IllegalArgumentException {
this(mimeType, signatures, false, "");
}
/**
* Creates a representation of a file type characterized by file signatures.
*
* @param mimeType The mime type to associate with this file type.
* @param signatures The signatures that characterize this file type.
* @param createInterestingFileHit Create interesting file hit for file type?
* @param setName Name of the interesting file set in which to create hit.
*
* @throws IllegalArgumentException If an empty list of signatures is given.
*/
FileType(String mimeType, List<Signature> signatures, boolean createInterestingFileHit, String setName) throws IllegalArgumentException {
if (signatures.isEmpty()) {
throw new IllegalArgumentException("Must have at least one signature.");
}
this.mimeType = mimeType;
this.signatures = new ArrayList<>(signatures);
this.createInterestingFileHit = createInterestingFileHit;
this.interestingFilesSetName = setName;
}
/**
* Gets the MIME type associated with this file type.
*
* @return The MIME type.
*/
String getMimeType() {
return mimeType;
}
/**
* Gets the name of the interesting files set associated with this file
* type.
*
* @return The interesting files set name.
*/
String getInterestingFilesSetName() {
return interestingFilesSetName;
}
/**
* Should an interesting files hit be created for this file type?
*
* @return true if an interesting files hit should be created, otherwise
* false
*/
boolean createInterestingFileHit() {
return createInterestingFileHit;
}
/**
* Gets the signatures associated with this file type.
*
* @return The signatures.
*/
List<Signature> getSignatures() {
return Collections.unmodifiableList(this.signatures);
}
/**
* Adds a signature to the file type
*
* @param sig The signature to add
*/
void addSignature(Signature sig) {
this.signatures.add(sig);
}
/**
* Determines whether or not a file is an instance of this file type.
*
* @param file The file to test.
*
* @return True or false.
*/
boolean matches(final AbstractFile file) {
for (Signature sig : this.signatures) {
if (!sig.containedIn(file)) {
return false;
}
}
return true;
}
@Override
public String toString() {
return this.mimeType;
}
@Override
public boolean equals(Object other) {
if (other != null && other instanceof FileType) {
FileType that = (FileType) other;
if (this.getMimeType().equals(that.getMimeType()) && this.getSignatures().equals(that.getSignatures())) {
return true;
}
}
return false;
}
@Override
public int hashCode() {
int hash = 7;
hash = 67 * hash + Objects.hashCode(this.mimeType);
hash = 67 * hash + Objects.hashCode(this.signatures);
return hash;
}
/**
* A file signature consisting of a sequence of bytes at a specific offset
* within a file.
* <p>
* Thread-safe (immutable).
*/
static class Signature implements Serializable {
private static final long serialVersionUID = 1L;
private static final Logger logger = Logger.getLogger(Signature.class.getName());
/**
* The way the signature byte sequence should be interpreted.
*/
enum Type {
RAW, ASCII
};
private final byte[] signatureBytes;
private final long offset;
private final Type type;
private final boolean isRelativeToStart;
/**
* Creates a file signature consisting of a sequence of bytes at a
* specific offset within a file.
*
* @param signatureBytes The signature bytes.
* @param offset The offset of the signature bytes.
* @param type The type of data in the byte array. Impacts how
* it is displayed to the user in the UI.
*/
Signature(final byte[] signatureBytes, long offset, Type type) {
this.signatureBytes = Arrays.copyOf(signatureBytes, signatureBytes.length);
this.offset = offset;
this.type = type;
this.isRelativeToStart = true;
}
/**
* Creates a file signature consisting of an ASCII string at a specific
* offset within a file.
*
* @param signatureString The ASCII string
* @param offset The offset of the signature bytes.
*/
Signature(String signatureString, long offset) {
this.signatureBytes = signatureString.getBytes(StandardCharsets.US_ASCII);
this.offset = offset;
this.type = Type.ASCII;
this.isRelativeToStart = true;
}
/**
* Creates a file signature consisting of a sequence of bytes at a
* specific offset within a file. If bytes correspond to an ASCII
* string, use one of the other constructors so that the string is
* displayed to the user instead of the raw bytes.
*
* @param signatureBytes The signatures bytes.
* @param offset The offset of the signatures bytes.
*/
Signature(final byte[] signatureBytes, long offset) {
this.signatureBytes = Arrays.copyOf(signatureBytes, signatureBytes.length);
this.offset = offset;
this.type = Type.RAW;
this.isRelativeToStart = true;
}
/**
* Creates a file signature consisting of a sequence of bytes at a
* specific offset within a file.
*
* @param signatureBytes The signature bytes.
* @param offset The offset of the signature bytes.
* @param type The type of data in the byte array. Impacts
* how it is displayed to the user in the UI.
* @param isRelativeToStart Determines whether this signature is
* relative to start.
*/
Signature(final byte[] signatureBytes, long offset, Type type, boolean isRelativeToStart) {
this.signatureBytes = Arrays.copyOf(signatureBytes, signatureBytes.length);
this.offset = offset;
this.type = type;
this.isRelativeToStart = isRelativeToStart;
}
/**
* Creates a file signature consisting of an ASCII string at a specific
* offset within a file.
*
* @param signatureString The ASCII string
* @param offset The offset of the signature bytes.
* @param isRelativeToStart Determines whether this signature is
* relative to start.
*/
Signature(String signatureString, long offset, boolean isRelativeToStart) {
this.signatureBytes = signatureString.getBytes(StandardCharsets.US_ASCII);
this.offset = offset;
this.type = Type.ASCII;
this.isRelativeToStart = isRelativeToStart;
}
/**
* Creates a file signature consisting of a sequence of bytes at a
* specific offset within a file. If bytes correspond to an ASCII
* string, use one of the other constructors so that the string is
* displayed to the user instead of the raw bytes.
*
* @param signatureBytes The signature bytes.
* @param offset The offset of the signature bytes.
* @param isRelativeToStart Determines whether this signature is
* relative to start.
*/
Signature(final byte[] signatureBytes, long offset, boolean isRelativeToStart) {
this.signatureBytes = Arrays.copyOf(signatureBytes, signatureBytes.length);
this.offset = offset;
this.type = Type.RAW;
this.isRelativeToStart = isRelativeToStart;
}
/**
* Gets the byte sequence of the signature.
*
* @return The byte sequence as an array of bytes.
*/
byte[] getSignatureBytes() {
return Arrays.copyOf(signatureBytes, signatureBytes.length);
}
/**
* Gets the offset of the signature.
*
* @return The offset.
*/
long getOffset() {
return offset;
}
/**
* Gets the interpretation of the byte sequence for the signature.
*
* @return The signature type.
*/
Type getType() {
return type;
}
boolean isRelativeToStart() {
return isRelativeToStart;
}
/**
* Determines whether or not the signature is contained within a given
* file.
*
* @param file The file to test
*
* @return True or false.
*/
boolean containedIn(final AbstractFile file) {
if (offset >= file.getSize()) {
return false; // File is too small, offset lies outside file.
}
long actualOffset = offset;
if (!isRelativeToStart) {
actualOffset = file.getSize() - 1 - offset;
}
if (file.getSize() < (actualOffset + signatureBytes.length)) {
return false; /// too small, can't contain this signature
}
try {
byte[] buffer = new byte[signatureBytes.length];
int bytesRead = file.read(buffer, actualOffset, signatureBytes.length);
return ((bytesRead == signatureBytes.length) && (Arrays.equals(buffer, signatureBytes)));
} catch (TskCoreException ex) {
/**
* This exception is swallowed rather than propagated because
* files in images are not always consistent with their file
* system meta data making for read errors.
*/
Signature.logger.log(Level.WARNING, "Error reading from file with objId = " + file.getId(), ex); //NON-NLS
return false;
}
}
@Override
public boolean equals(Object other) {
if (other != null && other instanceof Signature) {
Signature that = (Signature) other;
if (Arrays.equals(this.getSignatureBytes(), that.getSignatureBytes())
&& this.getOffset() == that.getOffset()
&& this.getType().equals(that.getType())) {
return true;
}
}
return false;
}
@Override
public int hashCode() {
int hash = 3;
hash = 97 * hash + Arrays.hashCode(this.signatureBytes);
hash = 97 * hash + (int) (this.offset ^ (this.offset >>> 32));
hash = 97 * hash + Objects.hashCode(this.type);
return hash;
}
@Override
public String toString() {
String signatureBytesString;
if (Signature.Type.RAW == this.getType()) {
signatureBytesString = DatatypeConverter.printHexBinary(this.getSignatureBytes());
signatureBytesString = "0x" + signatureBytesString;
} else {
try {
signatureBytesString = new String(this.getSignatureBytes(), "UTF-8");
} catch (UnsupportedEncodingException ex) {
JOptionPane.showMessageDialog(null,
ex.getLocalizedMessage(),
Bundle.AddFileTypeSignaturePanel_signatureStringFail_text(),
JOptionPane.ERROR_MESSAGE);
signatureBytesString = "";
}
}
String startOrEnd;
if (this.isRelativeToStart) {
startOrEnd = "start";
} else {
startOrEnd = "end";
}
return signatureBytesString + ", " + offset + " bytes from " + startOrEnd;
}
}
}