mirror of
https://github.com/NationalSecurityAgency/ghidra.git
synced 2025-10-05 02:39:44 +02:00
GP-3748: Added support for CaRT file system
This commit is contained in:
parent
e138d381ea
commit
ab40dbae46
24 changed files with 3948 additions and 0 deletions
|
@ -18,3 +18,4 @@ data/languages/minidump.opinion||GHIDRA||||END|
|
|||
data/languages/pagedump.opinion||GHIDRA||||END|
|
||||
src/main/help/help/TOC_Source.xml||GHIDRA||||END|
|
||||
src/main/help/help/topics/FileFormatsPlugin/FileFormats.html||GHIDRA||||END|
|
||||
src/main/help/help/topics/cart/CartFileSystem.html||GHIDRA||||END|
|
||||
|
|
|
@ -0,0 +1,75 @@
|
|||
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
|
||||
|
||||
<HTML>
|
||||
<HEAD>
|
||||
<META name="generator" content=
|
||||
"HTML Tidy for Java (vers. 2009-12-01), see jtidy.sourceforge.net">
|
||||
<META http-equiv="Content-Language" content="en-us">
|
||||
<META http-equiv="Content-Type" content="text/html; charset=windows-1252">
|
||||
<META name="GENERATOR" content="Microsoft FrontPage 4.0">
|
||||
<META name="ProgId" content="FrontPage.Editor.Document">
|
||||
|
||||
<TITLE>CaRT File Format</TITLE>
|
||||
<LINK rel="stylesheet" type="text/css" href="help/shared/DefaultStyle.css">
|
||||
</HEAD>
|
||||
|
||||
<BODY>
|
||||
<H1><a name="HelpAnchor"></a>CaRT File Format</H1>
|
||||
|
||||
<P>Compressed and ARC4 Transport (CaRT) neutering format is a file format that is used to
|
||||
neuter files for distribution. This is often used to neutralize malware in the malware
|
||||
analyst community, but could be used for non-malware as well. Using Ghidra's file system
|
||||
support the binary stored in the CaRT may be safely extracted and processed as normal
|
||||
without ever needing to store the original binary to disk.</P>
|
||||
|
||||
<H2>About CaRT</H2>
|
||||
<P>The CaRT format was developed by the Canadian government within their
|
||||
<I>Canadian Centre for Cyber Security</I>. The documentation and
|
||||
repository can be found on the <i>CaRT GitHub</i> page.
|
||||
</P>
|
||||
|
||||
<P>The official <I>CaRT python library</I> is usually used
|
||||
to create CaRT files via its command-line interface or within other python applications or
|
||||
libraries.</P>
|
||||
|
||||
<H2>Supported CaRT Format Versions</H2>
|
||||
<P>Currently CaRT only has a single format version, namely version <FONT face="Courier New">1
|
||||
</FONT>. If/when new versions are released this file system will be updated to support them.</P>
|
||||
|
||||
<H2>Decryption Keys</H2>
|
||||
<P>The CaRT format uses ARC4 encryption and supports two modes of keys: default and private.</P>
|
||||
<UL>
|
||||
<LI><B>Default</B> - In this mode a default key (the first 8 digits of PI, twice) will be used
|
||||
without any further interaction from the user. Binary data is safely neutered without the need
|
||||
to share and transmit passwords.</LI>
|
||||
<LI><B>Private</B> - This mode is appropriate when the key for the encrypted data should be
|
||||
transmitted and stored separately from the CaRT file itself. The key may be provided to
|
||||
Ghidra in two ways, attempted in the following order:
|
||||
<OL>
|
||||
<LI>
|
||||
<I>INI Configuration</I> - If the default CaRT configuration file exists (
|
||||
<FONT face="Courier New">${USER_HOME}/.cart/cart.cfg</FONT>) the key stored there, if
|
||||
any, will be attempted first. See the
|
||||
<I>CaRT GitHub</I> for more
|
||||
documentation on this configuration file.
|
||||
</LI>
|
||||
<LI>
|
||||
<I>User Prompt</I> - If the key is not found through the configuration file then the
|
||||
user will be prompted to input the key manually. The key may be entered as plaintext
|
||||
or in base-64 format (thus supporting arbitrary binary keys). The user will be
|
||||
repeatedly prompted until either the correct key is provided or they click 'Cancel'.
|
||||
</LI>
|
||||
</OL>
|
||||
</LI>
|
||||
</UL>
|
||||
<P>See the <I>CaRT GitHub</I> page for more
|
||||
documentation on keys, requirements, and formats.</P>
|
||||
|
||||
<H2>Metadata (and Hashes)</H2>
|
||||
<P>The CaRT format supports a number of metadata fields including MD5, SHA-1, and SHA-256
|
||||
hashes, and additional user-specified metadata. These hashes will be verified when Ghidra
|
||||
imports the binary for analysis. Warnings will be displayed if any of these hashes are missing
|
||||
and processing will be halted if any of them are present but do not match the binary contents.
|
||||
Additional metadata fields are visible via the "Get Info" context menu option.<P>
|
||||
</BODY>
|
||||
</HTML>
|
|
@ -0,0 +1,105 @@
|
|||
/* ###
|
||||
* IP: GHIDRA
|
||||
*
|
||||
* 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 ghidra.file.formats.cart;
|
||||
|
||||
import docking.widgets.OptionDialog;
|
||||
import ghidra.util.*;
|
||||
|
||||
/**
|
||||
* Helper class to show Continue or Cancel dialogs at various severity levels
|
||||
*/
|
||||
public class CartCancelDialogs {
|
||||
/**
|
||||
* Character width to which messages will be wrapped
|
||||
*/
|
||||
public static final int WRAP_WIDTH_CHARACTERS = 80;
|
||||
|
||||
/**
|
||||
* Wrap a String message to a default (80 characters) width and add front and back HTML
|
||||
* tags. Caller is responsible for neutering any internal unsafe HTML tags in their
|
||||
* message.
|
||||
*
|
||||
* @param message String message to display
|
||||
* @return HTML length-wrapped version of message.
|
||||
*/
|
||||
private static final String wrapHtml(String message) {
|
||||
String wrapped = StringUtilities.wrapToWidth(message, WRAP_WIDTH_CHARACTERS);
|
||||
return "<html>" + wrapped.replace("\n", "<br>") + "</html>";
|
||||
}
|
||||
|
||||
/**
|
||||
* Prompt the user with a given title and message with a specified message type for them
|
||||
* to "Continue" the operation or cancel. Message may contain HTML and should be
|
||||
* sanitized for safety by the caller. Returns true if the user wants to continue the
|
||||
* operation.
|
||||
*
|
||||
* <B>Note:</B> If in headless mode log the message at the appropriate level and then
|
||||
* treat as if the user chose to <B>cancel</B> the operation. Also, log a message stating
|
||||
* this decision was made.
|
||||
*
|
||||
* @param title The title of the dialog window
|
||||
* @param message Message prompt to display to user
|
||||
* @param messageType The type of message see {@link OptionDialog}
|
||||
* @return True if the user chooses to continue, False otherwise.
|
||||
*/
|
||||
public static final boolean promptContinue(String title, String message, int messageType) {
|
||||
if (SystemUtilities.isInHeadlessMode()) {
|
||||
message = title + " : " + message;
|
||||
|
||||
switch (messageType) {
|
||||
case OptionDialog.WARNING_MESSAGE:
|
||||
Msg.warn(CartCancelDialogs.class, message);
|
||||
break;
|
||||
case OptionDialog.ERROR_MESSAGE:
|
||||
Msg.error(CartCancelDialogs.class, message);
|
||||
break;
|
||||
default:
|
||||
Msg.info(CartCancelDialogs.class, message);
|
||||
break;
|
||||
}
|
||||
|
||||
Msg.info(CartCancelDialogs.class,
|
||||
"User can't respond to message, treating as cancellation.");
|
||||
return false;
|
||||
}
|
||||
return OptionDialog.showOptionDialogWithCancelAsDefaultButton(null, title,
|
||||
wrapHtml(message), "Continue", messageType) == OptionDialog.OPTION_ONE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper to prompt for Continue or Cancel at the warning level. Returns true if the user
|
||||
* wants to continue the operation.
|
||||
*
|
||||
* @param title The title of the dialog window
|
||||
* @param message Message prompt to display to user
|
||||
* @return True if the user chooses to continue, False otherwise.
|
||||
*/
|
||||
public static final boolean promptWarningContinue(String title, String message) {
|
||||
return promptContinue(title, message, OptionDialog.WARNING_MESSAGE);
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper to prompt for Continue or Cancel at the error level. Returns true if the user
|
||||
* wants to continue the operation.
|
||||
*
|
||||
* @param title The title of the dialog window
|
||||
* @param message Message prompt to display to user
|
||||
* @return True if the user chooses to continue, False otherwise.
|
||||
*/
|
||||
public static final boolean promptErrorContinue(String title, String message) {
|
||||
return promptContinue(title, message, OptionDialog.ERROR_MESSAGE);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,31 @@
|
|||
/* ###
|
||||
* IP: GHIDRA
|
||||
*
|
||||
* 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 ghidra.file.formats.cart;
|
||||
|
||||
/**
|
||||
* Exception subclass for apparent configuration errors; for instance, when an
|
||||
* ARC4 key in the configuration file is not valid base64-encoded data.
|
||||
*/
|
||||
public class CartConfigurationException extends Exception {
|
||||
|
||||
/**
|
||||
* Construct CartConfigurationException with specified message
|
||||
* @param message The reason for the exception
|
||||
*/
|
||||
public CartConfigurationException(String message) {
|
||||
super(message);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,376 @@
|
|||
/* ###
|
||||
* IP: GHIDRA
|
||||
*
|
||||
* 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 ghidra.file.formats.cart;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.*;
|
||||
import java.util.Map.Entry;
|
||||
|
||||
import org.apache.commons.text.StringEscapeUtils;
|
||||
|
||||
import com.google.gson.*;
|
||||
|
||||
import ghidra.app.util.bin.ByteProvider;
|
||||
import ghidra.app.util.bin.ByteProviderWrapper;
|
||||
import ghidra.formats.gfilesystem.*;
|
||||
import ghidra.formats.gfilesystem.annotations.FileSystemInfo;
|
||||
import ghidra.formats.gfilesystem.crypto.CryptoSession;
|
||||
import ghidra.formats.gfilesystem.fileinfo.*;
|
||||
import ghidra.framework.generic.auth.Password;
|
||||
import ghidra.util.HashUtilities;
|
||||
import ghidra.util.exception.CancelledException;
|
||||
import ghidra.util.task.TaskMonitor;
|
||||
|
||||
//@formatter:off
|
||||
@FileSystemInfo(
|
||||
type = "cart",
|
||||
description = "Compressed and ARC4 Transport (CaRT) neutering format.",
|
||||
factory = CartFileSystemFactory.class
|
||||
)
|
||||
//@formatter:on
|
||||
/**
|
||||
* File system for the CaRT format (Version 1). Includes creating objects for
|
||||
* relevant parsing and retrieving providers for access to decrypted and
|
||||
* decompressed data contents.
|
||||
*
|
||||
* This class does not contain a version identifier because it should be the
|
||||
* wrapper to load all versions of CaRT formated files. If/when new versions are
|
||||
* released new probe checks should be added and use the appropriate
|
||||
* (version-specific?) factories.
|
||||
*/
|
||||
public class CartFileSystem implements GFileSystem {
|
||||
|
||||
private final FSRLRoot fsFSRL;
|
||||
private final FileSystemRefManager refManager = new FileSystemRefManager(this);
|
||||
private final FileSystemService fsService;
|
||||
|
||||
private ByteProvider byteProvider;
|
||||
private ByteProvider payloadProvider;
|
||||
private SingleFileSystemIndexHelper fsIndexHelper;
|
||||
private CartV1File cartFile;
|
||||
|
||||
/**
|
||||
* CaRT file system constructor.
|
||||
*
|
||||
* @param fsFSRL The root {@link FSRL} of the file system.
|
||||
* @param fsService The file system service provided by Ghidra instance
|
||||
*/
|
||||
public CartFileSystem(FSRLRoot fsFSRL, FileSystemService fsService) {
|
||||
this.fsFSRL = fsFSRL;
|
||||
this.fsService = fsService;
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens the specified CaRT container file and initializes this file system with the
|
||||
* contents.
|
||||
*
|
||||
* @param bProvider container file
|
||||
* @param monitor {@link TaskMonitor} to allow the user to monitor and cancel
|
||||
* @throws CancelledException if user cancels
|
||||
* @throws IOException if error when reading data
|
||||
*/
|
||||
public void mount(ByteProvider bProvider, TaskMonitor monitor)
|
||||
throws CancelledException, IOException {
|
||||
byteProvider = bProvider;
|
||||
|
||||
try {
|
||||
try {
|
||||
cartFile = new CartV1File(byteProvider);
|
||||
}
|
||||
catch (CartInvalidARC4KeyException e) {
|
||||
// Could not auto detect key. Prompt user until we have a valid key or they cancel
|
||||
try (CryptoSession cryptoSession = fsService.newCryptoSession()) {
|
||||
|
||||
String prompt = this.fsFSRL.getContainer().getName() + " (plaintext or base64)";
|
||||
|
||||
// Iterate through GUI password request attempts until we succeed. hasNext
|
||||
// will return true until the user cancels. The prompt will also tell them
|
||||
// how many attempts they have made so it is clearer that they are not
|
||||
// being successful.
|
||||
// Log an error when the password is bad, but perhaps we should make them
|
||||
// acknowledge before attempting again?
|
||||
for (Iterator<Password> pwIt =
|
||||
cryptoSession.getPasswordsFor(this.fsFSRL.getContainer(), prompt); pwIt
|
||||
.hasNext();) {
|
||||
|
||||
try (Password passwordValue = pwIt.next()) {
|
||||
monitor.setMessage("Testing key...");
|
||||
|
||||
String password = String.valueOf(passwordValue.getPasswordChars());
|
||||
cartFile = new CartV1File(byteProvider, password);
|
||||
break;
|
||||
}
|
||||
catch (CartInvalidARC4KeyException arc4E) {
|
||||
if (!CartCancelDialogs.promptErrorContinue("Bad Key",
|
||||
"Error when testing key for " +
|
||||
this.fsFSRL.getContainer().getName() + ":\n" +
|
||||
(arc4E.getMessage() != null ? arc4E.getMessage() : "Unknown") +
|
||||
"\n Try another key?")) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (CartInvalidARC4KeyException e) {
|
||||
throw new IOException("Invalid CaRT ARC4 Key: " + e.getMessage());
|
||||
}
|
||||
catch (CartInvalidCartException e) {
|
||||
throw new IOException("Invalid CaRT file: " + e.getMessage());
|
||||
}
|
||||
catch (CartConfigurationException e) {
|
||||
throw new IOException("Invalid CaRT configuration file: " + e.getMessage());
|
||||
}
|
||||
|
||||
// If the CaRT File wasn't set, then we don't have a key, throw an error
|
||||
if (cartFile == null) {
|
||||
throw new IOException("ARC4 key not found or user cancelled.");
|
||||
}
|
||||
|
||||
// If/when future CaRT file versions exist, catch the appropriate error and
|
||||
// handle them here.
|
||||
payloadProvider = getPayload(null, monitor);
|
||||
|
||||
/**
|
||||
* If an MD5 value is provided here it will be carried through the rest of the
|
||||
* system. If null is used instead then the MD5 will be calculated from the
|
||||
* bytes of the file.
|
||||
*/
|
||||
this.fsIndexHelper = new SingleFileSystemIndexHelper(this, fsFSRL, cartFile.getPath(),
|
||||
cartFile.getDataSize(), null // Intentionally using null instead of actual MD5
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() throws IOException {
|
||||
refManager.onClose();
|
||||
if (fsIndexHelper != null) {
|
||||
fsIndexHelper.clear();
|
||||
}
|
||||
if (byteProvider != null) {
|
||||
byteProvider.close();
|
||||
byteProvider = null;
|
||||
}
|
||||
if (payloadProvider != null) {
|
||||
payloadProvider.close();
|
||||
payloadProvider = null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isClosed() {
|
||||
return (fsIndexHelper == null) || fsIndexHelper.isClosed();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return fsFSRL.getContainer().getName();
|
||||
}
|
||||
|
||||
@Override
|
||||
public FSRLRoot getFSRL() {
|
||||
return fsFSRL;
|
||||
}
|
||||
|
||||
@Override
|
||||
public FileSystemRefManager getRefManager() {
|
||||
return refManager;
|
||||
}
|
||||
|
||||
@Override
|
||||
public GFile lookup(String path) throws IOException {
|
||||
return fsIndexHelper.lookup(path);
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function to create byte provider for CaRT payload content that is
|
||||
* decompressed and decrypted.
|
||||
*
|
||||
* @param payloadFSRL The payload {@link FSRL} of the file system.
|
||||
* @param monitor The task monitor for this system handling
|
||||
* @return A {@link ByteProvider} for the payload content
|
||||
* @throws CancelledException If the user cancels via the monitor
|
||||
* @throws IOException If the file fails to read or CaRT fails
|
||||
*/
|
||||
private ByteProvider getPayload(FSRL payloadFSRL, TaskMonitor monitor)
|
||||
throws CancelledException, IOException {
|
||||
|
||||
return fsService.getDerivedByteProviderPush(byteProvider.getFSRL(), payloadFSRL, "cart", -1,
|
||||
os -> {
|
||||
CartV1PayloadExtractor extractor =
|
||||
new CartV1PayloadExtractor(byteProvider, os, cartFile);
|
||||
extractor.extract(monitor);
|
||||
}, monitor);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ByteProvider getByteProvider(GFile file, TaskMonitor monitor)
|
||||
throws IOException, CancelledException {
|
||||
if (fsIndexHelper.isPayloadFile(file)) {
|
||||
return new ByteProviderWrapper(payloadProvider, file.getFSRL());
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<GFile> getListing(GFile directory) throws IOException {
|
||||
return fsIndexHelper.getListing(directory);
|
||||
}
|
||||
|
||||
@Override
|
||||
public FileAttributes getFileAttributes(GFile file, TaskMonitor monitor) {
|
||||
FileAttributes result = new FileAttributes();
|
||||
|
||||
// If the specified file isn't the payload file or the cartFile object isn't defined, bail
|
||||
if (!fsIndexHelper.isPayloadFile(file) || cartFile == null) {
|
||||
return result;
|
||||
}
|
||||
|
||||
// Set of keys (lower-case) that are handled manually and should be skipped when
|
||||
// adding remaining keys to the file attributes.
|
||||
Set<String> skipKeys = new HashSet<>(Set.of("name"));
|
||||
|
||||
// Set to track all attributes that have been added. Used during bulk addition
|
||||
// to add with _# suffixes.
|
||||
Set<String> addedAttributes = new HashSet<>();
|
||||
|
||||
if (cartFile.getDataSize() >= 0) {
|
||||
result.add(FileAttributeType.SIZE_ATTR, cartFile.getDataSize());
|
||||
}
|
||||
skipKeys.add("length");
|
||||
|
||||
result.add(FileAttributeType.COMPRESSED_SIZE_ATTR, cartFile.getPackedSize());
|
||||
result.add(FileAttributeType.IS_ENCRYPTED_ATTR, true);
|
||||
|
||||
// Won't create the CaRT file object if we don't have a valid key
|
||||
result.add(FileAttributeType.HAS_GOOD_PASSWORD_ATTR, true);
|
||||
|
||||
// Keep warning as the first custom attribute to display it first
|
||||
// in that section of the file information
|
||||
result.add("WARNING", """
|
||||
CaRT format is often used to neuter and share malicious files.
|
||||
Please use caution if exporting original binary.""");
|
||||
|
||||
// Display the ARC4 key, in hex, that is being used
|
||||
result.add("ARC4 Key",
|
||||
new String(HashUtilities.hexDump(cartFile.getDecryptor().getARC4Key())));
|
||||
|
||||
// Display the stored hashes next
|
||||
for (String hashName : CartV1Constants.EXPECTED_HASHES.keySet()) {
|
||||
byte[] footerHashValue = cartFile.getFooterHash(hashName);
|
||||
skipKeys.add(hashName.toLowerCase());
|
||||
|
||||
if (footerHashValue != null) {
|
||||
result.add("Reported " + hashName,
|
||||
new String(HashUtilities.hexDump(footerHashValue)));
|
||||
}
|
||||
else {
|
||||
result.add("Reported " + hashName, "Missing");
|
||||
}
|
||||
}
|
||||
|
||||
// Generate set of keys to protect that we don't want the metadata to be able
|
||||
// to overwrite.
|
||||
// Warn the user if any of these exist because they may indicate an attempt to
|
||||
// mess with the shown information.
|
||||
// Also, set up the list of added attributes so far so that we can track and
|
||||
// add _# for non-protected.
|
||||
Set<String> protectedKeys = new HashSet<>();
|
||||
for (FileAttribute<?> attribute : result.getAttributes()) {
|
||||
protectedKeys.add(attribute.getAttributeDisplayName().toLowerCase());
|
||||
addedAttributes.add(attribute.getAttributeDisplayName());
|
||||
}
|
||||
|
||||
// Before processing final metadata for inclusion in file attributes check if
|
||||
// CaRT's header/footer merging is obscuring any attempted overwrite of
|
||||
// header data with footer data
|
||||
Set<String> warnKeys = new HashSet<>();
|
||||
for (Entry<String, JsonElement> entry : cartFile.getHeader()
|
||||
.optionalHeaderData()
|
||||
.entrySet()) {
|
||||
if (CartV1Constants.FOOTER_ONLY_KEYS.contains(entry.getKey().toLowerCase())) {
|
||||
warnKeys.add(entry.getKey());
|
||||
}
|
||||
}
|
||||
|
||||
if (!warnKeys.isEmpty()) {
|
||||
result.add("SECURITY WARNING",
|
||||
"CaRT file metadata may be attempting to overwrite protected file data: " +
|
||||
StringEscapeUtils.escapeHtml4(String.join(", ", warnKeys)) + ".");
|
||||
}
|
||||
|
||||
// Construct object to pretty print JSON elements to be shown in the display
|
||||
Gson gson = new GsonBuilder().serializeNulls().setPrettyPrinting().create();
|
||||
|
||||
// Clear any key warnings to prepare to collect a new set
|
||||
warnKeys.clear();
|
||||
|
||||
// Walk all the optional header JsonElements, then the optional footer
|
||||
// JsonElements adding each to the file's attributes. Skip any that are
|
||||
// in the list of keys handled manually.
|
||||
for (Entry<String, JsonElement> entry : cartFile.getMetadata().entrySet()) {
|
||||
if (skipKeys.contains(entry.getKey().toLowerCase())) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Key not being skipped, check if it is protected, if so record then skip
|
||||
if (protectedKeys.contains(entry.getKey().toLowerCase())) {
|
||||
warnKeys.add(entry.getKey());
|
||||
continue;
|
||||
}
|
||||
|
||||
String value = "<Unknown>";
|
||||
|
||||
try {
|
||||
value = gson.toJson(entry.getValue());
|
||||
}
|
||||
catch (IllegalStateException e) {
|
||||
value = "Invalid JSON String";
|
||||
}
|
||||
|
||||
String key = entry.getKey();
|
||||
int suffix_counter = 0;
|
||||
|
||||
while (addedAttributes.contains(key)) {
|
||||
suffix_counter++;
|
||||
// If more than 100 of the same key are found, stop trying to add them
|
||||
if (suffix_counter > 100) {
|
||||
suffix_counter = -1;
|
||||
break;
|
||||
}
|
||||
|
||||
key = entry.getKey() + "_" + suffix_counter;
|
||||
}
|
||||
|
||||
if (suffix_counter != -1) {
|
||||
result.add(key, value);
|
||||
addedAttributes.add(key);
|
||||
}
|
||||
}
|
||||
|
||||
// If any protected keys were skipped, notify the user
|
||||
if (!warnKeys.isEmpty()) {
|
||||
result.add("SECURITY WARNING",
|
||||
"CaRT file metadata may be attempting to overwrite protected file data: " +
|
||||
StringEscapeUtils.escapeHtml4(String.join(", ", warnKeys)) +
|
||||
". Skipped those keys.");
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,67 @@
|
|||
/* ###
|
||||
* IP: GHIDRA
|
||||
*
|
||||
* 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 ghidra.file.formats.cart;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import ghidra.app.util.bin.ByteProvider;
|
||||
import ghidra.formats.gfilesystem.*;
|
||||
import ghidra.formats.gfilesystem.factory.GFileSystemFactoryByteProvider;
|
||||
import ghidra.formats.gfilesystem.factory.GFileSystemProbeByteProvider;
|
||||
import ghidra.util.exception.CancelledException;
|
||||
import ghidra.util.task.TaskMonitor;
|
||||
|
||||
/**
|
||||
* File system factory for the CaRT format (Version 1). Probe to quickly
|
||||
* determine if proposed data appears to be CaRT format and provide the
|
||||
* appropriate file system object back.
|
||||
*/
|
||||
public class CartFileSystemFactory
|
||||
implements GFileSystemFactoryByteProvider<CartFileSystem>, GFileSystemProbeByteProvider {
|
||||
|
||||
@Override
|
||||
public CartFileSystem create(FSRLRoot targetFSRL, ByteProvider byteProvider,
|
||||
FileSystemService fsService, TaskMonitor monitor)
|
||||
throws IOException, CancelledException {
|
||||
|
||||
CartFileSystem fs = null;
|
||||
|
||||
try {
|
||||
fs = new CartFileSystem(targetFSRL, fsService);
|
||||
fs.mount(byteProvider, monitor);
|
||||
return fs;
|
||||
}
|
||||
catch (IOException | CancelledException e) {
|
||||
FSUtilities.uncheckedClose(fs, null);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean probe(ByteProvider byteProvider, FileSystemService fsService,
|
||||
TaskMonitor monitor) throws IOException, CancelledException {
|
||||
|
||||
// Quickly and efficiently examine the bytes in 'byteProvider' to determine if
|
||||
// it's a valid CaRT file system. If it is, return true.
|
||||
if (CartV1File.isCart(byteProvider)) {
|
||||
return true;
|
||||
}
|
||||
// If/when future CaRT file versions exist, check them here.
|
||||
|
||||
// If we make it to the end without a match, return false
|
||||
return false;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,31 @@
|
|||
/* ###
|
||||
* IP: GHIDRA
|
||||
*
|
||||
* 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 ghidra.file.formats.cart;
|
||||
|
||||
/**
|
||||
* CartInvalidCartException subclass for apparent ARC4 key errors; for instance,
|
||||
* when decrypted data does not meet the expected format.
|
||||
*/
|
||||
public class CartInvalidARC4KeyException extends CartInvalidCartException {
|
||||
|
||||
/**
|
||||
* Construct CartInvalidARC4KeyException with specified message
|
||||
* @param message The reason for the exception
|
||||
*/
|
||||
public CartInvalidARC4KeyException(String message) {
|
||||
super(message);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,30 @@
|
|||
/* ###
|
||||
* IP: GHIDRA
|
||||
*
|
||||
* 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 ghidra.file.formats.cart;
|
||||
|
||||
/**
|
||||
* Exception subclass for general CaRT format or access exceptions.
|
||||
*/
|
||||
public class CartInvalidCartException extends Exception {
|
||||
|
||||
/**
|
||||
* Construct CartInvalidCartException with specified message
|
||||
* @param message The reason for the exception
|
||||
*/
|
||||
public CartInvalidCartException(String message) {
|
||||
super(message);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,171 @@
|
|||
/* ###
|
||||
* IP: GHIDRA
|
||||
*
|
||||
* 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 ghidra.file.formats.cart;
|
||||
|
||||
import java.util.HashSet;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import ghidra.util.HashUtilities;
|
||||
|
||||
/**
|
||||
* Helper class from providing all the constants required for parsing a CaRT
|
||||
* format (Version 1) file.
|
||||
* <p>
|
||||
* From CaRT Source, cart.py
|
||||
*
|
||||
* <pre>{@code
|
||||
* # MANDATORY HEADER (Not compress, not encrypted.
|
||||
* # 4s h Q 16s Q
|
||||
* # 'CART<VERSION><RESERVED><ARC4KEY><OPT_HEADER_LEN>'
|
||||
* #
|
||||
* # OPTIONAL_HEADER (OPT_HEADER_LEN bytes)
|
||||
* # RC4(<JSON_SERIALIZED_OPTIONAL_HEADER>)
|
||||
* #
|
||||
* # RC4(ZLIB(block encoded stream ))
|
||||
* #
|
||||
* # OPTIONAL_FOOTER_LEN (Q)
|
||||
* # <JSON_SERIALIZED_OPTIONAL_FOOTER>
|
||||
* #
|
||||
* # MANDATORY FOOTER
|
||||
* # 4s QQ Q
|
||||
* # 'TRAC<RESERVED><OPT_FOOTER_LEN>'
|
||||
* }</pre>
|
||||
* Where s=1 ASCII string byte, h=short, Q=quadword
|
||||
* <p>
|
||||
* Note: There is an error in the documented mandatory footer. the 'QQ' marked
|
||||
* as reserved should be two separate 'Q' sized values, the first is actually
|
||||
* reserved (0) and the second is the position of the optional footer.
|
||||
*/
|
||||
public final class CartV1Constants {
|
||||
/**
|
||||
* Header magic value for CaRT
|
||||
*/
|
||||
public static final String HEADER_MAGIC = "CART";
|
||||
|
||||
/**
|
||||
* Version number required for CaRT version 1
|
||||
*/
|
||||
public static final short HEADER_VERSION = 1;
|
||||
|
||||
/**
|
||||
* Header reserved, required value
|
||||
*/
|
||||
public static final long HEADER_RESERVED = 0;
|
||||
|
||||
/**
|
||||
* Length of the mandatory CaRT header
|
||||
*/
|
||||
public static final int HEADER_LENGTH = 4 + 2 + 8 + 16 + 8;
|
||||
|
||||
/**
|
||||
* Footer magic value for CaRT
|
||||
*/
|
||||
public static final String FOOTER_MAGIC = "TRAC";
|
||||
|
||||
/**
|
||||
* Footer reserved, required value
|
||||
*/
|
||||
public static final long FOOTER_RESERVED = 0;
|
||||
|
||||
/**
|
||||
* Length of the mandatory CaRT footer
|
||||
*/
|
||||
public static final int FOOTER_LENGTH = 4 + 8 + 8 + 8;
|
||||
|
||||
/**
|
||||
* Length of the CaRT ARC4 key in bytes
|
||||
*/
|
||||
public static final int ARC4_KEY_LENGTH = 16;
|
||||
|
||||
/**
|
||||
* The default ARC4 key used by CaRT if not overridden with a private value.
|
||||
* Consists of the first 8 digits of PI, twice
|
||||
*/
|
||||
public static final byte[] DEFAULT_ARC4_KEY = {
|
||||
// First 8
|
||||
(byte) 0x03, (byte) 0x01, (byte) 0x04, (byte) 0x01,
|
||||
(byte) 0x05, (byte) 0x09, (byte) 0x02, (byte) 0x06,
|
||||
// Repeat
|
||||
(byte) 0x03, (byte) 0x01, (byte) 0x04, (byte) 0x01,
|
||||
(byte) 0x05, (byte) 0x09, (byte) 0x02, (byte) 0x06
|
||||
};
|
||||
|
||||
/**
|
||||
* The placeholder value that will be stored in the ARC4 key header position when a
|
||||
* private value is in use. Consists of all 16, 0x00 bytes.
|
||||
*/
|
||||
public static final byte[] PRIVATE_ARC4_KEY_PLACEHOLDER = {
|
||||
(byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,
|
||||
(byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,
|
||||
(byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,
|
||||
(byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,
|
||||
};
|
||||
|
||||
/**
|
||||
* Block size, in bytes, used for reading/writing payload data in CaRT
|
||||
*/
|
||||
public static final int BLOCK_SIZE = 64 * 1024;
|
||||
|
||||
/**
|
||||
* Minimum length, in bytes, of a CaRT file.
|
||||
* Really it should be longer for the payload bytes themselves. This value only accounts for
|
||||
* the mandatory header and footer.
|
||||
*/
|
||||
public static final int MINIMUM_LENGTH = HEADER_LENGTH + FOOTER_LENGTH;
|
||||
|
||||
/**
|
||||
* Map of CaRT optional footer hash name keys to MessageDigest hash names.
|
||||
* These are the hashes that are expected to be in a normal CaRT based on the default library
|
||||
* implementation.
|
||||
*/
|
||||
public static final Map<String, String> EXPECTED_HASHES = new LinkedHashMap<>() {
|
||||
{
|
||||
put("md5", HashUtilities.MD5_ALGORITHM);
|
||||
// SHA1 is not exported in the static variables of the HashUtilities class, but
|
||||
// is valid to the underlying MessageDigest
|
||||
put("sha1", "SHA1");
|
||||
put("sha256", HashUtilities.SHA256_ALGORITHM);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Set of keys (in lower case) that should only ever exist in the footer. Finding them
|
||||
* in the header could indicate an attempt to obfuscate the true value from the footer.
|
||||
*/
|
||||
public static final Set<String> FOOTER_ONLY_KEYS = new HashSet<>() {
|
||||
{
|
||||
add("length");
|
||||
addAll(CartV1Constants.EXPECTED_HASHES.keySet()
|
||||
.stream()
|
||||
.map(String::toLowerCase)
|
||||
.collect(Collectors.toList()));
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* First two header bytes for ZLIB in 3 modes: fast, default, and best compression.
|
||||
*/
|
||||
public static final List<byte[]> ZLIB_HEADER_BYTES = List.of(
|
||||
new byte[] { (byte) 0x78, (byte) 0x01 }, // Fast
|
||||
new byte[] { (byte) 0x78, (byte) 0x9c }, // Default
|
||||
new byte[] { (byte) 0x78, (byte) 0xda } // Best
|
||||
);
|
||||
}
|
|
@ -0,0 +1,227 @@
|
|||
/* ###
|
||||
* IP: GHIDRA
|
||||
*
|
||||
* 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 ghidra.file.formats.cart;
|
||||
|
||||
import java.io.*;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
import ghidra.formats.gfilesystem.FSUtilities;
|
||||
import ghidra.util.exception.CancelledException;
|
||||
import ghidra.util.task.TaskMonitor;
|
||||
|
||||
/**
|
||||
* Decryptor object for the CaRT format (Version 1).
|
||||
*/
|
||||
public class CartV1Decryptor {
|
||||
private byte[] arc4Key;
|
||||
|
||||
/**
|
||||
* Static function to decrypt the specified buffer with the specified key.
|
||||
*
|
||||
* @param arc4Key The ARC4 key to use
|
||||
* @param encryptedBuffer The buffer to decrypt
|
||||
* @return The decrypted buffer bytes or null on failure.
|
||||
* @throws CartInvalidARC4KeyException If the key is invalid
|
||||
* @throws IOException If there is an error reading from the buffer
|
||||
* @throws CancelledException If the decryption is cancelled from the monitor
|
||||
*/
|
||||
public static byte[] decrypt(byte[] arc4Key, byte[] encryptedBuffer)
|
||||
throws CartInvalidARC4KeyException, CancelledException, IOException {
|
||||
|
||||
return CartV1Decryptor.decrypt(arc4Key, encryptedBuffer, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Static function to decrypt the specified buffer with the specified key.
|
||||
*
|
||||
* @param arc4Key The ARC4 key to use
|
||||
* @param encryptedBuffer The buffer to decrypt
|
||||
* @param monitor The monitor UI element for the user to see progress or cancel
|
||||
* @return The decrypted buffer bytes or null on failure.
|
||||
* @throws CartInvalidARC4KeyException If the key is invalid
|
||||
* @throws IOException If there is an error reading from the buffer
|
||||
* @throws CancelledException If the decryption is cancelled from the monitor
|
||||
*/
|
||||
public static byte[] decrypt(byte[] arc4Key, byte[] encryptedBuffer, TaskMonitor monitor)
|
||||
throws CartInvalidARC4KeyException, CancelledException, IOException {
|
||||
CartV1Decryptor decryptor = new CartV1Decryptor(arc4Key);
|
||||
|
||||
return decryptor.decrypt(encryptedBuffer, monitor);
|
||||
}
|
||||
|
||||
/**
|
||||
* Static function to decrypt the specified buffer with the specified key and
|
||||
* convert the output to a UTF-8 String.
|
||||
*
|
||||
* @param arc4Key The ARC4 key to use
|
||||
* @param encryptedBuffer The buffer to decrypt
|
||||
* @return The decrypted String or null on failure.
|
||||
* @throws CartInvalidARC4KeyException If the key is invalid
|
||||
* @throws IOException If there is an error reading from the buffer
|
||||
*/
|
||||
public static String decryptToString(byte[] arc4Key, byte[] encryptedBuffer)
|
||||
throws CartInvalidARC4KeyException, IOException {
|
||||
return decryptToString(arc4Key, encryptedBuffer, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Static function to decrypt the specified buffer with the specified key and
|
||||
* convert the output to a UTF-8 String.
|
||||
*
|
||||
* @param arc4Key The ARC4 key to use
|
||||
* @param encryptedBuffer The buffer to decrypt
|
||||
* @param monitor The monitor UI element for the user to see progress or cancel
|
||||
* @return The decrypted String or null on failure.
|
||||
* @throws CartInvalidARC4KeyException If the key is invalid
|
||||
* @throws IOException If there is an error reading from the buffer
|
||||
*/
|
||||
public static String decryptToString(byte[] arc4Key, byte[] encryptedBuffer,
|
||||
TaskMonitor monitor) throws CartInvalidARC4KeyException, IOException {
|
||||
CartV1Decryptor decryptor = new CartV1Decryptor(arc4Key);
|
||||
return decryptor.decryptToString(encryptedBuffer, monitor);
|
||||
}
|
||||
|
||||
/**
|
||||
* Construct a decryptor with the specified key.
|
||||
*
|
||||
* @param arc4Key The ARC4 key to use
|
||||
* @throws CartInvalidARC4KeyException If the key is bad, null or too short.
|
||||
*/
|
||||
public CartV1Decryptor(byte[] arc4Key) throws CartInvalidARC4KeyException {
|
||||
setKey(arc4Key);
|
||||
}
|
||||
|
||||
/**
|
||||
* Throws an exception if the key is not valid.
|
||||
*
|
||||
* @throws CartInvalidARC4KeyException If the key is invalid.
|
||||
*/
|
||||
public void throwIfInvalid() throws CartInvalidARC4KeyException {
|
||||
throwIfInvalid(this.arc4Key);
|
||||
}
|
||||
|
||||
/**
|
||||
* Throws an exception if the specified key is not valid.
|
||||
*
|
||||
* @param proposedARC4Key The ARC4 key being proposed to use.
|
||||
* @throws CartInvalidARC4KeyException If the key is bad, null or too short.
|
||||
*/
|
||||
public void throwIfInvalid(byte[] proposedARC4Key) throws CartInvalidARC4KeyException {
|
||||
if (proposedARC4Key == null) {
|
||||
throw new CartInvalidARC4KeyException("Invalid null CaRT key.");
|
||||
}
|
||||
else if (proposedARC4Key.length != CartV1Constants.ARC4_KEY_LENGTH) {
|
||||
throw new CartInvalidARC4KeyException("Invalid CaRT key length.");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the ARC4 key for this decryptor instance.
|
||||
*
|
||||
* @param arc4Key The ARC4 key to use
|
||||
* @throws CartInvalidARC4KeyException If the key is bad, null or too short.
|
||||
*/
|
||||
public void setKey(byte[] arc4Key) throws CartInvalidARC4KeyException {
|
||||
throwIfInvalid(arc4Key);
|
||||
|
||||
this.arc4Key = arc4Key;
|
||||
}
|
||||
|
||||
/**
|
||||
* Decrypt the specified buffer with object's key.
|
||||
*
|
||||
* @param encryptedBuffer The buffer to decrypt
|
||||
* @return The decrypted buffer bytes or null on failure.
|
||||
* @throws CartInvalidARC4KeyException If the key is invalid
|
||||
* @throws IOException If there is an error reading from the buffer
|
||||
*/
|
||||
public byte[] decrypt(byte[] encryptedBuffer) throws CartInvalidARC4KeyException, IOException {
|
||||
return this.decrypt(encryptedBuffer, (TaskMonitor) null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Decrypt the specified buffer with object's key.
|
||||
*
|
||||
* @param encryptedBuffer The buffer to decrypt
|
||||
* @param monitor The monitor UI element for the user to see progress or cancel
|
||||
* @return The decrypted buffer bytes or null on failure.
|
||||
* @throws CartInvalidARC4KeyException If the key is invalid
|
||||
* @throws IOException If there is an error reading from the buffer
|
||||
*/
|
||||
public byte[] decrypt(byte[] encryptedBuffer, TaskMonitor monitor)
|
||||
throws CartInvalidARC4KeyException, IOException {
|
||||
try {
|
||||
CartV1StreamDecryptor decryptor =
|
||||
new CartV1StreamDecryptor(new ByteArrayInputStream(encryptedBuffer), arc4Key);
|
||||
ByteArrayOutputStream buffer = new ByteArrayOutputStream();
|
||||
|
||||
FSUtilities.streamCopy(decryptor, buffer, TaskMonitor.dummyIfNull(monitor));
|
||||
|
||||
buffer.flush();
|
||||
|
||||
return buffer.toByteArray();
|
||||
}
|
||||
catch (CancelledException e) {
|
||||
// ignore
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Decrypt the specified buffer with the object's key and convert the output to
|
||||
* a UTF-8 String.
|
||||
*
|
||||
* @param encryptedBuffer The buffer to decrypt
|
||||
* @return The decrypted String or null on failure.
|
||||
* @throws CartInvalidARC4KeyException If the key is invalid
|
||||
* @throws IOException If there is an error reading from the buffer
|
||||
*/
|
||||
public String decryptToString(byte[] encryptedBuffer)
|
||||
throws CartInvalidARC4KeyException, IOException {
|
||||
return this.decryptToString(encryptedBuffer, (TaskMonitor) null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Decrypt the specified buffer with the object's key and convert the output to
|
||||
* a UTF-8 String.
|
||||
*
|
||||
* @param encryptedBuffer The buffer to decrypt
|
||||
* @param monitor The monitor UI element for the user to see progress or cancel
|
||||
* @return The decrypted String or null on failure.
|
||||
* @throws CartInvalidARC4KeyException If the key is invalid
|
||||
* @throws IOException If there is an error reading from the buffer
|
||||
*/
|
||||
public String decryptToString(byte[] encryptedBuffer, TaskMonitor monitor)
|
||||
throws CartInvalidARC4KeyException, IOException {
|
||||
byte[] decryptedBuffer = decrypt(encryptedBuffer, monitor);
|
||||
|
||||
if (decryptedBuffer != null) {
|
||||
return new String(decryptedBuffer, StandardCharsets.UTF_8);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the ARC4 key being used by the object.
|
||||
*
|
||||
* @return The bytes of the ARC4 key.
|
||||
*/
|
||||
protected byte[] getARC4Key() {
|
||||
return arc4Key;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,687 @@
|
|||
/* ###
|
||||
* IP: GHIDRA
|
||||
*
|
||||
* 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 ghidra.file.formats.cart;
|
||||
|
||||
import java.io.*;
|
||||
import java.util.*;
|
||||
import java.util.Map.Entry;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import org.apache.commons.text.StringEscapeUtils;
|
||||
|
||||
import com.google.gson.JsonElement;
|
||||
import com.google.gson.JsonObject;
|
||||
|
||||
import ghidra.app.util.bin.BinaryReader;
|
||||
import ghidra.app.util.bin.ByteProvider;
|
||||
import ghidra.util.exception.CancelledException;
|
||||
|
||||
/**
|
||||
* Parse and manage object for the CaRT version 1 format, including managing the
|
||||
* header, footer, and decryptor objects as providing a combined view of the
|
||||
* optional header and footer metadata in a similar way to the original CaRT
|
||||
* python library.
|
||||
*/
|
||||
public class CartV1File {
|
||||
@SuppressWarnings("unused")
|
||||
private static short version = CartV1Constants.HEADER_VERSION;
|
||||
|
||||
private String name = "<Unknown>";
|
||||
private String path = "<Unknown>";
|
||||
private long dataOffset = -1;
|
||||
private long payloadOriginalSize = -1;
|
||||
private long packedSize = -1;
|
||||
private Map<String, byte[]> footerHashes;
|
||||
private CartV1Header header;
|
||||
private CartV1Footer footer;
|
||||
private CartV1Decryptor decryptor;
|
||||
private long readerLength = -1;
|
||||
private BinaryReader internalReader;
|
||||
|
||||
/**
|
||||
* Constructs a new CartV1File read from the byte provider.
|
||||
*
|
||||
* @param byteProvider The byte provider from which to read
|
||||
* @throws IOException If there was a problem reading from the
|
||||
* byte provider
|
||||
* @throws CartInvalidCartException If there was a formating error with the
|
||||
* CaRT file
|
||||
* @throws CartInvalidARC4KeyException If the decryption of the payload or JSON
|
||||
* deserialization fails.
|
||||
* @throws CartConfigurationException If the configuration data for CaRT was
|
||||
* found, but invalid.
|
||||
* @throws CancelledException If the user cancels due to data error/warning
|
||||
*/
|
||||
public CartV1File(ByteProvider byteProvider) throws IOException, CartInvalidCartException,
|
||||
CartInvalidARC4KeyException, CartConfigurationException, CancelledException {
|
||||
this(new BinaryReader(byteProvider, true));
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs a new CartV1File read from the byte provider and user provided
|
||||
* ARC4 key.
|
||||
*
|
||||
* @param byteProvider The byte provider from which to read
|
||||
* @param arc4Key The ARC4 key to use as a user provided string
|
||||
* @throws IOException If there was a problem reading from the
|
||||
* byte provider
|
||||
* @throws CartInvalidCartException If there was a formating error with the
|
||||
* CaRT file
|
||||
* @throws CartInvalidARC4KeyException If the decryption of the payload or JSON
|
||||
* deserialization fails.
|
||||
* @throws CartConfigurationException If the configuration data for CaRT was
|
||||
* found, but invalid.
|
||||
* @throws CancelledException If the user cancels due to data error/warning
|
||||
*/
|
||||
public CartV1File(ByteProvider byteProvider, String arc4Key)
|
||||
throws IOException, CartInvalidCartException, CartInvalidARC4KeyException,
|
||||
CartConfigurationException, CancelledException {
|
||||
this(new BinaryReader(byteProvider, true), arc4Key);
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs a new CartV1File read from the little-endian binary reader.
|
||||
*
|
||||
* @param reader The little-endian binary reader from with to read
|
||||
* @throws IOException If there was a problem reading from the
|
||||
* byte provider
|
||||
* @throws CartInvalidCartException If there was a formating error with the
|
||||
* CaRT file
|
||||
* @throws CartInvalidARC4KeyException If the decryption of the payload or JSON
|
||||
* deserialization fails.
|
||||
* @throws CartConfigurationException If the configuration data for CaRT was
|
||||
* found, but invalid.
|
||||
* @throws CancelledException If the user cancels due to data error/warning
|
||||
*/
|
||||
public CartV1File(BinaryReader reader) throws IOException, CartInvalidCartException,
|
||||
CartInvalidARC4KeyException, CartConfigurationException, CancelledException {
|
||||
this(reader, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs a new CartV1File read from the little-endian binary reader and
|
||||
* provided key. If key is null, attempt to autodetect.
|
||||
*
|
||||
* @param reader The little-endian binary reader from with to read
|
||||
* @param arc4Key The ARC4 key to use as a user provided string
|
||||
* @throws IOException If there was a problem reading from the
|
||||
* byte provider
|
||||
* @throws CartInvalidCartException If there was a formating error with the
|
||||
* CaRT file
|
||||
* @throws CartInvalidARC4KeyException If the decryption of the payload or JSON
|
||||
* deserialization fails.
|
||||
* @throws CartConfigurationException If the configuration data for CaRT was
|
||||
* found, but invalid.
|
||||
* @throws CancelledException If the user cancels due to data error/warning
|
||||
*/
|
||||
public CartV1File(BinaryReader reader, String arc4Key)
|
||||
throws IOException, CartInvalidCartException, CartInvalidARC4KeyException,
|
||||
CartConfigurationException, CancelledException {
|
||||
// Check that binary reader is Little-Endian
|
||||
if (!reader.isLittleEndian()) {
|
||||
throw new IOException("CaRT BinaryReader must be Little-Endian.");
|
||||
}
|
||||
|
||||
readerLength = reader.length();
|
||||
|
||||
// Check that there are at least enough bytes to contain a header and footer
|
||||
if (readerLength < CartV1Constants.MINIMUM_LENGTH) {
|
||||
throw new CartInvalidCartException("Data too small to be CaRT format.");
|
||||
}
|
||||
|
||||
internalReader = reader.clone(0);
|
||||
|
||||
// Load and validate header
|
||||
header = new CartV1Header(internalReader);
|
||||
|
||||
// Load and validate footer
|
||||
footer = new CartV1Footer(internalReader);
|
||||
|
||||
name = internalReader.getByteProvider().getName();
|
||||
dataOffset = header.dataStart();
|
||||
|
||||
packedSize = footer.optionalFooterPosition() - dataOffset;
|
||||
if (packedSize <= 0 || packedSize > readerLength) {
|
||||
throw new CartInvalidCartException("Error calculating CaRT compressed payload size.");
|
||||
}
|
||||
|
||||
if (arc4Key == null) {
|
||||
createDecryptor();
|
||||
}
|
||||
else {
|
||||
createDecryptor(arc4Key);
|
||||
}
|
||||
|
||||
try {
|
||||
header.loadOptionalHeader(decryptor);
|
||||
}
|
||||
catch (CartInvalidARC4KeyException e) {
|
||||
throw new CartInvalidARC4KeyException("Decryption failed for header metadata: " + e);
|
||||
}
|
||||
|
||||
try {
|
||||
footer.loadOptionalFooter(decryptor);
|
||||
}
|
||||
catch (CartInvalidARC4KeyException e) {
|
||||
throw new CartInvalidARC4KeyException("Decryption failed for footer metadata: " + e);
|
||||
}
|
||||
|
||||
JsonObject optionalHeaderData = header.optionalHeaderData();
|
||||
|
||||
// Get the payload name from the optional header data, if available;
|
||||
// if the CaRT file name ends with ".cart" drop the extension;
|
||||
// otherwise, add ".uncart"
|
||||
if (optionalHeaderData.has("name")) {
|
||||
path = optionalHeaderData.get("name").getAsString();
|
||||
}
|
||||
else {
|
||||
path = name;
|
||||
if (path.endsWith(".cart")) {
|
||||
path = path.substring(0, path.length() - 5);
|
||||
}
|
||||
else {
|
||||
path += ".uncart";
|
||||
}
|
||||
}
|
||||
|
||||
JsonObject optionalFooterData = footer.optionalFooterData();
|
||||
|
||||
// Load the length from the optional footer, if available. If the value
|
||||
// is less than 0 refuse to proceed. If the value is greater than 10 times
|
||||
// the compressed size warn the user that this seems odd.
|
||||
if (optionalFooterData.has("length")) {
|
||||
payloadOriginalSize = optionalFooterData.get("length").getAsLong();
|
||||
if (payloadOriginalSize < 0) {
|
||||
throw new CartInvalidCartException("Bad payload length in footer.");
|
||||
}
|
||||
else if (payloadOriginalSize > packedSize * 10) {
|
||||
if (!CartCancelDialogs.promptWarningContinue("Size Warning",
|
||||
"CaRT footer reports payload size <b>" +
|
||||
StringEscapeUtils.escapeHtml4(String.valueOf(payloadOriginalSize)) +
|
||||
"</b>, but this value seems unreasonable given the compressed size of <i>" +
|
||||
StringEscapeUtils.escapeHtml4((String.valueOf(packedSize))) +
|
||||
"</i>. Continue processing?")) {
|
||||
throw new CancelledException("Cancelled due to footer length field error.");
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
payloadOriginalSize = -1;
|
||||
}
|
||||
|
||||
footerHashes = new LinkedHashMap<>();
|
||||
|
||||
List<String> missingHashes = new ArrayList<>(); // List of hashes that are expected, but not
|
||||
// present
|
||||
|
||||
// If an expected hash is in the optional footer load it. Refuse to continue if
|
||||
// it fails to parse correctly.
|
||||
for (String hashName : CartV1Constants.EXPECTED_HASHES.keySet()) {
|
||||
if (optionalFooterData.has(hashName)) {
|
||||
try {
|
||||
footerHashes.put(hashName,
|
||||
HexFormat.of().parseHex(optionalFooterData.get(hashName).getAsString()));
|
||||
}
|
||||
catch (IllegalArgumentException e) {
|
||||
throw new CartInvalidCartException("Bad " + hashName + " hash format.");
|
||||
}
|
||||
}
|
||||
else {
|
||||
missingHashes.add(hashName);
|
||||
}
|
||||
}
|
||||
|
||||
if (footerHashes.isEmpty()) {
|
||||
if (!CartCancelDialogs.promptErrorContinue("No Hashes",
|
||||
"No hash data in CaRT footer metadata. Cannot verify content. Continue processing?")) {
|
||||
throw new CancelledException("Cancelled due to no hash data.");
|
||||
}
|
||||
}
|
||||
else if (!missingHashes.isEmpty()) {
|
||||
if (!CartCancelDialogs.promptErrorContinue("Missing Hashes",
|
||||
"Expected hash(es) missing: " + String.join(", ", missingHashes) +
|
||||
". Continue processing?")) {
|
||||
throw new CancelledException("Cancelled due to missing hash data (" +
|
||||
String.join(", ", missingHashes) + ").");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create the CartV1Decryptor object including determining the appropriate key.
|
||||
* First try the key in the header, if that fails check if a key is available in
|
||||
* the default CaRT configuration INI file. After that try the default key and
|
||||
* the private key placeholder. If all of those fail, raise an exception.
|
||||
*
|
||||
* @return The CartV1Decryptor object
|
||||
* @throws IOException If there is a read failure of the data
|
||||
* @throws CartInvalidCartException If there was a formating error with the
|
||||
* CaRT file
|
||||
* @throws CartInvalidARC4KeyException If no key could be determined
|
||||
* @throws CartConfigurationException If the configuration data for CaRT was
|
||||
* found, but invalid.
|
||||
* @throws CancelledException If the user cancels decryption
|
||||
*/
|
||||
private CartV1Decryptor createDecryptor() throws IOException, CartInvalidCartException,
|
||||
CartInvalidARC4KeyException, CartConfigurationException, CancelledException {
|
||||
if (header == null) {
|
||||
throw new CartInvalidCartException("CaRT header not initialized.");
|
||||
}
|
||||
// First, test the key from the header as-is, if it works return the decryptor
|
||||
// based on it.
|
||||
try {
|
||||
return createDecryptor(header.arc4Key());
|
||||
}
|
||||
catch (CartInvalidARC4KeyException e) {
|
||||
// ignore
|
||||
}
|
||||
|
||||
List<byte[]> possibleKeys = new ArrayList<>();
|
||||
|
||||
// Attempt to get a key from the default CaRT configuration INI file
|
||||
try {
|
||||
byte[] iniKey = getIniKey();
|
||||
|
||||
if (iniKey != null) {
|
||||
possibleKeys.add(iniKey);
|
||||
}
|
||||
}
|
||||
catch (IOException e) {
|
||||
// ignore
|
||||
}
|
||||
|
||||
// Try a couple default keys before reporting failure
|
||||
possibleKeys.add(CartV1Constants.DEFAULT_ARC4_KEY);
|
||||
possibleKeys.add(CartV1Constants.PRIVATE_ARC4_KEY_PLACEHOLDER);
|
||||
|
||||
for (byte[] key : possibleKeys) {
|
||||
try {
|
||||
return createDecryptor(key);
|
||||
}
|
||||
catch (CartInvalidARC4KeyException e) {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
|
||||
// If a valid key hasn't been determined, raise the appropriate exception
|
||||
throw new CartInvalidARC4KeyException("Private CaRT ARC4 key could not be determined.");
|
||||
}
|
||||
|
||||
/**
|
||||
* Create the CartV1Decryptor object using the specified key. Tested as-is, and as
|
||||
* base64 decoded.
|
||||
*
|
||||
* @param proposedArc4Key The ARC4 key being proposed for this CaRT as a String
|
||||
* @return The CartV1Decryptor object
|
||||
* @throws IOException If there is a read failure of the data
|
||||
* @throws CartInvalidCartException If there was a formating error with the
|
||||
* CaRT file
|
||||
* @throws CartInvalidARC4KeyException If no key could be determined
|
||||
* @throws CartConfigurationException If the configuration data for CaRT was
|
||||
* found, but invalid.
|
||||
* @throws CancelledException If the user cancels decryption
|
||||
*/
|
||||
private CartV1Decryptor createDecryptor(String proposedArc4Key)
|
||||
throws IOException, CartInvalidCartException, CartInvalidARC4KeyException,
|
||||
CartConfigurationException, CancelledException {
|
||||
|
||||
// Pad/truncate to the proper length, then test as-is, if it works return the decryptor
|
||||
// based on it.
|
||||
try {
|
||||
byte[] arc4Key =
|
||||
Arrays.copyOf(proposedArc4Key.getBytes(), CartV1Constants.ARC4_KEY_LENGTH);
|
||||
return createDecryptor(arc4Key);
|
||||
}
|
||||
catch (CartInvalidARC4KeyException e) {
|
||||
// ignore
|
||||
}
|
||||
|
||||
// *Very* basic base-64 checks before attempting
|
||||
if (proposedArc4Key.length() >= 4 && proposedArc4Key.length() <= 20 &&
|
||||
proposedArc4Key.length() % 4 == 0) {
|
||||
try {
|
||||
byte[] b64key = Base64.getDecoder().decode(proposedArc4Key);
|
||||
|
||||
// If the proposed key is valid base64 encoding, pad/truncate it to the
|
||||
// proper length
|
||||
if (b64key.length != CartV1Constants.ARC4_KEY_LENGTH) {
|
||||
b64key = Arrays.copyOf(b64key, CartV1Constants.ARC4_KEY_LENGTH);
|
||||
}
|
||||
|
||||
return createDecryptor(b64key);
|
||||
}
|
||||
catch (IllegalArgumentException | CartInvalidARC4KeyException e) {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
|
||||
// If a valid key hasn't been determined, raise the appropriate exception
|
||||
throw new CartInvalidARC4KeyException("Private CaRT ARC4 key could not be determined.");
|
||||
}
|
||||
|
||||
/**
|
||||
* Create the CartV1Decryptor object using the specified key. Tested padded/truncated
|
||||
* to the correct length.
|
||||
*
|
||||
* @param proposedArc4Key The ARC4 key being proposed for this CaRT
|
||||
* @return The CartV1Decryptor object
|
||||
* @throws IOException If there is a read failure of the data
|
||||
* @throws CartInvalidCartException If there was a formating error with the
|
||||
* CaRT file
|
||||
* @throws CartInvalidARC4KeyException If no key could be determined
|
||||
* @throws CartConfigurationException If the configuration data for CaRT was
|
||||
* found, but invalid.
|
||||
* @throws CancelledException If the user cancels decryption
|
||||
*/
|
||||
private CartV1Decryptor createDecryptor(byte[] proposedArc4Key)
|
||||
throws IOException, CartInvalidCartException, CartInvalidARC4KeyException,
|
||||
CartConfigurationException, CancelledException {
|
||||
|
||||
// Pad/truncate to the proper length if necessary
|
||||
if (proposedArc4Key.length != CartV1Constants.ARC4_KEY_LENGTH) {
|
||||
proposedArc4Key = Arrays.copyOf(proposedArc4Key, CartV1Constants.ARC4_KEY_LENGTH);
|
||||
}
|
||||
|
||||
if (testKey(proposedArc4Key)) {
|
||||
decryptor = new CartV1Decryptor(proposedArc4Key);
|
||||
return decryptor;
|
||||
}
|
||||
|
||||
// If a valid key hasn't been determined, raise the appropriate exception
|
||||
throw new CartInvalidARC4KeyException("Private CaRT ARC4 key could not be determined.");
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function to test a given key on the current CaRT data. Currently, this
|
||||
* only uses the payload extractor test, but in the future should also test that
|
||||
* the optional header and optional footer decrypt properly, if available.
|
||||
*
|
||||
* @param potentialARC4key The potential ARC4 key
|
||||
* @return True if decryption was successful, false otherwise
|
||||
* @throws IOException If there was a failure to read the data
|
||||
* @throws CartInvalidCartException If there was a formating error with the
|
||||
* CaRT file
|
||||
* @throws CartInvalidARC4KeyException If there was another key error
|
||||
* @throws CancelledException If the user cancels decryption
|
||||
*/
|
||||
private boolean testKey(byte[] potentialARC4key) throws IOException, CartInvalidCartException,
|
||||
CartInvalidARC4KeyException, CancelledException {
|
||||
return CartV1PayloadExtractor.testExtraction(this.internalReader, this, potentialARC4key);
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempt to get a decryption key from the standard CaRT configuration file
|
||||
* (~/.cart/cart.cfg).
|
||||
*
|
||||
* @return The key from the configuration file, if found; otherwise, null.
|
||||
* @throws IOException If there was a failure to read data.
|
||||
* @throws CartConfigurationException If the configuration data for CaRT was
|
||||
* found, but invalid.
|
||||
*/
|
||||
private byte[] getIniKey() throws IOException, CartConfigurationException {
|
||||
String configFileName = System.getProperty("user.home") + "/.cart/cart.cfg";
|
||||
byte[] validKey = null;
|
||||
|
||||
try (BufferedReader configReader = new BufferedReader(new FileReader(configFileName))) {
|
||||
// Look for an ini format line of rc4_key = <base64>
|
||||
// account for variable whitespace and allow for a ;-delimited comment at the
|
||||
// end
|
||||
// Note: While this should probably be arc4_key, it must be rc4_key to match
|
||||
// CaRT module.
|
||||
Pattern pattern = Pattern.compile("^\\s*rc4_key\\s*=\\s*(([^\\s]{4}){1,5})\\s*(;.*)?$",
|
||||
Pattern.CASE_INSENSITIVE);
|
||||
String line;
|
||||
|
||||
while ((line = configReader.readLine()) != null) {
|
||||
Matcher matcher = pattern.matcher(line);
|
||||
|
||||
if (matcher.find()) {
|
||||
try {
|
||||
byte[] potentialARC4Key = Base64.getDecoder().decode(matcher.group(1));
|
||||
|
||||
// Create a copy padded up to the key size; or truncated at the key size
|
||||
validKey = Arrays.copyOf(potentialARC4Key, CartV1Constants.ARC4_KEY_LENGTH);
|
||||
|
||||
break;
|
||||
}
|
||||
catch (IllegalArgumentException e) {
|
||||
throw new CartConfigurationException(
|
||||
"rc4_key in " + configFileName + " is not valid base64-encoded data.");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return validKey;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the name of the CaRT container file.
|
||||
*
|
||||
* @return The name of the CaRT container file.
|
||||
*/
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the name of the CaRT payload file.
|
||||
*
|
||||
* @return The name of the CaRT payload file.
|
||||
*/
|
||||
public String getPath() {
|
||||
return path;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the offset to where the payload data in the CaRT file starts.
|
||||
*
|
||||
* @return The offset to the payload data.
|
||||
*/
|
||||
public long getDataOffset() {
|
||||
return dataOffset;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the original size of the original payload (without compression and
|
||||
* encryption).
|
||||
*
|
||||
* @return The original size of the payload
|
||||
*/
|
||||
public long getDataSize() {
|
||||
return payloadOriginalSize;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the packed size of the payload (compressed and encrypted).
|
||||
*
|
||||
* @return The packed size of the payload
|
||||
*/
|
||||
public long getPackedSize() {
|
||||
return packedSize;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the value for the specified hash as stored in the footer or null if not
|
||||
* available. This value will not be tested for validity until the payload is
|
||||
* extracted.
|
||||
* <p>
|
||||
* <b>Note:</b> Only hash names specified in CartV1Constants.expectedHash (keys)
|
||||
* are supported.
|
||||
*
|
||||
* @param hashName The name of the hash to be read
|
||||
* @return The hash value bytes or null
|
||||
*/
|
||||
public byte[] getFooterHash(String hashName) {
|
||||
if (footerHashes.containsKey(hashName)) {
|
||||
return footerHashes.get(hashName);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the header object.
|
||||
*
|
||||
* @return The CartV1Header object.
|
||||
*/
|
||||
public CartV1Header getHeader() {
|
||||
return header;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the footer object.
|
||||
*
|
||||
* @return The CartV1Footer object.
|
||||
*/
|
||||
public CartV1Footer getFooter() {
|
||||
return footer;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the decryptor for the CaRT payload.
|
||||
*
|
||||
* @return The CartV1Decrypter object
|
||||
*/
|
||||
public CartV1Decryptor getDecryptor() {
|
||||
return decryptor;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the combined optional header and optional footer data as a single
|
||||
* combined JSON object. This is similar to how the original python CaRT library
|
||||
* works. Including, the fact that footer values will overwrite header values
|
||||
* when keys collide. An empty JSON object will be returned if neither the
|
||||
* optional header nor footer is available.
|
||||
*
|
||||
* @return The metadata JSON object.
|
||||
*/
|
||||
public JsonObject getMetadata() {
|
||||
JsonObject metadata = header.optionalHeaderData();
|
||||
|
||||
// If we weren't able to get the optional header data, create a new empty JSON
|
||||
// object
|
||||
if (metadata == null) {
|
||||
metadata = new JsonObject();
|
||||
}
|
||||
|
||||
JsonObject optionalFooterData = null;
|
||||
|
||||
try {
|
||||
optionalFooterData = footer.optionalFooterData();
|
||||
}
|
||||
catch (CartInvalidCartException e) {
|
||||
// ignore
|
||||
}
|
||||
|
||||
// If we were able to get the optional footer data then merge it's data with
|
||||
// whatever metadata is already available.
|
||||
if (optionalFooterData != null) {
|
||||
for (Entry<String, JsonElement> entry : optionalFooterData.entrySet()) {
|
||||
// This will overwrite any existing header values when the footer contains
|
||||
// a conflicting key. This is the same behavior as the python library.
|
||||
metadata.add(entry.getKey(), entry.getValue());
|
||||
}
|
||||
}
|
||||
|
||||
return metadata;
|
||||
}
|
||||
|
||||
/**
|
||||
* Static function to quickly determine if the data in the byte provider appears
|
||||
* to be CaRT version 1 format.
|
||||
*
|
||||
* @param byteProvider The byte provider from which to read
|
||||
* @return True if CaRT version 1 data; otherwise, false.
|
||||
*/
|
||||
public static boolean isCart(ByteProvider byteProvider) {
|
||||
return isCart(new BinaryReader(byteProvider, true));
|
||||
}
|
||||
|
||||
/**
|
||||
* Static function to quickly determine if the data in the little-endian binary
|
||||
* reader appears to be CaRT version 1 format.
|
||||
*
|
||||
* @param reader The little-endian binary reader from with to read
|
||||
* @return True if CaRT version 1 data; otherwise, false.
|
||||
*/
|
||||
public static boolean isCart(BinaryReader reader) {
|
||||
return hasCartHeader(reader) && hasCartFooter(reader);
|
||||
}
|
||||
|
||||
/**
|
||||
* Static function to quickly determine if the data in the byte provider appears
|
||||
* to have a CaRT version 1 format header.
|
||||
*
|
||||
* @param byteProvider The byte provider from which to read
|
||||
* @return True if CaRT version 1 header; otherwise, false.
|
||||
*/
|
||||
public static boolean hasCartHeader(ByteProvider byteProvider) {
|
||||
return hasCartHeader(new BinaryReader(byteProvider, true));
|
||||
}
|
||||
|
||||
/**
|
||||
* Static function to quickly determine if the data in the little-endian binary
|
||||
* reader appears to have a CaRT version 1 format header.
|
||||
*
|
||||
* @param reader The little-endian binary reader from with to read
|
||||
* @return True if CaRT version 1 header; otherwise, false.
|
||||
*/
|
||||
public static boolean hasCartHeader(BinaryReader reader) {
|
||||
try {
|
||||
@SuppressWarnings("unused")
|
||||
CartV1Header header = new CartV1Header(reader);
|
||||
return true;
|
||||
}
|
||||
catch (IOException | CartInvalidCartException e) {
|
||||
// ignore
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Static function to quickly determine if the data in the byte provider appears
|
||||
* to have a CaRT version 1 format footer.
|
||||
*
|
||||
* @param byteProvider The byte provider from which to read
|
||||
* @return True if CaRT version 1 footer; otherwise, false.
|
||||
*/
|
||||
public static boolean hasCartFooter(ByteProvider byteProvider) {
|
||||
return hasCartFooter(new BinaryReader(byteProvider, true));
|
||||
}
|
||||
|
||||
/**
|
||||
* Static function to quickly determine if the data in the little-endian binary
|
||||
* reader appears to have a CaRT version 1 format footer.
|
||||
*
|
||||
* @param reader The little-endian binary reader from with to read
|
||||
* @return True if CaRT version 1 footer; otherwise, false.
|
||||
*/
|
||||
public static boolean hasCartFooter(BinaryReader reader) {
|
||||
try {
|
||||
@SuppressWarnings("unused")
|
||||
CartV1Footer footer = new CartV1Footer(reader);
|
||||
return true;
|
||||
}
|
||||
catch (IOException | CartInvalidCartException e) {
|
||||
// ignore
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,219 @@
|
|||
/* ###
|
||||
* IP: GHIDRA
|
||||
*
|
||||
* 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 ghidra.file.formats.cart;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import com.google.gson.*;
|
||||
|
||||
import ghidra.app.util.bin.BinaryReader;
|
||||
import ghidra.app.util.bin.ByteProvider;
|
||||
|
||||
/**
|
||||
* Class to manage reading and access to a CaRT version 1 file footer.
|
||||
*/
|
||||
public class CartV1Footer {
|
||||
// Mandatory footer values
|
||||
private String magic;
|
||||
private long reserved = -1;
|
||||
private long optionalFooterPosition = -1;
|
||||
private long optionalFooterLength = -1;
|
||||
|
||||
// Optional footer data
|
||||
private JsonObject optionalFooterData;
|
||||
|
||||
// Internal helper storage
|
||||
private long footerPosition = -1;
|
||||
private long readerLength = -1;
|
||||
private BinaryReader internalReader;
|
||||
|
||||
/**
|
||||
* Constructs a new CartV1Footer read from the byte provider.
|
||||
*
|
||||
* @param byteProvider The byte provider from which to read
|
||||
* @throws IOException If there was a problem reading from the byte
|
||||
* provider
|
||||
* @throws CartInvalidCartException If there was a formating error with the CaRT
|
||||
* footer
|
||||
*/
|
||||
public CartV1Footer(ByteProvider byteProvider) throws IOException, CartInvalidCartException {
|
||||
this(new BinaryReader(byteProvider, true));
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs a new CartV1Footer, read from the little-endian binary reader.
|
||||
*
|
||||
* @param reader The little-endian binary reader from with to read
|
||||
* @throws IOException If there was a problem reading from the
|
||||
* binary reader
|
||||
* @throws CartInvalidCartException If there was a formating error with the CaRT
|
||||
* footer
|
||||
*/
|
||||
public CartV1Footer(BinaryReader reader) throws IOException, CartInvalidCartException {
|
||||
// Check that binary reader is Little-Endian
|
||||
if (!reader.isLittleEndian()) {
|
||||
throw new IOException("CaRT BinaryReader must be Little-Endian.");
|
||||
}
|
||||
|
||||
readerLength = reader.length();
|
||||
|
||||
// Check that there are at least enough bytes to contain a footer
|
||||
if (readerLength < CartV1Constants.FOOTER_LENGTH) {
|
||||
throw new CartInvalidCartException("Data too small to contain CaRT footer.");
|
||||
}
|
||||
|
||||
// Calculate and verify footer position
|
||||
footerPosition = readerLength - CartV1Constants.FOOTER_LENGTH;
|
||||
if (footerPosition < 0 || footerPosition > readerLength) {
|
||||
throw new CartInvalidCartException("Invalid CaRT footer position.");
|
||||
}
|
||||
|
||||
// Clone the existing reader, but set position to the start of the mandatory
|
||||
// footer
|
||||
internalReader = reader.clone(footerPosition);
|
||||
|
||||
// Read and verify footer magic value
|
||||
magic = internalReader.readNextAsciiString(CartV1Constants.FOOTER_MAGIC.length());
|
||||
if (!magic.equals(CartV1Constants.FOOTER_MAGIC)) {
|
||||
throw new CartInvalidCartException("Invalid CaRT footer magic value.");
|
||||
}
|
||||
|
||||
// Read and verify footer reserved value
|
||||
reserved = internalReader.readNextLong();
|
||||
if (reserved != CartV1Constants.FOOTER_RESERVED) {
|
||||
throw new CartInvalidCartException("Invalid CaRT footer reserved value.");
|
||||
}
|
||||
|
||||
// Read and verify optional footer position
|
||||
optionalFooterPosition = internalReader.readNextLong();
|
||||
if (optionalFooterPosition < 0 || optionalFooterPosition > footerPosition ||
|
||||
optionalFooterPosition > readerLength) {
|
||||
throw new CartInvalidCartException("Invalid CaRT optional footer position.");
|
||||
}
|
||||
|
||||
// Read and verify optional footer length
|
||||
optionalFooterLength = internalReader.readNextLong();
|
||||
if (optionalFooterLength < 0 ||
|
||||
(optionalFooterPosition + optionalFooterLength) != footerPosition ||
|
||||
(optionalFooterPosition + optionalFooterLength) > readerLength) {
|
||||
throw new CartInvalidCartException("Invalid CaRT optional footer length.");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the magic value read from the footer.
|
||||
*
|
||||
* @return The magic value read from the footer.
|
||||
* @throws CartInvalidCartException If the footer object is not valid.
|
||||
*/
|
||||
protected String magic() throws CartInvalidCartException {
|
||||
return magic;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the position of the optional footer. Should be 0 if no optional footer is
|
||||
* available.
|
||||
*
|
||||
* @return The position of the optional footer, 0 if no optional footer.
|
||||
* @throws CartInvalidCartException If the footer object is not valid.
|
||||
*/
|
||||
protected long optionalFooterPosition() throws CartInvalidCartException {
|
||||
return optionalFooterPosition;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the length of the optional footer. Should be 0 if no optional footer is
|
||||
* available.
|
||||
*
|
||||
* @return The length of the optional footer, 0 if no optional footer.
|
||||
* @throws CartInvalidCartException If the footer object is not valid.
|
||||
*/
|
||||
protected long optionalFooterLength() throws CartInvalidCartException {
|
||||
return optionalFooterLength;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a copy of the optional footer data.
|
||||
*
|
||||
* @return A copy of the JsonObject optional footer data or null if unavailable.
|
||||
* @throws CartInvalidCartException If the footer object is not valid.
|
||||
*/
|
||||
protected JsonObject optionalFooterData() throws CartInvalidCartException {
|
||||
JsonObject optionalFooterDataCopy;
|
||||
|
||||
if (optionalFooterData != null) {
|
||||
optionalFooterDataCopy = optionalFooterData.deepCopy();
|
||||
}
|
||||
else {
|
||||
optionalFooterDataCopy = null;
|
||||
}
|
||||
|
||||
return optionalFooterDataCopy;
|
||||
}
|
||||
|
||||
/**
|
||||
* Read and decrypt optional footer data and return a copy.
|
||||
*
|
||||
* @param decryptor An initialize decryptor with the correct ARC4 key
|
||||
* @return JsonObject read and decrypted from the footer. Will be empty if no
|
||||
* optional footer is available.
|
||||
* @throws CartInvalidCartException If the footer object is not valid.
|
||||
* @throws CartInvalidARC4KeyException If the decryption of JSON deserialization
|
||||
* fails.
|
||||
* @throws IOException If there is a failure to read the footer data.
|
||||
*/
|
||||
public JsonObject loadOptionalFooter(CartV1Decryptor decryptor)
|
||||
throws CartInvalidCartException, CartInvalidARC4KeyException, IOException {
|
||||
byte[] encryptedOptionalFooter = null;
|
||||
|
||||
if (optionalFooterLength > 0 &&
|
||||
(optionalFooterPosition + optionalFooterLength) == footerPosition &&
|
||||
(optionalFooterPosition + optionalFooterLength) < readerLength) {
|
||||
try {
|
||||
encryptedOptionalFooter = internalReader.readByteArray(optionalFooterPosition,
|
||||
(int) optionalFooterLength);
|
||||
}
|
||||
catch (IOException e) {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
|
||||
if (encryptedOptionalFooter != null) {
|
||||
String decryptedOptionalFooter = decryptor.decryptToString(encryptedOptionalFooter);
|
||||
|
||||
if (decryptedOptionalFooter == null) {
|
||||
throw new CartInvalidARC4KeyException("CaRT optional footer decryption failed.");
|
||||
}
|
||||
|
||||
try {
|
||||
optionalFooterData =
|
||||
JsonParser.parseString(decryptedOptionalFooter).getAsJsonObject();
|
||||
}
|
||||
catch (IllegalStateException | JsonSyntaxException e) {
|
||||
throw new CartInvalidARC4KeyException(
|
||||
"CaRT decrypted optional footer not valid JSON.");
|
||||
}
|
||||
}
|
||||
|
||||
// If there was no encrypted optional footer or parsing failed without an exception and
|
||||
// parseString returns `null` then set to empty JSON object
|
||||
if (optionalFooterData == null) {
|
||||
optionalFooterData = new JsonObject();
|
||||
}
|
||||
|
||||
return optionalFooterData();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,247 @@
|
|||
/* ###
|
||||
* IP: GHIDRA
|
||||
*
|
||||
* 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 ghidra.file.formats.cart;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import com.google.gson.*;
|
||||
|
||||
import ghidra.app.util.bin.BinaryReader;
|
||||
import ghidra.app.util.bin.ByteProvider;
|
||||
|
||||
/**
|
||||
* Class to manage reading and access to a CaRT version 1 file header.
|
||||
*/
|
||||
public final class CartV1Header {
|
||||
// Mandatory header values
|
||||
private String magic;
|
||||
private short version = -1;
|
||||
private long reserved = -1;
|
||||
private byte[] arc4Key;
|
||||
private long optionalHeaderLength = -1;
|
||||
|
||||
// Optional header data
|
||||
private JsonObject optionalHeaderData;
|
||||
|
||||
// Internal helper storage
|
||||
private long readerLength = -1;
|
||||
private BinaryReader internalReader;
|
||||
|
||||
/**
|
||||
* Constructs a new CartV1Header read from the byte provider.
|
||||
*
|
||||
* @param byteProvider The byte provider from which to read
|
||||
* @throws IOException If there was a problem reading from the byte
|
||||
* provider
|
||||
* @throws CartInvalidCartException If there was a formating error with the CaRT
|
||||
* header
|
||||
*/
|
||||
public CartV1Header(ByteProvider byteProvider) throws IOException, CartInvalidCartException {
|
||||
this(new BinaryReader(byteProvider, true));
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs a new CartV1Header, read from the little-endian binary reader.
|
||||
*
|
||||
* @param reader The little-endian binary reader from with to read
|
||||
* @throws IOException If there was a problem reading from the
|
||||
* binary reader
|
||||
* @throws CartInvalidCartException If there was a formating error with the CaRT
|
||||
* header
|
||||
*/
|
||||
public CartV1Header(BinaryReader reader) throws IOException, CartInvalidCartException {
|
||||
// Check that binary reader is Little-Endian
|
||||
if (!reader.isLittleEndian()) {
|
||||
throw new IOException("CaRT BinaryReader must be Little-Endian.");
|
||||
}
|
||||
|
||||
readerLength = reader.length();
|
||||
|
||||
// Check that there are at least enough bytes to contain a header
|
||||
if (readerLength < CartV1Constants.HEADER_LENGTH) {
|
||||
throw new CartInvalidCartException("Data too small to contain CaRT header.");
|
||||
}
|
||||
|
||||
// Clone the existing reader, but set position to the start of the header
|
||||
// (beginning of file, offset 0)
|
||||
internalReader = reader.clone(0);
|
||||
|
||||
// Read and verify header magic value
|
||||
magic = internalReader.readNextAsciiString(CartV1Constants.HEADER_MAGIC.length());
|
||||
if (!magic.equals(CartV1Constants.HEADER_MAGIC)) {
|
||||
throw new CartInvalidCartException("Invalid CaRT header magic value.");
|
||||
}
|
||||
|
||||
// Read and verify CaRT version number
|
||||
version = internalReader.readNextShort();
|
||||
if (version != CartV1Constants.HEADER_VERSION) {
|
||||
throw new CartInvalidCartException("Invalid CaRT header version number.");
|
||||
}
|
||||
|
||||
// Read and verify header reserved value
|
||||
reserved = internalReader.readNextLong();
|
||||
if (reserved != CartV1Constants.HEADER_RESERVED) {
|
||||
throw new CartInvalidCartException("Invalid CaRT header reserved value.");
|
||||
}
|
||||
|
||||
// Read ARC4 key, this may be an arbitrary value -- no verification until
|
||||
// decryption is attempted
|
||||
arc4Key = internalReader.readNextByteArray(CartV1Constants.ARC4_KEY_LENGTH);
|
||||
|
||||
// Read and verify optional header length
|
||||
optionalHeaderLength = internalReader.readNextLong();
|
||||
if (optionalHeaderLength < 0 || optionalHeaderLength > (readerLength -
|
||||
CartV1Constants.HEADER_LENGTH - CartV1Constants.FOOTER_LENGTH)) {
|
||||
throw new CartInvalidCartException("Invalid CaRT optional header length.");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the magic value read from the header.
|
||||
*
|
||||
* @return The magic value read from the header.
|
||||
*/
|
||||
protected String magic() {
|
||||
return magic;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the version of the CaRT file.
|
||||
*
|
||||
* @return The version of the CaRT file.
|
||||
*/
|
||||
protected short version() {
|
||||
return version;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a copy of the ARC4 key read from the header.
|
||||
*
|
||||
* @return A copy of the ARC4 key read from the header.
|
||||
* @throws CartInvalidCartException If the header object is not valid.
|
||||
*/
|
||||
protected byte[] arc4Key() throws CartInvalidCartException {
|
||||
byte[] arc4KeyCopy = new byte[CartV1Constants.ARC4_KEY_LENGTH];
|
||||
|
||||
if (this.arc4Key != null) {
|
||||
System.arraycopy(this.arc4Key, 0, arc4KeyCopy, 0, CartV1Constants.ARC4_KEY_LENGTH);
|
||||
}
|
||||
else {
|
||||
throw new CartInvalidCartException("No ARC4 key available for CaRT.");
|
||||
}
|
||||
|
||||
return arc4KeyCopy;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the location where data starts within the CaRT file, accounting for the
|
||||
* header and any optional header data.
|
||||
*
|
||||
* @return The starting offset to the payload data.
|
||||
*/
|
||||
protected long dataStart() {
|
||||
return CartV1Constants.HEADER_LENGTH + optionalHeaderLength;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the length of the optional header. Should be 0 if no optional header is
|
||||
* available.
|
||||
*
|
||||
* @return The length of the optional header, 0 if no optional header.
|
||||
*/
|
||||
protected long optionalHeaderLength() {
|
||||
return optionalHeaderLength;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a copy of the optional header data.
|
||||
*
|
||||
* @return A copy of the JsonObject optional header data or null if unavailable.
|
||||
*/
|
||||
protected JsonObject optionalHeaderData() {
|
||||
JsonObject optionalHeaderDataCopy;
|
||||
|
||||
if (optionalHeaderData != null) {
|
||||
optionalHeaderDataCopy = optionalHeaderData.deepCopy();
|
||||
}
|
||||
else {
|
||||
optionalHeaderDataCopy = null;
|
||||
}
|
||||
|
||||
return optionalHeaderDataCopy;
|
||||
}
|
||||
|
||||
/**
|
||||
* Read and decrypt optional header data and return a copy.
|
||||
*
|
||||
* @param decryptor An initialize decryptor with the correct ARC4 key
|
||||
* @return JsonObject read and decrypted from the header. Will be empty if no
|
||||
* optional header is available.
|
||||
* @throws CartInvalidARC4KeyException If the decryption of JSON deserialization
|
||||
* fails.
|
||||
* @throws IOException If there is a failure to read the header data.
|
||||
*/
|
||||
public JsonObject loadOptionalHeader(CartV1Decryptor decryptor)
|
||||
throws CartInvalidARC4KeyException, IOException {
|
||||
byte[] encryptedOptionalHeader = null;
|
||||
|
||||
if (optionalHeaderLength > 0 && optionalHeaderLength <= (readerLength -
|
||||
CartV1Constants.HEADER_LENGTH - CartV1Constants.FOOTER_LENGTH)) {
|
||||
try {
|
||||
encryptedOptionalHeader = internalReader
|
||||
.readByteArray(CartV1Constants.HEADER_LENGTH, (int) optionalHeaderLength);
|
||||
}
|
||||
catch (IOException e) {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
|
||||
if (encryptedOptionalHeader == null) {
|
||||
/**
|
||||
* Initialize the optionalHeaderData as an empty JSON object when the optional
|
||||
* header length or position is invalid, when the length is 0 which indicates
|
||||
* that there is no optional header, or when reading the data failed.
|
||||
*/
|
||||
optionalHeaderData = new JsonObject();
|
||||
}
|
||||
else {
|
||||
|
||||
String decryptedOptionalHeader = decryptor.decryptToString(encryptedOptionalHeader);
|
||||
|
||||
if (decryptedOptionalHeader != null) {
|
||||
try {
|
||||
optionalHeaderData =
|
||||
JsonParser.parseString(decryptedOptionalHeader).getAsJsonObject();
|
||||
}
|
||||
catch (IllegalStateException | JsonSyntaxException e) {
|
||||
throw new CartInvalidARC4KeyException(
|
||||
"CaRT decrypted optional header not valid JSON.");
|
||||
}
|
||||
|
||||
// If parsing failed without an exception and parseString returns `null`,
|
||||
// set to empty JSON object
|
||||
if (optionalHeaderData == null) {
|
||||
optionalHeaderData = new JsonObject();
|
||||
}
|
||||
}
|
||||
else {
|
||||
throw new CartInvalidARC4KeyException("CaRT optional header decryption failed.");
|
||||
}
|
||||
}
|
||||
|
||||
return optionalHeaderData();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,168 @@
|
|||
/* ###
|
||||
* IP: GHIDRA
|
||||
*
|
||||
* 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 ghidra.file.formats.cart;
|
||||
|
||||
import java.io.*;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.util.Arrays;
|
||||
|
||||
import ghidra.app.util.bin.*;
|
||||
import ghidra.formats.gfilesystem.FSUtilities;
|
||||
import ghidra.util.exception.CancelledException;
|
||||
import ghidra.util.task.TaskMonitor;
|
||||
|
||||
/**
|
||||
* Class to implement the payload extractor to decrypt and decompress the
|
||||
* payload contents. Provides output stream content of decrypted and decompressed file data.
|
||||
*/
|
||||
public class CartV1PayloadExtractor {
|
||||
/**
|
||||
* Internal little-endian {@link BinaryReader} object for access to original file bytes.
|
||||
*/
|
||||
private BinaryReader reader;
|
||||
|
||||
/**
|
||||
* Internal {@link OutputStream} that will received the payload bytes.
|
||||
*/
|
||||
private OutputStream tempFos;
|
||||
|
||||
/**
|
||||
* {@link CartV1File} object from which to pull all relevant metadata for payload extraction.
|
||||
*/
|
||||
private CartV1File cartFile;
|
||||
|
||||
/**
|
||||
* Create the payload extractor for the specified byte provider that will output
|
||||
* to the specified output stream.
|
||||
*
|
||||
* @param byteProvider The byte provider from which to read
|
||||
* @param os The output stream to write to
|
||||
* @param cartFile The CaRT file being read
|
||||
* @throws IOException If there is a failure to read the data.
|
||||
*/
|
||||
public CartV1PayloadExtractor(ByteProvider byteProvider, OutputStream os, CartV1File cartFile)
|
||||
throws IOException {
|
||||
this(new BinaryReader(byteProvider, true), os, cartFile);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create the payload extractor for the specified little-endian binary reader
|
||||
* that will output to the specified output stream.
|
||||
*
|
||||
* @param reader The little-endian binary reader from with to read
|
||||
* @param os The output stream to write to
|
||||
* @param cartFile The CaRT file being read
|
||||
* @throws IOException If there is a failure to read the data.
|
||||
*/
|
||||
public CartV1PayloadExtractor(BinaryReader reader, OutputStream os, CartV1File cartFile)
|
||||
throws IOException {
|
||||
// Check that binary reader is Little-Endian
|
||||
if (!reader.isLittleEndian()) {
|
||||
throw new IOException("CaRT BinaryReader must be Little-Endian.");
|
||||
}
|
||||
this.reader = reader;
|
||||
this.tempFos = os;
|
||||
this.cartFile = cartFile;
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback to perform actual extraction of CaRT payload data to the output
|
||||
* stream.
|
||||
*
|
||||
* @param monitor Monitor for status messages and cancellation
|
||||
* @throws CancelledException If the extraction is cancelled
|
||||
* @throws IOException If an error occurs with the data, CaRT format, or
|
||||
* hashes.
|
||||
*/
|
||||
public void extract(TaskMonitor monitor) throws CancelledException, IOException {
|
||||
|
||||
monitor.setMessage("Reading CaRT data");
|
||||
|
||||
ByteProviderWrapper subProvider = new ByteProviderWrapper(reader.getByteProvider(),
|
||||
cartFile.getDataOffset(), cartFile.getPackedSize());
|
||||
InputStream is = subProvider.getInputStream(0);
|
||||
CartV1StreamHasher hashedInputStream = null;
|
||||
try {
|
||||
is = new CartV1StreamDecryptor(is, cartFile.getDecryptor().getARC4Key());
|
||||
is = new CartV1StreamDecompressor(is);
|
||||
|
||||
hashedInputStream = new CartV1StreamHasher(is, cartFile);
|
||||
}
|
||||
catch (NoSuchAlgorithmException | CartInvalidCartException e) {
|
||||
throw new IOException(e);
|
||||
}
|
||||
|
||||
FSUtilities.streamCopy(hashedInputStream, tempFos, monitor);
|
||||
|
||||
monitor.setMessage("Checking hashes...");
|
||||
try {
|
||||
hashedInputStream.checkHashes();
|
||||
}
|
||||
catch (CartInvalidCartException e) {
|
||||
throw new IOException("Invalid CaRT ARC4 hashes: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Test that the first two bytes of the CaRT payload data decrypts to a valid
|
||||
* ZLIB prefix.
|
||||
*
|
||||
* @param reader The little-endian binary reader from with to read
|
||||
* @param cartFile The CaRT file being read
|
||||
* @param arc4Key The ARC4 key being tested.
|
||||
* @return True if decryption of the first 64 bytes succeeds and the first two
|
||||
* bytes match zlib header bytes.
|
||||
* @throws IOException If there is a failure to read the data.
|
||||
* @throws CartInvalidCartException If there is a format error to the data
|
||||
* @throws CartInvalidARC4KeyException If the decryption fails.
|
||||
* @throws CancelledException If the user cancels the UI. For this function the UI
|
||||
* isn't shown so this isn't expected to happen.
|
||||
*/
|
||||
public static boolean testExtraction(BinaryReader reader, CartV1File cartFile, byte[] arc4Key)
|
||||
throws IOException, CartInvalidCartException, CartInvalidARC4KeyException,
|
||||
CancelledException {
|
||||
if (cartFile.getDataOffset() <= 0) {
|
||||
throw new CartInvalidCartException("Bad CaRT payload data offset");
|
||||
}
|
||||
|
||||
int length = (int) (cartFile.getPackedSize());
|
||||
|
||||
// limit the length being used to just the first 64 bytes
|
||||
if (length > 64) {
|
||||
length = 64;
|
||||
}
|
||||
|
||||
byte[] encryptedAndCompressedData = reader.readByteArray(cartFile.getDataOffset(), length);
|
||||
|
||||
byte[] compressedData = CartV1Decryptor.decrypt(arc4Key, encryptedAndCompressedData);
|
||||
|
||||
if (compressedData != null) {
|
||||
// Trim down to the first two bytes for comparison
|
||||
compressedData = Arrays.copyOfRange(compressedData, 0, 2);
|
||||
|
||||
for (byte[] zlibBytes : CartV1Constants.ZLIB_HEADER_BYTES) {
|
||||
// On the first match, return true
|
||||
if (Arrays.equals(compressedData, zlibBytes)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If data didn't decompress or we didn't find matching header bytes then return
|
||||
// false
|
||||
return false;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,124 @@
|
|||
/* ###
|
||||
* IP: GHIDRA
|
||||
*
|
||||
* 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 ghidra.file.formats.cart;
|
||||
|
||||
import java.io.*;
|
||||
import java.util.Arrays;
|
||||
import java.util.zip.DataFormatException;
|
||||
import java.util.zip.Inflater;
|
||||
|
||||
/**
|
||||
* StreamProcessor implementation to decompress the data as it is being read.
|
||||
*
|
||||
* CaRT zlib decompress. Reimplementation from Ghidra's default to not
|
||||
* reinitialize the inflater between calls.
|
||||
*/
|
||||
public class CartV1StreamDecompressor extends CartV1StreamProcessor {
|
||||
private Inflater inflater;
|
||||
|
||||
/**
|
||||
* Construct a stream decompressor.
|
||||
*
|
||||
* @param delegate InputStream to read and apply decompression
|
||||
* @throws CartInvalidCartException If the CaRT data is invalid and/or missing ZLIB header
|
||||
*/
|
||||
public CartV1StreamDecompressor(InputStream delegate) throws CartInvalidCartException {
|
||||
this(new PushbackInputStream(delegate, 2));
|
||||
}
|
||||
|
||||
/**
|
||||
* Construct a stream decompressor.
|
||||
*
|
||||
* @param delegate PushbackInputStream to read and apply decompression
|
||||
* @throws CartInvalidCartException If the CaRT data is invalid and/or missing ZLIB header
|
||||
*/
|
||||
public CartV1StreamDecompressor(PushbackInputStream delegate) throws CartInvalidCartException {
|
||||
super(delegate);
|
||||
|
||||
inflater = new Inflater(false);
|
||||
|
||||
boolean validHeaderBytes = false;
|
||||
byte[] headerBytes = new byte[2];
|
||||
try {
|
||||
delegate.read(headerBytes, 0, 2);
|
||||
|
||||
// Check that the header bytes are valid values
|
||||
for (byte[] zlibBytes : CartV1Constants.ZLIB_HEADER_BYTES) {
|
||||
// On the first match, set true and stop
|
||||
if (Arrays.equals(headerBytes, zlibBytes)) {
|
||||
validHeaderBytes = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
delegate.unread(headerBytes);
|
||||
|
||||
if (!validHeaderBytes) {
|
||||
throw new IOException();
|
||||
}
|
||||
}
|
||||
catch (IOException e) {
|
||||
throw new CartInvalidCartException("CaRT compression format error");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Read the next chunk in the stream and decompress
|
||||
*
|
||||
* @return True if more data is now available, False otherwise.
|
||||
* @throws IOException If there is a read failure of the data
|
||||
*/
|
||||
@Override
|
||||
protected boolean readNextChunk() throws IOException {
|
||||
byte[] compressedBytes = new byte[DEFAULT_BUFFER_SIZE];
|
||||
byte[] decompressedBytes = null;
|
||||
|
||||
// Reset the current chunk and offset
|
||||
currentChunk = null;
|
||||
chunkPos = 0;
|
||||
|
||||
if (inflater == null || inflater.finished()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (inflater.needsInput()) {
|
||||
int bytesRead = delegate.read(compressedBytes);
|
||||
|
||||
if (bytesRead <= 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
inflater.setInput(compressedBytes, 0, bytesRead);
|
||||
}
|
||||
|
||||
try {
|
||||
decompressedBytes = new byte[DEFAULT_BUFFER_SIZE];
|
||||
int bytesDecompressed = inflater.inflate(decompressedBytes);
|
||||
|
||||
if (bytesDecompressed <= 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
currentChunk = Arrays.copyOf(decompressedBytes, bytesDecompressed);
|
||||
}
|
||||
catch (DataFormatException e) {
|
||||
throw new IOException("CaRT decompression failed: " + e.getMessage());
|
||||
}
|
||||
|
||||
return currentChunk != null && currentChunk.length > 0;
|
||||
|
||||
}
|
||||
}
|
|
@ -0,0 +1,100 @@
|
|||
/* ###
|
||||
* IP: GHIDRA
|
||||
*
|
||||
* 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 ghidra.file.formats.cart;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.security.*;
|
||||
import java.util.Arrays;
|
||||
|
||||
import javax.crypto.*;
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
|
||||
/**
|
||||
* StreamProcessor implementation to decrypt the data as it is being read.
|
||||
*/
|
||||
public class CartV1StreamDecryptor extends CartV1StreamProcessor {
|
||||
private Cipher cipher;
|
||||
|
||||
/**
|
||||
* Construct a stream decryptor with the specified key.
|
||||
*
|
||||
* @param delegate InputStream to read and apply decryption
|
||||
* @param arc4Key The ARC4 key to use
|
||||
* @throws CartInvalidARC4KeyException If the key is bad, null or too short.
|
||||
*/
|
||||
public CartV1StreamDecryptor(InputStream delegate, byte[] arc4Key)
|
||||
throws CartInvalidARC4KeyException {
|
||||
super(delegate);
|
||||
|
||||
if (arc4Key == null) {
|
||||
throw new CartInvalidARC4KeyException("Invalid null CaRT key.");
|
||||
}
|
||||
else if (arc4Key.length != CartV1Constants.ARC4_KEY_LENGTH) {
|
||||
arc4Key = Arrays.copyOf(arc4Key, CartV1Constants.ARC4_KEY_LENGTH);
|
||||
}
|
||||
|
||||
Key key = new SecretKeySpec(arc4Key, "ARCFOUR");
|
||||
try {
|
||||
cipher = Cipher.getInstance("ARCFOUR");
|
||||
cipher.init(Cipher.DECRYPT_MODE, key, cipher.getParameters());
|
||||
}
|
||||
catch (NoSuchAlgorithmException | NoSuchPaddingException | InvalidKeyException
|
||||
| InvalidAlgorithmParameterException e) {
|
||||
throw new CartInvalidARC4KeyException("CaRT key error " + e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Read the next chunk in the stream and decrypt
|
||||
*
|
||||
* @return True if more data is now available, False otherwise.
|
||||
* @throws IOException If there is a read failure of the data
|
||||
*/
|
||||
@Override
|
||||
protected boolean readNextChunk() throws IOException {
|
||||
byte[] readBuffer = new byte[DEFAULT_BUFFER_SIZE];
|
||||
|
||||
// Reset the current chunk and offset
|
||||
currentChunk = null;
|
||||
chunkPos = 0;
|
||||
|
||||
if (cipher == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
int bytesRead = delegate.read(readBuffer);
|
||||
|
||||
if (bytesRead <= 0) {
|
||||
// No more bytes available finalize the decryption
|
||||
try {
|
||||
currentChunk = cipher.doFinal();
|
||||
}
|
||||
catch (IllegalBlockSizeException | BadPaddingException e) {
|
||||
throw new IOException(e);
|
||||
}
|
||||
finally {
|
||||
// prevents trying to call doFinal() again
|
||||
cipher = null;
|
||||
}
|
||||
}
|
||||
else {
|
||||
currentChunk = cipher.update(readBuffer, 0, bytesRead);
|
||||
}
|
||||
|
||||
return currentChunk != null && currentChunk.length > 0;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,167 @@
|
|||
/* ###
|
||||
* IP: GHIDRA
|
||||
*
|
||||
* 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 ghidra.file.formats.cart;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.util.*;
|
||||
|
||||
/**
|
||||
* StreamProcessor implementation to hash the data as it is being read and
|
||||
* provide a method to verify hashes upon completion.
|
||||
*/
|
||||
public class CartV1StreamHasher extends CartV1StreamProcessor {
|
||||
private Map<String, MessageDigest> hashers = new LinkedHashMap<>();
|
||||
private Map<String, byte[]> hashes = new LinkedHashMap<>();
|
||||
private Map<String, byte[]> finalHashes = new LinkedHashMap<>();
|
||||
|
||||
/**
|
||||
* Constructor for StreamHasher
|
||||
*
|
||||
* @param delegate InputStream to read and apply hashing
|
||||
* @param cartFile The CaRT file from which it pull the expected hashes
|
||||
* @throws NoSuchAlgorithmException If a hash is not supported by the underlying
|
||||
* MessageDigest implementation.
|
||||
* @throws CartInvalidCartException If the CaRT format is bad
|
||||
*/
|
||||
public CartV1StreamHasher(InputStream delegate, CartV1File cartFile)
|
||||
throws NoSuchAlgorithmException, CartInvalidCartException {
|
||||
super(delegate);
|
||||
|
||||
// List of hashes that are expected, but not present
|
||||
List<String> missingHashes = new ArrayList<>();
|
||||
|
||||
// Iterate across the expected hashes, record which are missing and create hashers for
|
||||
// ones that are present
|
||||
for (Map.Entry<String, String> hashCheck : CartV1Constants.EXPECTED_HASHES.entrySet()) {
|
||||
byte[] footerHashValue = cartFile.getFooterHash(hashCheck.getKey());
|
||||
String hashName = hashCheck.getValue();
|
||||
|
||||
// Check hash, if available
|
||||
if (footerHashValue != null) {
|
||||
hashes.put(hashCheck.getKey(), footerHashValue);
|
||||
hashers.put(hashCheck.getKey(), MessageDigest.getInstance(hashName));
|
||||
}
|
||||
else {
|
||||
missingHashes.add(hashCheck.getKey());
|
||||
}
|
||||
}
|
||||
|
||||
if (!missingHashes.isEmpty()) {
|
||||
if (!CartCancelDialogs.promptErrorContinue("Missing Hashes",
|
||||
"Expected hash(es) missing: " + String.join(", ", missingHashes) +
|
||||
". Continue processing?")) {
|
||||
throw new CartInvalidCartException("Cancelled due to missing hash data (" +
|
||||
String.join(", ", missingHashes) + ").");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Read the next chunk in the stream and update all of the hashes
|
||||
*
|
||||
* @return True if more data is now available, False otherwise.
|
||||
* @throws IOException If there is a read failure of the data
|
||||
*/
|
||||
@Override
|
||||
protected boolean readNextChunk() throws IOException {
|
||||
byte[] readBuffer = new byte[DEFAULT_BUFFER_SIZE];
|
||||
int bytesRead = -1;
|
||||
|
||||
// Reset the current chunk and offset
|
||||
currentChunk = null;
|
||||
chunkPos = 0;
|
||||
|
||||
if (hashers.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
bytesRead = delegate.read(readBuffer);
|
||||
|
||||
if (bytesRead <= 0) {
|
||||
// No more data available, finalize the hashes
|
||||
for (Map.Entry<String, MessageDigest> hashCheck : hashers.entrySet()) {
|
||||
finalHashes.put(hashCheck.getKey(), hashCheck.getValue().digest());
|
||||
}
|
||||
|
||||
// prevents trying to call digest() again
|
||||
hashers.clear();
|
||||
|
||||
// Return false since no more data is available
|
||||
return false;
|
||||
}
|
||||
|
||||
// Update all the hashes with the new data read
|
||||
for (MessageDigest hasher : hashers.values()) {
|
||||
hasher.update(readBuffer, 0, bytesRead);
|
||||
}
|
||||
|
||||
currentChunk = Arrays.copyOf(readBuffer, bytesRead);
|
||||
|
||||
return currentChunk != null && currentChunk.length > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check that hashes of the data read so far and match the values in the footer. Warn
|
||||
* user if any hashes are missing. Throw an exception if any provided hash is
|
||||
* bad.
|
||||
*
|
||||
* @return True if there are no bad hashed, and at least one hash was matched;
|
||||
* otherwise, false.
|
||||
* @throws CartInvalidCartException If one or more hash is bad
|
||||
*/
|
||||
public boolean checkHashes() throws CartInvalidCartException {
|
||||
List<String> verifiedHashes = new ArrayList<>(); // List of hashes that are present and
|
||||
// correct
|
||||
List<String> badHashes = new ArrayList<>(); // List of hashes that are present, but wrong
|
||||
|
||||
// Iterate across the hashers, check that the match. Record which are
|
||||
// bad or verified.
|
||||
for (Map.Entry<String, byte[]> hashCheck : finalHashes.entrySet()) {
|
||||
byte[] footerHashValue = hashes.get(hashCheck.getKey());
|
||||
|
||||
try {
|
||||
if (Arrays.equals(hashCheck.getValue(), footerHashValue)) {
|
||||
verifiedHashes.add(hashCheck.getKey());
|
||||
}
|
||||
else {
|
||||
badHashes.add(hashCheck.getKey());
|
||||
}
|
||||
}
|
||||
catch (IllegalArgumentException e) {
|
||||
badHashes.add(hashCheck.getKey());
|
||||
}
|
||||
}
|
||||
|
||||
if (!badHashes.isEmpty()) {
|
||||
throw new CartInvalidCartException("Hash(es) " + String.join(", ", badHashes) +
|
||||
" in footer doesn't match CaRT data contents.");
|
||||
}
|
||||
|
||||
if (verifiedHashes.isEmpty()) {
|
||||
if (!CartCancelDialogs.promptErrorContinue("No Hashes",
|
||||
"No hash data in CaRT footer metadata. Cannot verify content. Continue processing?")) {
|
||||
throw new CartInvalidCartException("Cancelled due to no hash data.");
|
||||
}
|
||||
}
|
||||
|
||||
// Return true if there are no bad hashed and at least one verified hash;
|
||||
// otherwise false
|
||||
return (badHashes.size() == 0) && (verifiedHashes.size() > 0);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,116 @@
|
|||
/* ###
|
||||
* IP: GHIDRA
|
||||
*
|
||||
* 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 ghidra.file.formats.cart;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
|
||||
/**
|
||||
* Generic buffered InputStream processor base class. Subclasses should
|
||||
* implement per-chunk processing.
|
||||
*/
|
||||
public abstract class CartV1StreamProcessor extends InputStream {
|
||||
/**
|
||||
* Default buffer size to be used for input or output internal buffering
|
||||
*/
|
||||
@SuppressWarnings("unused")
|
||||
protected static final int DEFAULT_BUFFER_SIZE = 1024 * 64;
|
||||
|
||||
/**
|
||||
* Delegate for InputStrem from which to read
|
||||
*/
|
||||
protected InputStream delegate;
|
||||
|
||||
/**
|
||||
* Internal current chunk that has been read and processed and is awaiting
|
||||
* upstream access.
|
||||
*/
|
||||
protected byte[] currentChunk;
|
||||
|
||||
/**
|
||||
* Current position in the current chunk buffer.
|
||||
*/
|
||||
protected int chunkPos;
|
||||
|
||||
/**
|
||||
* Construct a stream processor with the provided delegate.
|
||||
*
|
||||
* @param delegate InputStream to read and apply processing
|
||||
*/
|
||||
public CartV1StreamProcessor(InputStream delegate) {
|
||||
this.delegate = delegate;
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements an InputStream compatible read()
|
||||
*
|
||||
* @return The value of the next byte in the stream
|
||||
* @throws IOException If there is a read failure of the data
|
||||
*/
|
||||
@Override
|
||||
public int read() throws IOException {
|
||||
if (!ensureChunkAvailable()) {
|
||||
return -1;
|
||||
}
|
||||
byte b = currentChunk[chunkPos];
|
||||
chunkPos++;
|
||||
|
||||
return Byte.toUnsignedInt(b);
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements an InputStream compatible read(buffer, offset, length)
|
||||
*
|
||||
* @param b The buffer in which to read the bytes
|
||||
* @param off Offset within the output offer to copy
|
||||
* @param len Length of bytes to read into the output buffer
|
||||
* @return The number of bytes copied to the output buffer
|
||||
* @throws IOException If there is a read failure of the data
|
||||
*/
|
||||
@Override
|
||||
public int read(byte[] b, int off, int len) throws IOException {
|
||||
if (!ensureChunkAvailable()) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
int bytesAvail = currentChunk.length - chunkPos;
|
||||
int bytesToCopy = Math.min(len, bytesAvail);
|
||||
System.arraycopy(currentChunk, chunkPos, b, off, bytesToCopy);
|
||||
chunkPos += bytesToCopy;
|
||||
return bytesToCopy;
|
||||
}
|
||||
|
||||
/**
|
||||
* Close the InputStream delegate.
|
||||
*/
|
||||
@Override
|
||||
public void close() throws IOException {
|
||||
delegate.close();
|
||||
}
|
||||
|
||||
/**
|
||||
* Function implemented by subclasses to do the stream processing of next
|
||||
* chunk in stream.
|
||||
*
|
||||
* @return True if more data is now available, False otherwise.
|
||||
* @throws IOException If there is a read failure of the data
|
||||
*/
|
||||
protected abstract boolean readNextChunk() throws IOException;
|
||||
|
||||
private boolean ensureChunkAvailable() throws IOException {
|
||||
return currentChunk == null || chunkPos >= currentChunk.length ? readNextChunk() : true;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,224 @@
|
|||
/* ###
|
||||
* IP: GHIDRA
|
||||
*
|
||||
* 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 ghidra.file.formats.cart;
|
||||
|
||||
import static org.junit.Assert.*;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
|
||||
public class CartV1DecryptorTest {
|
||||
CartV1Decryptor cartDecryptor;
|
||||
|
||||
@Before
|
||||
public void setupCartV1Decryptor() {
|
||||
try {
|
||||
cartDecryptor = new CartV1Decryptor(CartV1TestConstants.TEST_STD_KEY);
|
||||
}
|
||||
catch (Exception e) {
|
||||
fail("Failed to create CartV1Decryptor with standard test key.");
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCartV1Decryptor() {
|
||||
// If the @Before doesn't assert then this test passes be default
|
||||
return;
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testThrowIfInvalid() throws Exception {
|
||||
cartDecryptor.throwIfInvalid();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testThrowIfInvalidByteArrayPassesWhenValid() throws Exception {
|
||||
cartDecryptor.throwIfInvalid(CartV1TestConstants.TEST_STD_KEY);
|
||||
}
|
||||
|
||||
@Test(expected = CartInvalidARC4KeyException.class)
|
||||
public void testThrowIfInvalidByteArrayThrowsOnNullKey() throws Exception {
|
||||
cartDecryptor.throwIfInvalid(null);
|
||||
fail("CartV1Decryptor should not accept a null key");
|
||||
}
|
||||
|
||||
@Test(expected = CartInvalidARC4KeyException.class)
|
||||
public void testThrowIfInvalidByteArrayThrowsOnShortKey() throws Exception {
|
||||
cartDecryptor.throwIfInvalid(new byte[] { 0x01, 0x02 });
|
||||
fail("CartV1Decryptor should not accept a short key");
|
||||
}
|
||||
|
||||
@Test(expected = CartInvalidARC4KeyException.class)
|
||||
public void testThrowIfInvalidByteArrayThrowsOnLongKey() throws Exception {
|
||||
byte[] longKey = new byte[CartV1Constants.ARC4_KEY_LENGTH + 1];
|
||||
System.arraycopy(CartV1TestConstants.TEST_STD_KEY, 0, longKey, 0,
|
||||
CartV1TestConstants.TEST_STD_KEY.length);
|
||||
|
||||
cartDecryptor.throwIfInvalid(longKey);
|
||||
fail("CartV1Decryptor should not accept a long key");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSetKeyAcceptsStandardKey() throws Exception {
|
||||
cartDecryptor.setKey(CartV1TestConstants.TEST_STD_KEY);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSetKeyAcceptsPrivateKey() throws Exception {
|
||||
cartDecryptor.setKey(CartV1TestConstants.TEST_PRIVATE_KEY);
|
||||
}
|
||||
|
||||
@Test(expected = CartInvalidARC4KeyException.class)
|
||||
public void testSetKeyThrowsOnNullKey() throws Exception {
|
||||
cartDecryptor.setKey(null);
|
||||
fail("CartV1Decryptor should not accept a null key");
|
||||
}
|
||||
|
||||
@Test(expected = CartInvalidARC4KeyException.class)
|
||||
public void testSetKeyThrowsOnShortKey() throws Exception {
|
||||
cartDecryptor.setKey(new byte[] { 0x01, 0x02 });
|
||||
fail("CartV1Decryptor should not accept a short key");
|
||||
}
|
||||
|
||||
@Test(expected = CartInvalidARC4KeyException.class)
|
||||
public void testSetKeyThrowsOnLongKey() throws Exception {
|
||||
byte[] longKey = new byte[CartV1Constants.ARC4_KEY_LENGTH + 1];
|
||||
System.arraycopy(CartV1TestConstants.TEST_STD_KEY, 0, longKey, 0,
|
||||
CartV1TestConstants.TEST_STD_KEY.length);
|
||||
|
||||
cartDecryptor.setKey(longKey);
|
||||
fail("CartV1Decryptor should not accept a long key");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDecryptByteArrayByteArrayDecryptsCorrectKey() throws Exception {
|
||||
byte[] optionalHeader = Arrays.copyOfRange(CartV1TestConstants.TEST_CART_GOOD_STD_KEY,
|
||||
CartV1Constants.HEADER_LENGTH,
|
||||
CartV1Constants.HEADER_LENGTH + (int) CartV1TestConstants.OPTIONAL_HEADER_LENGTH);
|
||||
|
||||
byte[] decryptedOptionalHeader =
|
||||
CartV1Decryptor.decrypt(CartV1TestConstants.TEST_STD_KEY, optionalHeader);
|
||||
|
||||
assertEquals(CartV1TestConstants.OPTIONAL_HEADER_DATA_RAW,
|
||||
new String(decryptedOptionalHeader));
|
||||
}
|
||||
|
||||
@Test(expected = CartInvalidARC4KeyException.class)
|
||||
public void testDecryptByteArrayByteArrayThrowsOnNullKey() throws Exception {
|
||||
// Expected to throw, encrypted bytes don't matter
|
||||
CartV1Decryptor.decrypt(null, new byte[] { 0x01, 0x02 });
|
||||
fail("CartV1Decryptor should not accept a null key");
|
||||
}
|
||||
|
||||
@Test(expected = CartInvalidARC4KeyException.class)
|
||||
public void testDecryptByteArrayByteArrayThrowsOnShortKey() throws Exception {
|
||||
// Expected to throw, encrypted bytes don't matter
|
||||
CartV1Decryptor.decrypt(new byte[] { 0x01, 0x02 }, new byte[] { 0x01, 0x02 });
|
||||
fail("CartV1Decryptor should not accept a short key");
|
||||
}
|
||||
|
||||
@Test(expected = CartInvalidARC4KeyException.class)
|
||||
public void testDecryptByteArrayByteArrayThrowsOnLongKey() throws Exception {
|
||||
byte[] longKey = new byte[CartV1Constants.ARC4_KEY_LENGTH + 1];
|
||||
System.arraycopy(CartV1TestConstants.TEST_STD_KEY, 0, longKey, 0,
|
||||
CartV1TestConstants.TEST_STD_KEY.length);
|
||||
|
||||
// Expected to throw, encrypted bytes don't matter
|
||||
CartV1Decryptor.decrypt(longKey, new byte[] { 0x01, 0x02 });
|
||||
fail("CartV1Decryptor should not accept a long key");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDecryptByteArray() throws Exception {
|
||||
byte[] optionalHeader = Arrays.copyOfRange(CartV1TestConstants.TEST_CART_GOOD_STD_KEY,
|
||||
CartV1Constants.HEADER_LENGTH,
|
||||
CartV1Constants.HEADER_LENGTH + (int) CartV1TestConstants.OPTIONAL_HEADER_LENGTH);
|
||||
|
||||
byte[] decryptedOptionalHeader = cartDecryptor.decrypt(optionalHeader);
|
||||
|
||||
assertEquals(CartV1TestConstants.OPTIONAL_HEADER_DATA_RAW,
|
||||
new String(decryptedOptionalHeader));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDecryptToStringByteArrayByteArrayDecryptsCorrectKey() throws Exception {
|
||||
byte[] optionalHeader = Arrays.copyOfRange(CartV1TestConstants.TEST_CART_GOOD_STD_KEY,
|
||||
CartV1Constants.HEADER_LENGTH,
|
||||
CartV1Constants.HEADER_LENGTH + (int) CartV1TestConstants.OPTIONAL_HEADER_LENGTH);
|
||||
|
||||
String decryptedOptionalHeader =
|
||||
CartV1Decryptor.decryptToString(CartV1TestConstants.TEST_STD_KEY, optionalHeader);
|
||||
|
||||
assertEquals(CartV1TestConstants.OPTIONAL_HEADER_DATA_RAW, decryptedOptionalHeader);
|
||||
}
|
||||
|
||||
@Test(expected = CartInvalidARC4KeyException.class)
|
||||
public void testDecryptToStringByteArrayByteArrayThrowsOnNullKey() throws Exception {
|
||||
// Expected to throw, encrypted bytes don't matter
|
||||
CartV1Decryptor.decryptToString(null, new byte[] { 0x01, 0x02 });
|
||||
fail("CartV1Decryptor should not accept a null key");
|
||||
}
|
||||
|
||||
@Test(expected = CartInvalidARC4KeyException.class)
|
||||
public void testDecryptToStringByteArrayByteArrayThrowsOnShortKey() throws Exception {
|
||||
// Expected to throw, encrypted bytes don't matter
|
||||
CartV1Decryptor.decryptToString(new byte[] { 0x01, 0x02 }, new byte[] { 0x01, 0x02 });
|
||||
fail("CartV1Decryptor should not accept a short key");
|
||||
}
|
||||
|
||||
@Test(expected = CartInvalidARC4KeyException.class)
|
||||
public void testDecryptToStringByteArrayByteArrayThrowsOnLongKey() throws Exception {
|
||||
byte[] longKey = new byte[CartV1Constants.ARC4_KEY_LENGTH + 1];
|
||||
System.arraycopy(CartV1TestConstants.TEST_STD_KEY, 0, longKey, 0,
|
||||
CartV1TestConstants.TEST_STD_KEY.length);
|
||||
|
||||
// Expected to throw, encrypted bytes don't matter
|
||||
CartV1Decryptor.decryptToString(longKey, new byte[] { 0x01, 0x02 });
|
||||
fail("CartV1Decryptor should not accept a long key");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDecryptToStringByteArray() throws Exception {
|
||||
byte[] optionalHeader = Arrays.copyOfRange(CartV1TestConstants.TEST_CART_GOOD_STD_KEY,
|
||||
CartV1Constants.HEADER_LENGTH,
|
||||
CartV1Constants.HEADER_LENGTH + (int) CartV1TestConstants.OPTIONAL_HEADER_LENGTH);
|
||||
|
||||
String decryptedOptionalHeader = cartDecryptor.decryptToString(optionalHeader);
|
||||
|
||||
assertEquals(CartV1TestConstants.OPTIONAL_HEADER_DATA_RAW, decryptedOptionalHeader);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetARC4KeyAcceptsStdTestKey() throws Exception {
|
||||
cartDecryptor.setKey(CartV1TestConstants.TEST_STD_KEY);
|
||||
assertArrayEquals(CartV1TestConstants.TEST_STD_KEY, cartDecryptor.getARC4Key());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetARC4KeyAcceptsPlaceholderKey() throws Exception {
|
||||
cartDecryptor.setKey(CartV1Constants.PRIVATE_ARC4_KEY_PLACEHOLDER);
|
||||
assertArrayEquals(CartV1Constants.PRIVATE_ARC4_KEY_PLACEHOLDER, cartDecryptor.getARC4Key());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetARC4KeyAcceptsDefaultKey() throws Exception {
|
||||
cartDecryptor.setKey(CartV1Constants.DEFAULT_ARC4_KEY);
|
||||
assertArrayEquals(CartV1Constants.DEFAULT_ARC4_KEY, cartDecryptor.getARC4Key());
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,192 @@
|
|||
/* ###
|
||||
* IP: GHIDRA
|
||||
*
|
||||
* 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 ghidra.file.formats.cart;
|
||||
|
||||
import static org.junit.Assert.*;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
|
||||
import ghidra.app.util.bin.BinaryReader;
|
||||
import ghidra.app.util.bin.ByteArrayProvider;
|
||||
import ghidra.util.HashUtilities;
|
||||
|
||||
public class CartV1FileTest {
|
||||
CartV1File cartFile;
|
||||
|
||||
@Before
|
||||
public void setupCartV1File() {
|
||||
try {
|
||||
ByteArrayProvider provider =
|
||||
new ByteArrayProvider(CartV1TestConstants.TEST_CART_GOOD_STD_KEY);
|
||||
cartFile = new CartV1File(provider);
|
||||
}
|
||||
catch (Exception e) {
|
||||
fail("Exception setting up CaRT file tests.");
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCartV1FileByteProvider() {
|
||||
CartV1File cartFileByteProvider = null;
|
||||
|
||||
try {
|
||||
cartFileByteProvider =
|
||||
new CartV1File(new ByteArrayProvider(CartV1TestConstants.TEST_CART_GOOD_STD_KEY));
|
||||
}
|
||||
catch (Exception e) {
|
||||
assertNull("Exception creating normal CaRT file.", cartFileByteProvider);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCartV1FileByteProviderString() {
|
||||
CartV1File cartFileByteProvider = null;
|
||||
|
||||
try {
|
||||
cartFileByteProvider = new CartV1File(
|
||||
new ByteArrayProvider(CartV1TestConstants.TEST_CART_GOOD_PRIVATE_KEY_ABC),
|
||||
CartV1TestConstants.PRIVATE_KEY);
|
||||
}
|
||||
catch (Exception e) {
|
||||
assertNull("Exception creating normal CaRT file with private key.",
|
||||
cartFileByteProvider);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCartV1FileBinaryReaderPassesWithLittleEndian() {
|
||||
CartV1File cartFileBinaryReader = null;
|
||||
|
||||
try {
|
||||
cartFileBinaryReader = new CartV1File(new BinaryReader(
|
||||
new ByteArrayProvider(CartV1TestConstants.TEST_CART_GOOD_STD_KEY), true));
|
||||
}
|
||||
catch (Exception e) {
|
||||
assertNull("Exception creating CaRT file from BinaryReader.", cartFileBinaryReader);
|
||||
}
|
||||
}
|
||||
|
||||
@Test(expected = IOException.class)
|
||||
public void testCartV1FileBinaryReaderThrowsWithBigEndian() throws Exception {
|
||||
ByteArrayProvider provider =
|
||||
new ByteArrayProvider(CartV1TestConstants.TEST_CART_GOOD_STD_KEY);
|
||||
|
||||
CartV1File cartFileBinaryReader = new CartV1File(new BinaryReader(provider, false));
|
||||
|
||||
// assertNull here is equivalent to fail() but creates a used reference to the object
|
||||
assertNull("CaRT file shouldn't be parsed as big-endian", cartFileBinaryReader);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCartV1FileBinaryReaderStringPassesWithLittleEndian() {
|
||||
CartV1File cartFileBinaryReader = null;
|
||||
|
||||
try {
|
||||
cartFileBinaryReader = new CartV1File(new BinaryReader(
|
||||
new ByteArrayProvider(CartV1TestConstants.TEST_CART_GOOD_PRIVATE_KEY_ABC), true),
|
||||
CartV1TestConstants.PRIVATE_KEY);
|
||||
}
|
||||
catch (Exception e) {
|
||||
assertNull("Exception creating CaRT file with private key from BinaryReader.",
|
||||
cartFileBinaryReader);
|
||||
}
|
||||
}
|
||||
|
||||
@Test(expected = IOException.class)
|
||||
public void testCartV1FileBinaryReaderStringThrowsWithBigEndian() throws Exception {
|
||||
ByteArrayProvider provider =
|
||||
new ByteArrayProvider(CartV1TestConstants.TEST_CART_GOOD_PRIVATE_KEY_ABC);
|
||||
|
||||
CartV1File cartFileBinaryReader =
|
||||
new CartV1File(new BinaryReader(provider, false), CartV1TestConstants.PRIVATE_KEY);
|
||||
|
||||
// assertNull here is equivalent to fail() but creates a used reference to the object
|
||||
assertNull("CaRT file shouldn't be parsed as big-endian", cartFileBinaryReader);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetName() throws Exception {
|
||||
String testingName = "Test_Name";
|
||||
|
||||
ByteArrayProvider provider =
|
||||
new ByteArrayProvider(testingName, CartV1TestConstants.TEST_CART_GOOD_STD_KEY);
|
||||
|
||||
CartV1File namedCartFile = new CartV1File(provider);
|
||||
assertEquals(testingName, namedCartFile.getName());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetPath() throws Exception {
|
||||
assertEquals(CartV1TestConstants.CARTED_FILE_NAME, cartFile.getPath());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetDataOffset() throws Exception {
|
||||
assertEquals(CartV1Constants.HEADER_LENGTH + cartFile.getHeader().optionalHeaderLength(),
|
||||
cartFile.getDataOffset());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetDataSize() throws Exception {
|
||||
assertEquals(CartV1TestConstants.CARTED_FILE_SIZE, cartFile.getDataSize());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetPackedSize() throws Exception {
|
||||
assertEquals(CartV1TestConstants.CARTED_COMPRESSED_FILE_SIZE, cartFile.getPackedSize());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetFooterHashMd5() throws Exception {
|
||||
assertEquals(CartV1TestConstants.TEST_MD5,
|
||||
new String(HashUtilities.hexDump(cartFile.getFooterHash("md5"))));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetFooterHashSha1() throws Exception {
|
||||
assertEquals(CartV1TestConstants.TEST_SHA1,
|
||||
new String(HashUtilities.hexDump(cartFile.getFooterHash("sha1"))));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetFooterHashSha256() throws Exception {
|
||||
assertEquals(CartV1TestConstants.TEST_SHA256,
|
||||
new String(HashUtilities.hexDump(cartFile.getFooterHash("sha256"))));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetHeader() throws Exception {
|
||||
assertNotNull(cartFile.getHeader());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetFooter() throws Exception {
|
||||
assertNotNull(cartFile.getFooter());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetDecryptor() throws Exception {
|
||||
assertNotNull(cartFile.getDecryptor());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetMetadata() throws Exception {
|
||||
assertNotNull(cartFile.getMetadata());
|
||||
}
|
||||
}
|
|
@ -0,0 +1,114 @@
|
|||
/* ###
|
||||
* IP: GHIDRA
|
||||
*
|
||||
* 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 ghidra.file.formats.cart;
|
||||
|
||||
import static org.junit.Assert.*;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
|
||||
import ghidra.app.util.bin.BinaryReader;
|
||||
import ghidra.app.util.bin.ByteArrayProvider;
|
||||
|
||||
public class CartV1FooterTest {
|
||||
CartV1File cartFile;
|
||||
CartV1Footer cartFooter;
|
||||
|
||||
@Before
|
||||
public void setupCartV1Footer() {
|
||||
try {
|
||||
ByteArrayProvider provider =
|
||||
new ByteArrayProvider(CartV1TestConstants.TEST_CART_GOOD_STD_KEY);
|
||||
|
||||
cartFile = new CartV1File(provider);
|
||||
cartFooter = cartFile.getFooter();
|
||||
assertNotNull(cartFooter);
|
||||
}
|
||||
catch (Exception e) {
|
||||
fail("Exception setting up CaRT footer tests.");
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCartV1FooterByteProvider() {
|
||||
CartV1Footer cartFooterByteProvider = null;
|
||||
|
||||
try {
|
||||
cartFooterByteProvider =
|
||||
new CartV1Footer(new ByteArrayProvider(CartV1TestConstants.TEST_CART_GOOD_STD_KEY));
|
||||
}
|
||||
catch (Exception e) {
|
||||
assertNull("Exception creating normal CaRT footer.", cartFooterByteProvider);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCartV1FooterBinaryReaderPassesWithLittleEndian() {
|
||||
CartV1Footer cartFooterBinaryReader = null;
|
||||
|
||||
try {
|
||||
cartFooterBinaryReader = new CartV1Footer(new BinaryReader(
|
||||
new ByteArrayProvider(CartV1TestConstants.TEST_CART_GOOD_STD_KEY), true));
|
||||
}
|
||||
catch (Exception e) {
|
||||
assertNull("Exception creating CaRT footer from BinaryReader.", cartFooterBinaryReader);
|
||||
}
|
||||
}
|
||||
|
||||
@Test(expected = IOException.class)
|
||||
public void testCartV1FooterBinaryReaderThrowsWithBigEndian() throws Exception {
|
||||
ByteArrayProvider provider =
|
||||
new ByteArrayProvider(CartV1TestConstants.TEST_CART_GOOD_STD_KEY);
|
||||
|
||||
CartV1Footer cartFooterBinaryReader = new CartV1Footer(new BinaryReader(provider, false));
|
||||
|
||||
// assertNull here is equivalent to fail() but creates a used reference to the object
|
||||
assertNull("CaRT file shouldn't be parsed as big-endian", cartFooterBinaryReader);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testMagic() throws Exception {
|
||||
assertEquals(CartV1Constants.FOOTER_MAGIC, cartFooter.magic());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testOptionalFooterPosition() throws Exception {
|
||||
assertEquals(cartFile.getDataOffset() + cartFile.getPackedSize(),
|
||||
cartFooter.optionalFooterPosition());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testOptionalFooterLength() throws Exception {
|
||||
assertEquals(CartV1TestConstants.OPTIONAL_FOOTER_LENGTH, cartFooter.optionalFooterLength());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testOptionalFooterData() throws Exception {
|
||||
cartFooter.loadOptionalFooter(new CartV1Decryptor(CartV1TestConstants.TEST_STD_KEY));
|
||||
assertNotNull(cartFooter.optionalFooterData());
|
||||
|
||||
assertEquals(CartV1TestConstants.OPTIONAL_FOOTER_DATA, cartFooter.optionalFooterData());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testLoadOptionalFooter() throws Exception {
|
||||
cartFooter.loadOptionalFooter(new CartV1Decryptor(CartV1TestConstants.TEST_STD_KEY));
|
||||
assertNotNull(cartFooter.optionalFooterData());
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,128 @@
|
|||
/* ###
|
||||
* IP: GHIDRA
|
||||
*
|
||||
* 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 ghidra.file.formats.cart;
|
||||
|
||||
import static org.junit.Assert.*;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
|
||||
import ghidra.app.util.bin.BinaryReader;
|
||||
import ghidra.app.util.bin.ByteArrayProvider;
|
||||
|
||||
public class CartV1HeaderTest {
|
||||
CartV1Header cartHeader;
|
||||
|
||||
@Before
|
||||
public void setupCartV1Header() {
|
||||
try {
|
||||
ByteArrayProvider provider =
|
||||
new ByteArrayProvider(CartV1TestConstants.TEST_CART_GOOD_STD_KEY);
|
||||
|
||||
cartHeader = new CartV1Header(provider);
|
||||
}
|
||||
catch (Exception e) {
|
||||
fail("Exception setting up CaRT header tests.");
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCartV1HeaderByteProvider() {
|
||||
CartV1Header cartHeaderByteProvider = null;
|
||||
|
||||
try {
|
||||
cartHeaderByteProvider =
|
||||
new CartV1Header(new ByteArrayProvider(CartV1TestConstants.TEST_CART_GOOD_STD_KEY));
|
||||
}
|
||||
catch (Exception e) {
|
||||
assertNull("Exception creating normal CaRT header.", cartHeaderByteProvider);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCartV1HeaderBinaryReaderPassesWithLittleEndian() {
|
||||
CartV1Header cartHeaderBinaryReader = null;
|
||||
|
||||
try {
|
||||
cartHeaderBinaryReader = new CartV1Header(new BinaryReader(
|
||||
new ByteArrayProvider(CartV1TestConstants.TEST_CART_GOOD_STD_KEY), true));
|
||||
}
|
||||
catch (Exception e) {
|
||||
assertNull("Exception creating CaRT header from BinaryReader.", cartHeaderBinaryReader);
|
||||
}
|
||||
}
|
||||
|
||||
@Test(expected = IOException.class)
|
||||
public void testCartV1HeaderBinaryReaderThrowsWithBigEndian() throws Exception {
|
||||
ByteArrayProvider provider =
|
||||
new ByteArrayProvider(CartV1TestConstants.TEST_CART_GOOD_STD_KEY);
|
||||
|
||||
CartV1Header cartHeaderBinaryReader = new CartV1Header(new BinaryReader(provider, false));
|
||||
|
||||
// assertNull here is equivalent to fail() but creates a used reference to the object
|
||||
assertNull("CaRT file shouldn't be parsed as big-endian", cartHeaderBinaryReader);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testMagic() throws Exception {
|
||||
assertEquals(CartV1Constants.HEADER_MAGIC, cartHeader.magic());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testVersion() throws Exception {
|
||||
assertEquals(CartV1Constants.HEADER_VERSION, cartHeader.version());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testArc4Key() throws Exception {
|
||||
assertArrayEquals(CartV1TestConstants.TEST_STD_KEY, cartHeader.arc4Key());
|
||||
|
||||
ByteArrayProvider provider =
|
||||
new ByteArrayProvider(CartV1TestConstants.TEST_CART_GOOD_PRIVATE_KEY_ABC);
|
||||
|
||||
CartV1Header cartHeaderPrivateKey = new CartV1Header(provider);
|
||||
|
||||
assertArrayEquals(CartV1Constants.PRIVATE_ARC4_KEY_PLACEHOLDER,
|
||||
cartHeaderPrivateKey.arc4Key());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDataStart() throws Exception {
|
||||
assertEquals(cartHeader.optionalHeaderLength() + CartV1Constants.HEADER_LENGTH,
|
||||
cartHeader.dataStart());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testOptionalHeaderLength() throws Exception {
|
||||
assertEquals(CartV1TestConstants.OPTIONAL_HEADER_LENGTH, cartHeader.optionalHeaderLength());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testOptionalHeaderData() throws Exception {
|
||||
cartHeader.loadOptionalHeader(new CartV1Decryptor(CartV1TestConstants.TEST_STD_KEY));
|
||||
assertNotNull(cartHeader.optionalHeaderData());
|
||||
|
||||
assertEquals(CartV1TestConstants.OPTIONAL_HEADER_DATA, cartHeader.optionalHeaderData());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testLoadOptionalHeader() throws Exception {
|
||||
cartHeader.loadOptionalHeader(new CartV1Decryptor(CartV1TestConstants.TEST_STD_KEY));
|
||||
assertNotNull(cartHeader.optionalHeaderData());
|
||||
}
|
||||
}
|
|
@ -0,0 +1,133 @@
|
|||
/* ###
|
||||
* IP: GHIDRA
|
||||
*
|
||||
* 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 ghidra.file.formats.cart;
|
||||
|
||||
import static org.junit.Assert.*;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
|
||||
import ghidra.app.util.bin.BinaryReader;
|
||||
import ghidra.app.util.bin.ByteArrayProvider;
|
||||
import ghidra.util.task.DummyCancellableTaskMonitor;
|
||||
import ghidra.util.task.TaskMonitor;
|
||||
|
||||
public class CartV1PayloadExtractorTest {
|
||||
CartV1PayloadExtractor cartPayloadExtractor;
|
||||
|
||||
ByteArrayProvider provider;
|
||||
ByteArrayOutputStream os;
|
||||
CartV1File cartFile;
|
||||
|
||||
@Before
|
||||
public void setupCartV1PayloadExtractor() {
|
||||
try {
|
||||
provider = new ByteArrayProvider(CartV1TestConstants.TEST_CART_GOOD_STD_KEY);
|
||||
|
||||
os = new ByteArrayOutputStream(CartV1TestConstants.TEST_ORIGINAL_DATA.length * 2);
|
||||
cartFile = new CartV1File(provider);
|
||||
}
|
||||
catch (Exception e) {
|
||||
fail("Exception setting up CaRT payload extractor tests.");
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCartV1PayloadExtactorByteProviderOutputStreamCartV1File() {
|
||||
CartV1PayloadExtractor extractor = null;
|
||||
|
||||
try {
|
||||
extractor = new CartV1PayloadExtractor(provider, os, cartFile);
|
||||
}
|
||||
catch (Exception e) {
|
||||
assertNull("Exception creating normal CaRT payload extractor.", extractor);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCartV1PayloadExtactorBinaryReaderOutputStreamCartV1FilePassesWithLittleEndian() {
|
||||
CartV1PayloadExtractor extractor = null;
|
||||
|
||||
try {
|
||||
extractor = new CartV1PayloadExtractor(new BinaryReader(provider, true), os, cartFile);
|
||||
}
|
||||
catch (Exception e) {
|
||||
assertNull("Exception creating CaRT payload extractor from BinaryReader.", extractor);
|
||||
}
|
||||
}
|
||||
|
||||
@Test(expected = IOException.class)
|
||||
public void testCartV1PayloadExtactorBinaryReaderOutputStreamCartV1FileThrowsWithBigEndian()
|
||||
throws Exception {
|
||||
CartV1PayloadExtractor extractor =
|
||||
new CartV1PayloadExtractor(new BinaryReader(provider, false), os, cartFile);
|
||||
|
||||
// assertNull here is equivalent to fail() but creates a used reference to extractor
|
||||
assertNull("CaRT file shouldn't be parsed as big-endian", extractor);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testExtract() throws Exception {
|
||||
CartV1PayloadExtractor extractor =
|
||||
new CartV1PayloadExtractor(new BinaryReader(provider, true), os, cartFile);
|
||||
|
||||
TaskMonitor monitor = new DummyCancellableTaskMonitor();
|
||||
extractor.extract(monitor);
|
||||
|
||||
assertArrayEquals(CartV1TestConstants.TEST_ORIGINAL_DATA, os.toByteArray());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testExtractionTrueWithCorrectKey() throws Exception {
|
||||
assertTrue(CartV1PayloadExtractor.testExtraction(new BinaryReader(provider, true), cartFile,
|
||||
CartV1TestConstants.TEST_STD_KEY));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testExtractionFalseWithWrongKey() throws Exception {
|
||||
assertFalse(CartV1PayloadExtractor.testExtraction(new BinaryReader(provider, true),
|
||||
cartFile, CartV1TestConstants.TEST_PRIVATE_KEY));
|
||||
}
|
||||
|
||||
@Test(expected = CartInvalidARC4KeyException.class)
|
||||
public void testExtractionThrowsOnNullKey() throws Exception {
|
||||
// Expected to throw, encrypted bytes don't matter
|
||||
CartV1PayloadExtractor.testExtraction(new BinaryReader(provider, true), cartFile, null);
|
||||
fail("CartV1PayloadExtractor should not accept a null key");
|
||||
}
|
||||
|
||||
@Test(expected = CartInvalidARC4KeyException.class)
|
||||
public void testExtractionThrowsOnShortKey() throws Exception {
|
||||
// Expected to throw, encrypted bytes don't matter
|
||||
CartV1PayloadExtractor.testExtraction(new BinaryReader(provider, true), cartFile,
|
||||
new byte[] { 0x01, 0x02 });
|
||||
fail("CartV1PayloadExtractor should not accept a short key");
|
||||
}
|
||||
|
||||
@Test(expected = CartInvalidARC4KeyException.class)
|
||||
public void testExtractionThrowsOnLongKey() throws Exception {
|
||||
byte[] longKey = new byte[CartV1Constants.ARC4_KEY_LENGTH + 1];
|
||||
System.arraycopy(CartV1TestConstants.TEST_STD_KEY, 0, longKey, 0,
|
||||
CartV1TestConstants.TEST_STD_KEY.length);
|
||||
|
||||
// Expected to throw, encrypted bytes don't matter
|
||||
CartV1PayloadExtractor.testExtraction(new BinaryReader(provider, true), cartFile, longKey);
|
||||
fail("CartV1PayloadExtractor should not accept a long key");
|
||||
}
|
||||
}
|
|
@ -0,0 +1,215 @@
|
|||
/* ###
|
||||
* IP: GHIDRA
|
||||
*
|
||||
* 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 ghidra.file.formats.cart;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.JsonObject;
|
||||
|
||||
import generic.test.AbstractGTest;
|
||||
|
||||
public class CartV1TestConstants {
|
||||
/**
|
||||
* Original content that was CaRT-ed for these tests
|
||||
*/
|
||||
public static final byte[] TEST_ORIGINAL_DATA = AbstractGTest.bytes(0x00, 0x01, 0x02, 0x03,
|
||||
0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10, 0x11, 0x12,
|
||||
0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f, 0x20, 0x21,
|
||||
0x22, 0x23, 0x24, 0x25, 0x26, 0x27, 0x28, 0x29, 0x2a, 0x2b, 0x2c, 0x2d, 0x2e, 0x2f, 0x30,
|
||||
0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x3a, 0x3b, 0x3c, 0x3d, 0x3e, 0x3f,
|
||||
0x40, 0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, 0x49, 0x4a, 0x4b, 0x4c, 0x4d, 0x4e,
|
||||
0x4f, 0x50, 0x51, 0x52, 0x53, 0x54, 0x55, 0x56, 0x57, 0x58, 0x59, 0x5a, 0x5b, 0x5c, 0x5d,
|
||||
0x5e, 0x5f, 0x60, 0x61, 0x62, 0x63, 0x64, 0x65, 0x66, 0x67, 0x68, 0x69, 0x6a, 0x6b, 0x6c,
|
||||
0x6d, 0x6e, 0x6f, 0x70, 0x71, 0x72, 0x73, 0x74, 0x75, 0x76, 0x77, 0x78, 0x79, 0x7a, 0x7b,
|
||||
0x7c, 0x7d, 0x7e, 0x7f, 0x80, 0x81, 0x82, 0x83, 0x84, 0x85, 0x86, 0x87, 0x88, 0x89, 0x8a,
|
||||
0x8b, 0x8c, 0x8d, 0x8e, 0x8f, 0x90, 0x91, 0x92, 0x93, 0x94, 0x95, 0x96, 0x97, 0x98, 0x99,
|
||||
0x9a, 0x9b, 0x9c, 0x9d, 0x9e, 0x9f, 0xa0, 0xa1, 0xa2, 0xa3, 0xa4, 0xa5, 0xa6, 0xa7, 0xa8,
|
||||
0xa9, 0xaa, 0xab, 0xac, 0xad, 0xae, 0xaf, 0xb0, 0xb1, 0xb2, 0xb3, 0xb4, 0xb5, 0xb6, 0xb7,
|
||||
0xb8, 0xb9, 0xba, 0xbb, 0xbc, 0xbd, 0xbe, 0xbf, 0xc0, 0xc1, 0xc2, 0xc3, 0xc4, 0xc5, 0xc6,
|
||||
0xc7, 0xc8, 0xc9, 0xca, 0xcb, 0xcc, 0xcd, 0xce, 0xcf, 0xd0, 0xd1, 0xd2, 0xd3, 0xd4, 0xd5,
|
||||
0xd6, 0xd7, 0xd8, 0xd9, 0xda, 0xdb, 0xdc, 0xdd, 0xde, 0xdf, 0xe0, 0xe1, 0xe2, 0xe3, 0xe4,
|
||||
0xe5, 0xe6, 0xe7, 0xe8, 0xe9, 0xea, 0xeb, 0xec, 0xed, 0xee, 0xef, 0xf0, 0xf1, 0xf2, 0xf3,
|
||||
0xf4, 0xf5, 0xf6, 0xf7, 0xf8, 0xf9, 0xfa, 0xfb, 0xfc, 0xfd, 0xfe, 0xff);
|
||||
|
||||
/**
|
||||
* Standard key that will be used for testing.
|
||||
*
|
||||
* Usually matches the default ARC4 key, but may be easily changed here if a new value is used
|
||||
*/
|
||||
public static final byte[] TEST_STD_KEY = CartV1Constants.DEFAULT_ARC4_KEY;
|
||||
|
||||
/**
|
||||
* Test CaRT of a 256 byte walk using the standard default key
|
||||
*/
|
||||
public static final byte[] TEST_CART_GOOD_STD_KEY = AbstractGTest.bytes(0x43, 0x41, 0x52, 0x54,
|
||||
0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x01, 0x04, 0x01, 0x05,
|
||||
0x09, 0x02, 0x06, 0x03, 0x01, 0x04, 0x01, 0x05, 0x09, 0x02, 0x06, 0x17, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0xc2, 0xa4, 0xa5, 0x5c, 0x53, 0xd5, 0x43, 0xf7, 0x79, 0x56, 0x37,
|
||||
0xf6, 0x55, 0x6c, 0xb4, 0xc0, 0xcc, 0x92, 0xeb, 0x54, 0xfc, 0x6e, 0x01, 0xc1, 0x87, 0xca,
|
||||
0x3d, 0x3f, 0x4f, 0x9f, 0xcd, 0x5a, 0x17, 0x55, 0xa0, 0x04, 0x35, 0xe7, 0xad, 0xb6, 0xec,
|
||||
0xa2, 0x31, 0x9f, 0x42, 0x73, 0x07, 0x5b, 0x68, 0x7d, 0xa2, 0x95, 0xce, 0x41, 0x5b, 0x09,
|
||||
0x00, 0x29, 0xe8, 0x0e, 0x4a, 0x18, 0xac, 0x08, 0x07, 0x2a, 0x3e, 0x9c, 0x90, 0xb7, 0x9e,
|
||||
0x4a, 0x0c, 0x46, 0x0a, 0xf5, 0x3f, 0x3e, 0xc8, 0xa7, 0x5d, 0x5b, 0x2d, 0xfb, 0x43, 0xe5,
|
||||
0x1f, 0xdb, 0x53, 0x8a, 0xd8, 0xe1, 0xfe, 0x43, 0x8d, 0xd6, 0xce, 0xfc, 0xc3, 0x2e, 0x26,
|
||||
0x0c, 0x98, 0xf4, 0x1d, 0x1e, 0x26, 0x4e, 0xf6, 0x15, 0x8c, 0xaa, 0x13, 0xfb, 0xdf, 0xbd,
|
||||
0x4f, 0xc7, 0xe8, 0x3c, 0x2c, 0x65, 0x7a, 0x31, 0xef, 0x85, 0x0a, 0xa3, 0x12, 0x0c, 0xe0,
|
||||
0xf0, 0x7a, 0x39, 0x27, 0x41, 0xc7, 0x42, 0x2c, 0xeb, 0x9a, 0x29, 0x32, 0xca, 0x6b, 0x03,
|
||||
0xe5, 0xa7, 0x51, 0x11, 0xd1, 0xcb, 0xc1, 0x99, 0xc4, 0x46, 0xaf, 0x2e, 0x4b, 0xda, 0x50,
|
||||
0x93, 0x87, 0x06, 0x72, 0x54, 0x24, 0xd9, 0x99, 0x36, 0x3a, 0x0c, 0x21, 0x16, 0x35, 0xd1,
|
||||
0x2a, 0x49, 0xfa, 0x84, 0xff, 0xeb, 0x71, 0x2a, 0x1f, 0x9d, 0x58, 0xcb, 0xdb, 0xf8, 0xb9,
|
||||
0x33, 0x53, 0x61, 0x51, 0xa1, 0x21, 0xa2, 0x4f, 0x1c, 0x8f, 0xad, 0xd6, 0x01, 0x6d, 0x74,
|
||||
0x8d, 0xb5, 0xe8, 0x46, 0x0d, 0x72, 0x34, 0x2f, 0x3d, 0x69, 0x50, 0xb4, 0xc8, 0x85, 0xc2,
|
||||
0x3f, 0x82, 0x93, 0xfe, 0x7f, 0x70, 0xbe, 0x38, 0x12, 0x8f, 0xaa, 0x3a, 0x59, 0xb9, 0xa2,
|
||||
0x8b, 0x0a, 0xfc, 0xc8, 0x1c, 0x84, 0x32, 0x96, 0xcc, 0x5f, 0x8f, 0xb5, 0xee, 0xd9, 0x83,
|
||||
0x53, 0x2b, 0x9a, 0x30, 0x32, 0xb6, 0xcf, 0x3e, 0xa9, 0x41, 0x82, 0x9d, 0x4a, 0xa0, 0xda,
|
||||
0x79, 0xcb, 0xbc, 0x44, 0x28, 0xbc, 0x13, 0x52, 0xf9, 0x7d, 0x2e, 0xc0, 0x10, 0x0b, 0x5f,
|
||||
0x13, 0x61, 0xbb, 0xd0, 0xe6, 0x81, 0x71, 0x92, 0x1f, 0xc2, 0xa4, 0xa7, 0x58, 0x50, 0xd7,
|
||||
0x15, 0xa5, 0x79, 0x2f, 0x74, 0x96, 0x34, 0x05, 0xc2, 0x89, 0x9d, 0x8b, 0xcd, 0x08, 0xb0,
|
||||
0x76, 0x5e, 0x72, 0x78, 0x19, 0x56, 0x80, 0xb5, 0xbc, 0x34, 0x77, 0x21, 0x2c, 0x00, 0x96,
|
||||
0x76, 0x30, 0x3e, 0xba, 0x1a, 0x47, 0x6f, 0x7b, 0xd8, 0x8f, 0xf5, 0xd0, 0x55, 0x47, 0x0e,
|
||||
0x17, 0xe0, 0x77, 0x21, 0xda, 0xba, 0x4d, 0x1b, 0x71, 0xaf, 0x44, 0xf0, 0x1d, 0xc0, 0x5d,
|
||||
0x88, 0xd5, 0xea, 0xa4, 0x4a, 0xaf, 0xf3, 0xee, 0x88, 0xe1, 0x5c, 0x58, 0x2e, 0xe6, 0x85,
|
||||
0x67, 0x66, 0x5c, 0x3a, 0x80, 0x39, 0xbd, 0x99, 0x72, 0x9a, 0xef, 0xd9, 0x2c, 0xa8, 0x86,
|
||||
0x00, 0x17, 0x0a, 0x13, 0x5b, 0xd5, 0xbc, 0x09, 0xfa, 0x52, 0x43, 0xa6, 0xe6, 0x74, 0x3f,
|
||||
0x7d, 0x1d, 0x9b, 0x0b, 0x7a, 0xa4, 0xc0, 0x76, 0x23, 0xdd, 0x7f, 0x42, 0xf4, 0xeb, 0x43,
|
||||
0x54, 0xcd, 0x8a, 0x82, 0xd0, 0x8a, 0x5e, 0xe5, 0x66, 0xaa, 0x3d, 0xb6, 0x24, 0x35, 0xb7,
|
||||
0xcc, 0xb6, 0x9a, 0x69, 0x25, 0x8a, 0x82, 0xb8, 0x98, 0xa8, 0x90, 0x78, 0x8f, 0xe2, 0x5b,
|
||||
0x77, 0x0b, 0x18, 0xd8, 0xd7, 0xe4, 0x3e, 0xf3, 0x66, 0x20, 0x50, 0x28, 0xa3, 0xc1, 0xf0,
|
||||
0xc3, 0x32, 0xe5, 0x63, 0xde, 0x81, 0x11, 0x3e, 0x42, 0x9c, 0xe1, 0xa6, 0x54, 0x52, 0x41,
|
||||
0x43, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x48, 0x01, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0xb7, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00);
|
||||
|
||||
/**
|
||||
* Private key used to create TEST_CART_GOOD_PRIVATE_KEY_ABC
|
||||
*/
|
||||
public static final String PRIVATE_KEY = "abc";
|
||||
|
||||
/**
|
||||
* Standard key that will be used for testing.
|
||||
*
|
||||
* Usually matches the default ARC4 key, but may be easily changed here if a new value is used
|
||||
*/
|
||||
public static final byte[] TEST_PRIVATE_KEY = AbstractGTest.bytes(0x61, 0x62, 0x63, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00);
|
||||
|
||||
/**
|
||||
* Test CaRT of a 256 byte walk using a private key of "abc"
|
||||
*/
|
||||
public static final byte[] TEST_CART_GOOD_PRIVATE_KEY_ABC = AbstractGTest.bytes(0x43, 0x41,
|
||||
0x52, 0x54, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x17, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0xa2, 0xb0, 0x8c, 0x7f, 0xd4, 0xa9, 0xa5, 0xd5,
|
||||
0xb7, 0xfa, 0x7b, 0x6c, 0x6a, 0xa5, 0x8d, 0xdc, 0xe1, 0xfb, 0xc0, 0x1f, 0xf6, 0xbd, 0x02,
|
||||
0x81, 0xdf, 0xed, 0x13, 0x4e, 0x75, 0x9f, 0xf6, 0xf6, 0x98, 0x2d, 0x3d, 0x33, 0xf6, 0xe0,
|
||||
0xa6, 0x9f, 0xb2, 0xa5, 0x7c, 0xda, 0xcf, 0x83, 0xa1, 0xdf, 0xef, 0xe3, 0x7a, 0xd3, 0x5f,
|
||||
0xd2, 0x6c, 0x80, 0x3a, 0x36, 0x89, 0xa8, 0x5b, 0xac, 0xc1, 0xce, 0x8a, 0x45, 0xa9, 0x62,
|
||||
0x19, 0x2e, 0x3d, 0x68, 0x3c, 0x0a, 0xa0, 0xcd, 0x06, 0x02, 0x3e, 0x67, 0x3a, 0xbe, 0xe3,
|
||||
0xef, 0x93, 0x00, 0x50, 0xf1, 0x9d, 0x9c, 0xce, 0x64, 0xf4, 0x5e, 0x2d, 0x6a, 0x64, 0x76,
|
||||
0x6c, 0x3a, 0x83, 0xbd, 0xf9, 0x72, 0x0d, 0xc2, 0x96, 0x92, 0x9a, 0xa1, 0xff, 0xe2, 0xbf,
|
||||
0xd1, 0x08, 0x9e, 0xb2, 0xb6, 0x50, 0xb2, 0x7e, 0x1b, 0xd8, 0x38, 0x9b, 0x82, 0xca, 0xc4,
|
||||
0xb0, 0xcf, 0x57, 0x23, 0xc7, 0x97, 0x71, 0x9d, 0xa2, 0x31, 0x99, 0x6f, 0x27, 0x5e, 0xdb,
|
||||
0xb5, 0x73, 0xf4, 0xae, 0x5d, 0x7d, 0xad, 0x5e, 0xe1, 0xb3, 0x52, 0x5c, 0x81, 0x5a, 0x0e,
|
||||
0x0a, 0x51, 0x7f, 0x78, 0x3b, 0x8b, 0x8d, 0x29, 0x8c, 0xbd, 0xe5, 0x68, 0x8b, 0x1e, 0xfa,
|
||||
0x53, 0xa0, 0xce, 0x29, 0x6c, 0x83, 0xbd, 0x73, 0x5e, 0x6f, 0x9b, 0x54, 0x5a, 0x20, 0x74,
|
||||
0x8e, 0xa6, 0x12, 0x26, 0xe2, 0x62, 0xe9, 0xc0, 0x1a, 0x1e, 0x1c, 0xea, 0x1b, 0x5e, 0xa3,
|
||||
0xa2, 0xaa, 0x35, 0xfc, 0x73, 0xd2, 0x2b, 0x51, 0xd1, 0xa9, 0x65, 0x98, 0x23, 0x00, 0x08,
|
||||
0xf7, 0xaf, 0x4b, 0x27, 0x90, 0xe8, 0xd0, 0xd0, 0x23, 0x00, 0xe0, 0x2c, 0x11, 0xa7, 0x3a,
|
||||
0xca, 0x6e, 0x4f, 0x12, 0xb8, 0x09, 0x49, 0x16, 0x59, 0x59, 0x42, 0xc8, 0xe0, 0x5d, 0x11,
|
||||
0xdf, 0x7f, 0x87, 0x02, 0xbf, 0x5b, 0x73, 0x12, 0x95, 0xe9, 0x8c, 0x8b, 0xb1, 0x41, 0x48,
|
||||
0xa6, 0xc0, 0x2c, 0x32, 0xed, 0xae, 0x87, 0x2c, 0xc3, 0xd9, 0xfe, 0x37, 0xeb, 0x93, 0x09,
|
||||
0x5b, 0x8c, 0x6a, 0xb1, 0xab, 0xc6, 0xbf, 0xac, 0x37, 0x1f, 0x98, 0x01, 0xa2, 0xb2, 0x88,
|
||||
0x7c, 0xd6, 0xff, 0xf7, 0xd5, 0xce, 0xb9, 0x1b, 0x0d, 0x03, 0xd3, 0xc4, 0x8d, 0xf8, 0xdd,
|
||||
0x9c, 0x53, 0xee, 0xe2, 0xf6, 0x82, 0xae, 0xc4, 0xc1, 0x5a, 0xa1, 0x2a, 0xfe, 0x44, 0xac,
|
||||
0x13, 0x48, 0xf1, 0xd2, 0x7d, 0xba, 0xd3, 0x8e, 0xcf, 0x00, 0xed, 0x7d, 0x5b, 0x60, 0x22,
|
||||
0x23, 0x74, 0x17, 0xb5, 0x85, 0x19, 0x10, 0x23, 0x77, 0x7a, 0xe2, 0xb7, 0xe8, 0x86, 0x02,
|
||||
0x4b, 0xff, 0x9f, 0x91, 0xc5, 0x3e, 0xfd, 0x7c, 0x08, 0x4a, 0x10, 0x54, 0x1e, 0x44, 0xa1,
|
||||
0xc3, 0x88, 0x08, 0x75, 0xb8, 0xe2, 0xe4, 0xb6, 0x90, 0xcc, 0x83, 0xde, 0xe1, 0x6c, 0xfd,
|
||||
0xdd, 0xd8, 0x6c, 0x89, 0x11, 0x72, 0xb2, 0x02, 0xa2, 0x81, 0x93, 0x84, 0xff, 0x89, 0x41,
|
||||
0x2d, 0xc1, 0xcd, 0x2d, 0xc1, 0xeb, 0x67, 0xd6, 0x35, 0x78, 0x4f, 0xcc, 0xa1, 0x32, 0xe5,
|
||||
0xe2, 0x4f, 0x38, 0xb1, 0x1f, 0xa2, 0xfa, 0x1c, 0x44, 0xcb, 0x12, 0xef, 0xed, 0xb7, 0xc8,
|
||||
0xca, 0x8a, 0x35, 0x6f, 0x97, 0x3c, 0x01, 0x59, 0xd0, 0x3f, 0xa7, 0x44, 0xf6, 0x09, 0x6b,
|
||||
0x82, 0xcd, 0x70, 0x49, 0x80, 0xf7, 0x92, 0x60, 0xf7, 0xf1, 0x8d, 0x8f, 0x26, 0x37, 0x82,
|
||||
0xb4, 0x73, 0xf0, 0x7a, 0x04, 0xdb, 0x8f, 0x81, 0x74, 0x88, 0xca, 0x3e, 0x2e, 0x78, 0x54,
|
||||
0x52, 0x41, 0x43, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x48, 0x01, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0xb7, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00);
|
||||
|
||||
/**
|
||||
* Original file name of file CaRTed as TEST_CART_GOOD_STD_KEY and
|
||||
* TEST_CART_GOOD_PRIVATE_KEY_ABC
|
||||
*/
|
||||
public static final String CARTED_FILE_NAME = "CaRT_TestBin";
|
||||
|
||||
/**
|
||||
* Original file size of CaRT-ed test data
|
||||
*/
|
||||
public static final int CARTED_FILE_SIZE = 256;
|
||||
|
||||
/**
|
||||
* Compressed size of CaRT-ed test data.
|
||||
* Note: This is bigger than the original size because there are no repeated bytes and it
|
||||
* doesn't compress well.
|
||||
*/
|
||||
public static final int CARTED_COMPRESSED_FILE_SIZE = 267;
|
||||
|
||||
/**
|
||||
* MD5 of the original data
|
||||
*/
|
||||
public static final String TEST_MD5 = "e2c865db4162bed963bfaa9ef6ac18f0";
|
||||
|
||||
/**
|
||||
* SHA1 of the original data
|
||||
*/
|
||||
public static final String TEST_SHA1 = "4916d6bdb7f78e6803698cab32d1586ea457dfc8";
|
||||
|
||||
/**
|
||||
* SHA256 of the original data
|
||||
*/
|
||||
public static final String TEST_SHA256 =
|
||||
"40aff2e9d2d8922e47afd4648e6967497158785fbd1da870e7110266bf944880";
|
||||
|
||||
/**
|
||||
* The raw data (JSON string) of the optional header data in the test
|
||||
*/
|
||||
public static final String OPTIONAL_HEADER_DATA_RAW = "{\"name\":\"" + CARTED_FILE_NAME + "\"}";
|
||||
|
||||
/**
|
||||
* Length of the raw optional header data
|
||||
*/
|
||||
public static final long OPTIONAL_HEADER_LENGTH = OPTIONAL_HEADER_DATA_RAW.length();
|
||||
|
||||
/**
|
||||
* Raw optional header data as a parsed JSON object
|
||||
*/
|
||||
public static final JsonObject OPTIONAL_HEADER_DATA =
|
||||
new Gson().fromJson(OPTIONAL_HEADER_DATA_RAW, JsonObject.class);
|
||||
|
||||
/**
|
||||
* The raw data (JSON string) of the optional footer data in the test
|
||||
*/
|
||||
public static final String OPTIONAL_FOOTER_DATA_RAW =
|
||||
"{" + "\"length\":\"" + CARTED_FILE_SIZE + "\"," + "\"md5\":\"" + TEST_MD5 + "\"," +
|
||||
"\"sha1\":\"" + TEST_SHA1 + "\"," + "\"sha256\":\"" + TEST_SHA256 + "\"" + "}";
|
||||
|
||||
/**
|
||||
* Length of the raw optional footer data
|
||||
*/
|
||||
public static final long OPTIONAL_FOOTER_LENGTH = OPTIONAL_FOOTER_DATA_RAW.length();
|
||||
|
||||
/**
|
||||
* Raw optional footer data as a parsed JSON object
|
||||
*/
|
||||
public static final JsonObject OPTIONAL_FOOTER_DATA =
|
||||
new Gson().fromJson(OPTIONAL_FOOTER_DATA_RAW, JsonObject.class);
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue