GP-3748: Added support for CaRT file system

This commit is contained in:
jt8587 2023-10-04 20:23:53 +00:00 committed by Ryan Kurtz
parent e138d381ea
commit ab40dbae46
24 changed files with 3948 additions and 0 deletions

View file

@ -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|

View file

@ -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>

View file

@ -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);
}
}

View file

@ -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);
}
}

View file

@ -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;
}
}

View file

@ -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;
}
}

View file

@ -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);
}
}

View file

@ -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);
}
}

View file

@ -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
);
}

View file

@ -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;
}
}

View file

@ -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;
}
}

View file

@ -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();
}
}

View file

@ -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();
}
}

View file

@ -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;
}
}

View file

@ -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;
}
}

View file

@ -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;
}
}

View file

@ -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);
}
}

View file

@ -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;
}
}

View file

@ -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());
}
}

View file

@ -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());
}
}

View file

@ -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());
}
}

View file

@ -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());
}
}

View file

@ -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");
}
}

View file

@ -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);
}