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.
+
+
About CaRT
+
The CaRT format was developed by the Canadian government within their
+ Canadian Centre for Cyber Security. The documentation and
+ repository can be found on the CaRT GitHub page.
+
+
+
The official CaRT python library is usually used
+ to create CaRT files via its command-line interface or within other python applications or
+ libraries.
+
+
Supported CaRT Format Versions
+
Currently CaRT only has a single format version, namely version 1
+ . If/when new versions are released this file system will be updated to support them.
+
+
Decryption Keys
+
The CaRT format uses ARC4 encryption and supports two modes of keys: default and private.
+
+
Default - 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.
+
Private - 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:
+
+
+ INI Configuration - If the default CaRT configuration file exists (
+ ${USER_HOME}/.cart/cart.cfg) the key stored there, if
+ any, will be attempted first. See the
+ CaRT GitHub for more
+ documentation on this configuration file.
+
+
+ User Prompt - 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'.
+
+
+
+
+
See the CaRT GitHub page for more
+ documentation on keys, requirements, and formats.
+
+
Metadata (and Hashes)
+
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.
+
+
diff --git a/Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/cart/CartCancelDialogs.java b/Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/cart/CartCancelDialogs.java
new file mode 100644
index 0000000000..a46f932f1d
--- /dev/null
+++ b/Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/cart/CartCancelDialogs.java
@@ -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 "" + wrapped.replace("\n", " ") + "";
+ }
+
+ /**
+ * 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.
+ *
+ * Note: If in headless mode log the message at the appropriate level and then
+ * treat as if the user chose to cancel 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);
+ }
+}
diff --git a/Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/cart/CartConfigurationException.java b/Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/cart/CartConfigurationException.java
new file mode 100644
index 0000000000..96a69ee8b7
--- /dev/null
+++ b/Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/cart/CartConfigurationException.java
@@ -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);
+ }
+}
diff --git a/Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/cart/CartFileSystem.java b/Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/cart/CartFileSystem.java
new file mode 100644
index 0000000000..d8be26e7a7
--- /dev/null
+++ b/Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/cart/CartFileSystem.java
@@ -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 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 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 skipKeys = new HashSet<>(Set.of("name"));
+
+ // Set to track all attributes that have been added. Used during bulk addition
+ // to add with _# suffixes.
+ Set 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 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 warnKeys = new HashSet<>();
+ for (Entry 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 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 = "";
+
+ 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;
+ }
+}
diff --git a/Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/cart/CartFileSystemFactory.java b/Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/cart/CartFileSystemFactory.java
new file mode 100644
index 0000000000..e976fdedcb
--- /dev/null
+++ b/Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/cart/CartFileSystemFactory.java
@@ -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, 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;
+ }
+}
diff --git a/Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/cart/CartInvalidARC4KeyException.java b/Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/cart/CartInvalidARC4KeyException.java
new file mode 100644
index 0000000000..166106d20b
--- /dev/null
+++ b/Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/cart/CartInvalidARC4KeyException.java
@@ -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);
+ }
+}
diff --git a/Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/cart/CartInvalidCartException.java b/Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/cart/CartInvalidCartException.java
new file mode 100644
index 0000000000..b45188ea5c
--- /dev/null
+++ b/Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/cart/CartInvalidCartException.java
@@ -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);
+ }
+}
diff --git a/Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/cart/CartV1Constants.java b/Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/cart/CartV1Constants.java
new file mode 100644
index 0000000000..0dfd1ac1ac
--- /dev/null
+++ b/Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/cart/CartV1Constants.java
@@ -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.
+ *
+ * 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 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 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 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
+ );
+}
diff --git a/Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/cart/CartV1Decryptor.java b/Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/cart/CartV1Decryptor.java
new file mode 100644
index 0000000000..9e1bd7300a
--- /dev/null
+++ b/Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/cart/CartV1Decryptor.java
@@ -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;
+ }
+}
diff --git a/Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/cart/CartV1File.java b/Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/cart/CartV1File.java
new file mode 100644
index 0000000000..1b46d559a7
--- /dev/null
+++ b/Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/cart/CartV1File.java
@@ -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 = "";
+ private String path = "";
+ private long dataOffset = -1;
+ private long payloadOriginalSize = -1;
+ private long packedSize = -1;
+ private Map 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 " +
+ StringEscapeUtils.escapeHtml4(String.valueOf(payloadOriginalSize)) +
+ ", but this value seems unreasonable given the compressed size of " +
+ StringEscapeUtils.escapeHtml4((String.valueOf(packedSize))) +
+ ". Continue processing?")) {
+ throw new CancelledException("Cancelled due to footer length field error.");
+ }
+ }
+ }
+ else {
+ payloadOriginalSize = -1;
+ }
+
+ footerHashes = new LinkedHashMap<>();
+
+ List 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 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 =
+ // 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.
+ *
+ * Note: 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 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;
+ }
+}
diff --git a/Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/cart/CartV1Footer.java b/Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/cart/CartV1Footer.java
new file mode 100644
index 0000000000..2a0cf80f79
--- /dev/null
+++ b/Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/cart/CartV1Footer.java
@@ -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();
+ }
+}
diff --git a/Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/cart/CartV1Header.java b/Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/cart/CartV1Header.java
new file mode 100644
index 0000000000..df595f0297
--- /dev/null
+++ b/Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/cart/CartV1Header.java
@@ -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();
+ }
+}
diff --git a/Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/cart/CartV1PayloadExtractor.java b/Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/cart/CartV1PayloadExtractor.java
new file mode 100644
index 0000000000..7376f0468a
--- /dev/null
+++ b/Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/cart/CartV1PayloadExtractor.java
@@ -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;
+ }
+}
diff --git a/Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/cart/CartV1StreamDecompressor.java b/Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/cart/CartV1StreamDecompressor.java
new file mode 100644
index 0000000000..b5d058eb5c
--- /dev/null
+++ b/Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/cart/CartV1StreamDecompressor.java
@@ -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;
+
+ }
+}
diff --git a/Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/cart/CartV1StreamDecryptor.java b/Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/cart/CartV1StreamDecryptor.java
new file mode 100644
index 0000000000..7c7b014dd3
--- /dev/null
+++ b/Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/cart/CartV1StreamDecryptor.java
@@ -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;
+ }
+}
diff --git a/Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/cart/CartV1StreamHasher.java b/Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/cart/CartV1StreamHasher.java
new file mode 100644
index 0000000000..9677e583fd
--- /dev/null
+++ b/Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/cart/CartV1StreamHasher.java
@@ -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 hashers = new LinkedHashMap<>();
+ private Map hashes = new LinkedHashMap<>();
+ private Map 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 missingHashes = new ArrayList<>();
+
+ // Iterate across the expected hashes, record which are missing and create hashers for
+ // ones that are present
+ for (Map.Entry 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 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 verifiedHashes = new ArrayList<>(); // List of hashes that are present and
+ // correct
+ List 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 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);
+ }
+}
diff --git a/Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/cart/CartV1StreamProcessor.java b/Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/cart/CartV1StreamProcessor.java
new file mode 100644
index 0000000000..e0b531c42d
--- /dev/null
+++ b/Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/cart/CartV1StreamProcessor.java
@@ -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;
+ }
+}
diff --git a/Ghidra/Features/FileFormats/src/test/java/ghidra/file/formats/cart/CartV1DecryptorTest.java b/Ghidra/Features/FileFormats/src/test/java/ghidra/file/formats/cart/CartV1DecryptorTest.java
new file mode 100644
index 0000000000..13bc888d51
--- /dev/null
+++ b/Ghidra/Features/FileFormats/src/test/java/ghidra/file/formats/cart/CartV1DecryptorTest.java
@@ -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());
+ }
+
+}
diff --git a/Ghidra/Features/FileFormats/src/test/java/ghidra/file/formats/cart/CartV1FileTest.java b/Ghidra/Features/FileFormats/src/test/java/ghidra/file/formats/cart/CartV1FileTest.java
new file mode 100644
index 0000000000..9bed6b0ca2
--- /dev/null
+++ b/Ghidra/Features/FileFormats/src/test/java/ghidra/file/formats/cart/CartV1FileTest.java
@@ -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());
+ }
+}
diff --git a/Ghidra/Features/FileFormats/src/test/java/ghidra/file/formats/cart/CartV1FooterTest.java b/Ghidra/Features/FileFormats/src/test/java/ghidra/file/formats/cart/CartV1FooterTest.java
new file mode 100644
index 0000000000..936e28ff83
--- /dev/null
+++ b/Ghidra/Features/FileFormats/src/test/java/ghidra/file/formats/cart/CartV1FooterTest.java
@@ -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());
+ }
+
+}
diff --git a/Ghidra/Features/FileFormats/src/test/java/ghidra/file/formats/cart/CartV1HeaderTest.java b/Ghidra/Features/FileFormats/src/test/java/ghidra/file/formats/cart/CartV1HeaderTest.java
new file mode 100644
index 0000000000..08f4383c80
--- /dev/null
+++ b/Ghidra/Features/FileFormats/src/test/java/ghidra/file/formats/cart/CartV1HeaderTest.java
@@ -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());
+ }
+}
diff --git a/Ghidra/Features/FileFormats/src/test/java/ghidra/file/formats/cart/CartV1PayloadExtractorTest.java b/Ghidra/Features/FileFormats/src/test/java/ghidra/file/formats/cart/CartV1PayloadExtractorTest.java
new file mode 100644
index 0000000000..597851f3ac
--- /dev/null
+++ b/Ghidra/Features/FileFormats/src/test/java/ghidra/file/formats/cart/CartV1PayloadExtractorTest.java
@@ -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");
+ }
+}
diff --git a/Ghidra/Features/FileFormats/src/test/java/ghidra/file/formats/cart/CartV1TestConstants.java b/Ghidra/Features/FileFormats/src/test/java/ghidra/file/formats/cart/CartV1TestConstants.java
new file mode 100644
index 0000000000..f3af6caf26
--- /dev/null
+++ b/Ghidra/Features/FileFormats/src/test/java/ghidra/file/formats/cart/CartV1TestConstants.java
@@ -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);
+}