diff --git a/Ghidra/Features/FileFormats/certification.manifest b/Ghidra/Features/FileFormats/certification.manifest index ecc99fcb57..50eb24cab7 100644 --- a/Ghidra/Features/FileFormats/certification.manifest +++ b/Ghidra/Features/FileFormats/certification.manifest @@ -18,3 +18,4 @@ data/languages/minidump.opinion||GHIDRA||||END| data/languages/pagedump.opinion||GHIDRA||||END| src/main/help/help/TOC_Source.xml||GHIDRA||||END| src/main/help/help/topics/FileFormatsPlugin/FileFormats.html||GHIDRA||||END| +src/main/help/help/topics/cart/CartFileSystem.html||GHIDRA||||END| diff --git a/Ghidra/Features/FileFormats/src/main/help/help/topics/cart/CartFileSystem.html b/Ghidra/Features/FileFormats/src/main/help/help/topics/cart/CartFileSystem.html new file mode 100644 index 0000000000..ae5735df16 --- /dev/null +++ b/Ghidra/Features/FileFormats/src/main/help/help/topics/cart/CartFileSystem.html @@ -0,0 +1,75 @@ + + + + + + + + + + + CaRT File Format + + + + +

CaRT File Format

+ +

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.

+ +

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. + *

+ * From CaRT Source, cart.py + * + *

{@code
+ * #  MANDATORY HEADER (Not compress, not encrypted.
+ * #  4s     h         Q        16s         Q
+ * # 'CART'
+ * #
+ * # OPTIONAL_HEADER (OPT_HEADER_LEN bytes)
+ * # RC4()
+ * #
+ * # RC4(ZLIB(block encoded stream ))
+ * #
+ * # OPTIONAL_FOOTER_LEN (Q)
+ * # 
+ * #
+ * #  MANDATORY FOOTER
+ * #  4s      QQ           Q
+ * # 'TRAC'
+ * }
+ * Where s=1 ASCII string byte, h=short, Q=quadword + *

+ * 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); +}