Skip to content

Instantly share code, notes, and snippets.

@liamwhite
Last active November 6, 2023 14:09
Show Gist options
  • Save liamwhite/ba39ce769424b53a5505 to your computer and use it in GitHub Desktop.
Save liamwhite/ba39ce769424b53a5505 to your computer and use it in GitHub Desktop.
Celestia's ARK
/**
* celestias-ark.c
*
* This file is part of Twilight's Trick.
*
* Twilight's Trick is free software: you can redistribute
* it and/or modify it under the terms of the GNU General Public
* License as published by the Free Software Foundation, either
* version 3 of the License, or (at your option) any later version.
*
* Twilight's Trick is distributed in the hope that it will
* be useful, but WITHOUT ANY WARRANTY; without even the implied warranty
* of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
/* To build: gcc celestias-ark.c -o ark -lzstd */
#include <assert.h> /* assert() */
#include <errno.h>
#include <stdint.h> /* uint32_t */
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <sys/stat.h> /* mkdir() */
#include <sys/time.h> /* utimes() */
#include <unistd.h> /* chdir() */
#include <zstd.h> /* ZSTD_decompress() */
/* "Shared secret" known by all copies of the Android Gameloft game */
static uint32_t KEY[4] = {0x3d5b2a34, 0x923fff10, 0x00e346a4, 0x0c74902b};
typedef struct _ark_header_t {
uint32_t file_count;
uint32_t metadata_offset;
uint32_t ark_version;
} ark_header_t;
typedef struct _ark_file_metadata_t {
uint8_t filename[128]; /* (ASCII) name of the file */
uint8_t pathname[128]; /* (ASCII) name of the containing directory */
uint32_t file_location; /* fseek() to this position from the start of the ARK */
uint32_t original_filesize; /* unpacked buffer size */
uint32_t compressed_size; /* compressed size, equal to original_filesize if no compression */
uint32_t encrypted_nbytes; /* encrypted data size from XXTEA block cipher, zero if plain */
uint32_t timestamp; /* Unix timestamp of file */
uint32_t md5sum[4]; /* md5sum of original file */
uint32_t priority; /* Default is 0, these will never be extracted to disk by the game;
Items with priority 1 will be cached to disk instead. */
} ark_file_metadata_t;
/**
* Convert bytes to XXTEA n-bytes.
*/
static uint32_t get_xxtea_phdr_size(uint32_t phdr_off);
/**
* Read data from @a stream into @a hdr.
*/
static int parse_ark_header(ark_header_t *hdr, FILE *stream);
/**
* The caller must free the returned memory.
*/
static ark_file_metadata_t *retrieve_ark_metadata(ark_header_t *hdr, FILE *stream);
/**
* Retrieve decrypted and decompressed data from the file referred to by @a meta.
* The caller must free the returned memory.
*/
static void *dump_file_data(ark_file_metadata_t const *meta, FILE *stream);
/**
* Decrypt @a srclen words of data in @a src using the Corrected Block TEA cipher.
*/
static void xxtea_decrypt(void *src, uint32_t srclen, uint32_t const key[4]);
int main(int argc, char *argv[])
{
FILE *file, *stream;
ark_file_metadata_t *meta, *meta_ptr;
void *data;
size_t i = 0;
ark_header_t header;
assert(sizeof(ark_file_metadata_t) == 296);
/* Verify arguments */
if (argc != 2 || strcmp(argv[1], "--help") == 0) {
fprintf(stderr, "Celestia's ARK\nCopyright (C) 2014-2018 Liam P. White\n");
fprintf(stderr, "This is free software: you are free to change and redistribute it.\n\n");
fprintf(stderr, "Usage: %s path/to/arkfile.ark\n", argv[0]);
return 1;
}
file = fopen(argv[1], "rb");
if (file == NULL) {
fprintf(stderr, "Failed to fopen() path: %s", argv[1]);
return -1;
}
if (parse_ark_header(&header, file) == 1) {
printf("Number of files in ARK: %u\n", header.file_count);
printf("Location of file table: %u\n", header.metadata_offset);
printf("ARK version number: %u\n", header.ark_version);
assert(header.ark_version == 3);
} else {
fprintf(stderr, "Error parsing ARK header!\n");
return -1;
}
meta_ptr = meta = retrieve_ark_metadata(&header, file);
if (meta == NULL) {
fprintf(stderr, "Error retrieving ARK metadata. Are you sure this is actually an ARK file?\n");
return -1;
}
printf("File list:\n");
for (; i < header.file_count; ++i) {
printf("%s ", meta->filename);
printf("(length: %u)\n", meta->original_filesize);
data = dump_file_data(meta, file);
/* Write the output file
Note: this is a hack but it works pretty well */
if (*meta->pathname != '\0') {
if (mkdir(meta->pathname, 0777) == 0 || errno == EEXIST) {
assert(chdir(meta->pathname) == 0);
} else {
perror("mkdir");
assert(0); /* crash */
}
}
stream = fopen(meta->filename, "wb");
assert(stream != NULL);
fwrite(data, meta->original_filesize, 1, stream);
fclose(stream);
/* Set corresponding timestamp */
struct timeval tv[2];
tv[0].tv_sec = meta->timestamp;
tv[0].tv_usec = 0;
tv[1] = tv[0]; /* why bother doing it again? */
utimes(meta->filename, tv);
/* Go back to parent */
if (*meta->pathname != '\0') {
assert(chdir("../") == 0);
}
free(data);
meta++;
}
printf("\n\nDone!\n");
free(meta_ptr);
return 0;
}
static uint32_t get_xxtea_phdr_size(uint32_t phdr_off)
{
if (phdr_off & 3) {
phdr_off &= ~3u;
phdr_off += 4;
}
return phdr_off;
}
static int parse_ark_header(ark_header_t *hdr, FILE *stream)
{
assert(hdr != NULL);
return fread(hdr, sizeof(ark_header_t), 1, stream) == 1;
}
static ark_file_metadata_t *retrieve_ark_metadata(ark_header_t *hdr, FILE *stream)
{
uint32_t filesize;
if (fseek(stream, 0, SEEK_END) != 0) {
return NULL;
}
filesize = ftell(stream);
if (filesize < 0) {
return NULL;
}
uint32_t metadata_size = get_xxtea_phdr_size(filesize - hdr->metadata_offset);
uint32_t raw_metadata_size = hdr->file_count * sizeof(ark_file_metadata_t);
void *metadata = malloc(metadata_size);
if (fseek(stream, hdr->metadata_offset, SEEK_SET) != 0) {
free(metadata);
return NULL;
}
if (fread(metadata, metadata_size, 1, stream) != 1) {
free(metadata);
return NULL;
}
xxtea_decrypt(metadata, metadata_size / 4, KEY);
// Decompress
void *raw_metadata = malloc(raw_metadata_size);
ZSTD_decompress(raw_metadata, raw_metadata_size, metadata, metadata_size);
free(metadata);
return raw_metadata;
}
static void *dump_file_data(ark_file_metadata_t const *metadata, FILE *stream)
{
fseek(stream, metadata->file_location, SEEK_SET);
void *file_data = malloc(metadata->encrypted_nbytes ? metadata->encrypted_nbytes : metadata->original_filesize);
fread(file_data, metadata->encrypted_nbytes ? metadata->encrypted_nbytes : metadata->compressed_size, 1, stream);
/* File was encrypted */
if (metadata->encrypted_nbytes != 0) {
xxtea_decrypt(file_data, metadata->encrypted_nbytes / 4, KEY);
}
/* File was compressed */
if (metadata->compressed_size != metadata->original_filesize) {
size_t destlen = metadata->original_filesize;
void *uncompressed_data = malloc(destlen);
assert(ZSTD_decompress(uncompressed_data, destlen, file_data, metadata->compressed_size) == destlen);
free(file_data);
file_data = uncompressed_data;
}
return file_data;
}
#define DELTA 0x9e3779b9
#define MX (((z>>5^y<<2) + (y>>3^z<<4)) ^ ((sum^y) + (key[(p&3)^e] ^ z)))
static void xxtea_decrypt(void *src, uint32_t n, uint32_t const key[4])
{
uint32_t *v = (uint32_t *)src;
uint32_t y, z, sum;
uint32_t p, rounds, e;
rounds = 6 + 52/n;
sum = rounds * DELTA;
y = v[0];
do {
e = (sum >> 2) & 3;
for (p = n - 1; p > 0; p--) {
z = v[p-1];
y = v[p] -= MX;
}
z = v[n - 1];
y = v[0] -= MX;
sum -= DELTA;
} while (--rounds);
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment